【Unity】【シェーダ】Particle SystemのGPUインスタンシングに対応したシェーダを書く

Particle SystemのGPUインスタンシングに対応したシェーダの書き方をまとめました。

Unity2019.4.1

はじめに

本記事ではParticle SystemのGPUインスタンシングに対応したシェーダの書き方をまとめます。

Particle SystemのGPUインスタンシングの基礎知識については以下の記事にまとめていますので、必要に応じて参照してください。

light11.hatenadiary.com

インスタンシングに対応していないシェーダを適当に用意する

まずインスタンシングに対応していない状態のシェーダを用意します。
今回は下記のようなシンプルなシェーダを作りました。

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
        }
    }
}

何の変哲もないシンプルなシェーダです。

f:id:halya_11:20200627172740g:plain
何の変哲もない見た目

インスタンシングに対応する

さてそれでは前節のシェーダを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
        }
    }
}

コメントが記述してある部分が前節のシェーダからの変更点です。
細かい内容はコメントの内容を参照してください。
vertInstancingColorvertInstancingUVsUnityStandardParticleInstancing.cgincに定義されている関数です。

この状態でインスタンシングのチェックボックスをトグルするとバッチングの数が変わるため、
正常にインスタンシングされていることが確認できます。

f:id:halya_11:20200627173924g:plain
インスタンシング切り替え

Custom Vertex Streams対応

さてここまでで基本的なインスタンシングには対応しているのですが、
Particle SystemのCustom Vertex Streams機能に対応する場合にはもう一工夫必要です。

f:id:halya_11:20200627175800p:plain
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種類の値が設定されていることが確認できます。

f:id:halya_11:20200627175800p:plain
Custom Vertex Streams

まずanimFrameに渡す値が設定されていないのでUV > AnimFrameから値を追加します。
続けてNoise > Sum.xyzを追加します。
MyParticleInstanceDataに定義した順番に追加する点に注意してください。

f:id:halya_11:20200627180811p:plain
AnimFrameとSum.xyzを追加

これでCustom Vertex Streamsの対応が完了しました。
Noiseモジュールにチェックを付けると以下のようなレンダリング結果が得られます。

f:id:halya_11:20200627181104g:plain
Custom Vertex Streamsについて

Anim Frameについて

さて前節のシェーダではMyParticleInstanceDataとして独自のデータ構造を定義しました。
この中身のうち、transformcolorは座標と色を示し、必須の要素となります。
一方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を削除すると正常に表示されることが確認できます。

f:id:halya_11:20200627181736p:plain
AnimFrame

Custom Vertex Streamsを使いつつインスタンシングをOFFにしてもおかしくならないように

さて前節の状態でインスタンシングをOFFにすると表示に不具合が生じます。
またCustom Vertex Streamsにエラーメッセージが表示されていることが確認できます。

f:id:halya_11:20200627182201g:plain
表示不具合とエラー

これはインスタンシングを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)を追加します。

f:id:halya_11:20200627183721p:plain
UV2を追加

これでCustom Vertex Streamsを使いつつインスタンシングの切り替えをすることが可能になりました。
インスタンシングをONにした時になぜかCustom Vertex Streamsにエラー表示が出ていますが、
Particle Systemのインスペクタの表示不具合な気がしてます(バリアントの判定処理がうまくいってない?)。

f:id:halya_11:20200627183613g:plain
Custom Vertex Streams + インスタンシング切り替え

インスタンシング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とマテリアルの両方にチェックをしないといけないのであまりスマートな方法とは言えなさそうです。

参考

docs.pumachen.xyz