Unity GPU粒子模拟纹理紧凑化的并行实现
【Unity】GPUパーティクルのシミュレーションテクスチャのコンパクト化の並列化
疎なデータテクスチャを層別しつつ密に詰めます.
概要
GPUパーティクルで動的にパーティクルを生成する場合,死んだパーティクルを捨てるために定期的にシミュレーションテクスチャを整理する必要があります.
その時に便利な,有名な方法があります.
本記事では,この方法自体については解説しませんが,ざっくり言うと,
- カウント用テクスチャを作り,有効なパーティクルの場所に "1" を書き込む.
- カウントテクスチャの Mipmaps を生成.
- Z-Curve (Z-Order) の考え方により,任意の n 番目に有効なパーティクルの場所がわかる.
- 元のシミュレーションテクスチャのパーティクルを求めた位置で並べ替える.
といった感じです.
この方法はカウントテクスチャに1チャネルしか必要としないので,複数チャネル使えば複数種類のパーティクルを個別に整理(コンパクト化)することができます.順当な考え方ではありますが,便利なので手順を説明します.
カウントテクスチャの構成
説明する,といっても,単純に独立した次元を一つ増やすだけなので,理論の説明は特にありません.カウントテクスチャ上で n 番目のパーティクルを探すときに,それ以前にカウントしないといけないパーティクルの分のオフセットを考慮する,ということくらいです.
一方で,カウントテクスチャのサイズを小さくするという観点での工夫があるので,カウントテクスチャの構成について説明します.
本記事では,16種類のパーティクルを個別にコンパクト化することにします.
RGBA の4チャネルでそれぞれカウントするとして,カウントテクスチャが4枚あれば,16種類個別にカウントができます.レンダリングのコストを下げるため,カウントテクスチャ4枚を合体して1枚のカウントテクスチャとして扱うことにします.
また,カウントテクスチャに値を書き込む際,パーティクル16個を1単位
としてカウントテクスチャの1テクセルに割り当てます.Mipmaps を使ったカウント方法は,書き込む値が "1" でなくても正確にカウントできるので,パーティクルが16個あるのであれば "16" と書き込めばカウントできます.
ソースコード上での命名として,4x4=16 個のカウントの単位を "Unit",Unit ごとにカウントした,パーティクル1種類分のカウントテクスチャを "Block",Block を RGBA に割り当てた,パーティクル4種類分のカウントテクスチャを "Group",と呼ぶことにします.また,パーティクルの種類のことを "Spec" とします.
n 番目のパーティクルを探すときは,
- 全種類のパーティクルの累積和をそれぞれ求める.
- n 番目のパーティクルがどの種類のパーティクルにあたるか調べる(該当の Block を特定する.)
- d4rkpl4y3r の方法で,該当 Block で ( n - ( 累積和 ) ) 番目のパーティクルが含まれる Unit を探す.
- Unit の中で ( n - ( 累積和 ' ) ) 番目のパーティクルを探す.
といった手順になります.手順の 4. では元の "Particle Data Texture" を参照しながら有効なパーティクルを探すことにしています.紹介しませんが,カウントテクスチャを作るときにフラグ用のテクスチャを用意しておく,という手もあります.
実装
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" の4分の1のサイズになります."Auto Generate Mipmaps" を有効にしてください.
結果
インスペクターの "Update Compaction" で実行します.
雑記
こんなややこしいことしなくても,1,024x1,024=1,048,576 パーティクルくらいだったら 2048x2048 テクスチャでカウントできるので,PCだったら余裕なんですが,モバイルだと厳しいんですよね...あんまりスマートじゃない感じですが,まあ動けばいいんです.
説明してないですが,パーティクルのデータサイズが違っても1つのテクスチャでまとめて扱えるのも,この方法,というか Morton Index のいいところだと思います.
別件ですが,この方法を使うと,Radix ソートとかがやりやすいですね.
これもまた関係ない話ですが,本記事の考え方は,1次元のコンパクト化を2次元に拡張しているのだと思います.3次元にする場合,何の軸で拡張すればいいんでしょうか.
私,気になります.
Go to list of users who liked
0
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme
来源:Qiita · Unity 开发社区 · qiita.com



