【UE5】Niagara SimulationStageでNormalMapからHeightMapを復元する(Poisson方程式)

Unreal Engine Advent Calendar 2023 の16日の記事です. Unreal Engine (UE)のカレンダー | Advent Calendar 2023 - Qiita

最近HoudiniでNormalMapからHeightMapを復元するというものを見かけ,
Poisson方程式を解くのであればSimulationStageで実装することができるのではと考えました.


この記事では「NormalMapからHeightMapを復元する」という問題をSimulationStageで実装する方法を紹介します.
サンプルプロジェクトは以下のGoogleDriveになります (動作確認バージョン UE5.3).
ReconstructHeightMapFromNormal.zip - Google ドライブ

Niagara SimulationStage

Niagara SimulationStage はComputeShader的なGPGPU処理をNiagaraモジュールとして作成できる仕組みです.
Epic Games公式のNiagara Fluids等でも多用されています(本当にSimulationStageでほとんどの処理が作られています まじかよ).

SimulationStage は2D/3Dのグリッド構造に対するGPGPU処理を比較的簡単に作ることができるように設計されています.
これには数値シミュレーションで必要とされる反復実行なども含まれています.
目的のタスクを適切な形に変形してSimulationStageで実装することで, GPUによる高速な実行が可能になります.

NormalMapからHeightMapを復元する問題

Poisson方程式

ある関数の二階の空間微分と別の関数との関係式をPoisson方程式といいます.
Poisson方程式は離散化によって連立一次方程式へ変形することができ, 連立一次方程式を解く方法は様々に研究されています.
対象の問題をPoisson方程式, ひいては連立一次方程式の形にすることができれば, Solverや解法を利用して解くことができます.

\Delta \phi =-\dfrac{\rho}{\epsilon_0}

Poisson方程式への変形

今回は NormalMapとHeightMapの関係をPoisson方程式で表現して離散化, 連立一次方程式として解くことにします.

NormalMapのxy成分はHeightMapの空間微分(勾配)から求められます.
N_x(x,y)= -\dfrac { \partial H(x,y)}{\partial x}
N_y(x,y)= -\dfrac { \partial H(x,y)}{\partial y}
さらにそれぞれ x, y で偏微分します.

 \dfrac {\partial N_x(x,y)}{\partial x}= -\dfrac { \partial^2 H(x,y)}{\partial x^2}

 \dfrac {\partial N_y(x,y)}{\partial y}= -\dfrac { \partial^2 H(x,y)}{\partial y^2}
ここで上の式はPoisson方程式の形になっています.

差分方程式

このPoisson方程式を離散化すると以下のようになります.
(東海大学理学部遠藤研究室さんのページ等がわかりやすいです. -> 電磁場(SP) )

 \frac {N_x(x+h,y) - N_x(x-h,y)}{2h}= -\frac {H(x+h,y)+H(x-h,y) - 2H(x,y)}{h^2}
 \frac {N_x(x,y+h) - N_x(x,y-h)}{2h}= -\frac {H(x,y+h)+H(x,y-h) - 2H(x,y)}{h^2}
(左辺は中心差分)

2つの式を足して左辺のNormalMap, 右辺にHeightMapに関わる項をまとめて整理します.
またテクセルでの微分(差分)のため  h = 1 とします.
 - \frac {N_x(x+1,y) - N_x(x-1,y) + N_x(x,y+1) - N_x(x,y-1)}{2} \\= H(x+1,y)+H(x-1,y) + H(x,y+1)+H(x,y-1) - 4H(x,y)

上の式の左辺は入力NormalMapとして与えられる既知の値, 右辺は求めたいHeightMapのいくつかの地点の値に係数をかけたものの和です.
よってこれは左辺を定数 b, 未知のHeightMapを x, 近傍HeightMap値への係数を行列 Aとした連立一次方程式ということになります.
 b = Ax

連立一次方程式を解くために更に変形していきます.

Jacobi法

見通しをよくするために左辺は記号で置き換えておきます.
 -N_{d} = H(x+1,y)+H(x-1,y) + H(x,y+1)+H(x,y-1) - 4H(x,y)
(ここで   N_{d} =\frac {N_x(x+1,y) - N_x(x-1,y) + N_x(x,y+1) - N_x(x,y-1)}{2} )

H(x,y) に関する形に変形します.

 H(x,y) = \frac{(H(x+1,y)+H(x-1,y)+H(x,y+1)+H(x,y-1)) + N_{d} }{4}

この式を反復的に計算することで  H(x,y) を解に近づけていく方法をJacobi法といいます.
プログラム的に考えるとHeightMapの各テクセルについて上下左右の近傍値とNormalMapの値を使って更新しているといえます.
現在のHeightMapから次のHeightMapの各テクセルを並列に計算できるためGPGPUとも相性が良いです.
次はJacobi法によるHeightMapの計算をNiagara SimulationStageで実装します.

Niagara SimulationStageでの実装

先に必要となるデータや処理を整理しておきます.

外部データ一覧

外部からNiagaraに設定するデータになります.

  • 入力NormalMap Texture
    • HeightMap復元の元になる入力データ
  • HeightMap出力用RenderTargetTexture
    • 結果のHeightMap出力先として Floatフォーマットの適当なサイズで用意しておきます.
  • プレビュー用マテリアル
    • 計算結果のプレビューをするためのマテリアル M_PreviewHeight
    • InTex という名前でテクスチャパラメータを引き取って表示するだけのものです.

処理ステージ一覧

これらのステージをSimulationStageとして実装します.

  • 初期化
    • 作業用のGrid2DCollectionのクリアなど, メインの処理の準備をします.
    • 実行タイミングをリセット時の一度のみに設定することで, フレームを跨いでJacobi反復を継続させます.
  • Jacobi反復処理
    • HeightMapの反復計算の1回分を計算します.
    • SimulationStageの反復数指定によって1フレームに複数回実行可能です.
  • 出力
    • HeightMapを出力先RenderTargetに書き出します.


ここからNiagaraSystemのEmitterを実装していきます.

EmptyEmitterを追加

雛形のEmitterを追加します.
最低限の設定が含まれたテンプレートの Empty Emitterを使います.
(コンテキストメニューの「空のエミッタを追加」は本当に無なので面倒です. )

Emitterの設定

SimulationStageを使うためにGPUComputeに変更.

プレビューを簡単にするためにSpriteRendererのSourceModeをEmitterに変更.

Spriteを見やすい大きさにするためにEmitterSpawnステージにSpriteSizeの設定を追加.

ユーザーパラメータの作成と既定値

