Epic Gamesが配布している、Unreal Engineの様々な機能のサンプルをまとめたプロジェクト「機能別サンプル(Content Examples)」の中に、UE4.26では、Niagara AdvancedというMapがあります。
そこでは、Niagaraの新機能やそれを使った応用例など、とても参考になるサンプルが多く配置されていて、実装内容を見てみるとたくさんの学びがあります。
それらのサンプルについて、いくつかの記事に分けて解説を行っていきます。
ちなみに、機能別サンプルはEpic Games Launcherのラーニングタブからダウンロードできます。また、UEのバージョン毎に内容が違うので注意してください。
TLDR;)
- Grid2D Collectionは、「様々な情報を持つことが可能な仮想的なTexture」みたいなものだよ
- Simulation StageとGrid2D Collectionを組み合わせれば、画像処理や2D流体シミュレーションなどがNaiagara上でできるよ
- Grid2D Collectionの基本利用は、書き込み → 加工処理 → 読み込み だよ
はじめに
この記事では、Grid2D Collectionの基礎的な内容について、Niagara Advancedの「1.3 Communicate with External Render Targets」をもとに、解説していきます。
Grid2D CollectionはUE4.25の時からありますが、UE4.26の新機能Simulation Stageと組み合わせることで、新しいことができるようになっています。
なお、Simulation Stageが何かわからない人は、以下の記事を先に読むことをおすすめします。
Grid2D Collection とは
Grid2D Collectionは、簡単に言うと、「様々な情報を持つことが可能な仮想的なTexture」のようなものです。
みなさんも、TextureのRGBAチャンネルに色以外の情報をもたせ、shaderなどで活用したことがあると思いますが、TextureだとRGBAの4つのfloat値が限度です。
Grid2D Collectionは、解像度を設定し、各テクセルに自分で設定する好きな情報を持たせることができます。例えば、色(Vector4)、速度(Vector3)、寿命(float)などを設定し、前フレームの状態をもとに、今のフレームの動きや色を決めるなどもできます。
この記事では扱いませんが、2Dの流体シミュレーションでも、Grid2D CollectionとSimulation Stageが使われています。
ただし、もちろん解像度やもたせる情報の精度(float or half floatなど)で、大きくパフォーマンスに影響するので、注意が必要です。
1.3 Communicate with External Render Targets の解説
このサンプルでは、元となるTextureをGrid2D Collection上でブラー処理した上で、その結果をRender Textureに書き込み表示しています。
左側は、Niagaraで生成したものですが、右側はNiagaraではありません。これは、Niagaraが書き込むRender Targetをブループリントで外部から与えることで、Niagara外でも使えるようにして、右側はそのRender Targetを使って、Material上で処理を行い、普通のPlane上に描画しています。
このように、Niagaraで加工処理したものを、外部で使うといったことも可能であることを示すサンプルになっています。
具体的な処理を見ていきますが、一旦は左側の方にフォーカスして、どのようにブラー処理を行い、Render Targetに書き込んでいるかの解説からしていきます。
なお、【UE4.26】Niagara Advanced 解説基礎編~Simulation Stage~で解説済みの内容については、ここでは触れません。
Emitter Spawn, Updateで、各種Attributeを初期化しています。
そして、3つのSimulation Stageが使用されていて、ざっと説明してしまうと、
- Fill Grid With Textureで、Textureの情報をGrid2D Collectionにコピーします。
- Blur Gridで、ブラー処理をかけて、Grid2D Collectionの各値を更新します。
- Fill Render Target With Gridで、Grid2D Collectionの情報をRender Targetに書き込みます。
流れ自体はシンプルですね。では、各Simulation Stageの中身を見ていきます。
Fill Grid With Texture
Data Interfaceには、Grid2D Collectionを設定します。また、Emitter Reset Onlyにチェックを入れて、EmitterがResetされた時に1回だけこの処理を実行するようにします。
Scratch Pad内の処理もそこまで複雑なことはしていません。
Execution Index to Unitで、Execution Indexから、Grid2DのUV(0~1のVecotor2の値)を取得します。ここで、Execution Indexは、Data IntefaceをGrid2D Collectionにしているので、0~262,143(512×512-1)の通し番号です。
次に0.5かけて、0.25引き、0~1にClampしているのは、UVを少し中心に向かって縮小して、ブラーのスペースをあけるためのもので、なくてもそこまで問題はないものです。ちなみに、Scale UVs by Centerというノードで同じことができます。またマテリアルにも同じ機能のノードがあります。
そのUVで、Sample Texture 2Dを使って、元画像のTextureの色を取得し、グレースケール化する処理を行います。
そして、そのグレースケール値とアルファ値のVecotor2を、FloatPlusAlphaというAttributeを作って格納します。
まず、StackContextというラベルがついていますが、これも4.26からの新機能で、フレームやモジュールを跨いで読み書きできるAttributeを作りたい場合は、このNamespaceにします。
ちなみに、StackContextのAttributeを作るには、まず普通にAttributeを作り、そのAttributeの上で右クリックをし、Change Namespaceという項目から、StackContextを選ぶと設定できます。
このAttributeは、このSimulation StageのData Interfaceに設定したGrid2D Collectionに紐づいています。つまり、Grid2D Collectionの各テクセル毎にそれぞれの値をもっているということです。
これで、TextureからGrid2D Collectionに、少し加工したうえで、元画像をグレースケール化した情報がコピーできました。
Blur Grid
次にブラーの処理です。
また、Data Interfaceには同じGrid2D Collectionを設定します。繰り返し回数は8回です。これはブラー処理で、繰り返しが必要となるからです。ここの値を増やせば、より強いブラーがかかります(が負荷もあがります)。
Scratch Pad内の処理を見ていきますが、ブラー処理は、HLSLで書かれています。
ブラー処理については、本筋からそれるので詳しくは解説しませんが、ざっくり説明します。
ブラー処理にも色々な方法がありますが、簡潔に言えば自分のテクセルの周辺のテクセルの値を取得し、それらを適当に加重平均していくことで、ぼやっとした絵にすることができます。
その処理をHLSLで書いていますが、簡単にコードにコメントを載せるにとどめて、Grid2D Collectionとして重要な所だけいくつか抜き出して解説します。
// 値書き込み用の変数の初期化
OutputUnitGrid = float2(0,0);
OutputBlurGrid = float2(0,0);
// ①GPU Simオンリー
#if GPU_SIMULATION
// 4隅のテクセルを取ってくるのを簡潔に書くために用意
float2 Offsets = float2(0.5, -0.5);
// 何回目の繰り返しか
StageIterations += 1;
// 値書き込み用の変数の初期化
float2 Outputs[4];
float2 Taps[4];
float2 CornerTap;
// ②指定したUV(Unit)のFloatPlusAlphaというAttributeを取得し、OutputUnitGridに格納
Grid2DCollection.SampleGridVector2Value<Attribute=FloatPlusAlpha>(Unit, OutputUnitGrid);
// dxdyは、テクセル間のUV距離。繰り返し回数が増えるごとにより遠くのテクセルをサンプリングしに行く
CornerTap = dxdy * StageIterations;
Taps[0] = CornerTap * Offsets.yy;
Taps[1] = CornerTap * Offsets.xy;
Taps[2] = CornerTap * Offsets.yx;
Taps[3] = CornerTap * Offsets.xx;
// 自分のテクセルを中心に4隅にあるテクセルの値をサンプリング
Grid2DCollection.SampleGridVector2Value<Attribute=FloatPlusAlpha>(Unit+Taps[0], Outputs[0]);
Grid2DCollection.SampleGridVector2Value<Attribute=FloatPlusAlpha>(Unit+Taps[1], Outputs[1]);
Grid2DCollection.SampleGridVector2Value<Attribute=FloatPlusAlpha>(Unit+Taps[2], Outputs[2]);
Grid2DCollection.SampleGridVector2Value<Attribute=FloatPlusAlpha>(Unit+Taps[3], Outputs[3]);
// サンプリングした値の平均をだす。
OutputBlurGrid += (Outputs[0]+Outputs[1]+Outputs[2]+Outputs[3]) / 4;
if (TickCount <= 1)
{
}
else
{
// ③Tick Countが1より大きい時は、ブラー処理の値は使わない
OutputBlurGrid = OutputUnitGrid;
}
// ④IndexX, IndexYのFloatPlusAlpha Atributeに、OutputBlueGridを書き込む
Grid2DCollection.SetVector2Value<Attribute=FloatPlusAlpha>(IndexX, IndexY, OutputBlurGrid);
#endif
①「if GPU_SIMULATION ~ #endif」で囲んで、GPU Simであることを保証します。
② {Grid2DCollection}.SampleGrid{データ型}<Attribute={attribute名}>({Grid2D CollectionのUV}, {返り値格納変数});で、Grid2D CollectionからAttributeを取得できます。
③ TickCountが1より大きい時は元のOutputUnitGridの値を使っているので、実質1フレーム目でしか、ブラー処理はしていません。これをしないと、毎フレーム、ブラー処理をかけたものの上に、ブラー処理をしていくため、どんどんぼやけていきます。Fill Grid With TextureでEmitter Reset Onlyにチェックをしたので、最初にTextureからGrid2d Collectionにコピーをし、(おそらく)次のフレームのみでブラー処理をかけているのが実態です。
(これについては、Blur GridもEmitter Reset Onlyにチェックをいれて、最初だけ処理させればいいじゃんと思いやってみましたが、うまくいかなかったので、Attributeの読み書きに少し理解してない挙動がありそうです。)
④{Grid2DCollection}.SampleGrid{データ型}<Attribute={attribute名}>({Grid2D CollectionのインデックスX}, {Grid2D CollectionのインデックスY}, {セットする値});で、Grid2D CollectionのAttributeに値をいれることができます。
ここまでで、ブラー処理が入った、グレースケールの値とアルファが、Grid2D CollectionのFloatPlusAlphaというAttributeに格納されています。
あとは、これをRender Target 2Dに書き込むだけです。
Fill Render Target With Grid
今度は、Render Target 2DをData Interfaceに設定します。
処理はシンプルで、Grid2D Collectionから各テクセルのFloatPlusAlphaをとってきて、それを同じテクセルのRender Target 2Dに書き込んでいるだけです。
一点注意点としては、Render Target 2Dと、Grid2D Collectionの解像度が同じである必要がある点です。そうしないと、Execution Index To Unitで、正しいUVが取得できなくなります。なぜなら、今回はData InterfaceがRender Target 2Dなので、Execution Indexは、Render Target 2Dのテクセルの通し番号になります。Grid2D CollectionでもExecution Indexを利用してUVをとってくる場合は、同じ解像度にする必要があります。
あとは、書き込んだRender Target 2Dをマテリアルに渡せば、ブラーのかかった絵が表示されます。(ちなみに四角い枠はマテリアル側でつけられているものなので、ブラーがかかっていません。)
外部からRender Targetを設定し、利用する
最後に上の画像部分を作っているところの解説です。これはNiagaraで書き込んだRender Targetを外部で利用することで実現しています。
外部からRender Targetを設定しているところですが、まず単純にUser Parameterに、Texture Render Targetのパラメータを用意します。
ドロップダウンで直接指定もできますが、このサンプルではブループリントから設定しています。
そして、Render Target 2Dを初期化しているところの、Render Target User Parameterに、作成したTexture Render Targetのパラメータをバインドします。
ブループリントでは、コンストラクションスクリプトで、Rendet Target 2Dを作成し、Niagaraと外部で使っているMaterialに設定しています。
マテリアルでは、受け取ったRender TargetのTextureから、アルファ値をもとにローカルのZ方向にWorld Position Offsetをかけているだけです。
このようにして、Naiagaraで処理したRender Targetを別の所で利用することが可能となっています。
おわりに
今回は、ブラー処理という地味めの加工でしたが、応用編ではもっとおもしろい画像処理のサンプルを紹介したいと思っています。
また、Grid 2D CollectionとSimulation Stageを使えば、2Dの流体シミュレーションなども行え、実際にEpicでサンプルの作成等行っているので、そのうち標準モジュールとして追加されるのではないかと思っています。
また、Grid3D Collectionなども4.26から追加されているので、3D空間で色々な処理ができるようにもなりそうです。
この辺りは、使い方次第で色々できるところなので、まず基礎をしっかり押さえてから、色々創意工夫をしていきたいですね!
最後駆け足の解説になってしまいましたが、細かいところは実際に実装を見て、色々触ってみるとより深く理解できるのではないかと思います!
では、よきCGライフを!