凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

HDRP の Unlit シェーダを読んでみた(前半)

はじめに

HDRP 7.2.0 からプレビューではないパッケージになりました(DXR は現状プレビュー機能のようです)。Unity のバージョン的には 2019.3 から含まれるようになります。

blogs.unity3d.com

blogs.unity3d.com

blogs.unity3d.com

HDRP の機能は膨大でちょっと調べるのを尻込みしていましたが、プレビューでなくなったこともありそろそろ...と思いまして調べ始めることにしました。今の所の最終目標は uRaymarching での HDRP サポートです(URP は対応しました)。 パス内の必要な行を精査していく必要があり長くなるため、複数回に分けて調査をしていきます。そのため HDRP の入門の内容ではなく、シェーダを直書きできるよう見ていくことが目的となります。一度に全部やるのは大変なので、今回は Unlit なシェーダのいくつかのパスを見るところまでやってみたいと思います。内容も調査順で書いていく & 理解できないところも多いため体系だっていないものになってしまっているのはご容赦ください。Unlit だけに絞りますあそれでも前後半に分けようと思います。Lit 側も含めて色々と読み終えたらまた改めてまとめられたらいいなと思っています。

なお、URP については以下の記事で調査を行いました。

tips.hecomi.com

環境

  • Unity 2019.4.9f1
  • HDRP 7.5.1

Unlit シェーダ

早速 Unlit シェーダを見ていきましょう。適当なマテリアルを作成し、HDRP > Unlit を選択すると設定項目が見られます。

f:id:hecomi:20200905221455p:plain

従来のパイプラインや URP と比較すると設定の幅が広くてよいですね。このシェーダですが Packages > High Definition RP > Runtime > Material > Unlit > ShaderPass > Unlit からコードを見ることが出来ます。アウトラインを見てみると次のようになっています:

Shader "HDRP/Unlit"
{
    Properties
    {
        ...
    }

    HLSLINCLUDE

    #pragma target 4.5
    ...

    ENDHLSL

    SubShader
    {
        Tags
        { 
            "RenderPipeline" = "HDRenderPipeline" 
            "RenderType" = "HDUnlitShader" 
        }

        Pass
        {
            Name "SceneSelectionPass"
            Tags { "LightMode" = "SceneSelectionPass" }
            ...
        }

        Pass
        {
            Name "DepthForwardOnly"
            Tags { "LightMode" = "DepthForwardOnly" }
            ...
        }

        Pass
        {
            Name "MotionVectors"
            Tags { "LightMode" = "MotionVectors" }
            ...
        }

        Pass
        {
            Name "ForwardOnly"
            Tags { "LightMode" = "ForwardOnly" }
            ...
        }

        Pass
        {
            Name "META"
            Tags { "LightMode" = "META" }
            ...
        }

        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode" = "ShadowCaster" }
            ...
        }

        Pass
        {
            Name "DistortionVectors"
            Tags { "LightMode" = "DistortionVectors" }
            ...
        }
    }

    SubShader
    {
        Tags
        { 
            "RenderPipeline" = "HDRenderPipeline" 
        }

        Pass
        {
            Name "IndirectDXR"
            ...
        }

        Pass
        {
            Name "ForwardDXR"
            ...
        }

        Pass
        {
            Name "GBufferDXR"
            ...
        }

        Pass
        {
            Name "VisibilityDXR"
            ...
        }

        Pass
        {
            Name "PathTracingDXR"
            ...
        }
    }

    CustomEditor "Rendering.HighDefinition.UnlitGUI"
}

パスの数も少し多いですね。SubShader が 2 つ含まれていて通常のものと DXR 用とで分かれています。DXR 用のものは今回は触れません(...未勉強です)。Pass は以下のようなものが含まれています。

  • SceneSelectionPass
  • DepthForwardOnly
  • MotionVectors
  • ForwardOnly
  • META
  • ShadowCaster
  • DistortionVectors

一つずつ見ていきましょう。

SceneSelectionPass

その名の通り、エディタの Scene ビューで選択するときのアウトライン表示用のパスです。初っ端から HDRP の話と逸れてしまうのですが見ていきましょう。Unity では 5.5 からデフォルトのオブジェクト選択視覚効果がアウトラインになりました(オレンジ色になるやつです)。これはどうやってるかというと以下のフォーラムのスレッドで解説されています(GitHub にもコードが上がってます、ありがたい...)。

forum.unity.com

forum.unity.com

