Particle SystemのGPUインスタンシングに対応したシェーダの書き方をまとめました。
- はじめに
- インスタンシングに対応していないシェーダを適当に用意する
- インスタンシングに対応する
- Custom Vertex Streams対応
- Anim Frameについて
- Custom Vertex Streamsを使いつつインスタンシングをOFFにしてもおかしくならないように
- インスタンシングON/OFF対応かつCustom Vertex StreamのON/OFF両対応(あまりスマートにできない)
- 参考
Unity2019.4.1
はじめに
本記事ではParticle SystemのGPUインスタンシングに対応したシェーダの書き方をまとめます。
Particle SystemのGPUインスタンシングの基礎知識については以下の記事にまとめていますので、必要に応じて参照してください。
インスタンシングに対応していないシェーダを適当に用意する
まずインスタンシングに対応していない状態のシェーダを用意します。
今回は下記のようなシンプルなシェーダを作りました。
Shader "ParticleSystemInstancing" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Transparent" "Queue"="Transparent" } Blend SrcAlpha OneMinusSrcAlpha ColorMask RGB Cull Off Lighting Off ZWrite Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float4 color : COLOR; float2 uv : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; float4 color : COLOR; float2 uv : TEXCOORD0; }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.color = v.color; o.uv = v.uv; return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv) * i.color; return col; } ENDCG } } }
何の変哲もないシンプルなシェーダです。
インスタンシングに対応する
さてそれでは前節のシェーダをParticle SystemのGPUインスタンシングに対応させます。
Shader "ParticleSystemInstancing" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Transparent" "Queue"="Transparent" } Blend SrcAlpha OneMinusSrcAlpha ColorMask RGB Cull Off Lighting Off ZWrite Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag // インスタンシング用バリアントを作る #pragma multi_compile_instancing // プロシージャルインスタンシングを有効化 #pragma instancing_options procedural:vertInstancingSetup #include "UnityCG.cginc" // 上記のvertInstancingSetupが定義されているcgincをインクルード #include "UnityStandardParticleInstancing.cginc" struct appdata { float4 vertex : POSITION; float4 color : COLOR; float2 uv : TEXCOORD0; // 頂点情報にインスタンスIDを追加 UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; float4 color : COLOR; float2 uv : TEXCOORD0; }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; // インスタンスIDを初期化 UNITY_SETUP_INSTANCE_ID(v); o.vertex = UnityObjectToClipPos(v.vertex); o.color = v.color; o.uv = v.uv; #ifdef UNITY_PARTICLE_INSTANCING_ENABLED // インスタンシング対象の値を取得 vertInstancingColor(o.color); o.color.rgb = min(1, o.color.rgb); // これを書かないとこのあとo.colorを加工する際に一部端末でおかしくなる気がする vertInstancingUVs(v.uv, o.uv); #endif return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv) * i.color; return col; } ENDCG } } }
コメントが記述してある部分が前節のシェーダからの変更点です。
細かい内容はコメントの内容を参照してください。
vertInstancingColor
やvertInstancingUVs
はUnityStandardParticleInstancing.cginc
に定義されている関数です。
この状態でインスタンシングのチェックボックスをトグルするとバッチングの数が変わるため、
正常にインスタンシングされていることが確認できます。
Custom Vertex Streams対応
さてここまでで基本的なインスタンシングには対応しているのですが、
Particle SystemのCustom Vertex Streams機能に対応する場合にはもう一工夫必要です。
今回はCustom Vertex Streamsにノイズの三次元情報を受け渡して使うシェーダを書いてみます。
Shader "ParticleSystemInstancing" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Transparent" "Queue"="Transparent" } Blend SrcAlpha OneMinusSrcAlpha ColorMask RGB Cull Off Lighting Off ZWrite Off Pass { CGPROGRAM // OpenGL ES 2.0を対象外にする // MyParticleInstanceData内にfloat3x4型を使うことにより自動的に追加されるコード #pragma exclude_renderers gles #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #pragma instancing_options procedural:vertInstancingSetup // 独自のインスタンシング用のデータ構造を定義する #define UNITY_PARTICLE_INSTANCE_DATA MyParticleInstanceData struct MyParticleInstanceData { float3x4 transform; uint color; float animFrame; // ここまではDefaultParticleInstanceDataに定義されているもの // ここから独自のデータを定義 float3 noise; }; #include "UnityCG.cginc" #include "UnityStandardParticleInstancing.cginc" struct appdata { float4 vertex : POSITION; float4 color : COLOR; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; float4 color : COLOR; float2 uv : TEXCOORD0; }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); o.vertex = UnityObjectToClipPos(v.vertex); o.color = v.color; o.uv = v.uv; #ifdef UNITY_PARTICLE_INSTANCING_ENABLED vertInstancingColor(o.color); o.color.rgb = min(1, o.color.rgb); // これを書かないとこのあとo.colorを加工する際に一部端末でおかしくなる気がする vertInstancingUVs(v.uv, o.uv); // 独自に定義したデータから取得する UNITY_PARTICLE_INSTANCE_DATA data = unity_ParticleInstanceData[unity_InstanceID]; o.color.rgb *= data.noise; #endif return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv) * i.color; return col; } ENDCG } } }
まず、インスタンシング対象のデータを表す構造体はUnityStandardParticleInstancing.cginc
に以下のように定義されています。
#ifndef UNITY_PARTICLE_INSTANCE_DATA #define UNITY_PARTICLE_INSTANCE_DATA DefaultParticleInstanceData #endif struct DefaultParticleInstanceData { float3x4 transform; uint color; float animFrame; };
従って、UnityStandardParticleInstancing.cginc
をインクルードするより前のコードに
UNITY_PARTICLE_INSTANCE_DATAを定義すれば独自のデータ構造を使うことができます。
今回は以下のようにDefaultParticleInstanceData
にnoiseを追加したものを定義しました。
struct MyParticleInstanceData { float3x4 transform; uint color; float animFrame; // ここまではDefaultParticleInstanceDataに定義されているもの // ここから独自のデータを定義 float3 noise; };
インスタンシングによる値を取得する方法は頂点シェーダのコメント部分を参照してください。
さて次にCustom Vertex Streamsの設定をしていきます。
Custom Vertex Streamsのチェックボックスを有効にすると以下のように4種類の値が設定されていることが確認できます。
まずanimFrameに渡す値が設定されていないのでUV > AnimFrame
から値を追加します。
続けてNoise > Sum.xyz
を追加します。
MyParticleInstanceData
に定義した順番に追加する点に注意してください。
これでCustom Vertex Streamsの対応が完了しました。
Noiseモジュールにチェックを付けると以下のようなレンダリング結果が得られます。
Anim Frameについて
さて前節のシェーダではMyParticleInstanceDataとして独自のデータ構造を定義しました。
この中身のうち、transform
とcolor
は座標と色を示し、必須の要素となります。
一方animFrame
はテクスチャシートアニメーションを使わない場合には消すことができます。
struct MyParticleInstanceData { float3x4 transform; // 必須 uint color; // 必須 float animFrame; // テクスチャシートアニメーションを使わないなら消せる float3 noise; // 独自定義のもの };
具体的にはUNITY_PARTICLE_INSTANCE_DATA_NO_ANIM_FRAMEを定義した上でanimFrame変数を削除するだけです。
Shader "ParticleSystemInstancing" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Transparent" "Queue"="Transparent" } Blend SrcAlpha OneMinusSrcAlpha ColorMask RGB Cull Off Lighting Off ZWrite Off Pass { CGPROGRAM #pragma exclude_renderers gles #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #pragma instancing_options procedural:vertInstancingSetup #define UNITY_PARTICLE_INSTANCE_DATA MyParticleInstanceData // テクスチャシートアニメーションを使わない場合はこれを定義 #define UNITY_PARTICLE_INSTANCE_DATA_NO_ANIM_FRAME struct MyParticleInstanceData { float3x4 transform; uint color; // float animFrame; float3 noise; }; #include "UnityCG.cginc" #include "UnityStandardParticleInstancing.cginc" struct appdata { float4 vertex : POSITION; float4 color : COLOR; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; float4 color : COLOR; float2 uv : TEXCOORD0; }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); o.vertex = UnityObjectToClipPos(v.vertex); o.color = v.color; o.uv = v.uv; #ifdef UNITY_PARTICLE_INSTANCING_ENABLED vertInstancingColor(o.color); o.color.rgb = min(1, o.color.rgb); vertInstancingUVs(v.uv, o.uv); UNITY_PARTICLE_INSTANCE_DATA data = unity_ParticleInstanceData[unity_InstanceID]; o.color.rgb *= data.noise; #endif return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv) * i.color; return col; } ENDCG } } }
この状態で、Custom Vertex StreamsからもAnimFrameを削除すると正常に表示されることが確認できます。
Custom Vertex Streamsを使いつつインスタンシングをOFFにしてもおかしくならないように
さて前節の状態でインスタンシングをOFFにすると表示に不具合が生じます。
またCustom Vertex Streamsにエラーメッセージが表示されていることが確認できます。
これはインスタンシングをOFFにした時のバリアントがCustom Vertex Streamsに対応できていないためです。
これに対応するためにシェーダを以下のように修正します。
Shader "ParticleSystemInstancing" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Transparent" "Queue"="Transparent" } Blend SrcAlpha OneMinusSrcAlpha ColorMask RGB Cull Off Lighting Off ZWrite Off Pass { CGPROGRAM #pragma exclude_renderers gles #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #pragma instancing_options procedural:vertInstancingSetup #define UNITY_PARTICLE_INSTANCE_DATA MyParticleInstanceData #define UNITY_PARTICLE_INSTANCE_DATA_NO_ANIM_FRAME struct MyParticleInstanceData { float3x4 transform; uint color; float3 noise; }; #include "UnityCG.cginc" #include "UnityStandardParticleInstancing.cginc" struct appdata { float4 vertex : POSITION; float4 color : COLOR; float2 uv : TEXCOORD0; #ifndef UNITY_PARTICLE_INSTANCING_ENABLED // インスタンシングOFFの場合にはnoiseを頂点の入力データとして定義 float3 noise : TEXCOORD1; #endif UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; float4 color : COLOR; float2 uv : TEXCOORD0; }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); o.vertex = UnityObjectToClipPos(v.vertex); o.color = v.color; o.uv = v.uv; #ifdef UNITY_PARTICLE_INSTANCING_ENABLED vertInstancingColor(o.color); o.color.rgb = min(1, o.color.rgb); vertInstancingUVs(v.uv, o.uv); UNITY_PARTICLE_INSTANCE_DATA data = unity_ParticleInstanceData[unity_InstanceID]; o.color.rgb *= data.noise; #else // インスタンシングが無効な時の処理 o.color.rgb *= v.noise; #endif return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv) * i.color; return col; } ENDCG } } }
これでシェーダの対応は完了です。
次に非インスタンシング状態のCustom Vertex Streamsの仕様に合わせてUVの値に適当な二次元の値(今回はUV2)を追加します。
これでCustom Vertex Streamsを使いつつインスタンシングの切り替えをすることが可能になりました。
インスタンシングをONにした時になぜかCustom Vertex Streamsにエラー表示が出ていますが、
Particle Systemのインスペクタの表示不具合な気がしてます(バリアントの判定処理がうまくいってない?)。
インスタンシングON/OFF対応かつCustom Vertex StreamのON/OFF両対応(あまりスマートにできない)
上述の通り、インスタンシングに対応したシェーダでCustom Vertex Streamsを使うには、
インスタンシングさせるデータの構造をCustom Vertex Streamsに合わせて定義する必要があります。
struct MyParticleInstanceData { float3x4 transform; uint color; float animFrame; float3 noise; };
ここでCustom Vertex Streamsを無効にした場合には、
シェーダ内で使われるデータ構造があらかじめ用意されているDefaultParticleInstanceData
と同じものでなければ
インスタンシングの際の並列計算の結果がおかしくなってしまうようです。
従ってCustom Vertex Streamsが有効かどうかによってデータ構造の定義を分ける必要がありますが、
「Custom Vertex Streamsが有効かどうか」を判定する手段は用意されていません。
このようなケースに対応するには以下のようにCustom Vertex Streamsが有効かどうかを示すキーワードを用意するなどの対応が必要そうです。
Shader "ParticleSystemInstancing" { Properties { _MainTex ("Texture", 2D) = "white" {} // 追加 [Toggle] _UseInstancingCustomVertexStreams ("Use Instancing And Custom Vertex Streams", Float) = 0 } SubShader { Tags { "RenderType"="Transparent" "Queue"="Transparent" } Blend SrcAlpha OneMinusSrcAlpha ColorMask RGB Cull Off Lighting Off ZWrite Off Pass { CGPROGRAM #pragma exclude_renderers gles #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #pragma instancing_options procedural:vertInstancingSetup #pragma multi_compile _ _USEINSTANCINGCUSTOMVERTEXSTREAMS_ON // インスタンシングかつCustom Vertex Streams使用フラグが立っているときのみ独自の構造体を定義 #ifdef _USEINSTANCINGCUSTOMVERTEXSTREAMS_ON #define UNITY_PARTICLE_INSTANCE_DATA MyParticleInstanceData #define UNITY_PARTICLE_INSTANCE_DATA_NO_ANIM_FRAME struct MyParticleInstanceData { float3x4 transform; uint color; float3 noise; }; #endif #include "UnityCG.cginc" #include "UnityStandardParticleInstancing.cginc" struct appdata { float4 vertex : POSITION; float4 color : COLOR; float2 uv : TEXCOORD0; // Custom Vertex Streams使用フラグが立っていないときのみ独自のデータ(ノイズ)を定義する #if !(defined(_USEINSTANCINGCUSTOMVERTEXSTREAMS_ON) && defined(UNITY_PARTICLE_INSTANCING_ENABLED)) float3 noise : TEXCOORD1; #endif UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; float4 color : COLOR; float2 uv : TEXCOORD0; }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); o.vertex = UnityObjectToClipPos(v.vertex); o.color = v.color; o.uv = v.uv; #ifdef UNITY_PARTICLE_INSTANCING_ENABLED vertInstancingColor(o.color); o.color.rgb = min(1, o.color.rgb); vertInstancingUVs(v.uv, o.uv); #endif // Custom Vertex Streams使用フラグが立っているときのみ独自のデータ(ノイズ)を適用する #if defined(_USEINSTANCINGCUSTOMVERTEXSTREAMS_ON) && defined(UNITY_PARTICLE_INSTANCING_ENABLED) UNITY_PARTICLE_INSTANCE_DATA data = unity_ParticleInstanceData[unity_InstanceID]; o.color.rgb *= data.noise; #else // インスタンシングが無効な時の処理 o.color.rgb *= v.noise; #endif return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv) * i.color; return col; } ENDCG } } }
ただこの方法だとCustom Vertex Streamsを使うときにParticle Systemとマテリアルの両方にチェックをしないといけないのであまりスマートな方法とは言えなさそうです。