Epic Gamesが配布している、Unreal Engineの様々な機能のサンプルをまとめたプロジェクト「機能別サンプル(Content Examples)」の中に、UE4.26では、Niagara AdvancedというMapがあります。

そこでは、Niagaraの新機能やそれを使った応用例など、とても参考になるサンプルが多く配置されていて、実装内容を見てみるとたくさんの学びがあります。

それらのサンプルについて、いくつかの記事に分けて解説を行っていきます。

ちなみに、機能別サンプルはEpic Games Launcherのラーニングタブからダウンロードできます。また、UEのバージョン毎に内容が違うので注意してください。

TLDR;)

  • Neighbor Grid3Dを使えば、近くにあるParticleのIndexやIDがわかるよ
  • Neighbor Grid3Dの基本的な使い方は、3次元のグリッドを定義→自分(Particle)が位置するセルにIDやIndexを書き込む→自分が位置するセルやその隣のセルに保存されているIDやIndexを取得→Particle Attribute ReaderでそのID/IndexのParticleの情報を取得して利用する

はじめに

この記事では、Neighbor Grid3Dについて解説していきます。機能自体は、4.25の時からあったようですが、4.26から機能別サンプルに多くのサンプルが追加されています。

その中から、「3.1 Color Copy by Cell」、「3.2 Dynamic Grid Transform」、「3.3 Max Neighbors Per Cell」の3つを取り上げ、解説していきます。

また、Neighbor Grid3Dを使う際は、基本的にはParticle Attribute Reader及びSimulation Stageも一緒に使うため、それらを知らない人は、以下の記事を先に読むことをおすすめします。

【UE4.26】Niagara Advanced 解説基礎編~Particle Attribute Reader~

【UE4.26】Niagara Advanced 解説基礎編~Simulation Stage~

Neighbor Grid3D とは

具体的な解説に入る前に、簡単に概略だけ説明すると、

この機能では、まず任意の大きさの3次元のグリッドを定義し、その各セルにParticleのIDまたはExecution Indexを保存しておくことができます。

例えば、3×3×3のグリッドを定義し、(1, 0, 0)にはID2, 15, 98、(2,2,2)にはID12, 101のParticleがあるよ。といった感じです。

これにより、自分が位置するセルやその隣にあるセルに保存された(=近くにある)ParticleのIDを知ることができ、IDが分かれば、Particle Attribute Readerを使って、色などの各Attribute情報を取得することができるという寸法です。

また現状は、GPU Simのみに対応しています。

では、具体的な実装を見ていきます。

3.1 Color Copy by Cell の解説

このサンプルは、大きいParticleと小さいParticleがあり、大きいParticleはランダムに位置と色が生成され、小さいParticleはランダムな位置に生成された後、同じセル内にある大きいParticle(同じセル内に複数ある場合はより近い方)を見つけ、その色をコピーするという内容です。

ちなみに、薄緑色の枠は、定義された2×2×2のNeighbor Grid3Dをビジュアライズしているもので、同じNiagara Systemの中で実装されていますが、本質的には関係がないので、ここでは解説はしません。

まずは、Neigbor Grid3Dを定義しているところから見ていきます。

System Updateで定義しています。複数のEmitterで使うことになるので、Systemの所で定義します。また、Updateで定義しているので、毎フレーム作り直されることになります。

Max Neighbors Per Cellは、各セルが保存できるParticleの数です。これは、「3.3 Max Neighbors Per Cell」の所で詳しく見ていきます。

先にSet Resolution Methodを説明します。ドロップダウンを開くと、Independent, Max Axis, Cell Sizeの3つのオプションがあります。それぞれ、IndependentはNum Cells、Max AxisはNum Cells Max Axis、Cell SizeはCell Size、に対応していて、選択したもの以外は非アクティブになります。

これは、各セルの数と大きさを決めるものです。Num Cellsは、そのまま三次元のセル数になり、大きさは、World BBox Sizeで指定した大きさを分割したものになります。

Cell Sizeは反対に、それがセルの大きさになり、各次元のセル数はWorld BBox Sizeをセルの大きさで割った数になります。