ここでは Scene ビューでなくゲーム以外でも使うにはどうすれば良いかのコードですが、エディタ側でも同じような処理をしていると思われます。登録しているコマンドバッファの流れと該当のシェーダのパスのコードをざっと見てみると以下のような方法です。

  1. アウトライン描画用のバッファを用意してレンダーターゲットに指定
  2. デプスバッファはそのまま描画済みのものを使用(ここでは ZWrite Off する)
  3. まず遮蔽なし(ZTest Always)で選択対象のオブジェクトを描画(BA に書き込む)
  4. 次に遮蔽あり(ZTest LEqual)で _ObjectId を R に書き込む、また G に 1.0 を書き込む
  5. 3 で書いた ID を周囲ピクセルと比較し、違う ID を持っていたらオブジェクトの境界として記録(A を 0.0 にしておく)、これによりエッジ検出される
  6. 一時バッファを用意してブラーをかける(線を太く + アンチエイリアス
  7. バッファは次のようになっている => 下記画像参照
    • R: 選択したオブジェクトの遮蔽されていない場所がオブジェクト ID で塗られている
    • G: 選択したオブジェクトの遮蔽されていない場所が 1.0 で塗られている
    • B: 選択したオブジェクト全体が遮蔽を無視して 1.0 で塗られている
    • A: B と同じだが加えてオブジェクト境界が 0.0 になっている
  8. この RGBA 情報を使って選択効果を重畳
    • B の塗られたエリア(遮蔽無視した全体、ブラー分少し大きくなっている)を対象
    • GA をみて境界のみアルファを 1.0、他は薄くする
    • R が 0(遮蔽された領域)だったらアルファを 0.0 にする

f:id:hecomi:20200906150607p:plain

f:id:hecomi:20200906143508p:plain

と、言う具合のようです。ここではコマンドバッファでやっていましたが実際はシーンビューでは最初のパスのシェーダが使われるようです(2018.2.0b7 のリリースノートでユーザ定義できるアウトライン用のパスとして追加されたとの記述もあり、実際 SpeedTree など一部のビルトインシェーダで記述が見られます)。つまり、SceneSelectionPassLightMode が指定されたこのパスが実行されるわけですね。ではこのパスのコードをもう少し詳しく見てみましょう:

// Caution: The outline selection in the editor use the vertex shader/hull/domain shader of the first pass declare. So it should not be the meta pass.
Pass
{
    Name "SceneSelectionPass"
    Tags{ "LightMode" = "SceneSelectionPass" }

    Cull[_CullMode]

    ZWrite On

    HLSLPROGRAM

    #pragma only_renderers d3d11 ps4 xboxone vulkan metal switch

    //enable GPU instancing support
    #pragma multi_compile_instancing

    // Note: Require _ObjectId and _PassValue variables

    #define SHADERPASS SHADERPASS_DEPTH_ONLY
    #define SCENESELECTIONPASS // This will drive the output of the scene selection shader

    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Material.hlsl"
    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Unlit/Unlit.hlsl"
    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Unlit/ShaderPass/UnlitDepthPass.hlsl"
    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Unlit/UnlitData.hlsl"
    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/ShaderPass/ShaderPassDepthOnly.hlsl"

    #pragma vertex Vert
    #pragma fragment Frag

    #pragma editor_sync_compilation

    ENDHLSL
}

SCENESELECTIONPASS を定義した上で ShaderPassDepthOnly.hlsl を include しています。このコードの一部を見てみましょう。

...
void Frag( 
    PackedVaryingsToPS packedInput
#ifdef ...
    ...
#elif defined(SCENESELECTIONPASS)
    , out float4 outColor : SV_Target0
#endif
)
{
    ...
#ifdef ...
    ...
#elif defined(SCENESELECTIONPASS)
    outColor = float4(_ObjectId, _PassValue, 1.0, 1.0);
#endif
}

出力としては、float4(_ObjectId, _PassValue, 1.0, 1.0) を書き出しています。HDRP では Scene ビューでは _ObjectId を使ったオブジェクトの境界描画が効かない気がします(先述の 5. の内容)。下記のように複数オブジェクトを選択しても境界は消えた状態でエッジの描画がされます。

f:id:hecomi:20200906155837p:plain

_PassValue は明確な記述はどこにも見つからなかったですが、おそらく先程の 3. / 4. の内容を描画するのにこの同じパスを使いまわしており、その際に 3. のパス描画の際は 0.0 を、4. のパス描画の際は 1.0 を与えているものと思われます。

DepthForwardOnly

Render Graph を実装している HDRenderPipeline.RenderGraph.csRenderForwardOpaque() のコメントにて説明があります。基本的に Opqaue なオブジェクトは Deferred で描画されますが、ForwardOnly および DepthForwardOnly パスを利用することで Forward で Opaque なオブジェクトを描画することが出来ます。この際、Forward および DepthOnly パスは持ってはいけません(2 回描画されてしまう)。別記事でまとめますが、ForwardLit シェーダで Transparent 指定時に使われ、DepthOnly はデプスプリパスで使われています。デプスを必要としない Unlit なシェーダなら ForwardOnly だけ持っていれば良く、デプスも書く場合は DepthForwardOnly を追加する、という形です。

もう少しこれらのパスについて見ておきましょう。適当な Opaque な Unlit な球体を描画してみると、これらのパスは次の DepthPrepassDeferredForDecals および ForwardOpaque ステージで使われています。

f:id:hecomi:20200914234207p:plain

f:id:hecomi:20200914234414p:plain

Transparent な Unlit だと RenderSky よりも後に ForwardTransparent で描画されます。

f:id:hecomi:20200914234701p:plain

DepthPrepassDeferredForDecals はその名の通りデカールのためのデプスを書くステージで、Deferred ではデカールを使わない場合は必要ありません。HDRP やカメラの設定でオフに出来ます。Forward ではデプスプリパスとして使われるため、DepthPrepassForward が常に実行されます。

f:id:hecomi:20200915002117p:plain

docs.unity3d.com

ForwardOnly とセットで話しましたが、DepthForwardOnly に特化してコードをもう少し詳しく見ていきましょう。

頂点シェーダ

頂点・フラグメントシェーダ部分については先程の SceneSelectionPass 同様で ShaderPassDepthOnly.hlsl が使われます。まずは頂点シェーダから見てみましょう。

PackedVaryingsType Vert(AttributesMesh inputMesh)
{
    VaryingsType varyingsType;
    varyingsType.vmesh = VertMesh(inputMesh);
    return PackVaryingsType(varyingsType);
}

だいぶシンプルですね。AttributesMeshVaryingMesh.hlsl で次のように定義されています。

struct AttributesMesh
{
    float3 positionOS   : POSITION;
#ifdef ATTRIBUTES_NEED_NORMAL
    float3 normalOS     : NORMAL;
#endif
#ifdef ATTRIBUTES_NEED_TANGENT
    float4 tangentOS    : TANGENT;
#endif
#ifdef ATTRIBUTES_NEED_TEXCOORD0
    float2 uv0          : TEXCOORD0;
#endif
#ifdef ATTRIBUTES_NEED_TEXCOORD1
    float2 uv1          : TEXCOORD1;
#endif
#ifdef ATTRIBUTES_NEED_TEXCOORD2
    float2 uv2          : TEXCOORD2;
#endif
#ifdef ATTRIBUTES_NEED_TEXCOORD3
    float2 uv3          : TEXCOORD3;
#endif
#ifdef ATTRIBUTES_NEED_COLOR
    float4 color        : COLOR;
#endif
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

各パスの .hlsl 内で適宜これらの ATTRIBUTES_NEED_TECHCOORD* といったキーワードが define される形で使い回せる形になっています。

VaryingsTypeVertMesh.hlsl の中で define されていて、テッセレーションが効く場合はドメインシェーダへ渡すように VaryingsToDS、そうでない場合はピクセルシェーダに渡すように VaryingsToPS となります。今回は VaryingsToPS だけを見てみましょう。

struct VaryingsToPS
{
    VaryingsMeshToPS vmesh;
#ifdef VARYINGS_NEED_PASS
    VaryingsPassToPS vpass;
#endif
};

...入れ子の構造体になっています。更に追ってみましょう。VaryingsMeshToPsVaryingMesh.hlsl で定義されています。VarinysPassToPSMotionVectorVertexShaderCommon.hlsl で定義されていて、後述の MotionVector 用のパスだけで使われるので本項では省略します。

struct VaryingsMeshToPS
{
    float4 positionCS;
#ifdef VARYINGS_NEED_POSITION_WS
    float3 positionRWS;
#endif
#ifdef VARYINGS_NEED_TANGENT_TO_WORLD
    float3 normalWS;
    float4 tangentWS; 
#endif
#ifdef VARYINGS_NEED_TEXCOORD0
    float2 texCoord0;
#endif
#ifdef VARYINGS_NEED_TEXCOORD1
    float2 texCoord1;
#endif
#ifdef VARYINGS_NEED_TEXCOORD2
    float2 texCoord2;
#endif
#ifdef VARYINGS_NEED_TEXCOORD3
    float2 texCoord3;
#endif
#ifdef VARYINGS_NEED_COLOR
    float4 color;
#endif
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

ここも同じように各パス毎の設定で必要な変数が追加される形式になっており、使い回せる形になっています。使いまわし観点からもこういった設計は納得感ありますし、他にも Lit も Unlit も基本的には Uber シェーダになっているため必要な情報のみ使うよう #define によってメンバを切り替えられるようにもなっているのだと思います。また、これらはシェーダグラフからも利用されるため、シェーダグラフ上で必要なノードが追加されたら #define されてメンバが追加される、というような形にもなっているようです。シェーダグラフの説明については以下をご参照下さい。

tips.hecomi.com

さて、引数の説明だけで長くなってしまったのでコードの中身に戻りましょう。VertMesh()VertMesh.hlsl で次のように定義されています。説明はインラインで書いておきます。

VaryingsMeshType VertMesh(AttributesMesh input)
{
    VaryingsMeshType output;

    // インスタンシングの設定
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_TRANSFER_INSTANCE_ID(input, output);

    // メッシュの変形が適用される
    // 頂点アニメーションやカスタムパスなど
#if defined(HAVE_MESH_MODIFICATION)
    input = ApplyMeshModification(input, _TimeParameters.xyz);
#endif

    // positionWS はカメラ座標系における位置になったのでカメラ相対ワールド座標
    // という意味で positionRWS へと改名された(後述)
    // unity_ObjectToWorld は直接使わない。URP 同様 TransformObjectToWorld を使う
    float3 positionRWS = TransformObjectToWorld(input.positionOS);

    // 法線をワールド空間へ変換
#ifdef ATTRIBUTES_NEED_NORMAL
    float3 normalWS = TransformObjectToWorldNormal(input.normalOS);
#else
    // 法線を必要としない ApplyVertexModification 用に 0 を詰めておく
    float3 normalWS = float3(0.0, 0.0, 0.0);
#endif

    // ワールド空間の Tangent へ変換
#ifdef ATTRIBUTES_NEED_TANGENT
    float4 tangentWS = float4(TransformObjectToWorldDir(input.tangentOS.xyz), input.tangentOS.w);
#endif

    // 必要であればディスプレースメントなどの変形が適用される
#if defined(HAVE_VERTEX_MODIFICATION)
    ApplyVertexModification(input, normalWS, positionRWS, _TimeParameters.xyz);
#endif

    // 出力に位置・法線・接線を詰める
    // テッセレーションのためにキーワードの場合分けをしている
#ifdef TESSELLATION_ON
    output.positionRWS = positionRWS;
    output.normalWS = normalWS;
    #if defined(VARYINGS_NEED_TANGENT_TO_WORLD) || defined(VARYINGS_DS_NEED_TANGENT)
    output.tangentWS = tangentWS;
    #endif
#else
    #ifdef VARYINGS_NEED_POSITION_WS
    output.positionRWS = positionRWS;
    #endif
    output.positionCS = TransformWorldToHClip(positionRWS);
    #ifdef VARYINGS_NEED_TANGENT_TO_WORLD
    output.normalWS = normalWS;
    output.tangentWS = tangentWS;
    #endif
#endif

// 残りの情報を必要に応じて詰める
#if defined(VARYINGS_NEED_TEXCOORD0) || defined(VARYINGS_DS_NEED_TEXCOORD0)
    output.texCoord0 = input.uv0;
#endif
#if defined(VARYINGS_NEED_TEXCOORD1) || defined(VARYINGS_DS_NEED_TEXCOORD1)
    output.texCoord1 = input.uv1;
#endif
#if defined(VARYINGS_NEED_TEXCOORD2) || defined(VARYINGS_DS_NEED_TEXCOORD2)
    output.texCoord2 = input.uv2;
#endif
#if defined(VARYINGS_NEED_TEXCOORD3) || defined(VARYINGS_DS_NEED_TEXCOORD3)
    output.texCoord3 = input.uv3;
#endif
#if defined(VARYINGS_NEED_COLOR) || defined(VARYINGS_DS_NEED_COLOR)
    output.color = input.color;
#endif

    return output;
}

大体 URP の記事で見たのと同じ感じですが、一点、positionRWS という見慣れない変数名が出てきました。これは HDRP では Camera-relative Rendering というカメラローカルなワールド座標系で計算を行う手法を採用していることによります。従来のパイプラインのようにワールド座標系で描画を行うと、非常に遠い場所で float の精度が悪くなり、描画がガビガビになってしまいます。

f:id:hecomi:20200920150033p:plain

これを避けるために、カメラを原点とした座標系で描画の計算を行うことにより、ワールドの原点から遠い場所でも近い場所と同じような結果が得られるようになります。この関係で従来 positionWS としていた名前をカメラ相対ワールド座標(Relative World Spacec)の意味で positionRWS にしたようです。

さて、こうして得られた VaryingsToPS 構造体は PackVaryingsType() を通して PackedVaryingsType へと変換されフラグメントシェーダへ渡されます。これらも VaryingsType と同様、PackVaryingsToPs()PackedVaryingsToPs になるよう #define されています。

struct PackedVaryingsToPS
{
#ifdef VARYINGS_NEED_PASS
    // MotionVectors 用
    PackedVaryingsPassToPS vpass;
#endif
    PackedVaryingsMeshToPS vmesh;

    UNITY_VERTEX_OUTPUT_STEREO
};

VaryingsToPS 同様、構造体を内包する形になっています。PackedVaryingsPassToPS はモーションベクター用なので後述します。VaryingsMeshToPS は通常の構造体でしたが、PackedVaryingsMeshToPSSV_POSITION などのセマンティクス付きの構造体になります。また、XR のためのシングルパスインスタンシング用の ID を含む UNITY_VERTEX_OUTPUT_STEREO も定義されています。細かくコードは追いませんが、VaryingsToPS をパックするとフラグメントシェーダへ渡せる PackedVaryingsToPS になり、逆にフラグメントシェーダではこのパックされた構造体をアンパックして必要な情報を取り出すようになっています。ではフラグメントシェーダへ進みましょう。

フラグメントシェーダ

コードを見てみます。

void Frag(
    PackedVaryingsToPS packedInput
#if defined(SCENESELECTIONPASS)
    , out float4 outColor : SV_Target0
#else
    #ifdef WRITE_MSAA_DEPTH
    // Alphat to Coverage のために必要
    , out float4 depthColor : SV_Target0
        #ifdef WRITE_NORMAL_BUFFER
    , out float4 outNormalBuffer : SV_Target1
        #endif
    #else
        #ifdef WRITE_NORMAL_BUFFER
    , out float4 outNormalBuffer : SV_Target0
        #endif
    #endif
    #if defined(WRITE_DECAL_BUFFER) && !defined(_DISABLE_DECALS)
    // デカール用、SV_TARGET_DECAL は SV_Target0~2 になる
    , out float4 outDecalBuffer : SV_TARGET_DECAL
    #endif
#endif
#ifdef _DEPTHOFFSET_ON
, out float outputDepth : SV_Depth
#endif
)
{
    // XR 向けシングルパスステレオ用
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(packedInput);

    // パックされた情報を FragInputs へ変換
    FragInputs input = UnpackVaryingsMeshToFragInputs(packedInput.vmesh);

    // 位置情報を取り出す(ワールド座標やデプスなど)
    // input.positionSS が SV_Position
    PositionInputs posInput = GetPositionInput(
        input.positionSS.xy, 
        _ScreenSize.zw, 
        input.positionSS.z, 
        input.positionSS.w, 
        input.positionRWS);

    // ワールドスペースのビューの方向、中では Cemare-relative Rendering かどうか
    // Persepective か Orthographic かなどを場合分けして値を返してくれる
#ifdef VARYINGS_NEED_POSITION_WS
    float3 V = GetWorldSpaceNormalizeViewDir(input.positionRWS);
#else
    // 0 割を避けるためにセットしておく
    float3 V = float3(1.0, 1.0, 1.0);
#endif

    // SurfaceData / BuiltinData に情報を詰める
    SurfaceData surfaceData;
    BuiltinData builtinData;
    GetSurfaceAndBuiltinData(input, V, posInput, surfaceData, builtinData);

#ifdef _DEPTHOFFSET_ON
    outputDepth = posInput.deviceDepth;
#endif

#ifdef SCENESELECTIONPASS
    ... // 前の項で解説
#else
    // MSAA
    #ifdef WRITE_MSAA_DEPTH
    // デプスバッファから読むのは重いので positionCS.z の値を直接使う
    depthColor = packedInput.vmesh.positionCS.z;

    // アルファチャネルは Alpha to Coverage に使う
        #ifdef _ALPHATOMASK_ON
    depthColor.a = SharpenAlpha(builtinData.opacity, builtinData.alphaClipTreshold);
        #endif
    #endif

    // 法線のエンコード
    #if defined(WRITE_NORMAL_BUFFER)
    EncodeIntoNormalBuffer(ConvertSurfaceDataToNormalData(surfaceData), outNormalBuffer);
    #endif

    // デカール用の出力
    #if defined(WRITE_DECAL_BUFFER) && !defined(_DISABLE_DECALS)
    DecalPrepassData decalPrepassData;
    decalPrepassData.geomNormalWS = surfaceData.geomNormalWS;
    decalPrepassData.decalLayerMask = GetMeshRenderingDecalLayer();
    EncodeIntoDecalPrepassBuffer(decalPrepassData, outDecalBuffer);
    #endif
#endif
}

おおまかな流れとしては、先程見ました頂点シェーダからの情報(PackedVaryingsToPS)を UnpackVaryingsMeshToFragInputs() で展開し、GetSurfaceAndBuiltinData() でそのパス特有で必要なパラメタを格納した SurfaceData とパスに依存しない(= BSDF に関係のない)共通のパラメタを集めた BuiltinData へ情報を詰めてもらい、これをレンダリングの設定によって異なる要求された out 属性のついたパラメタに値を格納していくという流れです。メインの GetSurfaceAndBuiltinData() について見ていく前に、いくつか補足しておきます。

まずは MSAA についてです。HDRP では MSAA の設定は HDRP のアセットから行います。

f:id:hecomi:20200921002119p:plain

MSAA を利用するにはレンダリングの種類(Lit Shader Mode)を Forward Only または Both にする必要があります。その上で Multisample Anti-aliasing Quality を選択することで有効にできます。

docs.unity3d.com

ただし、MSAA を利用すると、SSR や SSAO といったスクリーンスペース系のエフェクトが使えなくなったりといった制約があります。MSAA は Deferred では使えない(Deferred では別の AA が選択される)のですが、レンダリングコスト的な観点で VR では Forward x MSAA の構成がよく使われているようです。

そして MSAA を ON にしているときの利点の一つに、Alpha Test によるジャギーを低減してくれる Alpha to Coverage が利用できる点が挙げられます。Unity だと AlphaToMask On を ShaderLab 内に記述すると使えるようになります。

tsumikiseisaku.com

ON のときは _ALPHATOMASK_ON が定義されそのパスを通るようになります。ただそのままではクオリティ面でのいくつかの問題があり、それを避けるために SharpenAlpha() という処理を行っています。これについては以下の記事で解説されています。

medium.com

WRITE_NORMAL_BUFFER はその名の通り法線情報を出力するかに関係のあるキーワードです。このキーワードは Forward 時に適用され、Frame Debugger でも Lit Shader Type を Forward Only に変更してキーワードを見てみると指定されているのが確認できます。

では、GetSurfaceAndBuiltinData() を見てみましょう。DepthForwardOnly パスではアルファテストおよび色やカットオフパラメタの書き出しがメインとなります。

void GetSurfaceAndBuiltinData(
    FragInputs input, 
    float3 V, 
    inout PositionInputs posInput, 
    out SurfaceData surfaceData, 
    out BuiltinData builtinData 
    RAY_TRACING_OPTIONAL_PARAMETERS)
{
    // _UnlitColorMap から RGB を取り出す
    float2 unlitColorMapUv = TRANSFORM_TEX(input.texCoord0.xy, _UnlitColorMap);
    surfaceData.color = SAMPLE_TEXTURE2D(
        _UnlitColorMap, 
        sampler_UnlitColorMap, 
        unlitColorMapUv).rgb 
        * _UnlitColor.rgb;

    // _UnlitColorMap から alpha を取り出す
    float alpha = SAMPLE_TEXTURE2D(_UnlitColorMap, sampler_UnlitColorMap, unlitColorMapUv).a * _UnlitColor.a;

    // アルファテストを行う
#ifdef _ALPHATEST_ON
    GENERIC_ALPHA_TEST(alpha, _AlphaCutoff);
#endif

    ZERO_INITIALIZE(BuiltinData, builtinData);
    builtinData.opacity = alpha;
    
    // Alpha to Coverage の Sharpen() で使われる
#ifdef _ALPHATEST_ON
    builtinData.alphaClipTreshold = _AlphaCutoff;
#endif

    ...
}

このパスでは _UnlitcColorMap(カラーに割り当てられたテクスチャ)からアルファを取り出し、BuiltinDataopacityalphaClipThreshold を詰め、またアルファテストを行う形になっています。これで DepthForwardOnly はだいたい一通り見終わりました。

MotionVectors

さて、MotionVectors はモーションブラー用のパスです(実際はモーションブラー以外にも TAA などのポストエフェクトで使われます)。モーションブラー用には 3 種類ステージがあり、1つ目がこの MotionVectors パスで、計算されるオブジェクトそれぞれでピクセルごとの移動を記録します。2 つ目はカメラの移動によって生じる移動を記録したもので、デプスとカメラの動きから算出できます。最後の 3 つ目はポストエフェクトで画面にモーションブラーを適用するものになります。ちなみに URP にはカメラの移動によるモーションブラーしかなかったのでこの MotionVectors パスはありませんでした。

MotionVectors パスは移動するオブジェクトのみに対して実行されます。また、その場合は DepthPrepass ではデプスが書き込まれず本パスの実行結果によってデプス(+ 必要なら法線)が書き込まれる挙動をするようです。

f:id:hecomi:20200927002947g:plain

また MotionVectors を適用するかどうかは MeshRenderer のインスペクタからも設定できます。

f:id:hecomi:20200927143430p:plain

このあたりはレガシーなパイプラインでもありましたので、流れとしては以前行われていた処理と同じなのではないかと思います。コードを見ていきましょう。

頂点シェーダ

PackedVaryingsType Vert(AttributesMesh inputMesh,
                        AttributesPass inputPass)
{
    VaryingsType varyingsType;
    varyingsType.vmesh = VertMesh(inputMesh);

    return MotionVectorVS(varyingsType, inputMesh, inputPass);
}

AttributeMeshVerteMesh は先程のデプスのパスと同じですが、AttributePassMotionVectorVS() が追加されています。これらはいずれも MotionVectorVertexShaderCommon.hlsl にて定義されています。AttributePass については以下のようになっています。

struct AttributesPass
{
    float3 previousPositionOS : TEXCOORD4;
#if defined (_ADD_PRECOMPUTED_VELOCITY)
    float3 precomputedVelocity : TEXCOORD5;
#endif
};

MotionVectors パスでは AttributeMesh に加えてこれらの情報が追加で渡されてやってきます。previousPositionOS は 1 つ前のフレームのオブジェクト空間位置がやってきます。スキニングされたオブジェクトで適用され、アニメーションしていても頂点単位で速度が求められます。また、precomputedVelocity_ADD_PRECOMUPTED_VELOCITY キーワードが指定されているときにやってくるようです。このキーワードはインスペクタで Add Precomputed Velocity にチェックをすると有効化されますが、Alembic にて使われるので通常は無視して良さそうです。

VaryingsTypes は今度は VARYINGS_NEED_PASS が有効化されているので、VaryingsPassToPS が含まれるようになります。

struct VaryingsToPS
{
    VaryingsMeshToPS vmesh;
#ifdef VARYINGS_NEED_PASS
    VaryingsPassToPS vpass;
#endif
};

struct VaryingsPassToPS
{
    float4 positionCS;
    float4 previousPositionCS;
};

VaryingsMeshToPS にも positionCS が含まれていますが、VaryingsMeshToPS の方は SV_POSITION セマンティクスのついた変数で、こちらは TEXCOORD8 へとパックされます。SV_POSITION の方はスクリーンスペース座標の計算で使われます。vpass 側は MotionVectorVS() の中を見てみると保存している理由がわかります。

void MotionVectorPositionZBias(VaryingsToPS input)
{
    // バイアスを与える
#if defined(UNITY_REVERSED_Z)
    input.vmesh.positionCS.z -= unity_MotionVectorsParams.z * input.vmesh.positionCS.w;
#else
    input.vmesh.positionCS.z += unity_MotionVectorsParams.z * input.vmesh.positionCS.w;
#endif
}

PackedVaryingsType MotionVectorVS(
    inout VaryingsType varyingsType, 
    AttributesMesh inputMesh, 
    AttributesPass inputPass)
{
#if !defined(TESSELLATION_ON)
    MotionVectorPositionZBias(varyingsType);
#endif

    // 現在のクリップ空間座標を求める
    varyingsType.vpass.positionCS = mul(
        UNITY_MATRIX_UNJITTERED_VP, 
        float4(varyingsType.vmesh.positionRWS, 1.0));

    bool forceNoMotion = unity_MotionVectorsParams.y == 0.0;
    if (forceNoMotion)
    {
        varyingsType.vpass.previousPositionCS = float4(0.0, 0.0, 0.0, 1.0);
    }
    else
    {
        // スキニングされたオブジェクトで適用
        bool hasDeformation = unity_MotionVectorsParams.x > 0.0;

        // スキニングされたオブジェクトは渡ってきた previousPositionOS を、
        // そうでない場合は現在位置を指定
        float3 effectivePositionOS = hasDeformation ? 
            inputPass.previousPositionOS : 
            inputMesh.positionOS;

        // インスペクタ上で AddPrecomptedVelocity をチェックすると走る
        // Alembic 用
#if defined(_ADD_PRECOMPUTED_VELOCITY)
        effectivePositionOS -= inputPass.precomputedVelocity;
#endif

        // 前のフレームの頂点のワールド座標を求める
        // 頂点アニメーションを持っている場合は前の時間を使って変形
#if defined(HAVE_MESH_MODIFICATION)
        AttributesMesh previousMesh = inputMesh;
        previousMesh.positionOS = effectivePositionOS ;
        previousMesh = ApplyMeshModification(previousMesh, _LastTimeParameters.xyz);
        float3 previousPositionRWS = TransformPreviousObjectToWorld(previousMesh.positionOS);
#else
        float3 previousPositionRWS = TransformPreviousObjectToWorld(effectivePositionOS);
#endif

        // ディスプレースメント用の法線計算
#ifdef ATTRIBUTES_NEED_NORMAL
        float3 normalWS = TransformPreviousObjectToWorldNormal(inputMesh.normalOS);
#else
        float3 normalWS = float3(0.0, 0.0, 0.0);
#endif

        // 前フレームのディスプレースメントを計算
#if defined(HAVE_VERTEX_MODIFICATION)
        ApplyVertexModification(
            inputMesh, 
            normalWS, 
            previousPositionRWS, 
            _LastTimeParameters.xyz);
#endif

        // 半透過時にカメラのみフラグが指定されていたら現在のメッシュの値で上書き
#ifdef _WRITE_TRANSPARENT_MOTION_VECTOR
        if (_TransparentCameraOnlyMotionVectors > 0)
        {
            previousPositionRWS = varyingsType.vmesh.positionRWS.xyz;
        }
#endif

        // 前フレームの VP をかけて前フレームのクリップ空間座標を求める
        varyingsType.vpass.previousPositionCS = mul(
            UNITY_MATRIX_PREV_VP, 
            float4(previousPositionRWS, 1.0));
    }

    // パックして終わり
    return PackVaryingsType(varyingsType);
}

UNITY_MATRIX_VP は TAA が適用されているときはプロジェクション行列にジッターが適用されています(少しずつずらして時間的に平均を取りアンチエイリアスをするため)。一方 UNITY_MATRIX_UNJITTERED_VP はこのジッター成分を除外した本来のプロジェクション行列を適用したビュー・プロジェクション行列になります(このあたりは HDCamera.cs に書いてあります)。vpass 側の positionCS はこのプロジェクション行列を適用したクリップ空間座標が保存されている形になっています。

また、unity_MotionVectorsParams について補足しておきます。この外から与えられる定数には次のような値が入っています。

  • X: 前フレーム位置(previousPositionOS を使うか)
  • Y: Force No Motion か
  • Z: Z バイアス値

フラグメントシェーダ

void Frag(
    PackedVaryingsToPS packedInput
#ifdef WRITE_MSAA_DEPTH
    , out float4 depthColor : SV_Target0
    , out float4 outMotionVector : SV_Target1
    #ifdef WRITE_DECAL_BUFFER
    , out float4 outDecalBuffer : SV_Target2
    #endif
#else
    , out float4 outMotionVector : SV_Target0
    #ifdef WRITE_DECAL_BUFFER
    , out float4 outDecalBuffer : SV_Target1
    #endif
#endif
#ifdef WRITE_NORMAL_BUFFER
    , out float4 outNormalBuffer : SV_TARGET_NORMAL
#endif
#ifdef _DEPTHOFFSET_ON
    , out float outputDepth : SV_Depth
#endif
)
{
    FragInputs input = UnpackVaryingsMeshToFragInputs(packedInput.vmesh);

    PositionInputs posInput = GetPositionInput(
        input.positionSS.xy, 
        _ScreenSize.zw, 
        input.positionSS.z, 
        input.positionSS.w, 
        input.positionRWS);

#ifdef VARYINGS_NEED_POSITION_WS
    float3 V = GetWorldSpaceNormalizeViewDir(input.positionRWS);
#else
    float3 V = float3(1.0, 1.0, 1.0); // 0 割を避ける
#endif

    SurfaceData surfaceData;
    BuiltinData builtinData;
    GetSurfaceAndBuiltinData(input, V, posInput, surfaceData, builtinData);

    // パスの情報を取り出す
    // マテリアルからデプスのオフセットが与えられていたらそれを足す
    VaryingsPassToPS inputPass = UnpackVaryingsPassToPS(packedInput.vpass);
#ifdef _DEPTHOFFSET_ON
    inputPass.positionCS.w += builtinData.depthOffset;
    inputPass.previousPositionCS.w += builtinData.depthOffset;
#endif

    // モーションベクターを計算!
    float2 motionVector = CalculateMotionVector(
        inputPass.positionCS, 
        inputPass.previousPositionCS);

    // クリップ座標(-1 ~ 1)を正規化デバイス座標(0 ~ 1)へ変換
    // (pos1 * 0.5 + 0.5) - (pos0 * 0.5 + 0.5) = motionVector * 0.5
    EncodeMotionVector(motionVector * 0.5, outMotionVector);

    // ForceNoMotion の場合は 2.0 をセットしておくと
    // 正規化デバイス座標では生じない値だと分かる
    bool forceNoMotion = unity_MotionVectorsParams.y == 0.0;
    if (forceNoMotion)
    {
        outMotionVector = float4(2.0, 0.0, 0.0, 0.0);
    }

    // Alpha to Coverage 用
#ifdef WRITE_MSAA_DEPTH
    depthColor = packedInput.vmesh.positionCS.z;
    #ifdef _ALPHATOMASK_ON
    depthColor.a = SharpenAlpha(builtinData.opacity, builtinData.alphaClipTreshold);
    #endif
#endif

    // 法線の出力
#ifdef WRITE_NORMAL_BUFFER
    EncodeIntoNormalBuffer(ConvertSurfaceDataToNormalData(surfaceData), outNormalBuffer);
#endif

    // デカール用の出力
#if defined(WRITE_DECAL_BUFFER)
    DecalPrepassData decalPrepassData;
    #ifdef _DISABLE_DECALS
    ZERO_INITIALIZE(DecalPrepassData, decalPrepassData);
    #else
    decalPrepassData.geomNormalWS = surfaceData.geomNormalWS;
    decalPrepassData.decalLayerMask = GetMeshRenderingDecalLayer();
    #endif
    EncodeIntoDecalPrepassBuffer(decalPrepassData, outDecalBuffer);
#endif

    // デプスの上書き
#ifdef _DEPTHOFFSET_ON
    outputDepth = posInput.deviceDepth;
#endif
}

GetSurfaceAndBuiltinData() まではおおよそ先程のデプスパスと同じです。モーションベクターの計算としては、現在のクリップ座標と 1 つまえのそれを CalculateMotionVector() に渡して行っており、この出力結果をエンコードして outMotionVector に出力しています。CalculateMotionVector() を見ていきましょう。BuiltinUtilities.hlsl に定義があります:

float2 CalculateMotionVector(float4 positionCS, float4 previousPositionCS)
{
#if (SHADERPASS == SHADERPASS_MOTION_VECTORS) || defined(_WRITE_TRANSPARENT_MOTION_VECTOR)
    positionCS.xy = positionCS.xy / positionCS.w;
    previousPositionCS.xy = previousPositionCS.xy / previousPositionCS.w;
    float2 motionVec = (positionCS.xy - previousPositionCS.xy);
#if UNITY_UV_STARTS_AT_TOP
    motionVec.y = -motionVec.y;
#endif
    return motionVec;
#else
    return float2(0.0, 0.0);
#endif
}

クリップ空間座標を w で割って正規化デバイス座標(-1 ~ +1)へと変換しています。その上で前フレームとの差分を取って移動量を計算している形ですね。こうして計算した値を EncodeMotionVector() へと渡します。コメントにも書きましたが、このとき 0.5 倍した値を渡しています。これは正規化デバイス座標は -1 ~ +1 の値ですが、0.5 をかけることで 0 ~ 1 の値へと変換できるからです。BuiltinData.hlsl を見てみます:

void EncodeMotionVector(float2 motionVector, out float4 outBuffer)
{
    outBuffer = float4(motionVector.xy, 0.0, 0.0);
}

単に outBufferfloat4 変換して出力しているだけです。モーションベクターのレンダーターゲットは float x 2(16bit, 16bit)なのでこのような形になっています。こうした値を出力しておくと、後のポストプロセスでモーションベクターや TAA などで活用されます。

おわりに

今回はここまでとしますが、長い道のりになりそうです。。次は ForwardOnly、ShadowCaster、DistortionVectors 周りを見ていきます。

参考

nanka.hateblo.jp