【Unity / URP14】ビルドログから理解するシェーダーストリッピング【シェーダーバリアント】

2024/05/29に公開

概要

シェーダーがコンパイルされるとき、Editor.logへシェーダーコンパイルのログが出力されます。
このログを見ることで、何個のシェーダーバリアントが作成され、ストリッピング後にどれくらいの数にまで減ったかを把握することができます。

Editor.log
Compiling shader "Unlit/NewUnlitShader" pass "" (vp)
    Full variant space:         16
    After settings filtering:   16
    After built-in stripping:   8
    After scriptable stripping: 8
    Processed in 0.03 seconds
    starting compilation...

今回の記事では、シェーダーコンパイルのログを見ながら、
シェーダーストリッピングについての理解を深めることをゴールとします。

環境

  • Windows 10
  • Unity2022.3.23f1
  • UniversalRP 14.0.10
  • CoreRP 14.0.10

UnityのTarget PlatformはStandalone (Windows) にしておきます。
(プラットフォームによってバリアントの数が変動します)

Chapter 0. シェーダーバリアント (Shader Variant)

シェーダーソースはコンパイル時、キーワードごとのシェーダープログラムの変異体(バリアント)を作成します。
参考 : [Unity]ShaderVariantについて~前編:ShaderVariantとは?~

例えば、_A , _B というシェーダーキーワードを定義した場合を考えます。

#pragma multi_compile _ _A
#pragma multi_compile _ _B

half4 frag (v2f i) : SV_Target
{
    #if _A
    half4 value1 = HeavyFuncA1(i.uv);
    #else
    half4 value1 = HeavyFuncA2(i.uv);
    #endif

    #if _B
    half4 value2 = HeavyFuncB1(i.uv);
    #else
    half4 value2 = HeavyFuncB2(i.uv);
    #endif

    return value1 + value2;
}

アプリをビルドする時、
キーワード_A を ON / OFF にした場合と キーワード_B を ON/OFFにした場合の
すべての組み合わせについてのシェーダーバリアントが作成されます。

そして、マテリアル側で有効化されたキーワードを元にして、
実行されるシェーダーバリアントが決定されます。

シェーダーバリアントの数

以下のケースではシェーダーバリアントの数は 2 * 2 = 4 となります。

#pragma multi_compile _ _A // 2通り
#pragma multi_compile _ _B // 2通り

以下のケースではシェーダーバリアントの数は 3 * 4 * 2 = 24 となります。

#pragma multi_compile _ _A1 _A2 // 3通り
#pragma multi_compile _ _B1 _B2 _B3 // 4通り
#pragma multi_compile _ _C1 // 2通り

参考 : シェーダーバリアント - Unity マニュアル

Chapter 1. シェーダーコンパイルのログを確認する

アプリをビルドした時などにシェーダーコンパイルが実行されます。

シェーダーコンパイルのログはEditor.logへ出力されます。

OS パス
Windows C:\Users{ユーザー名}\AppData\Local\Unity\Editor\Editor.log
Mac OS ~/Library/Logs/Unity/Editor.log

https://docs.unity3d.com/ja/2018.4/Manual/LogFiles.html

Editor.logの中を見ると、以下のようなシェーダーコンパイルのログが確認できます。

Editor.log
Compiling shader "Hidden/Universal Render Pipeline/UberPost" pass "UberPost" (vp)
    Full variant space:         2
    After settings filtering:   2
    After built-in stripping:   2
    After scriptable stripping: 1
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.00 seconds. Local cache hits 1 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants
    Prepared data for serialisation in 0.00s
Compiling shader "Hidden/Universal Render Pipeline/UberPost" pass "UberPost" (fp)
    Full variant space:         46080
    After settings filtering:   1920
    After built-in stripping:   1920
    After scriptable stripping: 720
    Processed in 0.01 seconds
    starting compilation...
    finished in 0.01 seconds. Local cache hits 720 (0.22s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants
    Prepared data for serialisation in 0.01s

上記のログから以下のことが読み取れます。

  • vp (vertex) のバリアントは2つ作成され、ストリッピングによって1つにまで減った
  • fp (fragment) のバリアントは46080個作成され、ストリッピングによって720にまで減った

vp は vertex program , fp は fragment program を表していると考えられます。


Chapter 2. シェーダーバリアントの数を確認してみる

シェーダーコンパイルのログから、どれくらいの数のシェーダーバリアントが生成されるかを確認してみましょう。

検証1. シェーダーキーワードが無い場合

シェーダーキーワードを定義しないシンプルなシェーダーを作成します。

使用したシェーダー (NewUnlitShader.shader)
Shader "Unlit/NewUnlitShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                return half4(1, 0, 0, 1);
            }
            ENDCG
        }
    }
}