Max Axisは、検証はしていませんが、名前からして、World BBox Sizeで一番大きいサイズの次元のセル数を指定して、セルの大きさを決まるのだと思います。

とりあえず、ここでは、各辺75の2×2×2のグリッドが定義されています。

さて次に、Neigbor Grid3Dに情報を保存する大きいParticleの方のEmitterを見ていきます。

基本は、Box Locationでボックス内にランダムな位置に、ランダムな色で出現させているだけです。

注目すべきは、Fill Gridという名前でSimulation Stageが使われています。Iteration SourceはParticleなので、全Particleに処理が走ります。そして処理自体は、Fill Neighbor Grid 3D(Scratch Padモジュール)で実装されていて、中では、Neighor Grid3Dの各セルに、その中にあるParticleのExecution Indexを書き込む処理をしています。

処理は基本HLSLで書かれています。HLSLの部分を簡単にコメントをつけて解説します。

// セルに情報を保存できたかの結果を最後に入れる
AddedToGrid = false;

// GPU Simであることを保証
#if GPU_SIMULATION

// Particleのワールド座標をNeighbor Grid3D内の座標系に変換(後述)
float3 UnitPos;
NeighborGrid.SimulationToUnit(Position, SimulationToUnit, UnitPos);

// UnitPosを元に、セルの3次元のIndexを取得
int3 Index;
NeighborGrid.UnitToIndex(UnitPos, Index.x,Index.y,Index.z);

// Neighbor Grid3Dのセル数を取得
int3 NumCells;
NeighborGrid.GetNumCells(NumCells.x, NumCells.y, NumCells.z);

// 取得したセルのIndexが、ちゃんと正しい値か保証
if (Index.x >= 0 && Index.x < NumCells.x && 
    Index.y >= 0 && Index.y < NumCells.y && 
	Index.z >= 0 && Index.z < NumCells.z)
{

  // 3次元のIndexから通し番号のIndexに変換
    int LinearIndex;
    NeighborGrid.IndexToLinear(Index.x, Index.y, Index.z, LinearIndex);

    // このセルに所属するParticleの数を取得すると共に、1増やした値をセットする
    int PreviousNeighborCount;
    NeighborGrid.SetParticleNeighborCount(LinearIndex, 1, PreviousNeighborCount);

  // 事前に設定したセル辺りに保存できる最大Particle数を取得
    int MaxNeighborsPerCell;
    NeighborGrid.MaxNeighborsPerCell(MaxNeighborsPerCell);

    // 最大数より所属している数がまだ小さければ、Particle情報を保存する
    if (PreviousNeighborCount < MaxNeighborsPerCell)
    {
        AddedToGrid = true;

    // セルに保存されているParticleの配列(?)のIndexを取得
        int NeighborGridLinear;
        NeighborGrid.NeighborGridIndexToLinear(Index.x, Index.y, Index.z, PreviousNeighborCount, NeighborGridLinear);

    // 上で取得したIndexの所に、Execution Indexを保存
        int IGNORE;
        NeighborGrid.SetParticleNeighbor(NeighborGridLinear, ExecIndex, IGNORE);
    }		
}
#endif

だいたいは、コメントを読めば理解できると思いますが、一点だけ後述と示した部分の解説をします。

NeighborGrid.SimulationToUnit(Position, SimulationToUnit, UnitPos); で、ワールドポジションから、Neigbor Grid3Dの座標空間のポジションに変換できます(グリッド内部のポジションは0~1の値になる)。

ここで、Positionには、Particleのワールドポジションをいれます。SimulationToUnitについては、Neigbor Grid3DのWorld BBox Sizeを使って作ったMatrixが入るようです。これは完全に定型文なので、コピペでいいかと思います。

これで、グリッド内に元となる大きいParticleのExecution Indexを書き込むことができました。

次は、グリッドを検索して近くの大きいParticleを見つけ、その色をコピーするEmitterの方です。

注目すべきところは、Particle Attribute Readerと、Query Gridという名前のSimulation Stageですね。

Particle Attribute Readerの方は、読み取り先は大きいParticle達なので、Emitter NameをGrid_Writeに指定しています。

Query Gridは、同じくParticleをIteration Sourceにして、処理は二つのScratch Padモジュールです。