外部から入力NormalMapと出力先RenderTargetを受け取るためのユーザーパラメータです.
Systemユーザーバラメータの + から 新規作成>オブジェクト で
TextureTexture Render Target を一つずつ作成します.
それぞれ InputNormalTex, OutputHeight と名前をつけておきます.

作業中のプレビューをしやすくするためにユーザーパラメータに既定値を設定しておきます.
InputNormalTex には適当なNormalMapアセット.
OutputHeight には準備しておいた出力用RenderTargetTexture.

ユーザーパラメータとDataInterfaceのバインド

内部でデータを参照できるように入力NormalMap等をバインドします.

EmitterSpawnステージの + から 「新規または既存のパラメータを直接設定」でSetParameter項目を追加.

SetParameterの + から Texture Sample を選択.

名前を DI_InputNormalTexに変更.
Texture User Parameter にユーザーパラメータの InputNormalTex を設定.

同様に SetParameter を追加して Render Target 2D に設定.
名前を DI_OutputRenderTarget に変更.
ユーザーパラメータを利用するために Inherit User Parameter Settings をON.
Render Target User Parameter に OutputHeight を設定.

プレビューマテリアルの設定

EmitterのSpriteRendererにプレビュー用マテリアル M_PreviewHeight を設定します.

マテリアルのテクスチャパラメータ InTex にプレビュー対象のテクスチャをバインドします.
SpriteRendererのバインディング>Material Parameters>Attribute Bindings に要素を追加.
Material Parameter Name からプレビュー用マテリアルのテクスチャパラメータ InTex が選択可能です.
Niagara Variable に出力先RenderTarget2Dである DI_OutputRenderTarget をバインドします.
これで計算結果が書き込まれたRenderTargetの内容をスプライトに表示できます.

Grid2DCollectionの準備

Grid2DCollectionは汎用2Dグリッドデータインターフェイスです.
指定解像度グリッドの集合(Collection)として様々なデータの格納ができます.
今回は反復更新中のHeightMap値を格納するために使用します.

EmitterSpawnステージの + からSetParameter項目を追加し,
SetParameterの + からGrid2DCollection を選択する.
名前を WorkGrid2D に変更.
パラメータを展開して Num Attributes を 1 以上に設定.
(本来必要ないと思われますが, ここで1以上の値を設定していないとGrid2Dが値を保持してくれない場合があります)

次にグリッド解像度を設定します.
今回は入力NormalMapと同じ解像度で計算をしたいのでそのための設定もします.
標準では解像度設定モジュールが無いのでスクラッチパッドモジュールで処理します.
EmitterSpawnステージの + から「新しいスクラッチパッドモジュール」.

空のモジュールが生成されるので, モジュール名は SetResolutionWorkGrid にしておきましょう.

SetResolutionWorkGridの入力Mapに
解像度設定対象のGrid2DCollection
サイズ情報を取得するためのTextureSample
の枠を追加します.

TextureSampleからGetTextureDimensionsでサイズ情報を取得し,
Grid2DCollectionのSetNumCellsで解像度を設定するように組んで適用ボタンを押します.

Emitterのモジュール詳細パネルに戻ると,
入力Mapに追加したGrid2DCollectionとTextureSampleの項目が現れます.
Grid2DCollectionには Link Inputs/エミッタ/WorkGrid2D
TextureSampleには Link Inputs/エミッタ/DI_InputNormalTex
を設定すると, ようやく入力NormalMapと同じ解像度のGridのセットアップが完了です.

初期化ステージ

Emitterの +ステージ から GenericSimulationStage を追加します.

詳細パネルでこのステージの設定をします. 名前はInitStage等にしておきます.

  • Simulation Stage Name は InitStage
    • わかりやすい名前ならなんでも良い
  • Iteration Source は Data Interface
    • Grid2DなどのDataInterfaceの要素を実行単位にするため.
  • Execute Behavior は On Simulation Reset
    • このステージはEmitterがSpawnしたタイミングなどの初回一度だけ実行するため.
  • Data Interface は WorkGrid2D
    • このGrid2Dの要素毎に処理を実行するため
      • ComputeShaderとして考えると, このGrid2Dの解像度分のThreadが起動する.


Stageの + からこのStageの処理を記述するスクラッチパッドモジュールを追加します.