ビルド結果

シェーダーコンパイルすると、以下のようなログが出力されます。

Editor.log
Compiling shader "Unlit/NewUnlitShader" pass "" (vp)
    Full variant space:         1
    After settings filtering:   1
    After built-in stripping:   1
    After scriptable stripping: 1
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.00 seconds. Local cache hits 1 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants
Compiling shader "Unlit/NewUnlitShader" pass "" (fp)
    Full variant space:         1
    After settings filtering:   1
    After built-in stripping:   1
    After scriptable stripping: 1
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.00 seconds. Local cache hits 1 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants

Full variant space は生成されたシェーダーバリアントの総数を表しています。

 Full variant space:         1

After ~の部分は、ストリッピングが行われた後のシェーダーバリアント数を表しています。
1のまま変化していないので、シェーダーバリアントは除去されていません。

    After settings filtering:   1
    After built-in stripping:   1
    After scriptable stripping: 1

検証2. シェーダーキーワードを定義してみる

以下の1行を定義し、シェーダーバリアントを増やしてみます。

    #pragma multi_compile _ _A 
使用したシェーダー (NewUnlitShader.shader)
Shader "Unlit/NewUnlitShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fragment _ _A
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                #if _A
                return half4(1, 0, 0, 1);
                #else
                return half4(0, 1, 0, 1);
                #endif
            }
            ENDCG
        }
    }
}

ビルド結果

シェーダーバリアントの数が2つになっていることが確認できます。

Editor.log
Compiling shader "Unlit/NewUnlitShader" pass "" (vp)
    Full variant space:         2
    After settings filtering:   2
    After built-in stripping:   2
    After scriptable stripping: 2
    Processed in 0.00 seconds
    starting compilation...
Compiling shader "Unlit/NewUnlitShader" pass "" (fp)
    Full variant space:         2
    After settings filtering:   2
    After built-in stripping:   2
    After scriptable stripping: 2
    Processed in 0.00 seconds
    starting compilation...

ここで注目したいのはvpのバリアントの数です。
vertex 関数に キーワード _A による分岐がなかったとしても、
コンパイルのログでは vertex のバリアントの数が2になります。

NewUnlitShader.shader
v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    return o;
}

half4 frag (v2f i) : SV_Target
{
    #if _A
    return half4(1, 0, 0, 1);
    #else
    return half4(0, 1, 0, 1);
    #endif
}

検証3 : fragmentのみシェーダーバリアントを増やす

fragmentだけシェーダーバリアントを作りたいときは、multi_compile_fragmentを使用します。

    #pragma multi_compile_fragment _ _A 

参考 : Indicate which shader keywords affect which shader stage

ビルド結果

fragmentのみシェーダーバリアントが増えていることが確認できます。

Compiling shader "Unlit/NewUnlitShader" pass "" (vp)
    Full variant space:         1
    After settings filtering:   1
    After built-in stripping:   1
    After scriptable stripping: 1
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.00 seconds. Local cache hits 1 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants
Compiling shader "Unlit/NewUnlitShader" pass "" (fp)
    Full variant space:         2
    After settings filtering:   2
    After built-in stripping:   2
    After scriptable stripping: 2
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.01 seconds. Local cache hits 1 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 1 variants (0.01s CPU time), skipped 0 variants

検証4. 複数のシェーダーキーワードを定義してみる

次に、以下のようなシェーダーキーワードを定義してみます。
シェーダーバリアントの数は2\times3\times4\times5 = 120 となるはずです。

#pragma multi_compile _ _A1 // 2
#pragma multi_compile _ _B1 _B2 // 3
#pragma multi_compile _ _C1 _C2 _C3 // 4
#pragma multi_compile _ _D1 _D2 _D3 _D4 // 5
使用したシェーダー (NewUnlitShader.shader)
Shader "Unlit/NewUnlitShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile _ _A1
            #pragma multi_compile _ _B1 _B2
            #pragma multi_compile _ _C1 _C2 _C3
            #pragma multi_compile _ _D1 _D2 _D3 _D4
            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                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);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

