DirectXの話 第127回
One-pass Surface Voxelization
少し前に発表されたUnreal Engine 4では動的なリアルタイムグローバルイルミネーションが実装されていました。
この技術として使用されているのがVoxelです。
Voxelとは、Pixelの3D表現で、3Dの各軸に平行な立方体のことです。
2Dではドット絵と呼ばれるグラフィックを3Dに拡張するとVoxelの集合体になるわけです。
Voxelization、Voxelizeとは、3Dポリゴンモデル等をVoxelモデルに変換する、”Voxel化(する)”という意味になります。
そもそもなぜVoxel化する必要があるのでしょうか?
Voxelモデルにはいくつかの利点と欠点が存在しています。
利点として上げられるものの1つがレイトレーシングなどの衝突判定を取る処理と親和性が高いという点でしょう。
Voxelは空間に均一に並べられた立方体なので、レイとの衝突判定を単純化することが可能です。
Octreeなどのデータ構造にすることで、より高速な判定も可能です。
もう1つの利点としてはポリゴンモデルでは表現が出来ない”オブジェクトの中身”を表現することが出来る点です。
ただし、今回の技術はあくまでもSurfaceのみのVoxel化なので、残念ながら中身までは表現できません。
もちろん欠点もあり、データサイズが大きくなりがちだったり、ポリゴンモデルでは当たり前に出来るボーンによるデフォームが面倒だったりします。
ドット絵でもそうですが、データサイズを下げようと思うとVoxel1つの大きさが大きくなり、モデルの見栄えが悪くなることも考えられます。
しかし、次世代グラフィックで使用されるVoxelはグローバルイルミネーションなどがメインになるのではないかと思います。
これであれば少々粗くても問題ない…と思います。
では、Surface Voxelizationの解説を行いましょう。
今回の技術は名前の通り、1パスの描画でSurface、つまりポリゴン自体をVoxel化します。
この方法は前述のUE4のGIで使用された技術の解説スライドに掲載されていた手法です。
細かな部分でよくわからないところもあったのですが、その辺はすっ飛ばしました。
このスライドでは他にも、Compute Shaderを用いて衝突判定を取る方法、3Dテクスチャの階層ごとに描画を行う方法が紹介されています。
前者は単純に衝突判定を取るだけですが、Voxelの色を衝突箇所から取得する方法とか面倒くさくて考えたくないです。
後者は階層、スライスごとでは2Dテクスチャになるので、それを利用して普通に描画する方法です。
しかし、例えば256^3の3Dテクスチャの場合、階層は256あるわけで、MRTで8枚に同時に描画できたとしても32パス必要になります。
とてもじゃないけどリアルタイムに処理できるとは思えません。
では、One-pass Surface Voxelizationの処理の流れを見てみましょう。
この方法はGeometry ShaderとUAVが必要となるため、DirectX11世代でないとできません。
描画手法は以下のようになります。
1.ポリゴンをワールド空間に変換する(VS)
↓
2.ワールド空間においてポリゴンが最も支配的な面を求める(GS)
↓
3.ポリゴンの辺をVoxelサイズに合わせて膨らませる(GS)
↓
4.支配的な面に対して描画を行う(GS)
↓
5.フレームバッファに描き込むべきカラーを3Dテクスチャ(UAV)に描き込む(PS)
こんな感じでしょうか。
()内はどのシェーダを利用するかを示しています。
VSはVertex Shader、GSはGeometry Shader、PSはPixel Shaderです。
今回のGeometry Shaderは頂点の増減を行っていません。三角ポリゴンの分析を行うために使用しています。
ではそれぞれの部分を見ていきます。サンプルにある"render_voxel.hlsl"が対応するシェーダファイルです。
1.はRenderVS, RenderTexVS, RenderBoneVS, RenderBoneTexVSの4つの関数が相当します。
内容はポリゴンの頂点をワールド空間に変換しているだけです。
法線も変換していますが、ライティングを行わないなら必要ありません。GIに使うならライティングは必要でしょうけど。
ここは難しいこともないのでスルーしましょう。
2.はポリゴンを三面図で見たときに、どの面に対して最も面積が大きくなるかを求めています。
つまり、XY平面、XZ平面、YZ平面の中で最も面積が大きくなる面を支配的な面として設定します。
面積はヘロンの公式によって求めていますが、面積の大小が必要なだけなのでsqrt()は行いません。
RenderGS関数内、324~352行目がその処理です。
こちらも難しいことはないでしょう。
3.は今回、正確な計算は行っていません。
本来は以下の図のように、投影した各頂点をVoxelサイズに拡大し、それを内包する三角形を計算するべきです。
しかし計算が面倒+正確にやると速度面で不利になりそうという理由でやってません。
この処理はやらなくてもある程度の結果は得られますが、小さなポリゴン、細いポリゴンなどは上手くVoxel化されない可能性があります。
コードとしては354~364行目ですが、#if 0 にすれば処理をすっ飛ばせます。
4.は2.で求めた支配的な面を描画面として描画を行うため、XYZの要素をswizzleしています。
XY平面ならそのままなんですが、他の面ではswizzleを行います。
また、この際に上図のクリップボックスも計算しています。
この処理は366~392行目で、その後は3頂点を出力してPixel Shaderに処理を流しているだけです。
5.はピクセル単位の処理となります。
カラーの取得は普通にテクスチャなり頂点カラーなりから取得できます。
もちろん、Per-Pixel Lightingをする場合もここで計算を行います。
求めたカラーは普通の描画バッファには出力しません。出力先はUAVとなります。
このUAVは3Dテクスチャで、ピクセル座標とZ値から出力するVoxel位置を求めます。
この処理はStoreVoxelUAV関数で行われています。
もちろん、Voxel位置は支配的な面がどこかという点を考慮して処理します。
なお、今回3DテクスチャはR8G8B8A8フォーマットは使用せず、R32フォーマットでuintによるアクセスを行っています。
この理由はUAVからのロード処理がuintでなければ出来ないというDirectX11の制約によるものです。
出力するだけならロードする必要はないのですが、複数のポリゴンがVoxelに影響を与える場合、ブレンドをした方がいいかと思ってロード→ブレンド→ストアという処理を行っています。
全てのポリゴンを処理すればVoxel化は完了です。Surfaceだけとはいえ、色々な処理に使えるでしょう。
今回はGIとかレイトレとかはやってません。単純に大量のVoxelを何も考えずに描画しているだけです。
本当に何も考えずにやったので、死ぬほど重いです。Voxel生成は1回だけしか行っていないので、処理速度がどの程度かはわかりにくいと思います。
自宅環境(RadeonHD 5870)だと、2.5万ポリゴンで3ms程度でした。
数十万ポリゴンを処理することを考えると心許ない速度とも言えますが、静的なものは予め生成しておけるので動的なオブジェクトのポリゴン数を妥当な線まで落とすか、
あるいは簡易モデルにする、カメラに近いオブジェクトだけ行うなどである程度の速度は確保できるかと思います。
何より最適化も何もしてないのにこの速度なら悪くないんじゃないかと思ったり思わなかったり。
サンプルは下からDLしてください。
マウス左クリックでカメラの回転が出来ます。
VoxelをtrueにするとVoxel描画となります。
戦車のアンテナ?部分は細いポリゴンとなっているため、いくらか途切れてしまっていますね。
SizeはVoxelの解像度です。今回は256が最大ですが、GIならこれくらいでも何とかなるんじゃないでしょうか?
次回はこれを踏まえてOctreeを制作してみたいな、と思っています。
その次はレイ判定を使って何かやりたいですね。