跳到正文
Qiita · Unity 开发社区· 来源· yuri_tsukimi·· 3 小时前AI 评分31

Unity GPU粒子模拟纹理紧凑化的并行实现

【Unity】GPUパーティクルのシミュレーションテクスチャのコンパクト化の並列化

正文 · AI 翻译

在分层稀疏数据纹理的同时紧密填充。

result.png

概要

在使用 GPU 粒子动态生成粒子时,需要定期整理模拟纹理以丢弃已死亡的粒子。
此时有一种著名的便捷方法。

本文不详细解释该方法本身,简而言之:

  1. 创建计数纹理,并在有效粒子的位置写入 "1"。
  2. 为计数纹理生成 Mipmap。
  3. 基于 Z-Curve(Z-Order)的思想,可以得知任意第 n 个有效粒子的位置。
  4. 在原始模拟纹理的粒子所求出的位置进行排列。

大致就是这样。

这种方法只需要计数纹理中的1个通道,因此如果使用多个通道,就可以将多种类型的粒子分别整理(紧凑化)。这是一种常规的思路,但因为它很方便,所以这里说明一下步骤。

计数纹理的构成

所谓说明,其实只是简单地增加一个独立的维度,因此理论上没有什么需要特别解释的。只是在计数纹理中查找第 n 个粒子时,需要考虑在此之前未计数的粒子的偏移量而已。

另一方面,为了缩小计数纹理的尺寸,有一些巧妙的做法,因此下面说明计数纹理的构成。

在本文中,我们将把16种粒子分别进行紧凑化。

假设使用RGBA的4个通道分别进行计数,那么如果有4张计数纹理,就可以对16种粒子分别进行计数。为了降低渲染成本,我们将这4张计数纹理合并为1张计数纹理来处理。

此外,在向计数纹理写入值时,将16个粒子作为1个单位
分配给计数纹理的一个纹素。由于使用Mipmaps的计数方法即使写入的值不是"1"也能准确计数,因此如果存在16个粒子,写入"16"即可进行计数。

在源代码中的命名方面,我们将4x4=16个计数的单位称为“Unit”,按Unit为单位计数得到的、对应1种粒子的计数纹理称为“Block”,将Block分配到RGBA上得到的、对应4种粒子的计数纹理称为“Group”,并将粒子的种类称为“Spec”。

structure.png