ビルド結果

シェーダーをコンパイルしてみると、シェーダーバリアントの数が120になっていることが確認できした。

Editor.log
Compiling shader "Unlit/NewUnlitShader" pass "" (vp)
    Full variant space:         120
    After settings filtering:   120
    After built-in stripping:   120
    After scriptable stripping: 120
    Processed in 0.00 seconds
    starting compilation...
Compiling shader "Unlit/NewUnlitShader" pass "" (fp)
    Full variant space:         120
    After settings filtering:   120
    After built-in stripping:   120
    After scriptable stripping: 120
    Processed in 0.00 seconds
    starting compilation...

まとめ (Chapter 2)

  • ビルドログ(Editor.log)を見ることで、生成されたシェーダーバリアントの数が確認できる
  • multi_compile を使うと vertex と fragment の両方についてバリアントが生成される
  • multi_compile_fragment を使用すると、 fragment のみバリアントが生成される


Chapter 3. shader_feature

ゲーム内で、シェーダーキーワードが動的に切り替わらない場合、
使っていないシェーダーバリアントはビルドに含める必要がありません。
含めないほうがシェーダーのデータサイズを削減できます。

使わないシェーダーバリアントをビルドから除外するときに使用するのがshader_featureです。

shader_feature と multi_compile の違い

shader_featureとmulti_compileの違いは以下の通りです。

  • #pragma multi_compile を使った場合、すべてのシェーダーキーワードの組み合わせについてのシェーダーバリアントがビルドに含まれます。
  • #pragma shader_feature を使った場合、使われないバリアントはビルドから除外されます。
    • アプリに入るマテリアルで使用されるシェーダーバリアントはビルドに入ります。

#pragma shader_feature は、#pragma multi_compile と非常によく似ています。
唯一の違いは、shader_feature のシェーダーの未使用のバリアントがゲームのビルドに含まれないことです。
https://docs.unity3d.com/ja/2018.4/Manual/SL-MultipleProgramVariants.html

検証1. shader_featureを定義したときのバリアントを確認

以下のシェーダーキーワードをシェーダー内で定義してみましょう。

#pragma shader_feature _A
#pragma shader_feature _B
使用したシェーダー
Shader "Unlit/NewUnlitShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma shader_feature _A
            #pragma shader_feature _B
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                #if _A
                return half4(1, 0, 0, 1);
                #else
                return half4(0, 1, 0, 1);
                #endif
            }
            ENDCG
        }
    }
}

ビルド結果

このShaderをResourcesフォルダに含めた状態でビルドを行うと、
シェーダーバリアントが1つだけビルドに含まれます。

Compiling shader "Unlit/NewUnlitShader" pass "" (vp)
    Full variant space:         1
    After settings filtering:   1
    After built-in stripping:   1
    After scriptable stripping: 1
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.00 seconds. Local cache hits 1 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants
    Prepared data for serialisation in 0.00s
Compiling shader "Unlit/NewUnlitShader" pass "" (fp)
    Full variant space:         1
    After settings filtering:   1
    After built-in stripping:   1
    After scriptable stripping: 1
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.00 seconds. Local cache hits 1 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants
    Prepared data for serialisation in 0.00s

検証2. ShaderVariantCollectionを使用した場合のバリアント数

ShaderVariantCollection

シェーダーキーワードをShaderVariantCollectionに登録することで、そのバリアントはビルドに含まれるようになります。

参考 : [Unity]ShaderVariantについて~中編:AssetBundleとShaderVariantの関係~

キーワードセットを4つ登録

すべてキーワードの組み合わせを登録した場合のバリアント数を確認してみます。

ビルド結果

これをビルドしてみると、バリアントの数は4となります。

コンパイルログ
Editor.log
Compiling shader "Unlit/NewUnlitShader" pass "" (vp)
    Full variant space:         4
    After settings filtering:   4
    After built-in stripping:   4
    After scriptable stripping: 4
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.00 seconds. Local cache hits 4 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants
    Prepared data for serialisation in 0.00s
Compiling shader "Unlit/NewUnlitShader" pass "" (fp)
    Full variant space:         4
    After settings filtering:   4
    After built-in stripping:   4
    After scriptable stripping: 4
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.00 seconds. Local cache hits 4 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants
    Prepared data for serialisation in 0.00s

キーワードセットを3つ登録

