render massive amount of cubes in Unity (2)

以前 検証した Unity で大量の cube を描く方法の続きです。あれからまたいくつか検証を重ねてきたのでまとめておこうと思います。

まず前提として、今回解説する方法は全て Graphics.DrawProcedual() を使うアプローチです。以前の記事を書いた時点では気づいていなかったのですが、Graphics.DrawProcedual() を使うと instancing 描画ができます。
残念ながらこの API、現状 OpenGL 系のプラットフォームでは使えません。つまり Android & iOS では使えず、PC でも Windows のみとなってしまいます。しかし近い将来いろんなプラットフォームでサポートされていくはずです。(近年のモバイルデバイスはハードウェア的には既に instancing 描画をサポートしています)

この Graphics.DrawProcedual()、D3D11 の DrawInstanced() とかとはやや違う不思議な仕様になっていて、vertex shader の入力に頂点データを渡せません。頂点 ID (SV_VertexID)、インスタンス ID (SV_InstanceID) のみが入力となります。頂点データは自力で ComputeBuffer を生成&アサインする必要があります。
また、この API を使う場合、たぶん Unity の surface shader は使えず、ライティング処理なども自力で書く必要があります。(Unity 5 であれば G-Buffer 書き出しだけ独自に行い、ライティングは共通処理を使えると思いますが、未検証)
総じて、遺憾ながら、汎用性や既存の Unity の機能との親和性では、以前の記事の擬似 instancing の方に分があると言わざるをえません。しかし使える環境であればこちらの方が実装はスマートかつ VRAM 使用量がずっと少なく済みます。


前回の記事同様、C++ でパーティクルの計算を行い、パーティクルデータを Unity 側のテクスチャに書き込みます。
立方体の頂点データは ComputeBuffer を用意してその中に格納しておきます。前回の擬似 instancing と違い、一個分の頂点で十分です。
そして DrawProcedual() で描きます。頂点シェーダの インスタンス ID からそのパーティクルのデータがあるテクスチャ座標を計算して取得、頂点を動かしたりなどの処理を行います。
(以下は Core i7 2.3 Ghz x4 & GetForce GT 750M での結果)
ss
ソース&プロジェクト一式

ComputeBuffer であれば任意の構造のデータを格納できるので、パーティクルのデータもテクスチャではなく ComputeBuffer に入れたいところですが、ComputeBuffer.SetData() に毎フレームでかいデータを渡すと超遅くて話にならないので諦めました。
実は、以前の Mono の API を使って C++ から C# の機能にアクセスする 方法の検証、あれの目的の半分くらいは ComputeBuffer.SetData() を C++ から呼ぶことで高速に GPU にデータを転送することでした。しかし、実際にやってみたところ全然速くなくて、もう Mono を介した時点で毎フレーム大量のデータをやりとりするのは無理がある、ということがわかりました。

可能性がありそうなまだ検証できていないアプローチとして、ComputeBuffer.SetData() の先にある Unity の内部 API を C++ から直接呼ぶ、というものがあります。
このアプローチの障害として、該当 API は dllexport すらされていないので、まともな方法ではアクセスできません。Development ビルドであれば生成されるデバッグ情報に該当 API の情報が残っているので dbghelp の API を用いることでアクセスできるはずですが、非 Development ビルドでそれをどうやるかは目下考え中です。
また、ComputeBuffer の内部変数からプラットフォーム固有オブジェクト (ID3DBuffer* など) を取れないか調べてみましたが、ComputeBuffer が保持してるのはハンドルであり、結局 Unity の内部 API を呼ぶ必要があるので諦めました。
とはいえ、これらの未検証アプローチも速度的には今回の テクスチャにインスタンスデータ置いて DrawProcedual() で描くのとほとんど差はないはずで、互換性重視の前回の擬似 instancing & もうちょっとスマートで低 VRAM 使用量の今回の汚い instancing が現実的な落とし所ではありそうな気がします。


