【Unity】【URP】Flip-Book Blendingに対応したParticle用シェーダを書く

UnityのURPで、Flip-Book Blendingに対応したParticle用シェーダを書く方法についてまとめました。

Unity2020.3.15f2

やりたいこと

ParticleのTexture Sheet Animationを使うとFlip-Book(パラパラ漫画)アニメーションができます。

light11.hatenadiary.com

さらにFlip-Book Blendingに対応したシェーダを使うとアニメーション間を補間して滑らかにできます。

light11.hatenadiary.com

本記事ではこのFlip-Book Blendingに対応したシェーダを記述する方法についてまとめます。
レンダリングパイプラインはUniversal Render Pipeline(URP)とします。
またGPUインスタンシングにも対応したシェーダを書くことを目標とします。

Flip-Book Blending対応シェーダを書く

まずGPUインスタンシングには対応せず、Flip-Book Blendingにのみ対応したシェーダを記述します。
この場合実装は単純で、Texture Sheet Animationを有効にすると、

  • Custom Vertex StreamsのUVでブレンド対象の1枚目のUV値を受け渡せる
  • Custom Vertex StreamsのUV2でブレンド対象の2枚目のUV値を受け渡せる
  • Custom Vertex StreamsのAnimBlendで上記2枚のブレンド率の値を受け渡せる

となります。
すなわちCustom Vertex Streamsを以下のように設定すれば任意のTEXCOORDを使って上記の値をシェーダに渡せます。

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

次にこの仕様に沿ってシェーダを書いていきます。
以下の通り、Flip-Book Blendingをマテリアルプロパティで有効/無効にできるシェーダを記述しました。

Shader "FlipBookBlendingExample"
{
    Properties
    {
        [MainTexture] _BaseMap("Base Map", 2D) = "white" {}
        [MainColor] _BaseColor("Base Color", Color) = (1, 1, 1, 1)
        [Toggle] _FlipBookBlending ("Flip-Book Blending", Float) = 0
    }

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

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma shader_feature_local _FLIPBOOKBLENDING_ON

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes
            {
                float4 positionOS : POSITION;
                
                #if defined(_FLIPBOOKBLENDING_ON)
                    float4 texcoords : TEXCOORD0;
                    float animBlend : TEXCOORD1;
                #else
                    float2 texcoord : TEXCOORD0;
                #endif
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                #if defined(_FLIPBOOKBLENDING_ON)
                    float4 texcoords : TEXCOORD0;
                    float animBlend : TEXCOORD1;
                #else
                    float2 texcoord : TEXCOORD0;
                #endif
            };

            sampler2D _BaseMap;
            float4 _BaseMap_ST;
            half4 _BaseColor;
            
            Varyings vert(Attributes input)
            {
                Varyings output;
                output.positionHCS = TransformObjectToHClip(input.positionOS.xyz);
                #if defined(_FLIPBOOKBLENDING_ON)
                    output.texcoords.xy = TRANSFORM_TEX(input.texcoords.xy, _BaseMap);
                    output.texcoords.zw = TRANSFORM_TEX(input.texcoords.zw, _BaseMap);
                    output.animBlend = input.animBlend;
                #else
                    output.texcoord = TRANSFORM_TEX(input.texcoord, _BaseMap);
                #endif
                return output;
            }

            half4 frag(Varyings input) : SV_Target
            {
                #if defined(_FLIPBOOKBLENDING_ON)
                    // 二つのテクスチャの値をanimBlendで補間
                    const float4 color1 = tex2D(_BaseMap, input.texcoords.xy) * _BaseColor;
                    const float4 color2 = tex2D(_BaseMap, input.texcoords.zw) * _BaseColor;
                    return lerp(color1, color2, input.animBlend);
                #else
                    return tex2D(_BaseMap, input.texcoord) * _BaseColor;
                #endif
            }
            ENDHLSL
        }
    }
}

これを適用すると以下のような結果となります。

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

マテリアルプロパティによりブレンドの有無が切り替わっていることが確認できます。

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

さて次にこれをGPUインスタンシングにも対応します。
GPUインスタンシングを使う場合には、上記のAnimBlendではなく、現在のテクスチャのインデックスを表すAnimFrameを使って処理する必要があります。
場合分けが多くのなるためシェーダは以下のようにかなり複雑になります。

