CORETECH ENGINEER BLOG

株式会社サイバーエージェント SGEコア技術本部 技術ブログ

【Unity 6.3新機能】ShaderBuildSettingsでソースコードを変更せずにShaderバリアントを最適化する

皆さんこんにちは、SGE技術本部(コアテク)所属のグラフィックスエンジニア チャンユービンです。

今回はUnity 6.3で追加された新機能「ShaderBuildSettings」を紹介します。この機能を使えば、Shaderのソースコードを変更せず、Shaderバリアントストリップ用のスクリプトを書かなくても、Shaderバリアントを最適化できます。

概要

執筆時のUnityバージョン: Unity 6000.3.2f1

ShaderBuildSettingsでは、以下の操作ができます:

  • キーワードタイプの変換: multi_compileshader_featuredynamic_branch間で相互変換
  • キーワードの除外: 不要なキーワードバリアントを選択的に削除

これらの操作により、Shaderバリアント数を効率的に最適化し、ビルド時間とパッケージサイズを削減できます。

設定場所

設定範囲 パス 優先度
プロジェクト全体 Project Settings → Graphics → Shader Build Settings
BuildProfile Add Settings → Graphics Settings → Shader Build Settings

前提知識:キーワードタイプ

ShaderBuildSettingsを使う前に、3種類のキーワードタイプの違いを理解しておきましょう。

multi_compile と shader_feature

特性 multi_compile shader_feature
バリアント生成 すべてのキーワード組み合わせをコンパイル マテリアルで使用されているバリアントのみコンパイル
未使用バリアント ビルドに含まれる ビルドから除外される
適用シーン コードでグローバル制御するキーワード マテリアル設定で使うキーワード

dynamic_branch

dynamic_branchはUnity 2022.1で導入されたキーワードタイプで、上記2つとは本質的に異なります:

特性 multi_compile / shader_feature dynamic_branch
コンパイル結果 複数のShaderバリアントを生成 単一のShaderのみ生成
分岐方式 コンパイル時の静的分岐 実行時の動的分岐(GPU実行)
キーワード実装 プリプロセッサマクロ Uniformブール変数

参考: Unity Manual - How Unity compiles branching shaders

宣言方法は他のキーワードタイプと同じです:

#pragma dynamic_branch _ _DY_KEYWORD1 _DY_KEYWORD2
#pragma dynamic_branch_local _ _DY_KEYWORD1 _DY_KEYWORD2  // local版

注意: dynamic_branchには_vertex_fragmentなどステージ特定版は存在しません。

キーワードの条件分岐方法