あとは前回放り投げた compute shader を使うアプローチを試してみました。Unity の compute shader は程よく抽象化されてて、既に GPGPU プログラミングをやったことある人であればかなり楽に感じられるんじゃないかと思います。このスレッドの Aras のサンプルプログラムを見れば大体の使い方はわかるでしょう。
パーティクルを compute shader で実装する場合、あらかじめパーティクルデータを ComputeBuffer で最大個数分確保しておき、compute shader でぶん回します。レンダリングの際はその ComputeBuffer をそのまま Material.SetBuffer() で設定し、vertex shader でインタンス ID からパーティクルデータを取得してよろしくやるだけです。

csparticle
ごく普通にパーティクル生成。いつも通り hash grid + bitonic sort で高速化してパーティクルの相互衝突を実現しています。

GBCollision
G-Buffer 見てスクリーンスペースで当たり判定を取るというアレ。床の穴は boolean で開けています。こういうのは GPU particle ならではという感じです。

VectorField
vector field。空間を適当に等間隔グリッドで切り、各グリッドに乱数で適当なベクトルを与え、それをパーティクルの動きに加算しています。実装は簡単ながら、劇的な効果が得られました。

TestException
exception ぽい簡単なゲーム。GPU <-> CPU 間データ転送は遅いとはいえ、今どきの PC ならこれくらいは割と動いてくれる感触です。

fluid
SPH。簡単なゲーム仕立て。SPH 自体は DirectX SDK のサンプルについてるもののほぼベタ移植です。

ソース&プロジェクトは これ に含まれています。

やってみたところ、これまでの記事で解説したような、C++ でプラグイン書いてヘンな方法で GPU にデータ渡すよりもずっと楽でした。WebPlayer で動くのも素晴らしいです。なかなかいいプログラマのおもちゃなんじゃないかと思います。
ただ、個人的な実感として、compute shader を使うアプローチはデスクトップ GPU 環境であれば十分ありだと思いますが、モバイル系の非力な GPU まで考慮すると慎重にならざるを得ない、という感触です。
今どき CPU パワーは大抵余ってると思いますが、GPU に関しては未だにちょっと凝った絵出そうとしたら簡単にパワー使い果たしてしまいます。その上 GPU は CPU と比べるとスペックの上限下限の幅が広く、オンボード GPU とデスクトップ向けハイエンド GPU では 10 倍とかのオーダーでパワーに差があります。
ゲームロジックには関係ないエフェクト類であれば、重ければ無効化すればいいので好きに実装していいのですが、例えば上の exception 的ゲームの例のようにゲームロジックに直結する機能を GPGPU で実装すると、GPU が非力な環境では GPGPU だけでほぼ手一杯になってグラフィックにパワーが割けなくなってしまいます。(atomic を作ってた時これを実感してパーティクルの実装を GPU から CPU に戻したという経緯があるのですが、今も同じ感触です)
ただ、PS4 とかだと逆に GPU がパワフルで CPU がしょぼいので、重いゲームロジックを実装する場合、PC とコンソールで大きくアプローチを変える必要が出てくるかもしれません…。


あとは、pixel shader でパーティクルの計算を行う方法 (=compute shader 登場以前の GPGPU) も試そう…と思ったのですが、RenderTexture には GetPixel() がなく、GPU -> CPU へのデータ転送が超面倒だとわかって結局やらずじまいになっています。
手順だけ解説すると、例えば私のパーティクルエンジンの場合、パーティクルデータは一つ 48 byte、float 12 個分です。なので、ARGB float フォーマットの RenderTexture を 3 枚用意し、各 pixel を 1 つのパーティクルとみなして計算を行い multi render target でこの 3 枚にデータを出力することで、pixel shader でパーティクルを計算 & 結果を保持することができます。
前時代的な方法ですが、遺憾ながらポータビリティを考えると今でも有用と言わざるを得ません。GPU 内で完結する純粋なエフェクト用パーティクルであればこの方法でも十分行けるとは思います。AssetStore を見てると、たぶんこの方法で実装してると思われるパーティクルエンジンが散見されます。