Find Closest Neighborで、同じセル内の一番近い、「大きいParticle」を見つけ、そのExecution Indexを取得します。そして、それを使って、Copy Colorで色をコピーします。

まずは、Find Closest Neighborを見ていきます。

これも、ほぼHLSLで書かれています。最後に、取得したExecution IndexをNeighbor Indexとして、OutputのAttributeに書き込んでいます。

HLSLにコメントをつけて解説します。なお、先ほどのHLSLと同じ部分の解説は省略します。

// 一番近くのParticleのExecution Indexが入る。同じセル内に一つもなかった場合は、そのまま-1が出力される。
NeighborIndex = -1;

#if GPU_SIMULATION

bool Valid;

float3 UnitPos;
NeighborGrid.SimulationToUnit(Position, SimulationToUnit, UnitPos);

// 自分が所属しているセルの3次元Indexを取得
int3 Index;
NeighborGrid.UnitToIndex(UnitPos, Index.x,Index.y,Index.z);

// 一旦、めっちゃ大きい値で一番近いParticleとの距離を初期化する
float neighbordist =  3.4e+38;

int MaxNeighborsPerCell;
NeighborGrid.MaxNeighborsPerCell(MaxNeighborsPerCell);

// セルに保持できる最大数でループを回す
for (int i = 0; i < MaxNeighborsPerCell; ++i)
{
    int NeighborLinearIndex;
    NeighborGrid.NeighborGridIndexToLinear(Index.x, Index.y, Index.z, i, NeighborLinearIndex);

  // セルに保持されているExecution Indexを取得
    int CurrNeighborIdx;
    NeighborGrid.GetParticleNeighbor(NeighborLinearIndex, CurrNeighborIdx);

    // Execution Indexが-1だったら見つからなかったということなので、フィルターする
    if (CurrNeighborIdx != -1)
    {
        // Execution Indexをもとに、Particle Attribute Readerで、保持されていたParticleのポジションを取得する
        float3 NeighborPos;
        AttributeReader.GetVectorByIndex<Attribute="Position">(CurrNeighborIdx, Valid, NeighborPos);

        // 現在の最短距離を比較する
        const float3 delta = Position - NeighborPos;
        const float dist = length(delta);

    // 最短距離を更新した場合は、そのExecution Indexを記録する
        if( dist < neighbordist )
        {
            neighbordist = dist;
            NeighborIndex = CurrNeighborIdx;
        }
    }  
}    

#endif

これで、自分が所属するセル内で一番近い「大きいParticle」のExecution Indexを取得し、Neighbor IndexというAttribute名で値を保持することができました。同じセル内に一つも「大きいParticle」がなかった場合は、-1を保持しています。

最後、色をコピーする所ですが、とってもシンプルで、Particle Attribute Readerを使って、色を取得しコピーしているだけです。

以上で、このサンプルの解説は終わりです。グリッドに書き込む・読み込む。これが、基本的なNeighbor Grid3Dの使い方です。

3.2 Dynamic Grid Transform の解説

このサンプルでは、Nieghbor Grid3Dを視覚化した薄緑のグリッドが、継続的に回転していて、グリッド上に並べられた赤いParticleが、Nieghbor Grid3Dの内部に入ると少し拡大され緑色になるというデモです。

これは、言ってしまえばNeighbor Grid3Dを回転させることもできますよ的なサンプルなので、その方法をサクッと解説します。

今回もSystem Updateの中で、Neoghbor Grid3Dを初期化しますが、前回と違い、Initialize Neighbor Gridというノードを使います。これを使うことで、Gridの位置や回転を設定できるようになります。

Grid Extentsで大きさ、Local PivotでPivot位置を決められます。少し試してみましたが、Resolution MethodはIndependentしか機能しなさそうです。つまり、Num Cellsを設定して、グリッドの数を決めます。

Transformed Imputsの、Offsetで位置、Rotationで回転を設定できます。ここでは、適当な軸に対してAgeに応じて回転するような設定になっています。これで、継続的に回転し続けることができます。

Initialize Neighbor Gridを使うもう一つの利点は、グリッドに関する様々な情報をAttributeに書き込んでくれることです。