プリプロセッサディレクティブ(#if / #ifdef)

#ifdef _KEYWORD
    // コンパイル時にこのコードを含めるか決定
#endif

通常のif文

if (_KEYWORD)
{
    // 実行時にこのコードを実行するか判断
}

3種類のキーワードタイプの分岐方法サポート状況:

キーワードタイプ #if / #ifdef if文
multi_compile
shader_feature
dynamic_branch

注意: multi_compileshader_feature_vertex_fragmentなどステージ特定版を使う場合、通常のif文は使えません。

推奨: Unity公式はif文の使用を推奨しています。キーワードタイプ間の切り替えが容易になるためです。

使い方

設定手順

  1. 設定画面を開く(Project SettingsまたはBuildProfile
    • Project Settingsの場合: Edit → Project Settings → Graphics → Shader Build Settings
    • BuildProfileの場合: File → Build Profile → 任意のBuildProfileを選択 → Add Settings → Graphics Settings → Shader Build Settings
  2. + ボタンで項目を追加
  3. Keyword set にキーワードセットを入力
    • Shaderで定義されたキーワードと完全一致させる(_プレースホルダーも含む)
    • 例:_ _KEYWORD1 _KEYWORD2
    • キーワードの記述順序は異なっていてもOK
  4. 左の矢印をクリックして展開し、保持するキーワードを選択
  5. Type override で目標のキーワードタイプを選択

Type Overrideオプション

オプション 説明
Default 元のキーワードタイプを維持
shader_feature shader_featureタイプに変換
multi_compile multi_compileタイプに変換
dynamic_branch dynamic_branchタイプに変換(制限あり)

反映タイミング

  • 設定をApplyすると、Unity保存時に反映
  • 関連するすべてのShaderが再インポート・コンパイルされる
  • Shaderソースコードは変更されないコンパイル結果のみ影響
  • 選択されなかったキーワードは強制的に除外される(dynamic_branch以外)
  • Asset Importフェーズで反映(Buildフェーズではない)ので、Editor上で結果を確認できる

注意事項と制限

設定粒度の制限

この機能はBuildProfile単位でしか設定できず、特定のマテリアルだけに適用することはできません。

グローバル影響

設定したキーワードは、そのキーワードを使用するすべてのShaderに影響します。特にURPなどレンダリングパイプライン内蔵のキーワードを変更する場合、予期しないShaderに影響する可能性があるので注意が必要です。

Defaultオプションの不具合(Unity 6000.3.2f1の時点)

Type overrideDefaultを選択した場合、キーワード除外機能に以下の問題があります:

Shader内の元タイプ キーワード除外
multi_compile ✓ 正常動作
shader_feature ✗ 機能しない

回避策: この不具合はDefaultを選択した時のみ発生します。明示的にType overrideを選択すれば(元と同じshader_featureshader_featureでも)、キーワード除外機能は正常に動作します。

dynamic_branchに変換できる条件

dynamic_branchプリプロセッサディレクティブをサポートしないため、以下の条件を満たす場合のみ安全に変換できます:

  1. if文でキーワードを判断している#if/#ifdefではなく)
    • 注:_vertex_fragmentなどステージ特定版を使用している場合はif文が使えないため、変換不可
  2. キーワードが変数宣言に影響しない

変換できない例

以下のコードはキーワードが変数やテクスチャの宣言に影響しているため、dynamic_branchに変換できません:

#pragma multi_compile _ _CUSTOM_KEYWORD

struct Varyings
{
    // ...
#if defined(_CUSTOM_KEYWORD)
    float4 customParams : TEXCOORD1;  // 変数宣言がキーワードに依存
#endif
};

#if defined(_CUSTOM_KEYWORD)
    TEXTURE2D(_CustomMap);             // テクスチャ宣言がキーワードに依存
    SAMPLER(sampler_CustomMap);
#endif

half4 frag(Varyings IN) : SV_Target
{
    if (_CUSTOM_KEYWORD)
    {
        // コンパイルエラー:_CUSTOM_KEYWORD未定義時にcustomParamsと_CustomMapが存在しない
        half4 customTex = SAMPLE_TEXTURE2D(_CustomMap, sampler_CustomMap, IN.uv);
        color *= customTex * IN.customParams;
    }
}

変換できる例

キーワードがコードロジックの分岐のみを制御し、変数宣言に影響しない場合は安全に変換できます:

#pragma multi_compile _ _EFFECT_ON

// すべての変数は常に宣言
TEXTURE2D(_EffectMap);
SAMPLER(sampler_EffectMap);

half4 frag(Varyings IN) : SV_Target
{
    half4 color = baseColor;

    if (_EFFECT_ON)
    {
        // このエフェクトを実行するかどうかのみ制御、変数は常に存在
        half4 effect = SAMPLE_TEXTURE2D(_EffectMap, sampler_EffectMap, IN.uv);
        color *= effect;
    }

    return color;
}

ユースケース

以下は代表的なユースケースの一例です。プロジェクトの要件に応じて、他にも様々な活用方法が考えられます。

開発時

開発中は、ビルド時間の短縮やデバッグ効率の向上に活用できます:

  • 素早くビルドして機能確認: dynamic_branchに変換し、単一Shaderのみコンパイル。バリアント数とコンパイル時間を大幅に削減
  • 特定機能のデバッグ、不要機能を除外: shader_featuremulti_compileに変換し、不要なキーワードを除外

リリース時

ShaderBuildSettingsはShaderバリアント数を抑える手段としても使えます。以下のケースに適しています:

  • UberShaderバリアント最適化: 大量のバリアントを持つ汎用Shaderを使っているが、実際には一部の機能しか使わない場合
  • マルチプラットフォーム対応: BuildProfileで各プラットフォームごとにキーワードタイプを調整したり、不要なキーワードを除外
    • タイプ調整:メモリ制限のあるプラットフォームはdynamic_branch、パフォーマンス重視のプラットフォームはmulti_compileshader_feature
    • キーワード除外:ローエンドプラットフォームは一部のハイエンドエフェクトキーワードを除外、ハイエンドプラットフォームはフル機能を維持

使用例

最後に、具体的な例でShaderBuildSettingsの効果を見てみましょう。

クリックしてShaderソースコードを表示

Shader "Custom/CustomUnlit"
{
    Properties
    {
        [MainColor] _BaseColor("Base Color", Color) = (1, 1, 1, 1)
        [MainTexture] _BaseMap("Base Map", 2D) = "white"
        [Toggle] _UseTexture("Use Texture", Float) = 0
        [KeywordEnum(None, Red, Green, Blue, Negative)] _Mode("Mode", Float) = 0
    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }

        Pass
        {
            HLSLPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            // 自前のキーワード定義
            #pragma multi_compile _ _USETEXTURE_ON
            #pragma multi_compile _ _MODE_RED _MODE_GREEN _MODE_BLUE _MODE_NEGATIVE

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

            struct Attributes
            {
                float4 positionOS : POSITION;
            #if defined(_USETEXTURE_ON)
                float2 uv : TEXCOORD0;
            #endif
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
            #if defined(_USETEXTURE_ON)
                float2 uv : TEXCOORD0;
            #endif
            };

        #if defined(_USETEXTURE_ON)
            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);
        #endif

            CBUFFER_START(UnityPerMaterial)
                half4 _BaseColor;
                float4 _BaseMap_ST;
            CBUFFER_END

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
            #if defined(_USETEXTURE_ON)
                OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
            #endif
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                half4 color = _BaseColor;

                // #ifでの条件分岐
            #if defined(_USETEXTURE_ON)
                half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
                color *= texColor;
            #endif

                // 通常ifでの条件分岐
                if (_MODE_RED)
                    color.rgb *= half3(1, 0, 0);
                else if (_MODE_GREEN)
                    color.rgb *= half3(0, 1, 0);
                else if (_MODE_BLUE)
                    color.rgb *= half3(0, 0, 1);
                else if (_MODE_NEGATIVE)
                    color.rgb = 1 - color.rgb;

                return color;
            }
            ENDHLSL
        }
    }
}

