Shader Variantについて調べてみた
Unityシェーダーの基本についてはこちらでスライドにまとめています
今回は上記のスライドの内容では扱っていないシェーダーのバリアントについてです。
色々とググッては見たのですが詳しく書いてあるサイトが見つからなかったため、色々と実験してやっと理解できました。 もし間違っている部分がありましたらご指摘下さると幸いです。
今回テストで作ったリポジトリは以下になります。必要があればダウンロードして見てみてください。
公式(日本語)の説明では
多くの場合、固定されたシェーダーコードの断片の大部分を保持するだけでなく、わずかに異なるシェーダー“変異体”を製造ができるようにしておくと便利です。一般に“メガシェーダー”や“ウーバーシェーダー”と呼ばれ、おのおののケースのために異なるプリプロセッサー指令でシェーダーコード複数回コンパイルすることで達成されます。
Unity では、これは シェーダースニペットに #pragma multi_compile または #pragma shader_feature を追加することによって達成することができます。これも サーフェースシェーダー で動作します。
実行時には、適切なシェーダーバリアントは、マテリアルのキーワード(Material.EnableKeyword と DisableKeyword)またはグローバルシェーダーキーワード( Shader.EnableKeyword と DisableKeyword )からピックアップされます。
とあります。ここまではなんとなーく頭に入るのですが、このあとの説明がよくわからない。。。 公式のマニュアルではバリアントの用法について詳細に書かれていないので、これを調査しました。 以下ではバリアントの説明と用法について書いていきます。
バリアントを作るコンパイル命令について
部分的に異なるシェーダーをコンパイル時に生成する命令があり、それが
- #pragma multi_compile
- #pragma shader_feature
という2つのコンパイル命令になります。この2つのコンパイル命令の違いは後述するとして、ここからはmulti_compile命令を例に説明していきます。
この命令の使用方法は以下の形になります。
#pragma multi_compile _ Lighting_ON Lighting_OFF
multi_compileには、自由にいくつかのキーワードを指定することができます。今回の場合
- _
- Lighting_ON
- Lighting_OFF
の3つのキーワードを指定しています。このうちLighting_ONとLighting_OFFはプリプロセッサディレクティブを用いて処理を分岐することができるようになります。
#ifdef Lighting_ON //ライティングONの時の処理 #elif Lighting_OFF //ライティングOFFの時の処理 #endif
このような感じで、Lighting_ONシンボルがONのとき、Lighting_OFFシンボルがONのときの処理を分けて書くことができます。
さて、先ほどのキーワードの中には_(アンダースコアのみ)といったキーワードも指定していました。これは何かというと、プリプロセッサマクロが定義されていない場合のバリアントを生成します。名前すべてがアンダースコアで形成されている場合(_とか__とか___とか)にこのような意味を持つことになります。
バリアントを定義すると、コンパイル時に複数のシェーダーコードを自動的に生成してほしいという命令をコンパイラに伝えることができます。
実際にシェーダーを書いてみる
言葉で表そうとすると難しいですが、実際に見てみると話は単純です。 以下に簡単な例をあげてみます。
Shader "Unlit/Test1" { SubShader { Pass { CGPROGRAM #pragma vertex vert_img #pragma fragment frag #include "UnityCG.cginc" #pragma multi_compile RED GREEN BLUE #pragma debug struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; fixed4 frag (v2f i) : SV_Target { fixed4 col = float4(0, 0, 0, 1); #ifdef RED col = float4(1, 0, 0, 1); #elif GREEN col = float4(0, 1, 0, 1); #elif BLUE col = float4(0, 0, 1, 1); #endif return col; } ENDCG } } }
実際にmulti_compile命令を使ったシェーダーの例です。
#pragma multi_compile RED GREEN BLUE
では
- RED
- GREEN
- BLUE
の3つのキーワードを指定しています。そして、フラグメントシェーダーの中で
#ifdef RED col = float4(1, 0, 0, 1); #elif GREEN col = float4(0, 1, 0, 1); #elif BLUE col = float4(0, 0, 1, 1); #endif
というように、multi_compileで定義したキーワードを使って処置を分岐させています。 このシェーダーをコンパイルしてみると
// Compiled shader for custom platforms, uncompressed size: 6.4KB Shader "Unlit/Test1" { SubShader { // Stats for Vertex shader: // d3d11 : 4 math Pass { GpuProgramID 59568 Program "vp" { SubProgram "d3d11 " { // Stats: 4 math Keywords { "RED" } Bind "vertex" Vertex Bind "texcoord" TexCoord0 ConstBuffer "UnityPerDraw" 352 Matrix 0 [glstate_matrix_mvp] BindCB "UnityPerDraw" 0 " ~省略~ " } SubProgram "d3d11 " { // Stats: 4 math Keywords { "GREEN" } Bind "vertex" Vertex Bind "texcoord" TexCoord0 ConstBuffer "UnityPerDraw" 352 Matrix 0 [glstate_matrix_mvp] BindCB "UnityPerDraw" 0 " ~省略~ " } SubProgram "d3d11 " { // Stats: 4 math Keywords { "BLUE" } Bind "vertex" Vertex Bind "texcoord" TexCoord0 ConstBuffer "UnityPerDraw" 352 Matrix 0 [glstate_matrix_mvp] BindCB "UnityPerDraw" 0 " ~省略~ " } } Program "fp" { SubProgram "d3d11 " { Keywords { "RED" } " ~省略~ " } SubProgram "d3d11 " { Keywords { "GREEN" } " ~省略~ " } SubProgram "d3d11 " { Keywords { "BLUE" } " ~省略~ " } } } } }
長いので省略していますがこんなコードにコンパイルされているのが確認できます。 注目すべきなのは、各プラットフォームごとのKeywardsセットです。
- Keywords { "RED" }
- Keywords { "GREEN" }
- Keywords { "BLUE" }
といった記述が確認できます。 バリアントを指定してコンパイルすることで、このように別々のKeywardsセットを持った複数のシェーダーにコンパイルすることができます。
この場合、特定のプラットフォームでゲームを実行すると
- RED
- GREEN
- BLUE
といったシンボルを別々に持った3つのシェーダーが使用できる。ということになります。
3つのシェーダーを使ってみる
先ほど作ったシェーダーを使ってみましょう。 以下のコードを適当なオブジェクトにアタッチしてみます。
using System.Collections; using UnityEngine; public class NewBehaviourScript : MonoBehaviour { public void OnGUI() { KeywordButtonGUI("RED"); KeywordButtonGUI("GREEN"); KeywordButtonGUI("BLUE"); } private void KeywordButtonGUI(string keyword) { if(GUILayout.Button(keyword + " : ON")) Shader.EnableKeyword(keyword); if(GUILayout.Button(keyword + " : OFF")) Shader.DisableKeyword(keyword); GUILayout.Label(keyword + " Enabled : " + Shader.IsKeywordEnabled(keyword).ToString()); } }
このスクリプトではOnGUIで、RED,GREEN,BLUEキーワードをそれぞれ有効/無効にするボタンを配置しています。
実際にボタンを押して動作を確認すると、以下のようにシェーダーの処理を切り替えることができていることが分かります。
まとめ
- シェーダーバリアントによって複数のシェーダーをコンパイルする命令を記述する事ができる
- シェーダーバリアントキーワードを使ってプリプロセッサディレクティブで各々の処理を分ける
- シェーダーの切り替えはShaderクラスやMaterialインスタンスのEnableKeyword/DisableKeywordメソッドで行うことができる
ちょっと応用的な内容
shader_feature と multi_compile の違い
#pragma shader_feature は、#pragma multi_compile と非常によく似ています。唯一の違いは、shader_featureのシェーダーの未使用のバリアントがゲームのビルドに含まれないことです。なので、shader_featureのキーワードの指定はマテリアル単位で設定され、multi_compile はグローバルコード(Shader.[ En | Dis ]ableKeyword)から設定するのが理想的です。
さらに、shader_featureではキーワードが1つの場合、簡略表記があります。
#pragma shader_feature FANCY_STUFF
この記述は#pragma shader_feature _ FANCY_STUFF のショートカットです。すなわち、2つのシェーダーバリアントに展開されます(最初のひとつは定義なし、もう1つは定義あり)。
バリアントの組み合わせ
multi_compileやshader_featureは複数定義する事ができます。例えば以下のように指定可能です。
#pragma multi_compile A B C #pragma multi_compile D E
最初の行で「A,B,C」3つのバリアントを生成し、2行目は「D,E」の2つのバリアントを生成しています。この場合コンパイルされるシェーダーは合計6つのシェーダー(A+ D、B + D、C + D、A+ E、B+ E、C+ E)を生成します。 複数に分けてバリアントを定義すると、そのバリアントの組み合わせの数だけシェーダーが生成されることになります。あまりに無茶をすると組合せ爆発的にシェーダーを生成しなければならなくなります。Unityでは(推測ですが恐らく)それを防ぐためにキーワードの種類は128個までという制限を設けています。
.cgincファイルで処理を書き、シェーダー側ではバリアントによって処理を分岐する
シェーダー記述の際のテクニックというかなんというかといった感じなんですが、よく使う関数やマクロ、構造体や変数は.cgincファイルにまとめておくと便利です。このバリアントでの処理の分岐は.cgincファイル内でも当然書くことができます。なので、シェーダーではバリアントを定義し、.cgincファイルでプリプロセッサディレクティブを使用して処理を分岐しておくことで、様々なシェーダーから利用可能な共通のインタフェースを持つ機能を作成することができます。これがものすごく便利です!。Unityのビルトインシェーダーでも一部でこのような使い方をしているのが確認できます。ぜひ参考にしてみてください。
ビルトインmultiple_compileショートカット
先程述べたように、ビルトインシェーダーでもバリアントを定義しているものがあります。これらのバリアントをレンダリングパスごとに一気に設定できるコンパイル命令が定義されています。
- multi_compile_fwdbaseは、ForwardBase(Forward Rendering ベース)pass 種類で必要とされるすべてのバリアントをコンパイルします。バリアントは、異なるライトマップタイプやシャドウをオン/オフに切り替える主要な異なるライトで対処します。
- multi_compile_fwdadd は、ForwardAdd (forward rendering additive) の pass タイプをコンパイルします。これは、次の種類(Directional、Point light、Spot)のライトを処理するためのバリアントとクッキーテクスチャによるバリアントをコンパイルします。
- multi_compile_fwdadd_fullshadows - 上記と同じですが、ライトがリアルタイムシャドウをつけるための機能も含まれています。
- multi_compile_fogは、異なるフォグ(霧)の種類(off/linear/exp/exp2)を処理するいくつかのバリアントに展開されます。
マテリアル単位でバリアントを指定する
スクリプトからキーワードを指定する必要が無い(オブジェクト生成時に一回だけ設定が反映されれば十分)場合、シェーダーのPropertyを使ってキーワード指定することが可能です。この値はシリアライズされるため、マテリアル毎にキーワードを保持する形になります。 シェーダーを以下のように書くことでバリアントをインスペクターから指定できるようになります(内部的にFloat値なのでこれ以外の方法でも数値さえ設定すればキーワードの変更が可能なのですが、KeywordEnum属性を使うとインスペクターが見やすい)。
Shader "Unlit/Test1" { Properties{ [KeywordEnum(RED,GREEN,BLUE)] _COLOR("COLOR KEYWORD", Float) = 0 } SubShader { Pass { CGPROGRAM #pragma vertex vert_img #pragma fragment frag #include "UnityCG.cginc" #pragma multi_compile _COLOR_RED _COLOR_GREEN _COLOR_BLUE struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; fixed4 frag (v2f i) : SV_Target { fixed4 col = float4(0, 0, 0, 1); #ifdef _COLOR_RED col = float4(1, 0, 0, 1); #elif _COLOR_GREEN col = float4(0, 1, 0, 1); #elif _COLOR_BLUE col = float4(0, 0, 1, 1); #endif return col; } ENDCG } } }