【Unity】【URP】Custom Vertex Streams + GPUインスタンシングに対応したParticleSystem用シェーダを書く

UnityのURPでCustom Vertex Streams + GPUインスタンシングに対応したParticleSystem用シェーダを書く方法についてまとめました。

Unity2020.3.15f2

やりたいこと

今、Universal Render Pipeline(URP)のParticleSystemで、Noiseの値をCustom Vertex Streamsで渡して色として出力したいとします。

f:id:halya_11:20210901161055p:plain
NoiseをCustom Vertex Streamsで渡す

そしてさらにこれをGPU Instancingに対応することを考えます。
本記事ではこの実装方法についてまとめます。

GPUインスタンシングの基礎知識は以下の記事を参照してください。

light11.hatenadiary.com

また、Custom Vertex Streamsの基礎知識は以下の記事にまとめています。

light11.hatenadiary.com

Custom Vertex Streams + GPUインスタンシング対応シェーダ

さてそれではCustom Vertex StreamsとGPUインスタンシングに対応したシェーダを書いていきます。
Custom Vertex Streamsで与えた3次元の値をそのまま色として出力するシンプルなシェーダです。

Shader "InstancingExample"
{
    Properties
    {
        [MainTexture] _BaseMap("Base Map", 2D) = "white" {}
        [MainColor] _BaseColor("Base Color", Color) = (1, 1, 1, 1)
    }

    SubShader
    {
        Tags
        {
            "RenderType" = "Opaque"
             "IgnoreProjector" = "True"
             "PreviewType" = "Plane"
             "PerformanceChecks" = "False"
             "RenderPipeline" = "UniversalPipeline"
        }

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_instancing
            #pragma instancing_options procedural:ParticleInstancingSetup

            // ParticlesInstancing.cgincのDefaultParticleInstanceDataの代わりにこちらを使う設定
            // ParticlesInstancing.cgincのインクルードよりも先に書くこと
            #define UNITY_PARTICLE_INSTANCE_DATA MyDefaultParticleInstanceData
            struct MyDefaultParticleInstanceData
            {
                float3x4 transform;
                float3 foo;
            };
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            // ParticleのシェーダでGPUインスタンシング対応する場合にはこれをインクルードする
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/ParticlesInstancing.hlsl"
            
            struct Attributes
            {
                float4 positionOS : POSITION;
#ifndef UNITY_PARTICLE_INSTANCING_ENABLED
                float3 foo : TEXCOORD0;
#endif
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                float3 foo : TEXCOORD0;
            };

            Varyings vert(Attributes input)
            {
                Varyings output;
                
                UNITY_SETUP_INSTANCE_ID(input);
                
                output.positionHCS = TransformObjectToHClip(input.positionOS.xyz);

                #ifdef UNITY_PARTICLE_INSTANCING_ENABLED
                    // インスタンスのデータを取得する
                    UNITY_PARTICLE_INSTANCE_DATA data = unity_ParticleInstanceData[unity_InstanceID];
                    output.foo = data.foo;
                #else
                    output.foo = input.foo;
                #endif
                return output;
            }

            half4 frag(Varyings input) : SV_Target
            {
                return float4(abs(input.foo), 1);
            }
            ENDHLSL
        }
    }
}

ポイントはコメントの形で記述しました。
Custom Vertex Streamsで設定した値を、GPUインスタンシングが有効なときにはインスタンス情報として、
GPUインスタンシングが無効なときには頂点情報として取得しています。

挙動を確認する

それではここまでの実装の挙動を確認します。

まずParticle SystemのNoiseモジュールを有効にして適当に設定をしておきます。
次にCustom Vertex Streamsを以下のように設定します。

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

さらにGPUインスタンシングを確認できるようにRender ModeはMeshにして適当なメッシュを設定しておきます。

この状態で再生すると以下の結果が得られます。

f:id:halya_11:20210901171916g:plain
結果

GPUインスタンシングをON/OFFしても同様の結果が得られることを確認できました。

フラグメントシェーダでインスタンシングされた情報を取得する

さて前節では頂点シェーダでインスタンスの情報を取得しました。
フラグメントシェーダにインスタンス情報を渡す場合には以下のようにもう少し対応が必要です。

Shader "InstancingExampleFragment"
{
    Properties
    {
        [MainTexture] _BaseMap("Base Map", 2D) = "white" {}
        [MainColor] _BaseColor("Base Color", Color) = (1, 1, 1, 1)
    }

    SubShader
    {
        Tags
        {
            "RenderType" = "Opaque"
             "IgnoreProjector" = "True"
             "PreviewType" = "Plane"
             "PerformanceChecks" = "False"
             "RenderPipeline" = "UniversalPipeline"
        }

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_instancing
            #pragma instancing_options procedural:ParticleInstancingSetup

            #define UNITY_PARTICLE_INSTANCE_DATA MyDefaultParticleInstanceData
            struct MyDefaultParticleInstanceData
            {
                float3x4 transform;
                float3 foo;
            };
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/ParticlesInstancing.hlsl"
            
            struct Attributes
            {
                float4 positionOS : POSITION;
#ifndef UNITY_PARTICLE_INSTANCING_ENABLED
                float3 foo : TEXCOORD0;
#endif
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
#ifndef UNITY_PARTICLE_INSTANCING_ENABLED
                float3 foo : TEXCOORD0;
#endif
                // インスタンスID受け渡し用
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            Varyings vert(Attributes input)
            {
                Varyings output;
                
                UNITY_SETUP_INSTANCE_ID(input);
                // VertexからFragmentへインスタンスIDを渡す
                UNITY_TRANSFER_INSTANCE_ID(input, output);
                
                output.positionHCS = TransformObjectToHClip(input.positionOS.xyz);
                #ifndef UNITY_PARTICLE_INSTANCING_ENABLED
                    output.foo = input.foo;
                #endif

                return output;
            }

            half4 frag(Varyings input) : SV_Target
            {
                // インスタンスID初期化
                UNITY_SETUP_INSTANCE_ID(input);
                #ifdef UNITY_PARTICLE_INSTANCING_ENABLED
                    // インスタンスデータを取得
                    UNITY_PARTICLE_INSTANCE_DATA data = unity_ParticleInstanceData[unity_InstanceID];
                    return float4(abs(data.foo), 1);
                #else
                    return float4(abs(input.foo), 1);
                #endif
            }
            ENDHLSL
        }
    }
}

主な変更点はコメントに記述しました。
要は頂点シェーダからフラグメントシェーダにインスタンスIDを受け渡しているだけですが、
そのためのマクロがUnity側でいくつか用意されているのでそれを使っています。

Flip-Book BlendingとGPUインスタンシング対応

本記事では任意のデータをCustom Vertex Streamsとしてシェーダに渡す方法について紹介しました。

これに対してFlip-Book Blendingをする場合、AnimBlendやAnimFrameといった情報をCustom Vertex Streamsに渡す必要があります。
さらにGPUインスタンシングと組み合わせると非常にややこしくなるため、これは別記事として書きました。

light11.hatenadiary.com

関連

light11.hatenadiary.com

light11.hatenadiary.com