すべてキーワードの組み合わせを1つ除外し、3つにしてみます。

ビルド結果

バリアントの数は3になります。

コンパイルログ
Editor.log
Compiling shader "Unlit/NewUnlitShader" pass "" (vp)
    Full variant space:         3
    After settings filtering:   3
    After built-in stripping:   3
    After scriptable stripping: 3
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.00 seconds. Local cache hits 3 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants
    Prepared data for serialisation in 0.00s
Compiling shader "Unlit/NewUnlitShader" pass "" (fp)
    Full variant space:         3
    After settings filtering:   3
    After built-in stripping:   3
    After scriptable stripping: 3
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.00 seconds. Local cache hits 3 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants
    Prepared data for serialisation in 0.00s

キーワードセットを2つ登録 (no keyword を除外)

次に、no keywords を除外してみます。

ビルド結果

バリアントの数は3のままです、
キーワードが存在しないバリアントについては、ShaderVariantCollectionに登録しなくてもビルドに含まれるようです。

コンパイルログ
Editor.log
Compiling shader "Unlit/NewUnlitShader" pass "" (vp)
    Full variant space:         3
    After settings filtering:   3
    After built-in stripping:   3
    After scriptable stripping: 3
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.00 seconds. Local cache hits 3 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants
    Prepared data for serialisation in 0.00s
Compiling shader "Unlit/NewUnlitShader" pass "" (fp)
    Full variant space:         3
    After settings filtering:   3
    After built-in stripping:   3
    After scriptable stripping: 3
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.00 seconds. Local cache hits 3 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants
    Prepared data for serialisation in 0.00s

まとめ(Chapter 3)

  • shader_featureを使用した場合、使われていないキーワードのシェーダーバリアントはビルドから除外される
  • ShaderVariantCollectionにキーワードを登録することで、そのキーワードに対応したバリアントがビルドに含まれる
  • キーワード無しのバリアントは自動的にビルドに含まれる


Chapter 4. multi_compile と shader_feature が混在する場合

次に、multi_compileshader_feature が混在しているケースでのバリアントについて見てみようと思います。

ドキュメントを読む限り、

  • multi_compileについては、使用されていなかったとしても含まれる
  • shader_featureについては、使われていない場合は除去される

と考えて良さそうですが、果たして本当にそうなのか気になります。

ビルドしてみてバリアントの数を確認してみたいと思います。

検証

以下のシェーダーキーワードを定義した場合、
バリアント数はShaderVariantCollectionに登録されたキーワードセットの数と一致します。

#pragma shader_feature _A
#pragma shader_feature _B

ここへ #pragma multi_compile _ _C を追加し、その時のシェーダーバリアントの数がどうなるかを見てみます。

#pragma shader_feature _A
#pragma shader_feature _B
#pragma multi_compile _ _C // 追加

multi_compileを追加したことによるバリアント数の違い

ShaderVariantCollectionに登録したキーワードセットと、バリアント数をまとめてみました。
#pragma multi_compile _ _C を追加したことで、バリアントの数が2倍になっています。

バリアント数(vp)
(multi_compile _ _C なし)
バリアント数(vp)
(multi_compile _ _C あり)
ShaderVariantCollection
4 8
3 6
2 4
1 2

キーワードを変えたときのバリアント数の変化

さらに、ShaderVariantCollectionに登録するキーワードセットを変えたときのバリアント数も調べてみました。
multi_compile で指定されているシェーダーキーワード_Cは、ShaderVariantCollectionに登録されていても無視されるようです

バリアント数(vp)
(multi_compile _ _C あり)
_Aと_Bのみを登録 _Aと_Bと_Cを登録
8
6
4
2

まとめ (Chapter 4)

ShaderVariantCollectionに登録されるキーワードセットと、生成されるシェーダーバリアントは以下のような形になりそうです。

登録されているキーワード 生成されるシェーダーバリアント
<no keyword> <no keyword>_C
_A _A_A _C
_B _B_B _C
_A _B _A _B_A _B _C


Chapter 5. ストリッピング(Built-in Stripping)を確認する

Unityには、シェーダーキーワードを定義するショートカットが用意されています。
参考 : Use shortcuts to create keyword sets

たとえば、multi_compile_fog を使うとFog用のキーワード FOG_LINEAR, FOG_EXP, FOG_EXP2 が定義されます。

