なぜこの題材か
2026年7月、Unite Seoulで発表された Unity 7 では、CoreCLRを含む開発パイプライン刷新の一環として、「シェーダービルドが最大90%高速化する」と予告され、大きな注目を集めました。
とはいえ、ビルドが速くなる仕組みを理解する前に、そもそもなぜシェーダービルドがそんなに重くなるのかを知っておくと理解が深まります。その鍵を握っているのが、今回のテーマである Shader Variant(シェーダーバリアント) です。
この記事では、Unity 7の詳細には踏み込まず、まず基礎となるShader Variantの考え方を整理します。
Shader Variantとは
Shader Variantとは、1つのシェーダーから、キーワードの状態などに応じて生成される複数のシェーダープログラムのことです。
シェーダー内の機能を切り替える方法の1つとして、GPU上で実行時に条件分岐する代わりに、条件ごとのコードをあらかじめコンパイルしておく方法があります。各Variantでは条件がコンパイル時に確定するため、不要な処理をコードから除去しやすくなります。
一方で、条件の組み合わせが増えると、そのぶんコンパイル・保存・ロードするシェーダープログラムも増えます。この条件の指定に使われるのが「シェーダーキーワード」です。
#pragma multi_compile _ FOG_LINEAR FOG_EXP FOG_EXP2
#pragma shader_feature _NORMALMAP
multi_compile の先頭にある _ は「どのキーワードも定義しない状態(フォグなし)」を表すプレースホルダーです。これも1つの状態として数えるので、フォグは3種類ではなく4種類(なし/Linear/Exp/Exp2)。ノーマルマップの2種類(ON/OFF)と掛け合わせると、
4 × 2 = 8通り
のバリアントが生成されます。行を1つ追加するたびに、既存の組み合わせ数に掛け算で効いてくる点がポイントです。
つまりShader Variantは、「キーワードごとに専用のシェーダープログラムを用意できる一方、組み合わせによってコンパイル対象が増殖しやすい仕組み」と言えます。
Variant数 ≒ 各キーワードセットの選択肢数の積
なお、ここで数えているのは、コード例に書いたキーワードの組み合わせだけです。実際のコンパイル数には、Shader Pass、対象のGraphics API、シェーダーステージなども関係するため、実プロジェクトではさらに多くなる場合があります。
補足
multi_compile shader_feature はどちらもenum形式のシンボルと考えてください。
#pragma shader_feature _NORMALMAP
には_が省略されており、正確には_と_NORMALMAPの2択です
増えると何が困るか
キーワードセットが増えるほど、バリアント数は各セットの選択肢数の積として、組み合わせ的に膨れ上がります。特に二択のキーワードを追加し続けた場合は、2ⁿの勢いで増加します。
- ビルド時間:バリアントの数だけコンパイルが必要になり、ビルドが長くなる
- アプリ容量:未使用のバリアントまで含めてしまうと、そのぶんデータが肥大化する
- ロード時間:実行時にシェーダーデータを読み込む時間が伸びる
- 初回描画時のカクつき:Variantを初めて使用するとき、Unityによるロードや、グラフィックスドライバーによるGPU向けプログラム・PSOの作成が発生し、一時的にフレームが止まることがある(shader hitch)
- メモリ使用量:ロード・展開されたシェーダーデータがCPUメモリを使用し、初回使用後のGPU側キャッシュにも影響する
「見た目を切り替えられて便利」の裏で、これらのコストが静かに積み上がっていくのがShader Variantの厄介なところです。
multi_compile / shader_feature
バリアントを増やすキーワード指定には、大きく2種類あります。
#pragma multi_compile _ FOG_LINEAR FOG_EXP FOG_EXP2
#pragma shader_feature _NORMALMAP
-
multi_compile:基本的に指定した全状態をコンパイル対象にする。ランタイムでコードやGlobal Keywordから切り替える可能性がある機能向け。別途Strippingされない限り、未使用の組み合わせも残りやすい。 -
shader_feature:ビルド時に参照できるマテリアルで使われていないVariantを除外できる。あらかじめMaterialごとに状態が決まる機能向け。
何でも multi_compile にしてしまうと、使っていない組み合わせまでビルドに残り続けてバリアントが増えやすくなります。逆に、マテリアルでON/OFFが決まる機能なら shader_feature の方が未使用分を削りやすい、という違いを覚えておくと設計判断がしやすくなります。
ただし、shader_featureだからランタイムで切り替えられない、という意味ではありません。実行時にキーワードを変更すること自体は可能ですが、ビルド時に使用しているマテリアルが見つからなければ、切り替え先のVariantが未使用としてStripされる可能性があります。
そのため、基本的には次のように使い分けます。
- ビルド時にマテリアルから使用状態を確認できる機能:
shader_feature - 実行時にコードやGlobal Keywordから切り替える機能:
multi_compile
対策の考え方
バリアントの増殖に対しては、次のような対策があります。
1. 生成候補を増やさない
- ランタイムで切り替えない機能には、可能な範囲で
shader_featureを使う - 同時に成立しない状態は、複数のON/OFFキーワードではなく、1つのキーワードセットとして定義する
-
Fragment Shaderだけで使用するキーワードには、shader_feature_fragmentやmulti_compile_fragmentなどのステージ指定を検討する - URP AssetやRenderer Featureを確認し、Additional Lights、SSAOなど、使っていない機能を無効にする
2. 不要なVariantをビルドから除外する
UnityやURPのShader Stripping設定を確認し、使用しないVariantをビルドから除外します。より細かく制御したい場合は、IPreprocessShadersによる独自のStrippingも可能です。
ただし、実行時に切り替えるキーワードや、Addressables/AssetBundleから読み込むMaterialは、Playerビルド時の使用状況解析から見えにくい場合があります。必要なVariantまでStripすると、Unityが近いVariantへフォールバックしたり、見つからない場合はピンク色のエラーシェーダーになったりします。
3. 必要なVariantの初回使用を前倒しする
Shader Variant Collectionなどを使って必要なVariantを記録し、ロード画面などでPrewarmしておくと、ゲーム中の初回描画によるstallを減らせます。
ただし、PrewarmはVariant数を減らす仕組みではありません。初回使用時の処理をロード画面などへ前倒しするため、起動時間やメモリとのバランスが必要です。
また、Metal、Vulkan、DirectX 12などでは、シェーダーだけでなくレンダーステートを含むPSOの準備も必要です。そのため、ShaderVariantCollection.WarmUp()だけではstallを完全に防げない場合があります。
まとめ
-
Shader Variantは、キーワードの状態ごとに専用のシェーダープログラムを用意する仕組み - Variant数は、各キーワードセットの選択肢数の積として組み合わせ的に増える
- 増えすぎると、ビルド時間・アプリ容量・メモリ・ロード時間・初回描画時のstallに影響する
-
multi_compileとshader_featureは、実行時に切り替えるか、ビルド時にMaterialから使用状態を確認できるかで使い分ける - Strippingは「残すVariantを減らす対策」、Prewarmは「必要なVariantの初回準備を前倒しする対策」
Shader Variant対策では、**「何個作るか」「何個残すか」「いつGPU向けに準備するか」**を分けて考えることが重要です。