Shader "InstancedFlipBookBlendingExample"
{
    Properties
    {
        [MainTexture] _BaseMap("Base Map", 2D) = "white" {}
        [MainColor] _BaseColor("Base Color", Color) = (1, 1, 1, 1)
        [Toggle] _FlipBookBlending ("Flip-Book Blending", Float) = 0
    }

    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
            #pragma shader_feature_local _FLIPBOOKBLENDING_ON
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            // UNITY_PARTICLE_INSTANCE_DATAを使いたいのでこれをインクルードする
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/ParticlesInstancing.hlsl"
            
            struct Attributes
            {
                float4 positionOS : POSITION;

                // Flip-Book Blendingが有効かつGPUインスタンシングが無効の場合のみAnimBlendの値を使う
                #if defined(_FLIPBOOKBLENDING_ON) && !defined(UNITY_PARTICLE_INSTANCING_ENABLED)
                    float4 texcoords : TEXCOORD0;
                    float texcoordBlend : TEXCOORD1;
                #else
                    float2 texcoords : TEXCOORD0;
                #endif
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                float2 texcoord : TEXCOORD0;

                #if defined(_FLIPBOOKBLENDING_ON)
                    float3 texcoord2AndBlend    : TEXCOORD1;
                #endif
            };

            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);
            float4 _BaseMap_ST;
            half4 _BaseColor;

            // Particles.hlslからコピーしたもの
            void GetParticleTexcoords(out float2 outputTexcoord, out float3 outputTexcoord2AndBlend, in float4 inputTexcoords, in float inputBlend)
            {
                #if defined(UNITY_PARTICLE_INSTANCING_ENABLED)
                if (unity_ParticleUVShiftData.x != 0.0)
                {
                    UNITY_PARTICLE_INSTANCE_DATA data = unity_ParticleInstanceData[unity_InstanceID];

                    float numTilesX = unity_ParticleUVShiftData.y;
                    float2 animScale = unity_ParticleUVShiftData.zw;
                    #ifdef UNITY_PARTICLE_INSTANCE_DATA_NO_ANIM_FRAME
                        float sheetIndex = 0.0;
                    #else
                        float sheetIndex = data.animFrame;
                    #endif

                    float index0 = floor(sheetIndex);
                    float vIdx0 = floor(index0 / numTilesX);
                    float uIdx0 = floor(index0 - vIdx0 * numTilesX);
                    float2 offset0 = float2(uIdx0 * animScale.x, (1.0 - animScale.y) - vIdx0 * animScale.y);

                    outputTexcoord = inputTexcoords.xy * animScale.xy + offset0.xy;

                    #ifdef _FLIPBOOKBLENDING_ON
                        float index1 = floor(sheetIndex + 1.0);
                        float vIdx1 = floor(index1 / numTilesX);
                        float uIdx1 = floor(index1 - vIdx1 * numTilesX);
                        float2 offset1 = float2(uIdx1 * animScale.x, (1.0 - animScale.y) - vIdx1 * animScale.y);

                        outputTexcoord2AndBlend.xy = inputTexcoords.xy * animScale.xy + offset1.xy;
                        outputTexcoord2AndBlend.z = frac(sheetIndex);
                    #endif
                }
                else
                #endif
                {
                    outputTexcoord = inputTexcoords.xy;
                #ifdef _FLIPBOOKBLENDING_ON
                    outputTexcoord2AndBlend.xy = inputTexcoords.zw;
                    outputTexcoord2AndBlend.z = inputBlend;
                #endif
                }

                #ifndef _FLIPBOOKBLENDING_ON
                    outputTexcoord2AndBlend.xy = inputTexcoords.xy;
                    outputTexcoord2AndBlend.z = 0.5;
                #endif
            }

            void GetParticleTexcoords(out float2 outputTexcoord, in float2 inputTexcoord)
            {
                float3 dummyTexcoord2AndBlend = 0.0;
                GetParticleTexcoords(outputTexcoord, dummyTexcoord2AndBlend, inputTexcoord.xyxy, 0.0);
            }
            
            Varyings vert(Attributes input)
            {
                Varyings output;
                UNITY_SETUP_INSTANCE_ID(input);
                output.positionHCS = TransformObjectToHClip(input.positionOS.xyz);

                // インスタンシングとFlip-Book Blendingの条件により使用するオーバーロードを変える
                #if defined(_FLIPBOOKBLENDING_ON)
                #if defined(UNITY_PARTICLE_INSTANCING_ENABLED)
                    GetParticleTexcoords(output.texcoord, output.texcoord2AndBlend, input.texcoords.xyxy, 0.0);
                #else
                    GetParticleTexcoords(output.texcoord, output.texcoord2AndBlend, input.texcoords, input.texcoordBlend);
                #endif
                #else
                    GetParticleTexcoords(output.texcoord, input.texcoords.xy);
                #endif
                
                return output;
            }

            half4 frag(Varyings input) : SV_Target
            {
                float3 blendUv = 0;
                #if defined(_FLIPBOOKBLENDING_ON)
                    blendUv = input.texcoord2AndBlend;
                #endif
                            
                half4 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.texcoord);
                #ifdef _FLIPBOOKBLENDING_ON
                    // Flip-Book Blendingが有効な時のみブレンドする
                    half4 color2 = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, blendUv.xy);
                    color = lerp(color, color2, blendUv.z);
                #endif
                return color;
            }
            ENDHLSL
        }
    }
}

ポイントはGetParticleTexcoordsです。
これはParticles.hlslからコピーしたものですが、この中でインスタンシングの場合にAnimFrameなどを使ってFlip-Book Blendingを行う処理を行なっています。

このシェーダを使ってGPUインスタンシングを効かせた状態でFlip-Book Blendingするには以下の設定を行います。

  • マテリアルプロパティからFlip-Book Blendingを有効にする
  • Particle SystemのRendererモジュールのRender ModeをMeshにして適当なメッシュをアサイ
  • Particle SystemのRendererモジュールのEnable Mesh GPU Instancingを有効にする
  • Custom Vertex StreamsにAnimFrameをInstanced.xとして渡す

この設定で再生した結果が以下となります。

f:id:halya_11:20210831154520g:plain
Instancing

正常に描画されていることが確認できました。

関連

light11.hatenadiary.com

light11.hatenadiary.com