#pragma multi_compile_fog
サンプルシェーダー(NewUnlitShader.shader)
Shader "Unlit/NewUnlitShader"
{
    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
        }
    }
}

ビルド結果

multi_compile_fog を使用したシェーダーをコンパイルしたときのログは以下のようになりました。

Compiling shader "Unlit/NewUnlitShader" pass "" (vp)
    Full variant space:         4
    After settings filtering:   4
    After built-in stripping:   1
    After scriptable stripping: 1
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.00 seconds. Local cache hits 1 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants
Compiling shader "Unlit/NewUnlitShader" pass "" (fp)
    Full variant space:         4
    After settings filtering:   4
    After built-in stripping:   1
    After scriptable stripping: 1
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.00 seconds. Local cache hits 1 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants

vertexとfragment両方についてFogのシェーダーバリアントが4個生成されていますが、
built-in strippingによって1個にまで減っています。

使わないシェーダーバリアントはストリッピングされることが分かります。

フォグは使われていない場合は除外される (公式ドキュメント)

Unity公式ドキュメントには、使用されていないバリアントは除外される旨の記載があります。

いずれのシーンでも使用されていないフォグとライトマップモードを処理するシェーダーバリアントは、ゲームデータに含まれません。この動作をオーバーライドしたい場合は、Graphics ウィンドウを参照してください。
https://docs.unity3d.com/ja/2021.1/Manual/shader-compilation.html

Fogを有効化する

シーンの設定から、Fogを有効化してみましょう。

ビルド結果

コンパイル結果は以下のようになりました。
シェーダーバリアントの数はがストリッピングによって2にまで減っていることが確認できます。

Editor.log
Compiling shader "Unlit/NewUnlitShader" pass "" (vp)
    Full variant space:         4
    After settings filtering:   4
    After built-in stripping:   2
    After scriptable stripping: 2
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.03 seconds. Local cache hits 1 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 1 variants (0.03s CPU time), skipped 0 variants
    Prepared data for serialisation in 0.00s
Compiling shader "Unlit/NewUnlitShader" pass "" (fp)
    Full variant space:         4
    After settings filtering:   4
    After built-in stripping:   2
    After scriptable stripping: 2
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.01 seconds. Local cache hits 1 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 1 variants (0.01s CPU time), skipped 0 variants
    Prepared data for serialisation in 0.00s
Serialized binary data for shader Unlit/NewUnlitShader in 0.00s
    d3d11 (total internal programs: 4, unique: 4)


Chapter 6. IPreprocessShadersによるストリッピング

IPreprocessShaders を使用することで、ビルド時に不要なシェーダーバリアントを自分で除去(フィルタリング)することができます。

たとえば、デバッグ用のシェーダーバリアントを開発時には使用し、リリース用のビルドには含めない、といったことが実現できます。
参考 : スクリプタブルシェーダーバリアントの除去

デバッグキーワードを含むシェーダー

先ほどのシェーダーに、新しく DEBUG というシェーダーキーワードを定義してみます。

#pragma multi_compile_fog
#pragma multi_compile _ DEBUG
シェーダーサンプル (NewUnlitShader.shader)
Shader "Unlit/NewUnlitShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #pragma multi_compile_fog
            #pragma multi_compile _ DEBUG

            #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);
                #if DEBUG
                return col; // フォグを適用する前のカラーを出力 (デバッグ)
                #endif
                
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);

                return col;
            }
            ENDCG
        }
    }
}

ビルド結果

DEBUG定義前と比べると、シェーダーバリアントの数が2倍になっていることが分かります。

Compiling shader "Unlit/NewUnlitShader" pass "" (vp)
    Full variant space:         8
    After settings filtering:   8
    After built-in stripping:   4
    After scriptable stripping: 4
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.04 seconds. Local cache hits 1 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 3 variants (0.12s CPU time), skipped 0 variants
    Prepared data for serialisation in 0.00s
Compiling shader "Unlit/NewUnlitShader" pass "" (fp)
    Full variant space:         8
    After settings filtering:   8
    After built-in stripping:   4
    After scriptable stripping: 4
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.01 seconds. Local cache hits 1 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 3 variants (0.03s CPU time), skipped 0 variants
    Prepared data for serialisation in 0.00s

シェーダーバリアントの除去

IPreprocessShaders を使用して、シェーダーバリアントを除去してみます。

