SPARKCREATIVE Tech Blog

https://www.spark-creative.jp/

シェーダーをUniversal Render Pipelineに対応させたかった覚書

初めまして、SPARKCREATIVEクライアントエンジニアの中島です。 先に投稿された方達が予想していたよりも趣味に走ったことを書いているなあと思いながらの初投稿です。

さて、SPARKCREATIVEではSPARKGEARだけに限らない開発のお手伝いも承っています。 そのような中で最近何度かUnityのバージョンも結構上がったので、そろそろバージョンを上げておきましょうという場面に遭遇しました。

それらのタイトルではグラフィックス周りを担当していたのですが、プロジェクトで使っていたLightweight Render Pipelineはいつの間にやらUniversal Render Pipeline(以下URPと略します)へと変わっていて、移行する作業が必要になりました。 そしてついでにシェーダーもScriptable Render Pipelineに最適な形にしましょう、となったわけです。

今回はその覚書のようなものを書こうと思います。

デフォルトのUnlitをURP対応にする

URPのシェーダーのテンプレートがあれば良かったのですが、残念ながら用意されていないようです。 そこで例としてAssetメニューから作成できるお馴染みのUnlitのテンプレートをURPに対応した形にしていきたいと思います。 また詰め込みすぎると大変なので標準のForwardRendererかそれに近いものを使うことを前提とします。

作成直後の全貌はこのようになっています。

// デフォルトのUnlitテンプレート
Shader "Unlit/MyUnlit"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

まずやるべきことはSubShaderPassTagsへの項目の追加です。

SubShaderTagsに対応するパイプラインとして"RenderPipeline"="UniversalPipeline"を追加します。 必須ではないのですが、これがあると指定したパイプライン以外では描画されなくなります。 よそからシェーダーを持ってきてみたらなぜか動かない、などという時にこの指定が違っていたりする可能性もあるので頭の片隅に置いておきましょう。

またPassTags { "LightMode"="UniversalForward" }を追加してレンダリングされるパスを指定します。 UniversalForwardは通常のオブジェクトが描画されるパスです。 これも指定しなくても描画はされるのですが、カスタマイズしてフィーチャーを追加した時などに意図しないところで描かれないためにもつけておくと安心です。

ついでにName "適当な名前"もつけておくとFrame Debuggerで見たときにどのPassを使って描画されたのかわかりやすくなります。

    ...
    SubShader
    {
        Tags {
            "RenderType"="Opaque"
            "RenderPipeline"="UniversalPipeline"      // <-
        }
        ...
        Pass
        {
            Name "ForwardLit"                         // <-
            Tags { "LightMode"="UniversalForward" }   // <-
            
            CGPROGRAM
            ...

// <-を付けてある行が変更箇所です。

この段階でシェーダーのインスペクタを確認してみるとSRP Batcherという項目に何やらメッセージが出ていると思います。

f:id:spark-nakajima-satoru:20210112145814p:plain

SRP Batcher自体についてはUnityのマニュアル機能紹介のブログ記事をご参照ください。

これを消すのは簡単で、Propertiesにあるテクスチャ以外のパラメーターをUnityPerMaterialというcbufferにまとめるだけです。cbufferの開始と終了のマクロがあるのでそれで挟みます。 暗黙的に定義されるテクスチャ_STも忘れずにまとめましょう。

            ...
            sampler2D _MainTex;

            CBUFFER_START(UnityPerMaterial) // <-
            float4 _MainTex_ST;
            CBUFFER_END                     // <-

SRP Batcherの項目が "compatible" になりメッセージが消えればOKです。 もしならなかったら考えられることは3つです。

  • どこかが間違っている
  • Windows以外の環境で開発している
  • WindowsDirectX以外の描画APIを選択している

はいもうお分かりいただけると思いますが、なんとこの状態ではWindowsDirectX環境以外ではSRP Batcherに対応できていないのです。

ビルドインのシェーダーの中を探してみるとcbufferの定義が無効になってしまうプラットフォームが多数あることがわかります。 有効にするためにはCGPROGRAMの中に#pragma enable_cbufferを書かなくてはいけません。 これは2019.3から追加されたそうで、Passが1つしかないような簡単なシェーダーならこれでも良いかもしれません。

一方でURPのシェーダーを見てみると全てHLSLで書かれています。 つまりURPが定義している関数などを使いまわしたかったらHLSLにしておくべきだということです。*1 具体的に何かあるかといえば思い当たるのはシャドウ関連でしょうか。

それからこれは私も最近知ったことなのですが、UnityのマニュアルのShading language used in Unityの記述の最初のあたりを適当に省略しつつ翻訳して引用します。

Unityでは、HLSLプログラミング言語を使用してシェーダープログラムを作成します。

Unityは元々Cg言語を使用していたため、... UnityはCgを使用しなくなりました、しかし...

というわけでおとなしくHLSLにしてしまいましょう。CGPROGRAM ... ENDCGHLSLPROGRAM ... ENDHLSLに変更します。それから#include "UnityCG.cginc"も削除してしまいます。

            ...
            HLSLPROGRAM                 // <-
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            // #include "UnityCG.cginc" // <-
            ...
            ENDHLSL                     // <-
            ...

変更するとコンパイルが通らないと思います。 UnityCG.cgincが定義していたものが無くなっているので当然です。 代わりになるものは com.unity.render-pipelines.universal と com.unity.render-pipelines.core パッケージの中にあります。

パッケージからのインクルードは#include "Packages/パッケージ名/ディレクトリ/ファイル"のように書きます。 よく使いそうなものは下のようなものでしょうか。

内容 ファイル
基本 Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl
座標変換 Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransform.hlsl *2
フォグ Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl
シャドウ Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl

座標変換の関数はUnityObjectToClipPos()の代わりがTransformObjectToHClip()というように、微妙に名前が違うので面倒ですが基本的には単純に置き換えていけば大丈夫なはずです。 また頂点シェーダーからピクセルシェーダーへ渡すフォグやシャドウのパラメーターは以前のようなマクロではなく、自分で定義して受け渡す形になるようです。

それからテクスチャの定義もマクロで行うのが正統派のようです。TEXTURE2D(テクスチャ名)SAMPLER(サンプラー名)を合わせて定義します。 サンプリングもSAMPLE_TEXTURE2D(テクスチャ, サンプラー, UV)マクロを使います。もちろん形式の違うTEXTURE2D_HALF()TEXTURE2D_SHADOW()なども用意されています。

            ...
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"  // <-
            ...
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float fogFactor: TEXCOORD1;     // <-
                float4 vertex : SV_POSITION;
            };

            TEXTURE2D(_MainTex);                // <-
            SAMPLER(sampler_MainTex);           // <-
            ...
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex);    // <-
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.fogFactor = ComputeFogFactor(o.vertex.z);     // <-
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                // sample the texture
                float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv); // <-
                // apply fog
                col.rgb = MixFog(col.rgb, i.fogFactor); // <-
                return col;
            }
            ENDHLSL
            ...

