_CameraNormalsTextureにNormalMapを適用する


追記

最新のUnity2021.2.9f(UniversalRP 12.1.4)にて、標準のDepthNormalsPassでNormalMapの適用がなされるようになっていることが確認できました。
本記事は役目を終えたものと考えられますが、ログとして残しておきます。

はじめに

先日UniversalRPでNormalMap(法線マップ)を入れたモデルを表示して、URPのSSAOの具合を確認しようとしました。
モデルにUniversalRenderPipeline/Litシェーダを充てたマテリアルを付けて、SSAORendererFeatureを設定して描画。

うーん、確かに地面とオブジェクトの接地辺りに影が付くようになったけど、何か効果が弱いような…?


SourceもDepth Normalsにしたんだから、もっと法線が向き合う溝の辺りとかも暗くなっても良いような…??

という事で、色々確認した結果、_CameraNormalsTextureにはNormalMapの法線が入っていないことが分かりました。
_CameraNormalsTextureをそのまま表示した結果はこちら↓

_CameraNormalsTextureを有効化する方法は以前の記事を見てもらうとして、

何とかNormalMapを適用した法線を_CameraNormalsTextureに書き込めないかな??と調べたところ下記の記載を発見しました。

DepthNormals Pass
Starting from version 10.0.x, URP can generate a normal texture called _CameraNormalsTexture. To render to this texture in your custom shader, add a Pass with the name DepthNormals. For example, see the implementation in Lit.shader.

DepthNormalsパスの描画で_CameraNormalsTextureが描画されるとの事。
という事でLit.shaderをベースに、DepthNormalsパスでNormalMapを反映した法線を書き込んでみます。

Lit.shaderの内部を確認する

作業環境は下記です

  • Unity 2020.3.1f1
  • Universal RP 10.3.2

ProjectツリーでPackages/com.unity.render-pipelines.universal/Shaders/Lit.shaderを開いてDepthNormalsパスを見てみます。

Lit.shaderのDepthNormalsパスのみ抜粋
        // This pass is used when drawing to a _CameraNormalsTexture texture
        Pass
        {
            Name "DepthNormals"
            Tags{"LightMode" = "DepthNormals"}

            ZWrite On
            Cull[_Cull]

            HLSLPROGRAM
            #pragma exclude_renderers gles gles3 glcore
            #pragma target 4.5

            #pragma vertex DepthNormalsVertex
            #pragma fragment DepthNormalsFragment

            // -------------------------------------
            // Material Keywords
            #pragma shader_feature_local _NORMALMAP
            #pragma shader_feature_local_fragment _ALPHATEST_ON
            #pragma shader_feature_local_fragment _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A

            //--------------------------------------
            // GPU Instancing
            #pragma multi_compile_instancing
            #pragma multi_compile _ DOTS_INSTANCING_ON

            #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthNormalsPass.hlsl"
            ENDHLSL
        }

DepthNormalsPass.hlsl内のシェーダを見てみると、

DepthNormalsPass.hlslの一部
Varyings DepthNormalsVertex(Attributes input)
{
    Varyings output = (Varyings)0;
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

    output.uv         = TRANSFORM_TEX(input.texcoord, _BaseMap);
    output.positionCS = TransformObjectToHClip(input.positionOS.xyz);

    VertexNormalInputs normalInput = GetVertexNormalInputs(input.normal, input.tangentOS);
    output.normalWS = NormalizeNormalPerVertex(normalInput.normalWS);

    return output;
}

float4 DepthNormalsFragment(Varyings input) : SV_TARGET
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

    Alpha(SampleAlbedoAlpha(input.uv, TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap)).a, _BaseColor, _Cutoff);
                                                       //注目すべきはここ!↓
    return float4(PackNormalOctRectEncode(TransformWorldToViewDir(input.normalWS, true)), 0.0, 0.0);
}

確かに、input.normalWS(頂点のWorld空間法線)をTransformWorldToViewDir()でView空間に変形して、PackNormalOctRectEncode()でRGに落とし込んでいるだけっぽい??

自前のカスタムLit.shaderを作り、NormalMapを適用した法線情報を書きだす