ShaderDebugBuildProcessor.cs
using System.Collections.Generic;
using UnityEditor.Build;
using UnityEditor.Rendering;
using UnityEngine;
using UnityEngine.Rendering;

// デバッグビルド設定の除去の簡単な例
class ShaderDebugBuildProcessor : IPreprocessShaders
{
    ShaderKeyword m_KeywordDebug;

    public ShaderDebugBuildProcessor()
    {
        m_KeywordDebug = new ShaderKeyword("DEBUG");
    }

    // 複数のコールバックを実装可能です。
    // 最初に実行されるのは、callbackOrder が最も小さい数を戻すものです。
    public int callbackOrder { get { return 0; } }

    public void OnProcessShader(
        Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> shaderCompilerData)
    {
        for (int i = 0; i < shaderCompilerData.Count; ++i)
        {
            // DEBUGキーワードが有効だったら
            if (shaderCompilerData[i].shaderKeywordSet.IsEnabled(m_KeywordDebug))
            {
                // シェーダーバリアントを除去
                shaderCompilerData.RemoveAt(i);
                --i;
            }
        }
    }
}

ビルド結果

アプリケーションをビルドすると、以下のようなログが出力されます.

Editor.log
Compiling shader "Unlit/NewUnlitShader" pass "" (vp)
    Full variant space:         8
    After settings filtering:   8
    After built-in stripping:   4
    After scriptable stripping: 2
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.00 seconds. Local cache hits 2 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants
    Prepared data for serialisation in 0.00s
Compiling shader "Unlit/NewUnlitShader" pass "" (fp)
    Full variant space:         8
    After settings filtering:   8
    After built-in stripping:   4
    After scriptable stripping: 2
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.00 seconds. Local cache hits 2 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants
    Prepared data for serialisation in 0.00s

After built-in stripping ではバリアントの数が4ですが、
After scriptable stripping によってシェーダーバリアントの数が2にまで減ることができます。

まとめ (Chapter 6)

  • IPreprocessShadersを利用することで、シェーバーバリアントを自分で除去できる
  • 自作したストリッピングは、ビルドログのAfter scriptable strippingの数値に現れる


Chapter 7. ストリッピングをカスタマイズできるIShaderVariantStripper

CoreRP14から、IShaderVariantStripperというインターフェースが生えました。
これを使用することで、URPのストリッピング処理(scriptable stripping)をカスタマイズすることができます。

ShadowCasterパスのストリッピングを確認してみる

URPの設定で、Main Light が無効化されている場合、シェーダーのShadowCasterパスは、ストリッピングされます。
_MAIN_LIGHT_SHADOWS などのシャドウマッピング関連のバリアントも除外されます。

参考 : Shader Stripping | Universal RP | 14.0.11


Main Light無効

URPシェーダーのShaderCasterパスの実装例
Pass
{
    Name "ShadowCaster"
    Tags
    {
        "LightMode" = "ShadowCaster"
    }

    // -------------------------------------
    // Render State Commands
    ZWrite On
    ZTest LEqual
    ColorMask 0
    Cull[_Cull]

    HLSLPROGRAM
    #pragma target 2.0

    // -------------------------------------
    // Shader Stages
    #pragma vertex ShadowPassVertex
    #pragma fragment ShadowPassFragment
    
    #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl"
    ENDHLSL
}

ビルド結果

アプリをビルドしたときに、バリアントの数がゼロになることが確認できます。

Editor.log
Compiling shader "Unlit/NewUnlitShader" pass "ShadowCaster" (vp)
    Full variant space:         1
    After settings filtering:   1
    After built-in stripping:   1
    After scriptable stripping: 0
    Processed in 0.00 seconds    

IShaderVariantStripperについて

CoreRPパッケージのShaderPreprocessorクラスのCanRemoveVariantメソッドで、バリアントをストリッピングするかどうかを判定しています。

PackageCache/com.unity.render-pipelines.core@14.0.10/Editor/ShaderStripping/ShaderPreprocessor.cs
bool CanRemoveVariant([DisallowNull] TShader shader, TShaderVariant shaderVariant, ShaderCompilerData shaderCompilerData)
{
    return strippers
        .Where(s => s is not IVariantStripperSkipper<TShader, TShaderVariant> skipper || !skipper.SkipShader(shader, shaderVariant))
        .All(s => s.CanRemoveVariant(shader, shaderVariant, shaderCompilerData));
}

IShaderVariantStripperSkipper を利用したクラスを定義することで、ストリッピングのふるまいを拡張できます。