これで元のUnlitと同等の機能でSRP Bacherもcompatibleになりました。

シャドウ

せっかくなのでシャドウもつけてみましょう。 メインライトのシャドウを受けるだけならまずワールド空間の位置を用意してTransformWorldToShadowCoord(worlsSpaceCoord)でシャドウ空間の位置を計算します。 それを使うとGetMainLight(shadowCoord)Lightという構造体が取得でき、その中に影の強さshadowAttenuationがあります。

ただし以下のmulti_compileのpragmaを定義しておかなくてはいけません。

// Universal Pipeline shadow keywords
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
#pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS
#pragma multi_compile _ _SHADOWS_SOFT

パイプラインの設定や使っているライトの種類から必要なものを選びましょう。

それから以前は"LightMode"="DepthOnly"Passを定義しないとシャドウが受けられませんでしたが、URPでは基本的にスクリーンスペースでのシャドウの解決はやめたらしいので不要です。 余談ですがこれによってTransparentでもシャドウを受けることができるようになりました。

またシャドウを落としたかったら"LightMode"="ShadowPass"Passが必要です。Universal/Litを参考にPackages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlslを少し直せば問題ないと思います。

以上を組み合わせると最終的にこのくらいになります。

Shader "Unlit/MyUnlit"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }

    SubShader
    {
        Tags {
            "RenderType"="Opaque"
            "RenderPipeline"="UniversalPipeline"
        }
        LOD 100
        
        // 各Passでcbufferが変わらないようにここに定義する
        HLSLINCLUDE
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

        TEXTURE2D(_MainTex);
        SAMPLER(sampler_MainTex);

        CBUFFER_START(UnityPerMaterial)
        float4 _MainTex_ST;
        CBUFFER_END
        ENDHLSL
        
        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForward" }

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
            
            // Universal Pipeline shadow keywords
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
            #pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS
            #pragma multi_compile _ _SHADOWS_SOFT

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float fogFactor: TEXCOORD1;
                float3 posWS : TEXCOORD2;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.fogFactor = ComputeFogFactor(o.vertex.z);
                o.posWS = TransformObjectToWorld(v.vertex.xyz);
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
                
                float4 shadowCoord = TransformWorldToShadowCoord(i.posWS);
                Light mainLight = GetMainLight(shadowCoord);
                half shadow = mainLight.shadowAttenuation;
                Light addLight0 = GetAdditionalLight(0, i.posWS);
                shadow *= addLight0.shadowAttenuation;
                col.rgb *= shadow;
                
                col.rgb = MixFog(col.rgb, i.fogFactor);
                return col;
            }
            ENDHLSL
        }
    
        Pass
        {
            Tags { "LightMode"="ShadowCaster" }

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #pragma multi_compile_instancing
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            // ShadowsCasterPass.hlsl に定義されているグローバルな変数
            float3 _LightDirection;
            
            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f {
                float4 pos : SV_POSITION;
            };

            v2f vert(appdata v)
            {
                UNITY_SETUP_INSTANCE_ID(v);
                v2f o;
                // ShadowsCasterPass.hlsl の GetShadowPositionHClip() を参考に
                float3 positionWS = TransformObjectToWorld(v.vertex.xyz);
                float3 normalWS = TransformObjectToWorldNormal(v.normal);
                float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, _LightDirection));
#if UNITY_REVERSED_Z
                positionCS.z = min(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE);
#else
                positionCS.z = max(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE);
#endif
                o.pos = positionCS;

                return o;
            }

            float4 frag(v2f i) : SV_Target
            {
                return 0.0;
            }

            ENDHLSL
        }
    }
}

これでシャドウに対応して、SRP Batcherもcompatibleになっているシェーダーの出来上がりです。

サンプルコードがだいぶ幅を取ってしまいましたが、今回はこのあたりで。

R.I.P. UnityCG.cginc

*1:言語的にはHLSLとCgは互換性があるのですが、Unity上では色々と多重定義が発生するなど共存させるのはとても大変です。

*2:SpaceTransform.hlslはCore.hlslの中で間接的にincludeされているので直接includeする必要はありません。