DX11.Particlesを拡張する

技術メモの題材としてちょうどいいので、前に作ったTrail RendererをDX11.Particlesの周りに絞ってまとめます。自分も全容はまるで把握してませんので、間違い等ありましたらご指摘いただけたらと思います。

Trail Renderer

これです。

Indie Visual Labさんが前にあった技術書典で販売されていたUnity Graphics Programming Vol.2の電子書籍版を買ったので、汎用的で使いやすそうなTrailのアルゴリズムを実装してみました。

そんななかで、やっぱりTrailといったらパーティクルだろうということで、vvvvのとても便利なparticle系cotributionであるDX11.Particlesの拡張モジュールとしても実装してみることにしました。

DX11.Particlesの仕様

有料のドキュメントのアルゴリズムのため詳しいことは書きませんが、Trailのレンダリングには、最低でも3DのStructurd Bufferによる位置の入力を必要とします。

というわけで、パーティクルの位置情報がどこにあるのかを見るために、DX11.Particlesにおいてどのようにデータが計算され、ノード間を流れていくのかざっくり見ていきます。

パッチ例

Spin(DX11.Particles.Modifiers)のヘルプパッチです。細かい説明は長くなるので割愛して、データの流れに絞ってみていきます。

Emitterモジュール

名前の通り、パーティクルをEmit(発射)するモジュールです。Particle構造体を定義し、発生させた各パーティクルにVelocityやAgeなどのパラメータを与えてBufferの形で出力しています。なので基本的にEmitterノードのいずれかの種類がなければParticleは生まれません。

実際の構造体の定義部分のコードです。COMPOSITESTRUCTによる定義がなければ基本的なパラメータのセットが定義されます。

Modifierモジュール

Emitterで発生したパーティクルに対し、色や速度等、様々な影響を及ぼすのがModifierモジュールです。spin等のMotion系のModifierでは、大体Particle構造体のForceのパラメータに加算するなりそのまま書き換えるなりして影響を与えています。

Spin Modifierの内部のForce更新部分はこんな感じです。

ParticleSystemモジュール

Emitterで発生したパーティクルの情報と、Modifierで与えられた影響をGroupノードでひとまとめにしています。この辺の挙動はよくわかっていないのですが、たぶんGroupでまとめられた2つのシェーダーが一つのコードとして展開されているような状況になっているものだと勝手に考えてます。

ここまででまとめられたパーティクルのデータがParticleSystemノードによって、実際に位置やフラグの更新が行われるとともに、サブパッチ内部のRendererノードでGPUのメモリに計算結果が書き出されます。

Renderingモジュール

ParticleSystemノードで書きだされた位置情報や色情報を基に、実際にレンダリングにされるジオメトリ等が用意されます。Trail Rendererもここでの実装になります。このパッチではSpriteノードが使われています。

内部では生存しているパーティクルの数をカウントするALIVECOUNTERBUFFERを使って、パーティクルの数分のVertexを生成してシェーダーに入力しつつ、パーティクルのパラメータが格納されているPARTICLEBUFFERを入力してVertexShaderで位置情報などを頂点の位置などとして参照しています。この頂点を基にGeometryShaderでSpriteを生成してレンダリングする流れになっています。

つまるところ、ParticleSystemによって出力されたPARTICLEBUFFERに諸々の計算がなされたパーティクルの位置情報が格納されていることになります。

生存しているパーティクルのindex情報が格納されたALIVEPOINTERBUFFERを使用して、生存しているパーティクルの位置情報を参照すれば、あとはGeometryShaderなりPixelShaderなりを活用してDX11.ParticlesのカスタムEffectが作れると思います。

同様に、ForceやColorなどのパラメータに手を加えればカスタムModifierが作れますし、パーティクル発生時の位置や色などを定義すればカスタムEmitterを作れると思います。infoノードやshiftノードなど、パラメータを取得するのに便利なノードがたくさん用意されているうえに、ほぼすべてのノードにちゃんとしたヘルプパッチが用意されているため、わかる人にはとても作りやすくなっていると思います。

 

ここからは、小ネタ的にTrailの実装ででてきたわかりづらかったところをメモしていきます。

Unity→vvvvローカライズ

UnityWorldToClipPos

名前の通り、ワールド座標からクリップ座標に変換するメソッドです。なのでvvvv的には

よくあるこんな感じで行けます。あくまでView行列とProjection行列を乗算するものだったので、ローカル座標からワールド座標への変換はvvvv的にはWORLDのセマンティクスを付けた行列を乗算するなりしてワールド座標へ変換しておきます。

 

RWStructuredBufferとStructuredBuffer

RWStructuredBufferは名前の通り読み書きの両方ができるBufferリソースなので、計算結果を書き込んで次のフレームで同じリソースから値を読むようなシミュレーション的なことに使います。StructuredBufferは読み取り専用なので外部から出力されたBufferリソースを入力するのに用います。なのでシェーダー内でStructuredBufferを宣言すると、ノードにインレットが生成されます。

 

MultiStructuredBuffer

通常、vvvvでComputeShaderを使う際は、BUCKBUFFERのセマンティクスが付いたものだけが実際にGPUメモリに書き出されるのですが、これだと不便な状況があったりします。実際Trailの実装ではEmitterとは別にComputeShaderを使用し、StrideとElement Countが違う2つのBufferを出力する必要がありました。

こういう時は、DX11.Particleで用意されているRenderer(DX11.Multi Structured Buffer)を使うと指定したセマンティクスが付いた複数のBufferをそれぞれ指定したElement Countなどで出力できます。

 

パーティクルの参照と生死

DX11.ParticleはLifeTimeのパラメータが設定できるため、パーティクルが死んだり、その分また新しくパーティクルが生まれたりします。なので生きているパーティクルのみを参照するためにALIVEPOINTERBUFFERやALIVECOUNTERBUFFERを使ったりするのですが、ALIVEPOINTERBUFFERには、確かに生きているパーティクルのindexが入ってくるのですが、フレームごとにまるでバラバラな値が入ってくるので、これを使うと同じパーティクルを参照し続けることができません。

ではどうするのかというと、どうもこうもなく安直にPARTICLEBUFFERから直接同一のindexを参照し続けるしか方法がなさそうです。このことで時々おかしな描画が出てきますが、それなりに数を生成するとそこまで気にならないのでこのままにしています。

また、RendererでのBufferの生成時のElement Countの指定には上記の理由でALIVECOUNTERBUFFERを使いたくなるのですが、ALIVECOUNTERBUFFERの値が変化すると、ComputeShader内のBufferが初期化されてしまうみたいで、Trailがリセットされてしまいました。そこで、ALIVECOUNTERBUFFERではなく、ParticleSystem全体の最大パーティクル格納数であるParticleSystem Element Countを使用して、Element Countが変化しないようにしています。これも時々おかしなものが出てきますが、ALIVECOUNTERBUFFERがParticleSystem Element Countを大きく下回らないようにすればさほど気になりません。

 

ところどころ不親切なところがありそうですが、何かあれば適当に質問してもらえたらわかる範囲で回答します。

DX11.Particlesはとてもうまいことモジュール化されており、中身を見るだけでだいぶ勉強になるので、普段シェーダーをいじらない人も挑戦してみるといいかもしれません。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です