3Dで大量のオブジェクトを操作するような表現をする際にはInstancingというテクニックが使われます。
Instancingがどういうものなのかの説明はここでは割愛します。
UE4ではInstanced Static Meshコンポーネントを使うことで実現可能ですが、複製されたオブジェクトの一つ一つ制御しようとブループリント(つまりはCPU)側で実装すると数が多い場合には思うようなパフォーマンスが出ません。
実際にはInstanced Static MeshコンポーネントのUpdate Instance Transformというノードを使用するのですが、
そのノードを使って位置情報を更新する度に、全頂点のVRAMの頂点バッファへの乗せ換えというコストが発生します。
そのコストをなくす為には全てをシェーダー内、つまりはマテリアルBP内でやってしまえばいいんです。
各オブジェクトの位置情報の更新を全てマテリアルBP内で行うのですが、単純な動きであれば問題ありませんが
現在どの位置に自分自身がいるのかが必要な動きを実現したい場合にはテクニックが必要です。
その場合、各オブジェクトの現在位置を次フレームに持ち越さないといけないのですが、マテリアルBP内では変数等に値をとって
次フレームに持ち越すといった事はできません。
ではどうするのか?
現在位置をテクスチャに書き込めばいいのです。
各オブジェクトの現在位置XYZをRGBの値としてテクスチャのピクセルに書き込み、そのテクスチャを次フレーム、マテリアルBPに渡し、
その中でテクスチャの任意の座標のRGBを読み取りそれを前フレームのXYZとして取得するのです。
重い計算処理をGPU側で行い計算結果をテクスチャに書き込み、CPU側でテクスチャから計算結果を取り出し手利用するテクニックをGPGPUと言います。
ツッコミが入ったので修正。Wikiによると
画像処理を専門とする演算装置であるGPUを画像処理以外の目的に応用する技術のことである。
一昔前はシェーダーによるGPU側で計算を行い、結果をテクスチャに書き込み、CPU側でテクスチャのRGBから計算結果を読み取るといった古典的テクニックがあった。
このテクスチャへの書き込みにはFBO(FrameBufferObject)が使われます。
テクスチャを介したGPGPUは古の古典的なテクニックなのですが、僕はGPGPU自体については特に詳しいわけではありません。
今回はCPUで取り出すのではなくGPUで書き込んだ結果をGPU側で読み取り使うといった事に使いたいと思います。
この手法はGPUパーティクルシステムとか呼ばれたりします。
UE4のカスケードを使ったGPUパーティクル等もGPGPUよりもさらには次世代的なアプローチであるComputeShaderを使った方法をエンジン内部で行われているかと思います。
残念ながら現状ではエンジンの改造なしにComputeShaderを利用する事は出来ないようです。
UE4では古典的なテクスチャを介したGPGPUで大量のオブジェクトを操作するテクニックがやりづらくなっております。
どういう事か?
一言でいうと
「何でマテリアルBP内でInstanceIDって取得できないの?」
という点でまず第一歩躓きました。
PerInstanceRandomというノードがありますが、違うんだ!
オレが求めているのはそんなのじゃない!
そんなんで誤魔化されないぞ!!
GPGPUでInstancingを使った大量のオブジェクトを操作するにはInstanceIDが必須で是が非でも必要なのです。
この記事は、UE4のエンジン改造なしでブループリントだけで自力でInstanceIDを振り分けてGPGPUまで行おうという試みを順に説明していこうかなという連載の記念すべき第一回目です。
UE4が用意していないInstanceIDの自力での割り振り方を書きます。
InstanceIDを自力で発行する事でその後のGPGPUによるテクニックを使う事でエンジン改造なしでも下記の様な表現が可能になります。
40000個キューブをマテリアル上で移動させるテスト。ドローコールは一回。ピンポンバッファでテクスチャへの毎フレームの各位置保存とVelocityMapno的なやつっを使って移動させるのはまだ作成中。とりあえず動作確認なのでとりあえずVectorNoiseで #UE4Study pic.twitter.com/72ZoebYnQW
— selflash (@selflash) 2017年6月26日
InscaneIDを計算取得する部分で致命的な勘違いで根幹の部分のアプローチ変えてごり押しな方法でも意外と動作した。けど根幹変わったのになんで上手くいってるのか自分でも説明できないノードの繋ぎがある・・とりあえず125000キューブの制御。マイクラみたいだ #UE4 #GPGPU pic.twitter.com/tgh7awGVtn
— selflash (@selflash) 2017年7月5日
GPGPUじゃないけど、各インスタンスの個別の拡縮と回転アニメーションのテスト。マテリアルノードだけでこのくらいの表現ならすんなり60FPSでる。Rotate About Axisノードついて2軸以上の回転の仕方が全然わからなかった、、 #UE4 pic.twitter.com/ygo9PblKXo
— selflash (@selflash) 2017年7月10日
UE4でのGPGPUについての進捗は気ままにYoutubeのここに載せています。
でわ行きます。
まず初めに適当にレベルを作って、Actorを継承したBP_InstanceIDTestという名前のブループリントクラスを作ります。
その後M_InstancedIDTestという名前のマテリアルを作ります。
BP_InstanceIDTestを開きInstanced Static Meshコンポーネントを追加しましょう。
Instanced Static MeshコンポーネントのStatic Meshには適当にStatic MeshのCubeを設定しておきましょう。
次にキャプチャの通りに変数も用意しましょう。
Private : Material Instance Dynamic : Result Material = None
Public : Integer : Num = 2
Public : Float : Margin = 100
Public : Float : Size = 100
Private : Vector : IndexZeroPosition = 0, 0, 0
NumとMarginはパブリックにしておき、レベルに配置した時に適当な値を設定します。
ここでいうSizeとは複製元のスタティックメッシュのサイズの事です。
今回はInstanced Static MeshのMeshにはStatic MeshのCubeを設定しています。
このCubeの一辺の長さが100なのでSizeのデフォルトValueは100としてください。
ブループリントのConstruction Scriptの中身の実装はこんな感じです。入りきらなかったのでキャプチャ画像が二つに分かれています。
Create Dynamic Material Instanceに指定しているマテリアルは先ほど作ったM_InstancedIDTestというマテリアルです。
M_InstancedIDTestを開き、詳細タブのUsed with Instanced Static Meshesにチェックを入れます。こちらにチェックを入れないと動作しませんので気を付けてください。
マテリアルBP内の実装はこんな感じです。
何をしているのかというとまずBP_InstanceIDTestのConstruction Script内ではキューブをグリッド上に並べます。
この時に各キューブの間に隙間を開けておきます。
マテリアルBPの内部ではこの各キューブの頂点位置からInstanceIDとなる値を計算しています。
グリッド上に並べたキューブの頂点は同一グリッド上の頂点であれば同じ値を返す様な計算がマテリアルBP内で行われています。
この値はBP_InstanceIDTest内でAddInstanceされた際の位置をUpdate Instance Transformノードなどで位置が変わらない限り、
マテリアルBP内では変わる事のなく、且つ各キューブ毎にユニークな値として保障されます。
例え、マテリアルBP内で頂点の移動が行われようともそこは変わりません。
[追記]上記のキャプチャ内で使っているWorldPositionノードですが、後にNormalの計算をする時にこの状態だと具合が悪いのですが、Twitterで解決方法を教えてもらいました。下記のキャプチャの様にExcluding Material Offsetsを選択しておいてください。そうしないと後々困った事になります、、何に困るのかはここをご覧ください。
BP_InstanceIDTestをレベルに配置し、NumとMarginに適当な値を入れましょう。
例えば、Numを3とすると、3 x 3 x 3 = 27個のキューブがMarginの分だけ間隔を保って配置されます。
実行すると下記の様に表示されると思います。
各キューブに表示されているのはInstanceIDです。
左下から右に、順番に数字がインクリメントされて表示されています。
InstanceIDは0から始まるInt型として計算していますが、表示上に0以下の小数点が表示されているのはDebugScalarValuesノードの
仕様の様です。ノードのつなぎ自体は最後にちゃんとFloorした値をInstanceIDとしているのでこの端数は無視してください。
次に、この中のInstanceIDが0番のキューブだけを移動させてみましょう。
下記の通りノードを追加してWorld Position Offsetにつないでみてください。
下記の通りに一つだけキューブが離れていけば成功です。
離れていく毎にキューブの表示が0から別の数字になるかと思いますが、これは表示上だけの様でInstanceIDの計算がズレてきたわけでは
ありません。どうもDebugScalarValuesでのEmissive Colorへの表示がWorld Position Offsetの処理よりも後に実行されている様です。
この事で数日間悩んだ事がありますが、気にする事はなかったようです、、
無視しましょう。
[追記]Emissive Color出力ピンへの繋ぎはWorld Position Offset出力ピンへの繋ぎよりも後に処理される為です。ですがこの件については先のInstanceIDを求める際に使っているWorld PositionノードのShader Offsetsの設定でExcluding Material Offsetsを選択しておけば起きない現象だと思います。
この様に、マテリアルBP内でInstanceID毎に、位置や、スケール、カラーやマテリアルなど個別の処理をさせる事が可能になります。
単純な動きであればこのままでいろいろな動きをさせる事が可能でしょう。
ですが、複雑な動きをさせたい場合はマテリアルBPを最大限に活用したテクニックが必要となってきます。
そして、今回のInstanceIDを得る事はそれをする為に必要な第一歩です。
次回はレンダーターゲットを活用したGPGPUの方法についてまた書いていこうと思います。