初期状態

まず、このShaderのデフォルト状態でのバリアント数を確認します。このShaderは合計10個のバリアントを生成します(2 × 5 = 10)。

クリックしてスクリーンショットと詳細を表示

Shaderファイルを選択すると、InspectorでShaderに含まれるキーワードを確認できます:

Compile and show codeの右にある矢印をクリックすると、実際のバリアント数が表示されます。Showをクリックすると詳細が見られます:

Keywords stripped away when not used: STEREO_CUBEMAP_RENDER_ON STEREO_INSTANCING_ON STEREO_MULTIVIEW_ON UNITY_SINGLE_PASS_STEREO
Keywords always included into build: _USETEXTURE_ON _MODE_RED _MODE_GREEN _MODE_BLUE _MODE_NEGATIVE

10 keyword variants used in scene:

<no keywords defined>
_MODE_RED
_MODE_GREEN
_MODE_BLUE
_MODE_NEGATIVE
_USETEXTURE_ON
_MODE_RED _USETEXTURE_ON
_MODE_GREEN _USETEXTURE_ON
_MODE_BLUE _USETEXTURE_ON
_MODE_NEGATIVE _USETEXTURE_ON

キーワードタイプをshader_featureに変換

両方のキーワードグループをshader_featureに変換すると、バリアント数が10個から2個に減少します。これはshader_featureの特性どおり、デフォルトバリアントと実際に使用されているバリアントのみが保持されます。

クリックしてスクリーンショットと詳細を表示

補足:シーンにこのShaderを使用するマテリアルを配置し、_MODE_GREEN_USETEXTURE_ONキーワードを有効にしています。

保持されたバリアント:

  • デフォルトバリアント(キーワードなし)
  • 実際に使用されているバリアント(MODE_GREEN USETEXTURE_ON)
Keywords stripped away when not used: STEREO_CUBEMAP_RENDER_ON STEREO_INSTANCING_ON STEREO_MULTIVIEW_ON UNITY_SINGLE_PASS_STEREO _MODE_BLUE _MODE_GREEN _MODE_NEGATIVE _MODE_RED _USETEXTURE_ON

2 keyword variants used in scene:

<no keywords defined>
_MODE_GREEN _USETEXTURE_ON

一部のキーワードのみ保持

_MODEキーワードグループのタイプをmulti_compileのまま、_MODE_REDのみ保持します。保持されなかったキーワードは完全に除外され、バリアント数は1個に減少します。キーワード自体が除外されるため、マテリアルでその機能を有効にすることもできなくなります。