ShadowCasterパスのストリッピングの実装箇所 (ShaderScriptableStripper.cs)

影が無効だった場合にShadowCasterPassを無効にするという判定は、ShaderScriptableStripperクラスで実装されています。
このクラスは IShaderVariantStripper を利用しています。

Library/PackageCache/com.unity.render-pipelines.universal@14.0.10/Editor/ShaderScriptableStripper.cs
    internal class ShaderScriptableStripper : IShaderVariantStripper, IShaderVariantStripperScope


ShadowCasterPassのストリッピング処理は以下になります。
MainLightShadows と AdditionalLightShadows 両方が無効だと ストリッピングが行われます。

Library/PackageCache/com.unity.render-pipelines.universal@14.0.10/Editor/ShaderScriptableStripper.cs
internal bool StripUnusedPass_ShadowCaster(ref IShaderScriptableStrippingData strippingData)
{
    if (strippingData.passType == PassType.ShadowCaster)
    {
        if (   !strippingData.IsShaderFeatureEnabled(ShaderFeatures.MainLightShadows)
            && !strippingData.IsShaderFeatureEnabled(ShaderFeatures.AdditionalLightShadows))
            return true;
    }
    return false;
}

実装例 : ShadowCasterパスのストリッピングを無効にする

IShaderVariantStripperの実装例を以下に示します。
URPのScripting Strippingが実行されるとき、ShadowCasterパスのストリッピングがスキップされるようになります。
このクラスはEditorフォルダ内に格納しておきます。

CustomShaderVariantStripper.cs
using System.Linq;
using UnityEditor.Rendering;
using UnityEngine;
using UnityEngine.Rendering;

/// <summary>
/// カスタムのShaderVariantStripper
/// </summary>
public class CustomShaderVariantStripper : IShaderVariantStripper
{
    public bool active => true; // trueを返すと、このStripperが実行される

    /// <summary>
    /// falseを返すと、バリアントが除去されなくなる
    /// </summary>
    public bool CanRemoveVariant(Shader shader, ShaderSnippetData shaderVariant, ShaderCompilerData shaderCompilerData)
    {
        if (shaderVariant.passType == PassType.ShadowCaster)
        {
            Debug.Log("スキップします");
            return false;
        }
        return true;
    }
}

ビルド結果

ShadowCasterパスがストリッピングされなくなります。

Compiling shader "Unlit/NewUnlitShader" pass "ShadowCaster" (vp)
    Full variant space:         1
    After settings filtering:   1
    After built-in stripping:   1
    After scriptable stripping: 1
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.00 seconds. Local cache hits 1 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants
    Prepared data for serialisation in 0.00s
Compiling shader "Unlit/NewUnlitShader" pass "ShadowCaster" (fp)
    Full variant space:         1
    After settings filtering:   1
    After built-in stripping:   1
    After scriptable stripping: 1
    Processed in 0.00 seconds
    starting compilation...
    finished in 0.00 seconds. Local cache hits 1 (0.00s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants
    Prepared data for serialisation in 0.00s


Chapter 8. RenderPipelineタグの指定が間違っているとストリッピングされる

Unityプロジェクトに登録されているのRenderPipelineAssetが持つタグと、
ShaderのRenderPipelineタグの指定が異なる場合、シェーダーはストリッピングされます。

Tags 
{ 
    "RenderPipeline"="UniversalRenderPipeline" 
    "RenderType"="Opaque" 
}
Editor.log
Compiling shader "Unlit/NewUnlitShader" pass "" (vp)
    Full variant space:         4
    After settings filtering:   4
    After built-in stripping:   4
    After scriptable stripping: 0
    Processed in 0.00 seconds
    Prepared data for serialisation in 0.00s
使用したシェーダー
Shader "Unlit/NewUnlitShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags 
        { 
            "RenderPipeline"="UniversalRenderPipeline" 
            "RenderType"="Opaque" 
        }
        LOD 100

        Pass
        {
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile _ _A
            #pragma multi_compile _ _B
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                #if _A
                return half4(1, 0, 0, 1);
                #else
                return half4(0, 1, 0, 1);
                #endif
            }
            ENDCG
        }
    }
}

ストリッピング処理の実装箇所

このストリッピング処理は、CoreRPパッケージ内のShaderPreprocessor.csにて実装されています。
RenderPipelineタグの記述が間違っている場合にストリッピングされることになります。