実際に、この中のWorldToGridUnitを使って、Neighbor Grid3Dの中にParticleがあるかないかを判断しています。

このEmitterが、グリッド上に赤いParticleを配置しているものです。配置は、Spawn Particles in GridとGrid Locationでやっています。

Neighbor Grid3Dの内部になったら拡大して緑色にするというのは、DynamicMaterialParameterを使って、Materialで実現しています。

Transform Position by Matrixを使い、ParticleのPositionをWorldToGridUnitで、Neighbor Grid3D上にNormalizeされた座標に変換します。つまり、グリッド内にある場合は、各次元0~1の値になります。そしてそれが、DynamicMaterialParameterのrgbに入ります。

詳細は割愛しますが、Material側では、DynamicMaterialParamterのrgb値を見て、0~1に入っているかをチェックし、色と大きさを制御しています。

これでこのサンプルの解説は終わりですが、おまけ情報として、Neighbor Grid3Dを視覚化しているMaterialがSplineThickenという珍しいノードを使っているので、見てみると面白いかもしれません。

遊んでいたら、面白げなものができた

3.3 Max Neighbors Per Cell の解説

このサンプルは、Max Neighbors Per Cellを設定した時の挙動と、Simulation StageのExecution Indexは順序がバラバラ(毎フレーム違う)ということを示すためのデモです。

内容は、Neighbor Grid3DにParticleを保存する処理を行い、保存がうまくいけば白色、失敗したら黒色になるというものです。Max Neighbors Per Cell = 1の時は、一つのセルに一つのParticleまでしか保存できないので、ほとんど保存に失敗(各セルで一番最初に処理されたParticleだけ成功する)し黒色が多いですが、Max Neighbors Per Cell = 25の時は全部保存できて白色になっています。

またgif画像を見ると、白くなるParticleが毎フレーム入れ替わっているのがわかります。これは、先述した毎フレーム処理が実行されるParticleの順序が異なるためです。Simulation StageでExecution Indexを使う場合は、このことに留意しておく必要があります。

では、具体的に中身を見ていきましょう。

今回は、User ParameterとしてNeighbor Grid3Dを作っています。これで、レベルに配置した後、インスタンス毎にMax Neighbors Per Cellなど各パラメータを設定することができます。なので、ここでは適当に100と設定されています。

続いて、処理を行っているEmitterですが、実は多くのモジュール使ってないです。

というのも、このNiagara Systemは、「3.4 Color Propagation」というサンプルと兼用されています。それを、ShowGridParticlesというUser Parameterのフラグでどっちにするか管理しています。

こっちのサンプルでは、画像内で消したところの機能は、使われていません。

最後の、Set Colorのところで、ShowGridParticlesのフラグで処理が分かれ、このサンプルはtrueの方です。そして、AddedToGridというAttributeでtrueなら白、falseなら黒に色付けされるようになっています。

名前からもわかる通り、AddedToGridは、Neighbor Grid3Dに保存できたかどうかのフラグです。

このフラグは、Fill GridのSimulation Stage内で設定されています。

Fill Neighbor Grid 3DのアウトプットAttributeをバインドしているだけですね。Fill Neighbor Grid 3DはScratch Padモジュールなので、中身を見てみます。

どっかで見ましたね。そうです、「3.1 Color Copy by Cell」のFill Neighbor Grid 3Dとまったく同じです。 「3.1 Color Copy by Cell」の方でも、Max Neighbors Per Cellの値を見て、フィルターかける処理が入っていましたね。

逆に言うと、HLSL内ではじいているだけなので、超えて保存しようと思えば、保存できそうな感じですね。パフォーマンスがやばくなるのかな?

とりあえず、これでこのサンプルの解説は終わりです。「3.4 Color Propagation」の方は、別記事で解説できればと思います。

おわりに

自分の近くにいるParticleを検索するという機能は、応用範囲が広く、次に解説する、Position Based Dynamicsでも使われています。

基本はGridへの書き込みと読み取りで、その処理もほとんど定型文なので、そのうちデフォルトで入っているモジュールになりそうです。ですが、中身の挙動を知っていると何かと便利だと思うので、この記事がお役に立てれば幸いです。

では、よきCGライフを!

One Comment

コメントを残す

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