クリックしてスクリーンショットと詳細を表示

Keywords stripped away when not used: STEREO_CUBEMAP_RENDER_ON STEREO_INSTANCING_ON STEREO_MULTIVIEW_ON UNITY_SINGLE_PASS_STEREO
Keywords always included into build: _MODE_RED

1 keyword variants used in scene:

_MODE_RED

dynamic_branchに変換

_MODEキーワードグループをdynamic_branchに変換します(_USETEXTURE_ON#ifプリプロセッサディレクティブを使用し変数宣言に影響するため、変換条件を満たしません)。変換後、_MODE関連のバリアントは消えますが、キーワード自体は保持され、バリアント数は2個に減少します。dynamic_branchはバリアントを生成しないため、キーワード除外機能は効きません。

クリックしてスクリーンショットと詳細を表示

Keywords stripped away when not used: STEREO_CUBEMAP_RENDER_ON STEREO_INSTANCING_ON STEREO_MULTIVIEW_ON UNITY_SINGLE_PASS_STEREO
Keywords always included into build: _USETEXTURE_ON _MODE_RED _MODE_GREEN _MODE_BLUE _MODE_NEGATIVE

2 keyword variants used in scene:

<no keywords defined>
_USETEXTURE_ON

コンパイル後のShaderコードを見ると、dynamic_branchの仕組みがよく分かります。_MODEキーワードがConstant Buffer内の整数変数になり、フラグメントシェーダー内でif文で判定しています。

クリックしてコンパイル後のコードを表示

// ...

// _MODE関連キーワードがConstant Buffer変数になった
Constant Buffer "UnityDynamicKeywords" (16 bytes) on slot 0 {
  ScalarInt _MODE_RED at 0
  ScalarInt _MODE_GREEN at 4
  ScalarInt _MODE_BLUE at 8
  ScalarInt _MODE_NEGATIVE at 12
}

// ...

struct UnityDynamicKeywords_Type
{
    int _MODE_RED;
    int _MODE_GREEN;
    int _MODE_BLUE;
    int _MODE_NEGATIVE;
};

// ...

// フラグメントシェーダー内でif文による_MODEキーワードの判定
fragment Mtl_FragmentOut xlatMtlMain(
    constant UnityDynamicKeywords_Type& UnityDynamicKeywords [[ buffer(0) ]],
    constant UnityPerMaterial_Type& UnityPerMaterial [[ buffer(1) ]])
{
    Mtl_FragmentOut output;
    float3 u_xlat0;
    float3 u_xlat1;
    u_xlat0.xyz = (-UnityPerMaterial._BaseColor.xyz) + float3(1.0, 1.0, 1.0);
    u_xlat0.xyz = (UnityDynamicKeywords._MODE_NEGATIVE != 0) ? u_xlat0.xyz : UnityPerMaterial._BaseColor.xyz;
    u_xlat1.xyz = UnityPerMaterial._BaseColor.xyz * float3(0.0, 0.0, 1.0);
    u_xlat0.xyz = (UnityDynamicKeywords._MODE_BLUE != 0) ? u_xlat1.xyz : u_xlat0.xyz;
    u_xlat1.xyz = UnityPerMaterial._BaseColor.xyz * float3(0.0, 1.0, 0.0);
    u_xlat0.xyz = (UnityDynamicKeywords._MODE_GREEN != 0) ? u_xlat1.xyz : u_xlat0.xyz;
    u_xlat1.xyz = UnityPerMaterial._BaseColor.xyz * float3(1.0, 0.0, 0.0);
    output.SV_Target0.xyz = (UnityDynamicKeywords._MODE_RED != 0) ? u_xlat1.xyz : u_xlat0.xyz;
    output.SV_Target0.w = UnityPerMaterial._BaseColor.w;
    return output;
}

最後に

以上、Unity 6.3で追加されたShaderBuildSettings機能の紹介でした。

Unity 6.3以前は、Shaderバリアント数を制御するためにストリップスクリプトを手動で書く必要がありました。また、ストリップスクリプトはBuild時にしか効果を発揮しないため、Editor上で結果を確認することが困難でした。ShaderBuildSettingsというUnityエンジンレベルでサポートされた機能により、プロジェクト全体でShaderバリアント数を簡単に制御でき、Editor上で即座に結果を確認できるようになりました。

最後まで読んでいただき、ありがとうございました。皆さんのお役に立てれば幸いです!