Lit.shaderをコピーして自前のCustomLit.shaderを作成しましょう。
そしてForwardLitパスの初期化周りを参考に、CustomLit.shaderのDepthNormalsパスを以下のように書き換えて確認。

CustomLit.shaderのDepthNormalsパスのみ抜粋
        // This pass is used when drawing to a _CameraNormalsTexture texture
        Pass
        {
            Name "DepthNormals"
            Tags{"LightMode" = "DepthNormals"}

            ZWrite On
            Cull[_Cull]

            HLSLPROGRAM
            #pragma exclude_renderers gles gles3 glcore
            #pragma target 4.5

            #pragma vertex DepthNormalsVertex
            #pragma fragment DepthNormalsFragment

            // -------------------------------------
            // Material Keywords
            #pragma shader_feature_local _NORMALMAP
            #pragma shader_feature_local_fragment _ALPHATEST_ON
            #pragma shader_feature_local_fragment _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A

            //--------------------------------------
            // GPU Instancing
            #pragma multi_compile_instancing
            #pragma multi_compile _ DOTS_INSTANCING_ON
            #define _NORMALMAP

            #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/LitForwardPass.hlsl"

            Varyings DepthNormalsVertex(Attributes input)
            {
                Varyings output = (Varyings)0;
                UNITY_SETUP_INSTANCE_ID(input);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

                output.uv         = TRANSFORM_TEX(input.texcoord, _BaseMap);
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);

                VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS);
                output.normalWS = NormalizeNormalPerVertex(normalInput.normalWS);

                //LitPassVertexから持ってきた
#if defined(REQUIRES_WORLD_SPACE_TANGENT_INTERPOLATOR) || defined(REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR)
                real sign = input.tangentOS.w * GetOddNegativeScale();
                half4 tangentWS = half4(normalInput.tangentWS.xyz, sign);
#endif
#if defined(REQUIRES_WORLD_SPACE_TANGENT_INTERPOLATOR)
                output.tangentWS = tangentWS;
#endif

                return output;
            }

            float4 DepthNormalsFragment(Varyings input) : SV_TARGET
            {
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

                //LitPassFragmentから持ってきた
                SurfaceData surfaceData;
                InitializeStandardLitSurfaceData(input.uv, surfaceData);
                InputData inputData;
                InitializeInputData(input, surfaceData.normalTS, inputData);

                Alpha(SampleAlbedoAlpha(input.uv, TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap)).a, _BaseColor, _Cutoff);
                //input.normalWSからinputData.normalWSに変更
                return float4(PackNormalOctRectEncode(TransformWorldToViewDir(inputData.normalWS, true)), 0.0, 0.0);
            }
            ENDHLSL
        }

ForwardLitパス内でNormalMapの計算はなされているので、そこから必要な部分のみを移植しました。

ポイントは

  • #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthNormalsPass.hlsl" を止めて、自分でDepthNormalsVertex()とDepthNormalsFragment()を定義
  • ForwardLitパスと合わせるために、Core.hlsl、LitForwardPass.hlslをincludeする
  • DepthNormalsVertex()は、基本はDepthNormalsPass.hlslのDepthNormalsVertex()と合わせつつ、Tangentの取得部分のみ、ForwardLitパスのLitPassVertexから持ってきた
  • DepthNormalsFragment()も基本はDepthNormalsPass.hlslのDepthNormalsFragment()と合わせつつ、input.normalWSではなくinputData.normalWSに切り替える

辺りでしょうか。

という事で自前のCustomLit.shaderを充てたマテリアルをモデルに設定し、_CameraNormalsTextureを表示した結果がこちら。

NormalMapが反映されたことが確認できるかと思います。

 まとめ

並べてみた結果がこちら。

↓対応後

ちゃんと溝の内側もしっかり暗くなって、SSAOの効きがより明確になったかと思います。

UniversalRPは日々更新されているので、今後はLit.shader側で対応されるかもしれません(というかフラグか何かで切り替わるようにして欲しい…)が、
現状はこの方法以外で_CameraNormalsTextureにNormalMapを書き込む方法が見つけられなかったので記事化しておきます。

参考資料