入力Mapに Grid2DCollection と TextureSample を追加します.
それぞれ作業用GridとNormalMapを取り込むために使います.
出力Mapに Vector2D と float を追加します.
それぞれ名前を NormalXY と Height に変更します.
さらに右クリックから 名前空間を変更>STACKCONTEXT に設定します.
STACKCONTEXT はStageやフレームを跨いで読み書きできる名前空間(カテゴリ)です.
使用する場所でもっとも適切な名前空間になってくれるようです.
まとめると以下のようになります.

  • 入力Map
    • Grid2DCollection
      • Grid2D上での作業のため
    • TextureSample
      • NormalMap取り込みのため
  • 出力Map
    • NormalXY (STACKCONTEXT Vector2D
      • NormalMapの値をGrid2D上に保存して読み取るため.
    • Height ( STACKCONTEXT float
      • Grid2D上でのHeight更新作業用.

処理内容としては

  • NormalMapからサンプリングしたベクトルをGrid2DにNormalXYという名前で登録
    • 後段のStageではTextureにアクセスせずGrid2Dで完結させるため
  • Grid2DにHeightという名前でfloat値を登録.
    • 初期値は0. このHeightが反復処理で更新されていく.


適用ボタンを押したらEmitterのモジュール詳細から

  • Grid2DCollection にWorkGrid2D を設定
  • TextureSample にDI_InputNormalTex を設定

Jacobi反復処理ステージ

GenericSimulationStageを新規追加して以下のように設定します.

  • Simulation Stage Name は JacobiIteration
  • Iteration Source は Data Interface
  • Num Iterations は 1
    • ここが1フレームで実行される反復数になります. 1024とか入れるとUEがハングするので注意.
      • 初期化ステージのInitStageを On Simulation Reset にしたことで, フレームを跨いで大量の反復を継続するようにしています.
  • Execute Behavior は Always
    • このステージは毎フレーム実行するため.
  • Data Interface は WorkGrid2D

クラッチパッドを追加します.

  • 入力Map
    • Grid2DCollection
      • Grid2D上での作業のため
  • 出力Map
    • Height ( STACKCONTEXT float
      • Grid2D上のHeightを更新するため.

クラッチパッドにはJacobi法によるHeight値更新計算をCustomHLSLで記述します.
CustomHLSLノードの入出力は以下のように設定します.

  • 入力
    • Grid2DCollection
    • Cell座標X(int)
    • Cell座標Y(int)
  • 出力
    • 更新後のHeight値(float)

CustomHLSLの中身は先のJacobi法をそのままコードに落とし込み, NormalXYやHeightと名付けしたGrid上の値を読み取って計算しています.
面白い点としては GetPreviousVector2DValue() などの関数で, 前回反復時のGridの値 を取得できることでしょうか.
GPGPUで並列計算を書いたことがある方にはお馴染みですが, 並列計算における近傍値読み取りと書き込みの衝突解決のためにダブルバッファ化して読み取りは前回バッファ, 書き込みは今回バッファとするということを仕組みとしてサポートしてくれているようです.
また, 最後のほうでHeightのmax(0, )をとって正の値に制約していますが. こちらはProjectionという値の取りうる値にクランプする手法になります.
(Height値は正の値であるべきなので)

// Get Grid Size.
int NumCellX; int NumCellY;
WorkGrid.GetNumCells(NumCellX, NumCellY);

float cur_height;
WorkGrid.GetPreviousFloatValue<Attribute="Height">(CellPosX, CellPosY, cur_height);

// Neighbor Normal.
float2 nnx0 = float2(0,0);
float2 nnx1 = float2(0,0);
float2 nny0 = float2(0,0);
float2 nny1 = float2(0,0);

// Neighbor Current Height.
float nhx0 = 0;
float nhx1 = 0;
float nhy0 = 0;
float nhy1 = 0;

// Get Neighbor Normal and Height.
if(0 < CellPosX)
{
    WorkGrid.GetPreviousVector2DValue<Attribute="NormalXY">(CellPosX - 1, CellPosY, nnx1);
    WorkGrid.GetPreviousFloatValue<Attribute="Height">(CellPosX - 1, CellPosY, nhx1);
}
if(NumCellX-1 > CellPosX)
{
    WorkGrid.GetPreviousVector2DValue<Attribute="NormalXY">(CellPosX + 1, CellPosY, nnx0);
    WorkGrid.GetPreviousFloatValue<Attribute="Height">(CellPosX + 1, CellPosY, nhx0);
}
if(0 < CellPosY)
{
    WorkGrid.GetPreviousVector2DValue<Attribute="NormalXY">(CellPosX, CellPosY - 1, nny1);
    WorkGrid.GetPreviousFloatValue<Attribute="Height">(CellPosX, CellPosY - 1, nhy1);
}
if(NumCellY-1 > CellPosY)
{
    WorkGrid.GetPreviousVector2DValue<Attribute="NormalXY">(CellPosX, CellPosY + 1, nny0);
    WorkGrid.GetPreviousFloatValue<Attribute="Height">(CellPosX, CellPosY + 1, nhy0);
}

// Calc Jacobi Iteration Step for Height.
const float dn = ((nnx0.x - nnx1.x) + (nny0.y - nny1.y)) / 2.0;
float next_height = (nhx0 + nhx1 + nhy0 + nhy1 + dn) / 4.0;
// Projected. Constrain to a number greater than 0.
OutHeight = max(0.0, next_height);

クラッチパッドを適用したらInitStageと同様にスクラッチパッド入力のGrid2DCollectionにWorkGrid2Dを設定します.

出力ステージ

GenericSimulationStageを新規追加して以下のように設定します.

  • Simulation Stage Name は JacobiIteration
  • Iteration Source は Data Interface
  • Num Iterations は 1
  • Execute Behavior は Always
    • 毎フレーム結果を外部RenderTargetに出力します.
  • Data Interface は DI_OutputRenderTarget
    • このステージは結果のHeightMap出力を目的のため, 出力RenderTargetである DI_OutputRenderTarget を指定.

クラッチパッドは特に複雑なことはなくGrid2DからHeightを取得してRenderTargetへ出力します.

  • 入力Map
    • Grid2DCollection
      • 計算結果のHeightの取り出しのため
    • RenderTarget2D
      • 出力先RenderTargetのData Interfaceとして

注意点としては, 今回のStageはRenderTarget2DをDataInterfaceとしているため, UV座標やテクセル座標を取得するためにGrid2DではなくRenderTarget2Dを使う点です.
(Exec to Unit や Exec to Index)

忘れないようにスクラッチパッド入力の設定をします.
Grid2DCollectionにWorkGrid2D, RenderTarget2DにDI_OutputRenderTarget.

結果

完成するとプレビューや出力先RenderTarget2Dで計算結果のHeightMapが表示されます.
Jacobi反復ステージの反復数を 1 にしているため, フレームごとに1回の反復で徐々に計算が収束していきます.
もっと早く収束させたい場合はJacobi反復ステージの Num Iterations を 16 などに増やしてみてください.
結果のHeightMapをテクスチャとして保存したい場合は
「RenderTarget2Dからスタティックテクスチャを作成」
をしてください.



まとめ

  • NormalMapからHeightMapを復元するために
  • Poisson方程式の反復解法(Jacobi法)を
  • Niagara SimulationStageで実装する

ということをしました.

SimulationStageはNiagaraモジュールとしてComputeShader的なGPGPU計算が実装できて面白いですね.
ランタイムVFX用途ではなく今回のようなツール的な使い方も模索のし甲斐があると思いました.
ただ一部挙動が怪しかったりUIが親切でなかったりドキュメントがなかったりする点でハードルが高いです.
今後のアップデートでどう変わっていくのか注目したいですね.

~~~

次の17日記事は @razupi さんとのことです!

Simplex頂点に任意の乱数を利用できるSimplex Noise Custom Node[UE][UE5]

題名の通りです.
個人的な興味により Simplex Noiseの頂点毎の乱数計算 を差し替えられるCustomNodeを作成したので公開.

Simplex Noise には以下のShadertoyを参考にしています. thanks!
Shader - Shadertoy BETA
(XsX3zB)

前置き

CustomNodeは3段階(コア部分は2段階)で,

  1. 前処理
    • Simplexの頂点情報他を出力
  2. 乱数ノード
    • (ここが任意に差し替え可)
  3. 後処理
    • Simplex頂点の乱数からノイズ出力

という構成です.

使用例としては以下のようになります.

使用例

コピペ用テキスト

マテリアルエディタにうまくコピペできない等の問題がありましたらTwitter等でお知らせください.

前処理CustomNode

Begin Object Class=/Script/UnrealEd.MaterialGraphNode Name="MaterialGraphNode_109"
   Begin Object Class=/Script/Engine.MaterialExpressionCustom Name="MaterialExpressionCustom_0"
   End Object
   Begin Object Name="MaterialExpressionCustom_0"
      Code="\r\n/*\r\n    3D SimplexNoise の前段処理\r\n[Reference materials]\r\nhttps://www.shadertoy.com/view/XsX3zB\r\n\r\n\r\n    input:\r\n            // ノイズ計算座標.\r\n            in_pos\r\n\r\n    output:\r\n            // Simplex(3DではTetrahedron)の頂点座標\r\n            out_simplex_vtx_pos0\r\n            out_simplex_vtx_pos1\r\n            out_simplex_vtx_pos2\r\n            out_simplex_vtx_pos3\r\n            \r\n            // Simplexの頂点へのベクトル. 後段でのGradientNoise計算用.\r\n            out_simplex_d0\r\n            out_simplex_d1\r\n            out_simplex_d2\r\n            out_simplex_d3\r\n\r\n*/\r\n\r\nconst float F3 = 0.33333;\r\nconst float G3 = 0.16667;\r\n// Skew\r\nfloat3 s = floor(in_pos + dot(in_pos, F3));\r\nfloat3 x = in_pos - s + dot(s, G3);\r\n//\r\nfloat3 e = step(0.0, x - x.yzx);\r\nfloat3 i1 = e * (1.0 - e.zxy);\r\nfloat3 i2 = 1.0 - e.zxy*(1.0-e);\r\n\r\n//\r\nfloat3 x1 = x - i1 + G3;\r\nfloat3 x2 = x - i2 + 2.0 * G3;\r\nfloat3 x3 = x - 1.0 + 3.0*G3;\r\n\r\nout_simplex_vtx_pos0 = s;\r\nout_simplex_vtx_pos1 = s + i1;\r\nout_simplex_vtx_pos2 = s + i2;\r\nout_simplex_vtx_pos3 = s + 1;\r\n\r\nout_simplex_d0 = x;\r\nout_simplex_d1 = x1;\r\nout_simplex_d2 = x2;\r\nout_simplex_d3 = x3;\r\n\r\nreturn 1;"
      Description="Custom_SimplexNoise3to1_Setup"
      Inputs(0)=(InputName="in_pos",Input=(Expression=/Script/Engine.MaterialExpressionAdd'"MaterialExpressionAdd_5"'))
      AdditionalOutputs(0)=(OutputName="out_simplex_vtx_pos0",OutputType=CMOT_Float3)
      AdditionalOutputs(1)=(OutputName="out_simplex_vtx_pos1",OutputType=CMOT_Float3)
      AdditionalOutputs(2)=(OutputName="out_simplex_vtx_pos2",OutputType=CMOT_Float3)
      AdditionalOutputs(3)=(OutputName="out_simplex_vtx_pos3",OutputType=CMOT_Float3)
      AdditionalOutputs(4)=(OutputName="out_simplex_d0",OutputType=CMOT_Float3)
      AdditionalOutputs(5)=(OutputName="out_simplex_d1",OutputType=CMOT_Float3)
      AdditionalOutputs(6)=(OutputName="out_simplex_d2",OutputType=CMOT_Float3)
      AdditionalOutputs(7)=(OutputName="out_simplex_d3",OutputType=CMOT_Float3)
      MaterialExpressionEditorX=-2048
      MaterialExpressionEditorY=2512
      MaterialExpressionGuid=4244B9A4494561E6D73F14B3D48C4E67
      Material=/Script/UnrealEd.PreviewMaterial'"/Engine/Transient.M_InnerLayerPm2"'
      bShowOutputNameOnPin=True
      bCollapsed=True
      Outputs(0)=(OutputName="return")
      Outputs(1)=(OutputName="out_simplex_vtx_pos0")
      Outputs(2)=(OutputName="out_simplex_vtx_pos1")
      Outputs(3)=(OutputName="out_simplex_vtx_pos2")
      Outputs(4)=(OutputName="out_simplex_vtx_pos3")
      Outputs(5)=(OutputName="out_simplex_d0")
      Outputs(6)=(OutputName="out_simplex_d1")
      Outputs(7)=(OutputName="out_simplex_d2")
      Outputs(8)=(OutputName="out_simplex_d3")
   End Object
   MaterialExpression=/Script/Engine.MaterialExpressionCustom'"MaterialExpressionCustom_0"'
   NodePosX=-2048
   NodePosY=2512
   NodeGuid=694DDEB64074FA334F4FD9A71A7FD483
   CustomProperties Pin (PinId=14B3EA4E44D4A873700D33A55F7AE5AD,PinName="in_pos",PinType.PinCategory="required",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_103 5246F4EE4F841D0B87A775BDC4A8D155,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=B819619E4D1A27419AB524893B6ECD84,PinName="return",Direction="EGPD_Output",PinType.PinCategory="",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=9EE1744F4571B813B1BBDE890386E4B4,PinName="out_simplex_vtx_pos0",Direction="EGPD_Output",PinType.PinCategory="",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_111 9D0A05744027353F9E7118A2194B1162,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=1E152C9B4E3FDD460C71EBBD7728B4F5,PinName="out_simplex_vtx_pos1",Direction="EGPD_Output",PinType.PinCategory="",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_113 8126FF97480C0E2317A4F7A582DAD0A3,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=EDB6385B40CB4F839646B784E3E9D173,PinName="out_simplex_vtx_pos2",Direction="EGPD_Output",PinType.PinCategory="",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_112 164ABAAD466D2377D81D0CA5A60C9B3C,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=72873D064EE5FD985F001AA3892D9F49,PinName="out_simplex_vtx_pos3",Direction="EGPD_Output",PinType.PinCategory="",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_114 7D8DCA5C4A5DE0ACA7BD0C82724D301A,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=7746174D4FB60AF7C5C5F095B5CA2773,PinName="out_simplex_d0",Direction="EGPD_Output",PinType.PinCategory="",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_110 DB76CCAB48E57973572CEC889D1AD7A3,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=00AD30144201B89E1E00758A88479315,PinName="out_simplex_d1",Direction="EGPD_Output",PinType.PinCategory="",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_110 62FD7FB84275385A5AE2688318A0242F,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=A101A4114BE2E007A7A9DE8B88751614,PinName="out_simplex_d2",Direction="EGPD_Output",PinType.PinCategory="",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_110 68133C4D4BE01D7D4C180184CC5A24C8,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=CF20B6B54351C38C12EB46B59994524D,PinName="out_simplex_d3",Direction="EGPD_Output",PinType.PinCategory="",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_110 1419241C4116FF68392D659CFBE3035C,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
End Object

乱数CustomNode (3D座標から3D乱数出力ができるものに置き換え可能)

Begin Object Class=/Script/UnrealEd.MaterialGraphNode Name="MaterialGraphNode_111"
   Begin Object Class=/Script/Engine.MaterialExpressionCustom Name="MaterialExpressionCustom_2"
   End Object
   Begin Object Name="MaterialExpressionCustom_2"
      Code="float j = 4096.0*sin(dot(in_pos,float3(17.0, 59.4, 15.0)));\r\nfloat3 r;\r\nr.z = frac(512.0*j);\r\nj *= .125;\r\nr.x = frac(512.0*j);\r\nj *= .125;\r\nr.y = frac(512.0*j);\r\nreturn r * 2.0 - 1.0; // [-1.0, +1.0]"
      Description="Custom_Rand3to3"
      Inputs(0)=(InputName="in_pos",Input=(Expression=/Script/Engine.MaterialExpressionCustom'"MaterialExpressionCustom_0"',OutputIndex=1))
      MaterialExpressionEditorX=-1758
      MaterialExpressionEditorY=2471
      MaterialExpressionGuid=8E96CA8240B4E2A0E551B78CCB82D4BB
      Material=/Script/UnrealEd.PreviewMaterial'"/Engine/Transient.M_InnerLayerPm2"'
      bCollapsed=True
   End Object
   MaterialExpression=/Script/Engine.MaterialExpressionCustom'"MaterialExpressionCustom_2"'
   NodePosX=-1758
   NodePosY=2471
   NodeGuid=DFAA6DB9408EE68647E4ED8C95D200B5
   CustomProperties Pin (PinId=9D0A05744027353F9E7118A2194B1162,PinName="in_pos",PinType.PinCategory="required",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_109 9EE1744F4571B813B1BBDE890386E4B4,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=88D45E3940F121BFE10297B3111BCD2E,PinName="Output",PinFriendlyName=NSLOCTEXT("MaterialGraphNode", "Space", " "),Direction="EGPD_Output",PinType.PinCategory="",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_110 85C6354E4423C26466A77DBADE314039,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
End Object

後処理CustomNode

Begin Object Class=/Script/UnrealEd.MaterialGraphNode Name="MaterialGraphNode_110"
   Begin Object Class=/Script/Engine.MaterialExpressionCustom Name="MaterialExpressionCustom_1"
   End Object
   Begin Object Name="MaterialExpressionCustom_1"
      Code="\r\n/*\r\n    3D SimplexNoise の後段処理\r\n[Reference materials]\r\nhttps://www.shadertoy.com/view/XsX3zB\r\n    \r\n    input:\r\n            // 各頂点の3D乱数 [-1.0, +1.0]\r\n            in_rand0\r\n            in_rand1\r\n            in_rand2\r\n            in_rand3\r\n\r\n            // Simplexの頂点へのベクトル (前段からの出力).\r\n            in_simplex_d0\r\n            in_simplex_d1\r\n            in_simplex_d2\r\n            in_simplex_d3\r\n\r\n    output:\r\n            [-1.0, +1.0]\r\n*/\r\n\r\nfloat4 d;\r\nd.x = dot(in_rand0, in_simplex_d0);\r\nd.y = dot(in_rand1, in_simplex_d1);\r\nd.z = dot(in_rand2, in_simplex_d2);\r\nd.w = dot(in_rand3, in_simplex_d3);\r\n\r\n// 入力が [-1, +1] の場合は幅が1になるように 0.5 を乗算.\r\nd *= 0.5;\r\n\r\nfloat4 w;\r\nw.x = dot(in_simplex_d0, in_simplex_d0);\r\nw.y = dot(in_simplex_d1, in_simplex_d1);\r\nw.z = dot(in_simplex_d2, in_simplex_d2);\r\nw.w = dot(in_simplex_d3, in_simplex_d3);\r\nw = max(0.6 - w, 0.0);\r\n\r\n// w^4\r\nw *= w;\r\nw *= w;\r\n\r\nd *= w;\r\n\r\nreturn dot(d, 52.0);"
      Description="Custom_SimplexNoise3to1_Post"
      Inputs(0)=(InputName="in_rand0",Input=(Expression=/Script/Engine.MaterialExpressionCustom'"MaterialExpressionCustom_2"'))
      Inputs(1)=(InputName="in_rand1",Input=(Expression=/Script/Engine.MaterialExpressionCustom'"MaterialExpressionCustom_4"'))
      Inputs(2)=(InputName="in_rand2",Input=(Expression=/Script/Engine.MaterialExpressionCustom'"MaterialExpressionCustom_3"'))
      Inputs(3)=(InputName="in_rand3",Input=(Expression=/Script/Engine.MaterialExpressionCustom'"MaterialExpressionCustom_5"'))
      Inputs(4)=(InputName="in_simplex_d0",Input=(Expression=/Script/Engine.MaterialExpressionCustom'"MaterialExpressionCustom_0"',OutputIndex=5))
      Inputs(5)=(InputName="in_simplex_d1",Input=(Expression=/Script/Engine.MaterialExpressionCustom'"MaterialExpressionCustom_0"',OutputIndex=6))
      Inputs(6)=(InputName="in_simplex_d2",Input=(Expression=/Script/Engine.MaterialExpressionCustom'"MaterialExpressionCustom_0"',OutputIndex=7))
      Inputs(7)=(InputName="in_simplex_d3",Input=(Expression=/Script/Engine.MaterialExpressionCustom'"MaterialExpressionCustom_0"',OutputIndex=8))
      MaterialExpressionEditorX=-1584
      MaterialExpressionEditorY=2544
      MaterialExpressionGuid=4244B9A4494561E6D73F14B3D48C4E67
      Material=/Script/UnrealEd.PreviewMaterial'"/Engine/Transient.M_InnerLayerPm2"'
   End Object
   MaterialExpression=/Script/Engine.MaterialExpressionCustom'"MaterialExpressionCustom_1"'
   NodePosX=-1584
   NodePosY=2544
   NodeGuid=F9EB2DB2403AD4D08D45A2AD2E0586CE
   CustomProperties Pin (PinId=85C6354E4423C26466A77DBADE314039,PinName="in_rand0",PinType.PinCategory="required",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_111 88D45E3940F121BFE10297B3111BCD2E,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=F335C65B48656877255E93868DD98C1F,PinName="in_rand1",PinType.PinCategory="required",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_113 E9C8906544E68AF3921D6D8B25F759C1,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=7BC722274BA713B60F780DA43DB00165,PinName="in_rand2",PinType.PinCategory="required",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_112 17377D3848EAE93152FBE08DAA4D3BDC,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=8F11DF88467900E40B32219130541347,PinName="in_rand3",PinType.PinCategory="required",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_114 5B3AE366474C1AA7E6CB5DA8D83F19DB,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=DB76CCAB48E57973572CEC889D1AD7A3,PinName="in_simplex_d0",PinType.PinCategory="required",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_109 7746174D4FB60AF7C5C5F095B5CA2773,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=62FD7FB84275385A5AE2688318A0242F,PinName="in_simplex_d1",PinType.PinCategory="required",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_109 00AD30144201B89E1E00758A88479315,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=68133C4D4BE01D7D4C180184CC5A24C8,PinName="in_simplex_d2",PinType.PinCategory="required",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_109 A101A4114BE2E007A7A9DE8B88751614,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=1419241C4116FF68392D659CFBE3035C,PinName="in_simplex_d3",PinType.PinCategory="required",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_109 CF20B6B54351C38C12EB46B59994524D,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
   CustomProperties Pin (PinId=E8FC28CB48E48C51AF32CFA4F9CF207C,PinName="Output",PinFriendlyName=NSLOCTEXT("MaterialGraphNode", "Space", " "),Direction="EGPD_Output",PinType.PinCategory="",PinType.PinSubCategory="",PinType.PinSubCategoryObject=None,PinType.PinSubCategoryMemberReference=(),PinType.PinValueType=(),PinType.ContainerType=None,PinType.bIsReference=False,PinType.bIsConst=False,PinType.bIsWeakPointer=False,PinType.bIsUObjectWrapper=False,PinType.bSerializeAsSinglePrecisionFloat=False,LinkedTo=(MaterialGraphNode_115 40255E7447B0E72C17139B99CFBF22F9,),PersistentGuid=00000000000000000000000000000000,bHidden=False,bNotConnectable=False,bDefaultValueIsReadOnly=False,bDefaultValueIsIgnored=False,bAdvancedView=False,bOrphanedPin=False,)
End Object

おまけ

前処理ノードの出力である out_simplex_di はSimplex頂点へのベクトルなので
その長さの反転を重みとして頂点毎の乱数をブレンドすると面白い見た目になったりします.
( 1.0 - length(out_simplex_di) )^16 のような重みでブレンド

UE5.2でC++プロジェクトのビルド時間が増加した問題の対処 [UE][UE5.2]

UE5.1からUE5.2へアップデートしてから, C++プロジェクトのビルド時間が5倍以上に増えて困った.

ソースコードを色々書き換えて調べたところ,

GlobalShader派生クラスへのSRV, UAV設定関数をフラットに記述する数が大きく影響

というところまでわかったのでメモ.
私と同じ沼にハマったひとの役にたてば幸い.

具体的には以下のUtil関数が該当

RenderCore/Public/ShaderParameterUtils.h
SetSRVParameter()
SetUAVParameter()


これらを一つの関数内で直接大量に(フラットに)記述するとビルド時間が大きく増加する模様.
対策としては Shader単位等で関数化してそれを呼び出すような記述 とするとUE5.1以前と同じくらいのビルド時間だった.

// ビルド時間が増えるパターン 100個くらい並んでました.
void MainFunc()
{
    // 全てのShaderに対するSetSRV/SetUAVをフラットに記述.
    SetSRVParameter(ShaderA, SRV);
    SetSRVParameter(ShaderA, SRV);
    SetUAVParameter(ShaderA, UAV);
    SetUAVParameter(ShaderA, UAV);
    ...
    SetSRVParameter(ShaderZ, SRV);
    SetSRVParameter(ShaderZ, SRV);
    SetUAVParameter(ShaderZ, UAV);
    SetUAVParameter(ShaderZ, UAV);
}


エンジン側のソースコードを見るとシェーダ毎等で関数に分けていたため,
そのように書き直したところビルド時間が以前と同じくらいになった.

// ビルド時間が増えないパターン.
void SetParamShaderA()
{
    SetSRVParameter(ShaderA, SRV);
    SetSRVParameter(ShaderA, SRV);
    SetUAVParameter(ShaderA, UAV);
    SetUAVParameter(ShaderA, UAV);
    ...
}
...
void SetParamShaderZ()
{
    SetSRVParameter(ShaderZ, SRV);
    SetSRVParameter(ShaderZ, SRV);
    SetUAVParameter(ShaderZ, UAV);
    SetUAVParameter(ShaderZ, UAV);
    ...
}
void MainFunc()
{
    // Shader毎のSRV/UAV設定メソッドを記述.
    SetParamShaderA(...);
    ...
    SetParamShaderZ(...);
}

私のプロジェクトでは前者の場合 8分前後 かかっていたものが, 後者に修正したところ 1分前後 に短縮された.
========== リビルド は 08:04.257 分 かかりました ==========

========== リビルド は 01:12.661 分 かかりました ==========

Inlineやtemplate展開が関係していたりするのでしょうか?
詳しい人がいれば教えてください

以上

TextureArrayに対して実行可能なDrawMaterialToRenderTargetプラグイン[UE][Plugin]

TextureRenderTarget2DArray のSlice(配列の要素)を指定して DrawMaterialToRenderTarget ができるUEプラグインの公開.

これは何

テクスチャ配列アセットの要素に対してマテリアル描画を実行できるプラグイン.
UE標準のDrawMaterialToRenderTargetノードとほぼ同じで, TextureRenderTarget2DArrayとその何番目にDrawするかを指定できる.

NglDrawMaterialToRenderTargetArraySliceノード

内部で一時テクスチャの生成やコピーをする必要があったため, ゲーム中にリアルタイム利用するような用途向きではない点に注意.
エンジン改造無しで実装するためにこうなったが今後修正できるかもしれない(未定),

動作確認 UE5.1 Win64

導入

リポジトリの NglTextureRt2dArrayUtil をコピー

ue_plugin/distribution/NglTextureRt2dArrayUtil at main · nagakagachi/ue_plugin · GitHub

自身のプロジェクトに Plugins ディレクトリを作成し, その中に上記Githubの NglTextureRt2dArrayUtil ディレクトリをコピーペーストする.


プラグインが読み込まれているか確認

プロジェクトを開いて 編集/プラグイン のウィンドウを開き, NglTextureRt2dArrayUtilプラグインが有効になっていればOK.

サンプル

プラグインコンテンツのEditorUtilityWidget EUW_DrawToRenderTargetArraySample を右クリック->エディターユーティリティウィジットを実行 で簡易サンプルが起動する


  1. ウィンドウ左側にマテリアルと描画先Array要素番号(Slice Index)を指定.
  2. ウィンドウ右側に書き込み先のTextureRTArrayアセットを指定。
  3. Runボタン を押すとArray指定要素にマテリアル描画が実行される.

下図はWidgetでSlice 0番に対して実行して, エディタでTextureRTArrayの Slice 0 を表示して結果を確認している様子.


出来上がったTextureRtArrayをテクスチャ配列アセットに変換するなどご自由に.

最後

役にたった場合はGithubのStarや使ったよというメッセージを貰えると嬉しいです!

そもそも作った理由はエディタでマテリアルを使って自由に色々なテクスチャ配列を作りたかったため.
面白い使い方やもっと良い機能案があればご連絡ください.


以上...

DepthPrepass有効時のCoplanar PolygonによるGPU負荷増大 [UE][Rendering]

DepthPrepassレンダリングに関連して,
モデルやレベルの作りによってGPU負荷が増大してしまう特殊な状況について説明します.

UEに限ったものではありません.
また検証のためForward RenderingモードのUE5を利用していますが, Deferredでも同様の問題が発生します.

問題

同一平面で重なり合うポリゴン(Coplanar Polygon)は, 重なりの数だけBasePass PixelShaderが実行される可能性があります.
これにより本来DepthPrepass環境で除去される不要なBasePass負荷が追加で発生します.

同一平面で重なり合うポリゴン を Coplanar Polygon と呼んでいます.
Lamina Faces や Z-Fighting状態のポリゴン というような表現もあると思います.
もっと良い呼び名があれば教えてください!


以下のサンプルシーンはOpaqueで適当ワールドノイズマテリアルのCubeをいくつか配置した状態です.
よくOverDraw負荷が問題となる半透明が無いシーンなので安心と思いきや, 表示モードを「シェーダ複雑度」へ切り替えてみると.

サンプルシーン

何故か高負荷を示す真っ白な部分があります.

RenderDocでキャプチャして 「Quad Over Draw (Pass)」でOverDrawを確認します.

RenderDoc Quad Over Draw (Pass)

RenderDoc上でも確かに右側の配置でBasePassのOverDrawが発生しているようです.

BasePassではDeferredの場合はGBuffer書き込み, Forwardであれば複雑なLighting計算が実行されるため,本来実行されないはずのOpaqueでのBasePass OverDrawは
GPU負荷に大きな影響を与えます.

原因

この問題の原因は深度が完全に一致するポリゴンが複数重なっている点と, DepthPrepass有りBasePassの仕組みの関係によるものです.

まずシーンの配置ですが, 左側の配置と同じ3つのCubeをポリゴンが重なるように配置しています *1.

このような状況では, DepthPrepass有効時のBasePassは重なり合ったポリゴンそれぞれについて全て実行されてしまい, 高負荷なBasePassが何度も実行されることによりGPU負荷を増大させます.

BasePassが複数回実行される理由

本来のDepthPrepass手法は, DepthPreapssで先に生成したDepthBufferを頼りに, 各Meshの可視ピクセルのマテリアルBasePass PixelShaderだけを最小限実行することで高速化するためのものです.
例として背景, 円柱, 板で構成されたシーンのDepthPrepass有りBasePassの挙動を以下に示します.
右側図の緑色部分がDepthTestを通過してBasePassのPixelShaderが実行されたピクセルになります.

  1. DepthPrepassでDepthBufferが完成
  2. 円柱モデルBasePass描画
  3. 背景モデルBasePass描画
  4. 板モデルBasePass描画

各モデルのBasePassが, 最終的に画面に見えている部分でだけ働いているのがわかると思います.
このようにBasePassが無駄に実行されないようにして高速化するというのがDepthPrepassの目的となります.

これを実現するためにBasePassは

DepthWrite=OFF 且つ DepthTest=GreaterEqual

という設定をされます *2.

この設定によって, 各モデルのBasePass描画では

DepthPrepassで確定した深度値に対して
同じかそれより近い場合にのみ描画する

という挙動をすることになります.

この 同じかそれより近い という点と, 深度が完全に一致してしまう Coplanar Polygon の関係によって今回のような問題が起きています.

終わり

Opaqueであるにも関わらずOverDrawによってBasePass負荷を増加させるCoplanar Polygonと, 関連したDepthprepassの簡単な説明でした.
この問題はときに異常な高負荷を発生させ, 更に原因がわかりにくいという厄介さを持っています (UE5ではシェーダ複雑度で確認できますが).
またMaskedでも同様に問題が起きます.

モデル制作やレベル配置をする際には多少気をつけてもらえると最適化担当の人間に平穏がもたらされます.

この記事が2022最後の記事となります, 良いお年を.


おまけ


最後に問題のCoplanar Polygon状況でのRenderDocキャプチャを貼っておきます.
問題のない配置とCoplanar Polygon配置.
右図の緑がDepthTest通過ピクセル表示.
OverDrawが発生している重なり部分でどのDrawでもDepthTest通過してしまっているのがわかります.

参考文献

*1:1モデル内のポリゴン同士でも, レベルに配置した別モデルのポリゴン同士でも関係なくこの問題は発生します.

*2:GreaterEqualなのはReverseZ環境でのことで, StandardZの場合はLessEqualです.

UE5 Geometry Script を利用した頂点単位リアルタイムアニメーション

Geometry Scriptの利用の一環で

  • 入力メッシュの頂点インデックスをUV1に埋め込み、外部テクスチャに頂点座標をベイク
  • 上記で生成したメッシュとテクスチャを利用してマテリアルで頂点単位バネアニメーション

というものを試してみました

サンプル

レベルに配置されている BP_ControllerInEditorTick を動かすとプルプルします.
Playを実行すると物理で動くモデルがプルプルします.
github.com

構成

  • BP_GenerateMeshForPerVertexAnim
    • 入力StaticMeshアセットから以下のようなアセットを生成する
      • UV1に頂点インデックスを埋め込んだStaticMesh
      • 頂点インデックスに対応したテクセルにローカル頂点座標をベイクしたテクスチャ
  • BP_PerVertexAnimationTest
    • 上記で生成したMeshとテクスチャを利用して頂点単位バネシミュレーションをマテリアル(GPU)で計算する
  • BP_ControllerInEditorTick
    • Editorモードのマニピュレータ操作でバネシミュをプレビューするためのコントローラ

導入

プロジェクトで Geometry Script プラグインを有効化

編集/プラグインエディタから Geometry Script のチェックボックスをONにする
必要ならEditorの再起動をする

PerVtxAnimWithGeometryScriptを取り込み

Windowエクスプローラ上などからプロジェクトのContentフォルダにサンプルの
PerVtxAnimWithGeometryScriptフォルダ
をコピー

生成用サンプルアクター をレベルに配置

BP_GenerateMeshForPerVertexAnim をレベルに配置

生成用サンプルアクターにアセットを設定

BP_GenerateMeshForPerVertexAnimアクターに変換元Mesh, 出力先Mesh & RenderTargetを設定
Sm Src Mesh に変換元のStaticMeshを設定, 図は同梱しているウシモデル(Spot)を設定している様子
Tex Bake Position Target にベイク情報を出力するRenderTargetを設定
Sm Bake Target に変換結果のMeshを出力するStaticMeshを設定

アセット生成

Geometry Scritpが駆動して Sm Bake Target と Tex Bake Position Target のアセットに生成物が保存される
Geometry Scritpはアクターのパラメータ変更等で駆動するため, 位置の変更や Enable Regenerate Mesh のONOFF等でトリガーされる

Sm Bake Target にはUV1に頂点インデックスを埋め込んだStaticMeshが出力される
Tex Bake Position Target には頂点インデックスに対応するテクセルに頂点座標がベイクされたテクスチャが出力される
これらのアセットを利用してリアルタイムに頂点単位アニメーションを実現する

ランタイムサンプルアクターをレベルに配置

BP_PerVertexAnimationTest は 上記アクターで生成されたメッシュとテクスチャを利用するサンプル

ランタイムサンプルアクターにアセットを設定

BP_PerVertexAnimationTest で生成されたメッシュとテクスチャをそれぞれ
Mesh Vtx Attr Baked と Tex Vtx Attr 0 に設定

概要

頂点単位のバネシミュレーションをGPUで実行するためにGeometry Scriptを利用している

Draw Material To RenderTarget で頂点単位計算をGPU実行する
そのためにメッシュのローカル頂点座標をテクスチャにベイク
(Geometry ScriptでのアクセスとCanvasDrawでのテクスチャ書き込み)
更にサーフェイスマテリアルで頂点毎の情報を上記テクスチャから取得するために
Geometry ScriptでUV1に頂点インデックスを埋め込んでいる


Draw Material To RenderTarget でローカル頂点座標テクスチャを利用してバネ計算した結果を変位テクスチャに出力

サーフェイスマテリアルでUV1から頂点インデックスをデコードし, そのインデックスからテクセル座標を計算,
テクセル座標で変位テクスチャから変位ベクトルを取得してWorldPositionOffsetで頂点を動かしている

InstantNeRFで任意画像から3D再構成をするまでの流れメモ

NVIDIA InstantNeRF (InstantNGP) の環境セットアップと,
自身で用意した画像群を使った3D再構成をするフローのメモ.

基本的に公式リポジトリのセットアップ手順そのまま.
github.com

セットアップ

実行環境

WIndows10 64bit
RTX 3070

Requirementsのセットアップ

CUDA

"v10.2 or higher"
11.7 をインストール.
developer.nvidia.com

CMAKE

"v3.21 or higher"
3.24.0-rc5 をインストール.
インストーラによる system Path へのCMAKEパス追加 は true とした.
自分で追加する場合は環境変数Pathに "C:\Program Files\CMake\bin" あたりを追加する.
cmake.org

Python

"3.7 or higher"
自身の環境のAnacondaに 3.9 が含まれていたので改めてインストールはせず.

Python Modules

公式リポジトリの requirements.txt をローカルにダウンロード.
このtxtに必要なモジュールが記述されているのでpipでPython環境にインストール.
https://github.com/NVlabs/instant-ngp/blob/master/requirements.txt

pip install -r requirements.txt
OptiX

"7.3 or higher"
7.5 をインストール.
インストール後に以下の環境変数が設定されていなければ手動で追加する.
変数名 "OptiX_INSTALL_DIR"
値 "C:\ProgramData\NVIDIA Corporation\OptiX SDK バージョン番号"
developer.nvidia.com

InstantNgpのビルド

リポジトリの取得

適当なディレクトリにcloneする.

git clone --recursive https://github.com/nvlabs/instant-ngp
ビルド

cloneしたinstant-ngpディレクトリでCMAKEによるビルドを実行.

cmake . -B build
cmake --build build --config RelWithDebInfo -j
サンプルを実行

ビルドが成功したらサンプルが実行できるか確認する.
instant-ngpディレクトリでコマンドラインからnerf/foxサンプルを実行.

.\build\testbed.exe --scene .\data\nerf\fox
NeRF_Fox_Sample

狐のNeRFシーンサンプルのリアルタイム学習が確認できる.

3D再構成の準備

NeRFの学習には画像毎のカメラ姿勢情報が必要.
COLMAPを利用することで画像群からカメラ姿勢を推定することができ, NeRFの学習に利用できるようになる.
InstantNgpでは colmap2nerf.py というスクリプトが提供されており, COLMAPで推定したカメラ姿勢情報をNeRF向けに変換できる.
詳細は以下の公式tipsの「Preparing new NeRF datasets」を参考.
github.com

COLMAP

ダウンロード

COLMAP公式から pre-build binaries リンクを辿ってビルド済みバイナリを取得.
今回は COLMAP-3.7-windows-cuda.zip をダウンロードして適当なディレクトリに展開.
colmap.github.io

環境変数Pathへの追加

環境変数PathにCOLMAPディレクトリを追加する.
(colmap2nerf.pyでCOLMAP.batを参照するため)

3D再構成

新規に用意した画像群でInstatnNeRFを学習する.
簡単のために画像データは instant-ngp ディレクトリの data/nerf 下に新規にディレクトリを作成するものとする.

画像ファイルの準備

instant-ngp/data/nerf/new_data ディレクトリを作成.
instant-ngp/data/nerf/new_data/images ディレクトリを作成.
instant-ngp/data/nerf/new_data/images に画像群をコピー.

COLMAPによる画像群カメラ姿勢推定

instant-ngp/data/nerf/new_data ディレクトリでコマンドラインからcolmap2nerf.pyを実行してカメラ姿勢情報を生成する.

python [path-to-instant-ngp]/scripts/colmap2nerf.py --colmap_matcher exhaustive --run_colmap --aabb_scale 16

これで画像毎のカメラ姿勢情報を格納したtransforms.jsonなどが生成される.

InstantNeRFを実行

instant-ngpディレクトリでコマンドラインからnew_dataでNeRFを実行する.

.\build\testbed.exe --mode nerf --scene .\data\nerf\new_data

以上で新たに用意した画像群からカメラ姿勢推定情報を計算してInstantNeRFで再構成することができる.
現実で手頃な被写体がなかったので、ゲームのスクリーンショット画像群で試した例がこちら(ELDEN RING)
スクリーンショットからでもカメラ姿勢推定ができるCOLMAPの威力を見た...