Library/PackageCache/com.unity.render-pipelines.core@14.0.10/Editor/ShaderStripping/ShaderPreprocessor.cs
string renderPipelineTag = string.Empty;
if (typeof(TShader) == typeof(Shader) && typeof(TShaderVariant) == typeof(ShaderSnippetData))
{
    if (TryGetShaderVariantRenderPipelineTag(shader, shaderVariant, out renderPipelineTag))
    {
        if (!renderPipelineTag.Equals(globalRenderPipeline, StringComparison.OrdinalIgnoreCase))
            afterStrippingShaderVariantCount = 0;
    }
}

ストッピングの詳細を確認する (CoreRPのカスタマイズ)

CoreRPパッケージをカスタムパッケージ化し、ストリッピングが行われたときに警告ログを出すようにしてみます。

string renderPipelineTag = string.Empty;
if (typeof(TShader) == typeof(Shader) && typeof(TShaderVariant) == typeof(ShaderSnippetData))
{
    if (TryGetShaderVariantRenderPipelineTag(shader, shaderVariant, out renderPipelineTag))
    {
        if (!renderPipelineTag.Equals(globalRenderPipeline, StringComparison.OrdinalIgnoreCase))
        {
            afterStrippingShaderVariantCount = 0;
            Debug.LogWarning($"RenderPipelineタグが一致しないため、ストリッピングされました。shader={shader} renderPipelineTag={renderPipelineTag} globalRenderPipeline={globalRenderPipeline}");
        }
    }
}

ビルド結果

ビルドすると、ストリッピングが行われていることが確認できます。

RenderPipelineタグが一致しないため、ストリッピングされました。shader=Unlit/NewUnlitShader (UnityEngine.Shader) renderPipelineTag=UniversalRenderPipeline globalRenderPipeline=UniversalPipeline

URPを使用している場合、シェーダーのRenderPipelineタグは "UniversalPipeline" にしておく必要があるようです。


まとめ

ビルドログ について

  • ビルドログ(Editor.log)を見ることで、生成されたシェーダーバリアントの数が確認できる
  • multi_compile を使うと vertex と fragment の両方についてバリアントが生成される
  • multi_compile_fragment を使用すると、 fragment のみバリアントが生成される

ShaderVariantCollection について

  • #pragma shader_feature を使った場合、使用されないキーワードのバリアントは除外される
  • #pragma multi_compile で指定されているシェーダーキーワードは、ShaderVariantCollectioに登録されていても無視される
  • ShaderVariantCollectionに記録したキーワードのバリアントはビルドに含まれる
  • キーワードが無いバリアントについては、ShaderVariantCollectionへ登録してなくてもビルドに含まれる

Scriptable Stripping (スクリプタブルストリッピング) について

  • IPreprocessShadersを利用することで、ストリッピング(バリアントの除外処理)を自作できる
    • 使用例 : デバッグ用のシェーダーバリアントはストリッピングする
  • IShaderVariantStripper を利用すると、URPのシェーダーストリッピングをカスタマイズできる
    • 使用例 : ShadowCasterパスはストリッピングされないようにする
  • 実装したストリッピングは、ビルドログのAfter scriptable strippingの数値に現れる

リンク

[Unity] ShaderVariantについて~前編:ShaderVariantとは?~
https://note.com/wotakuro/n/n48d40ec0f006

複数のシェーダープログラムのバリアントを作る
https://docs.unity3d.com/ja/2018.4/Manual/SL-MultipleProgramVariants.html

Indicate which shader keywords affect which shader stage
https://docs.unity3d.com/2022.3/Documentation/Manual/shader-variant-stripping.html

スクリプタブルシェーダーバリアントの除去
https://blog.unity.com/ja/engine-platform/stripping-scriptable-shader-variants

IShaderVariantStripper
https://docs.unity3d.com/Packages/com.unity.render-pipelines.core@14.0/api/UnityEditor.Rendering.IShaderVariantStripper.html

IShaderVariantStripperSkipper
https://docs.unity3d.com/Packages/com.unity.render-pipelines.core@14.0/api/UnityEditor.Rendering.IShaderVariantStripperSkipper.html

Unity 2021 LTS におけるシェーダーのビルド時間とメモリ使用量の改善
https://blog.unity.com/ja/engine-platform/2021-lts-improvements-to-shader-build-times-and-memory-usage

Discussion