要查找第n个粒子时,

  1. 需分别求出所有种类粒子的累积和。
  2. 确定第 n 个粒子属于哪种类型的粒子(即指定对应的 Block)。
  3. 按照 d4rkpl4y3r 的方法,在对应的 Block 中查找包含第 (n - (累积和)) 个粒子的 Unit。
  4. 在该 Unit 中查找第 (n - (累积和')) 个粒子。

具体步骤如下。在步骤 4 中,我们参考原始的“Particle Data Texture”来查找有效的粒子。此处不再赘述,另一种方法是在创建计数纹理时预先准备一个用于标志位的纹理。

search.png

実装

这是为 Render Texture 实现的。(Custom Render Texture 也是同样的方式。)

CompactSparseTextureInParallel.shader

CompactSparseTextureInParallel.shader

// ref: https://github.com/d4rkc0d3r/CompactSparseTextureDemo/tree/main

Shader "Test/CompactSparseTextureInParallel"
{
    Properties
    {
        _ActiveRatio ("Active Ratio", Range(0,1)) = 0.5
        [PowerSlider(5)] _SpecBias ("Particle Spec Distribution Bias", Range(0.1,10)) = 1
        [NoScaleOffset] _ParticleDataTexture ("Particle Data Texture", 2D) = "black" {}
        [NoScaleOffset] _CountTexture ("Count Texture", 2D) = "black" {}
    }

    CGINCLUDE

    /*** 汎用関数: Morton Index *************************************************************/

    uint Interleave2d( uint2 a )
    {
        // ref: https://lemire.me/blog/2018/01/08/how-fast-can-you-bit-interleave-32-bit-integers/
        a = ( a | ( a << 8 ) ) & 0x00FF00FF;  // 0b00000000111111110000000011111111
        a = ( a | ( a << 4 ) ) & 0x0F0F0F0F;  // 0b00001111000011110000111100001111
        a = ( a | ( a << 2 ) ) & 0x33333333;  // 0b00110011001100110011001100110011
        a = ( a | ( a << 1 ) ) & 0x55555555;  // 0b01010101010101010101010101010101
        return a.x | ( a.y << 1 );
    }

    uint2 Deinterleave2d( uint b )
    {
        // ref: https://stackoverflow.com/questions/3137266/how-to-de-interleave-bits-unmortonizing
        uint2 a = uint2( b, b >> 1 );
        a = ( a              ) & 0x55555555;  // 0b01010101010101010101010101010101
        a = ( a | ( a >> 1 ) ) & 0x33333333;  // 0b00110011001100110011001100110011
        a = ( a | ( a >> 2 ) ) & 0x0F0F0F0F;  // 0b00001111000011110000111100001111
        a = ( a | ( a >> 4 ) ) & 0x00FF00FF;  // 0b00000000111111110000000011111111
        a = ( a | ( a >> 8 ) ) & 0x0000FFFF;  // 0b00000000000000001111111111111111
        return a;
    }

    uint2 MortonIndex2d( uint index1d )
    {
        return Deinterleave2d( index1d );
    }

    uint DemortonIndex2d( uint2 index2d )
    {
        return Interleave2d( index2d );
    }

    /*** 汎用関数: 乱数 *********************************************************************/

    float TimeHash1( float seed, float time, float offset, float chainedHash = 0.0 )
    {
        /*
         * IbukiHash by Andante (https://twitter.com/andanteyk)
         *   This work is marked with CC0 1.0. To view a copy of this license,
         *   visit https://creativecommons.org/publicdomain/zero/1.0/
         *
         * ref: https://andantesoft.hatenablog.com/entry/2024/12/19/193517
         *
         * Modified by Yuri Tsukimi (2026/10).
         */

        uint4 mult = uint4( 0xae3cc725, 0x9fe72885, 0xae36bfb5, 0x82c1fcad );

        uint4 u = asuint( float4( seed, time, offset, chainedHash ) );
        u = u * mult;
        u ^= u.wxyz ^ u >> 13;

        uint r = dot( u, mult );

        r ^= r >> 11;
        r = ( r * r ) ^ r;
            
        return float( r ) / 4294967295.0f;  // 2^32 - 1
    }

    float2 TimeHash2( float seed, float time, float offset )
    {
        float2 result;
        result.x = TimeHash1( seed, time, offset );
        result.y = TimeHash1( seed, time, offset, result.x );
        return result;
    }

    /*** 汎用関数: 色 ***********************************************************************/

    float3 h2rgb( float h )
    {
        return saturate( abs( frac( h + float3( 0, 2, 1 ) / 3.0 ) * 6.0 - 3.0 ) - 1.0 );
    }

    /***************************************************************************************/

    ENDCG

    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        CGINCLUDE

        #include "UnityCG.cginc"

        struct appdata
        {
            float4 vertex : POSITION;
            float2 uv : TEXCOORD0;
        };

        struct v2f
        {
            float4 vertex : SV_POSITION;
            float2 uv : TEXCOORD0;
        };

        v2f vert ( appdata v )
        {
            v2f o;
            o.vertex = UnityObjectToClipPos( v.vertex );
            o.uv = v.uv;
            return o;
        }

        /*** 共通定義 ***********************************************************************/

        // ParticleDataTexture の 4x4 の範囲のセルを CountTexture の1テクセルに要約する.
        // ユニットは正方形であることを前提とする.
        #define COUNT_UNIT_WIDTH     4
        #define COUNT_UNIT_HEIGHT    COUNT_UNIT_WIDTH
        #define COUNT_UNIT_SIZE      ( COUNT_UNIT_WIDTH * COUNT_UNIT_HEIGHT )
        // 16種類のパーティクルをそれぞれコンパクト化する
        #define PARTICLE_SPEC_COUNT  16
        // 1テクセルの次元
        #define VECTOR_DIMENSION     4

        Texture2D<float4> _ParticleDataTexture;
        float4 _ParticleDataTexture_TexelSize;
        Texture2D<float4> _CountTexture;
        float4 _CountTexture_TexelSize;
        SamplerState sampler_point_clamp;
        float _ActiveRatio;
        float _SpecBias;

        /*** パーティクルの定義 **************************************************************/

        struct ParticleData
        {
            // パーティクルの種類.この値ごとにパーティクルを Compaction する.
            uint spec;
            // アクティブかどうかのフラグ
            bool isActive;
            // パーティクルの色
            float3 color;
        };

        ParticleData GetParticleData( uint2 index2d )
        {
            float2 uv = ( index2d + 0.5 ) * _ParticleDataTexture_TexelSize.xy;
            float4 data = _ParticleDataTexture.SampleLevel( sampler_point_clamp, uv, 0 );

            ParticleData pd;
            pd.spec = uint( data.w ) >> 1;              // 2ビット目以降が spec
            pd.isActive = ( uint( data.w ) & 1 ) != 0;  // 1ビット目が isActive
            pd.color = data.xyz;

            return pd;
        }
        
        ParticleData GetParticleData( uint index )
        {
            return GetParticleData( MortonIndex2d( index ) );
        }

        float4 SetParticleData( ParticleData pd )
        {
            float4 data;
            data.xyz = pd.color;
            data.w = ( pd.spec << 1 ) | ( pd.isActive ? 1 : 0 );

            return data;
        }

        /***********************************************************************************/

        ENDCG

        Pass
        {
            Name "Particle Data"

            /*
             * ランダムな値をセットする Pass
             */

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            float4 frag ( v2f IN ) : SV_Target
            {
                float2 uv = IN.uv;

                // パーティクルのデータ
                ParticleData particleData;
                {
                    // 乱数で適当に初期化することにする.
                    float2 rnd = TimeHash2( uv.x, _Time.y, uv.y );

                    // このパーティクルの種類.ここに設定した種類ごとにコンパクト化を行う.
                    float r = pow( rnd.x, _SpecBias );
                    uint spec = floor( r * PARTICLE_SPEC_COUNT );
                    spec = clamp( spec, 0, PARTICLE_SPEC_COUNT - 1 );
                    particleData.spec = spec;

                    // このパーティクルがアクティブかどうか.適当な値を入れておく.
                    bool isActive = ( rnd.y < _ActiveRatio );
                    particleData.isActive = isActive;

                    // このパーティクルの色.
                    float hue = float( spec ) / float( PARTICLE_SPEC_COUNT );
                    float3 color = h2rgb( hue );
                    particleData.color = isActive ? color : float3( 0.0, 0.0, 0.0 );
                }

                return SetParticleData( particleData );
            }
            ENDCG
        }

        Pass
        {
            Name "Count"

            /*
             * アクティブなパーティクルをカウントする Pass
             */

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            float4 frag ( v2f IN ) : SV_Target
            {
                float2 uv = IN.uv;
                float4 texelSize = _CountTexture_TexelSize;

                // FlagTexture 全体に対するインデックス
                uint2 index2d = uv * texelSize.zw;
                uint index = DemortonIndex2d( index2d );

                // CountTexture では ParticleDataTexture の 4x4 を1ユニットとして1つのテクセルに
                // まとめるので,その分テクスチャサイズが小さくなる.
                // まとめた後のテクスチャを「ブロック」と呼ぶことにする.
                uint2 unitSize2d = uint2( COUNT_UNIT_WIDTH, COUNT_UNIT_HEIGHT );
                uint2 blockSize2d = uint2( _ParticleDataTexture_TexelSize.zw ) / unitSize2d;
                uint blockSize = blockSize2d.x * blockSize2d.y;

                // 16種類のパーティクルを個別に Compaction したいので,ブロックも16個必要.
                // ただし,CountTexture は float4 なので,CountTexture の同じ範囲に4つの
                // ブロックを格納できる.4ブロックを1つにまとめた状態を「グループ」と呼ぶことにする.
                // ブロックのインデックスは1グループに4つのブロックが格納されることを考慮する必要がある
                uint groupIndex = index / blockSize;
                uint blockBaseIndex = groupIndex * VECTOR_DIMENSION;

                if ( blockBaseIndex >= PARTICLE_SPEC_COUNT )
                {
                    // 範囲外
                    return float4( 0, 0, 0, 0 );
                }

                // ParticleDataTexture から4x4個のパーティクルを取得する.
                ParticleData particles[ COUNT_UNIT_SIZE ];
                {
                    // CountTexture における処理中のブロックでの座標を ParticleDataTexture の座標に変換する.
                    // ParticleDataTexture における該当ユニットの左上の座標を求める.
                    uint2 sampleBaseIndex2d = ( index2d % blockSize2d ) * unitSize2d;

                    // Z-Curve の順に16個取得する.ちなみに,ここでのサンプル順は何でもいい.
                    [ unroll( COUNT_UNIT_SIZE ) ]
                    for ( uint i = 0; i < COUNT_UNIT_SIZE; ++i )
                    {
                        uint2 sampleIndex2d = sampleBaseIndex2d + MortonIndex2d( i );
                        particles[ i ] = GetParticleData( sampleIndex2d );
                    }
                }

                // 種類ごとにパーティクルを数える
                uint particleCount[ PARTICLE_SPEC_COUNT ];
                {
                    [ unroll( PARTICLE_SPEC_COUNT ) ]
                    for ( uint i = 0; i < PARTICLE_SPEC_COUNT; ++i )
                    {
                        particleCount[ i ] = 0;
                    }
                }
                {
                    [ unroll( COUNT_UNIT_SIZE ) ]
                    for ( uint i = 0; i < COUNT_UNIT_SIZE; ++i )
                    {
                        if ( particles[ i ].isActive )
                        {
                            uint spec = particles[ i ].spec;
                            if ( spec < PARTICLE_SPEC_COUNT )
                            {
                                particleCount[ spec ]++;
                            }
                        }
                    }
                }

                // 現在処理しているグループに属する種類のパーティクルに絞る
                uint4 targetParticleCount;
                {
                    [ unroll( VECTOR_DIMENSION ) ]
                    for ( uint i = 0; i < VECTOR_DIMENSION; ++i )
                    {
                        targetParticleCount[ i ] = particleCount[ blockBaseIndex + i ];
                    }
                }

                return targetParticleCount;
            }
            ENDCG
        }
        
        Pass
        {
            Name "Compaction"

            /*
             * ParticleDataTexture を Compaction する Pass
             */

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            uint4 GetParticleCount( float2 uv, uint lod )
            {
                float4 tex = _CountTexture.SampleLevel( sampler_point_clamp, uv, lod );
                uint4 counts = uint4( tex * float( 1 << ( lod + lod ) ) );
                return counts;
            }

            bool GetActiveParticleIndex( uint targetIndex, out uint activeIndex )
            {
                activeIndex = 0;

                // ブロックのサイズとグループの個数を求めておく
                uint2 unitSize2d = uint2( COUNT_UNIT_WIDTH, COUNT_UNIT_HEIGHT );
                uint2 blockSize2d = uint2( _ParticleDataTexture_TexelSize.zw ) / unitSize2d;
                uint2 groupCount2d = _CountTexture_TexelSize.zw / blockSize2d;

                // ブロック全体が1テクセルになる LoD
                uint maxLod = uint( log2( min( blockSize2d.x, blockSize2d.y ) ) );
                
                // ブロックごとのパーティクル数を取得
                uint particleCounts[ PARTICLE_SPEC_COUNT ];
                {
                    [ unroll( PARTICLE_SPEC_COUNT / VECTOR_DIMENSION ) ]
                    for ( uint i = 0; i < PARTICLE_SPEC_COUNT / VECTOR_DIMENSION; ++i )
                    {
                        float2 sampleUv = ( float2( MortonIndex2d( i ) ) + 0.5 ) / groupCount2d;
                        uint4 counts = GetParticleCount( sampleUv, maxLod );

                        [ unroll( VECTOR_DIMENSION ) ]
                        for ( uint j = 0; j < VECTOR_DIMENSION; ++j )
                        {
                            particleCounts[ i * VECTOR_DIMENSION + j ] = counts[ j ];
                        }
                    }
                }

                // 探索対象のブロックと,ブロック内でのインデックスを求める
                uint blockIndex = 0xFFFFFFFF;
                uint targetLocalIndex = targetIndex;
                {
                    [ unroll( PARTICLE_SPEC_COUNT ) ]
                    for ( uint i = 0; i < PARTICLE_SPEC_COUNT; ++i )
                    {
                        if ( targetLocalIndex < particleCounts[ i ] )
                        {
                            blockIndex = i;
                            break;
                        }
                        targetLocalIndex -= particleCounts[ i ];
                    }
                }
                if ( 0xFFFFFFFF == blockIndex )
                {
                    // アクティブなパーティクルが見つからなかった場合
                    return false;
                }

                // 探索対象のグループ
                uint groupIndex = blockIndex / VECTOR_DIMENSION;
                uint groupSubindex = blockIndex % VECTOR_DIMENSION;
                
                // 探索対象のグループの中心座標
                float2 uvScale = 1.0 / float2( groupCount2d );
                float2 baseUv = float2( MortonIndex2d( groupIndex ) );
                float2 deltaUv = float2( 0.5, 0.5 );
                float2 uv = deltaUv;

                // 目的のインデックスが属するユニットの,ブロック内での位置を求める
                uint count = 0;
                {
                    uint dim = groupSubindex;
                    [ loop ]
                    for ( float lod = maxLod - 1; lod >= 0; --lod )
                    {
                        // 一つ下の LoD へ進む
                        deltaUv *= 0.5;

                        // 周囲の3つのカウントを取得する.右上のカウントは取得する必要がない.
                        float2 sampleUvs[ 3 ] = {
                            ( baseUv + uv + float2( -1, -1 ) * deltaUv ) * uvScale,
                            ( baseUv + uv + float2(  1, -1 ) * deltaUv ) * uvScale,
                            ( baseUv + uv + float2( -1,  1 ) * deltaUv ) * uvScale
                        };
                        uint3 counts = uint3(
                            GetParticleCount( sampleUvs[ 0 ], lod )[ dim ],
                            GetParticleCount( sampleUvs[ 1 ], lod )[ dim ],
                            GetParticleCount( sampleUvs[ 2 ], lod )[ dim ]
                        );
                        counts = count + uint3( counts.x,
                                                counts.x + counts.y,
                                                counts.x + counts.y + counts.z );

                        // カウントを累積して,目的のインデックスがどの領域に属するか判定する
                        float tmpCount = float( count );  /* 最適化対策 */
                        if ( targetLocalIndex < counts.x )
                        {
                            uv += float2( -1, -1 ) * deltaUv;
                        }
                        else if ( targetLocalIndex < counts.y )
                        {
                            tmpCount = counts.x;
                            uv += float2(  1, -1 ) * deltaUv;
                        }
                        else if ( targetLocalIndex < counts.z )
                        {
                            tmpCount = counts.y;
                            uv += float2( -1,  1 ) * deltaUv;
                        }
                        else
                        {
                            tmpCount = counts.z;
                            uv += float2(  1,  1 ) * deltaUv;
                        }
                        count = uint( tmpCount );
                    }
                }

                // 求めた座標を ParticleDataTexture のスケールに変換する.
                uint2 unitBaseIndex2d = uint2( uv * blockSize2d ) * unitSize2d;
                uint unitBaseIndex = DemortonIndex2d( unitBaseIndex2d );

                // ユニット内で目的のインデックスを探す
                uint remainCount = targetLocalIndex - count;
                uint indexOffset = 0;
                {
                    [ unroll( COUNT_UNIT_SIZE ) ]
                    for ( uint i = 0; i < COUNT_UNIT_SIZE; ++i )
                    {
                        uint2 sampleIndex2d = unitBaseIndex2d + MortonIndex2d( i ); 
                        ParticleData particleData = GetParticleData( sampleIndex2d );

                        if ( particleData.isActive && particleData.spec == blockIndex )
                        {
                            if ( 0 == remainCount )
                            {
                                indexOffset = i;
                                break;
                            }
                            remainCount--;
                        }
                    }
                }

                activeIndex = unitBaseIndex + indexOffset;

                return true;
            }

            float4 frag( v2f IN ) : SV_Target
            {
                float4 output = float4( 0, 0, 0, 0 );

                float2 uv = IN.uv;
                float4 texelSize = _ParticleDataTexture_TexelSize;

                uint2 index2d = uint2( uv * texelSize.zw );
                uint index = DemortonIndex2d( index2d );

                uint activeIndex;
                bool isFound = GetActiveParticleIndex( index, activeIndex );
                if ( isFound )
                {
                    ParticleData particleData = GetParticleData( activeIndex );
                    output = SetParticleData( particleData );
                }

                return output;
            }
            ENDCG
        }
    }
}

在Blit()中执行。

CompactSparseTextureInParallelControl.cs

CompactSparseTextureInParallelControl.cs

using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif

namespace Test.CompactSparseTextureInParallel
{
    public class CompactSparseTextureInParallelControl : MonoBehaviour
    {
        public enum CompactionBlitPass
        {
            ParticleData,
            Count,
            Compact
        }

        [SerializeField] private Material material;
        [SerializeField] private RenderTexture particleDataTexture;
        [SerializeField] private RenderTexture countTexture;
        [SerializeField] private RenderTexture compactTexture;

        public void UpdateCompaction()
        {
            Graphics.Blit(null, particleDataTexture, material,
                          (int)CompactionBlitPass.ParticleData);
            Graphics.Blit(null, countTexture, material,
                          (int)CompactionBlitPass.Count);
            Graphics.Blit(null, compactTexture, material,
                          (int)CompactionBlitPass.Compact);
        }
    }

    #if UNITY_EDITOR
    [CustomEditor(typeof(CompactSparseTextureInParallelControl))]
    public class CompactSparseTextureInParallelControlEditor : Editor
    {
        public override void OnInspectorGUI()
        {
            DrawDefaultInspector();

            EditorGUILayout.Space();

            var control = (CompactSparseTextureInParallelControl)target;
            if (GUILayout.Button("Update Compaction"))
            {
                control.UpdateCompaction();
                if (!Application.isPlaying)
                {
                    SceneView.RepaintAll();
                }
            }
        }
    }
    #endif
}

设置材质和纹理。根据本次设置,“Count Texture”的大小将是“Particle Data Texture”的四分之一。请启用“Auto Generate Mipmaps”。

config.png

結果

通过检查器中的“Update Compaction”来执行。

(再掲)
result.png

雑記

其实没必要搞这么复杂,如果是1,024x1,024=1,048,576个粒子左右的话,用2048x2048的纹理就能计数,在PC上完全没问题,但在移动端就比较吃力了……虽然看起来不太优雅,但只要能跑起来就行。

虽然没特别说明,但即使粒子的数据大小不同,也能用一个纹理统一处理,这正是这种方法——或者说Morton Index的优点所在。

另外,使用这种方法的话,基数排序之类的操作会变得比较容易。

这又是题外话了,我觉得本文的思路是将一维的压缩扩展到二维。如果要扩展到三维的话,该沿着哪个轴来扩展呢?
我挺好奇的。

0

查看点赞用户列表

0

注册新用户并更便捷地使用Qiita

  1. 获取符合您需求的文章
  2. 您可以高效地回顾有用信息
  3. 您可以使用深色主题

注册后您可以做什么

来源:Qiita · Unity 开发社区 · qiita.com