DirectXの話 第162回

ライトマップベイク

19/06/05 up

前回は頂点単位のライトベイクでしたが、今回はライトマップのベイクです。

ある程度リアルタイムにベイク出来ます。

カメラ中心に近いところを、みたいな感じではないですが、それなりに十分な速度かなと思ってます。

ソースコードはいつものGitHubで。

Sample014が今回のサンプルです。

GitHub

今回の処理の流れは以下のとおりです。

1.UVAtlasを利用してユニークUVの自動展開(初期化時)

2.ユニークUVに対応した位置とノーマルの描画(最初のフレームのみ)

3.ライトマップベイク(ベイク中は毎フレーム)

4.デノイズ(ベイク中は毎フレーム)

それぞれ解説していきます。

UVAtlasを利用してユニークUVの自動展開

現在利用しているフォーマットであるglTFは複数UVを保存することが可能なフォーマットです。

なのでDCCツールでユニークUVを展開してglTFに書き出すほうがいいのですが、私のサンプル用ワークフローはSubstance PainterからglTF(.glb)を出力してライブラリを使って読み込む形です。

残念ながらSubstance Painterでは複数UVは出力されないっぽいので、今回は別の形でユニークUVを作成しなければなりません。

Sponzaのメッシュが持つUVがユニークUVになっているのであれば苦労はしません。もちろんなってません。

ではUVをユニークUVに変更してテクスチャも書き直すかと言われれば、それなら別のメッシュでも使いますってくらいに面倒です。

そこでユニークUVを展開できるライブラリを使用することにしました。

それがマイクロソフト社がGitHubで公開している UVAtlas です。

もともとはD3DXに入っていた関数を切り出したものですが、より一般的な形で切り出されて公開されてます。

使用するにはDirectXMeshのライブラリも必要ですが、こちらも.xファイル限定ではないので特に使用するには問題ないです。

実際にユニークUVに展開するにはいくつかの関数を使用して事前にデータを作る必要があります。

こちらは SampleLib12 の glb_mesh.cpp 内、GenerateAtlas() メソッドに実装があります。

GlbMeshクラスはサブメッシュごとに頂点バッファとインデックスバッファを別々に持っています。

このまま展開してしまうとサブメッシュごとにユニークUVが生成されてしまうのですが、それはそれでどうなのよ?ってことで、メッシュ全体で1つのユニークUVを生成するようにしています。

まず最初にすべてのサブメッシュの頂点とインデックスをマージします。

次にこの情報を用いて DirectXMesh の機能を使ってエッジ情報等が保存されるバッファを生成します。

そして UVAtlas の機能を使って Integrated Metric Tensor (IMT) というデータを生成します。

これがどのようなデータなのかいまいちわかってませんが、とりあえず計算用の関数が UVAtlas に用意されているのでそれをそのまま使います。

このあとに実際の展開を行い、最後に新しい頂点情報やインデックス情報を各サブメッシュに書き戻します。

ソースコードの抜粋は以下。

DirectXMesh の関数は GenerateAdjacencyAndPointReps() 関数です。

頂点座標とトライアングルインデックス情報から生成できます。

IMTを生成するには UVAtlas の UVAtlasComputeIMTFromPerVertexSignal() を利用します。

この関数は座標とインデックスに加えてもう1つの頂点情報を用います。今回はノーマル、もしくはUVを選択できるようにしています。

他にもテクスチャを利用することもできるようですが、この関数内ではテクスチャを使用するオプションを作っていません。

最後に UVAtlasCreate() 命令でユニークUVを展開しますが、この関数は非常に重いです。

Debugビルドで実行するのはもう諦めています。それくらい重いですし、Releaseビルドでもだいぶ重いです。

実行する際には注意しましょう。

まあ、そんなわけもあって、実際にライトマップベイクのシステムを作成する際には UVAtlas の使用はオススメしません。

品質の問題もあるので、アーティストさんにユニークUVを展開してもらうほうがいいでしょう。

この関数呼び出しは初期化時の1回だけです。ライトマップなのでメッシュは動かないこと前提ですので、最初に生成されてしまえばずっと使い続けることになります。

ユニークUVに対応した位置とノーマルの描画

ライトマップのベイクを行う際にはユニークUVの空間でレンダリングを行います。

この際、あるピクセルの位置とノーマルが必要になるわけですが、これをいちいち逆算するのは面倒です。

なので、ライトマップと同じサイズのバッファにメッシュの情報を書き込みます。

Deferred Rendering の GBuffer と同じ考え方ですね。

ユニークUVをスクリーン座標としてレンダリングするだけなので特に難しいことはありませんが、ちょっと特殊な部分としては Conservative Rasterization を使っているというところでしょうか。

通常のラスタライズはピクセルのサンプルポイントがポリゴンに内包される場合に塗りつぶしが発生しますが、Conservative Rasterization の場合はピクセルにポリゴンがかぶっている場合に塗りつぶしが発生します。

MSAA不使用の場合はピクセルのサンプルポイントはピクセル中心ですので、通常のラスタライズを使っている左はかなり少ないピクセルしか描画されません。

この手法で座標とノーマルを描画してしまうとトライアングルの境界部分でライトマップベイクが行われず、ライトなし、つまり真っ黒な状態の色を引っ張ってきてしまいます。

これでは困るので Conservative Rasterization を用いて境界部分でもライトマップベイクが行われるようにしています。

Conservative Rasterization を使用するには Pipeline State を生成する際に使用するフラグを設定するだけなので簡単です。

ただし、古いGPUの場合は対応していない可能性があります。対応していない場合はMSAAを利用するなどして同じような効果を得られるようにすると良いでしょう。

この処理はやはり最初の1回だけでOKですが、ライトマップの解像度を変更した場合は作成し直します。

ライトマップベイク

ライトマップベイクは頂点単位のライトベイクとやってることは変わりません。

頂点単位の場合は頂点バッファから座標とノーマルを取得していましたが、ライトマップベイクでは前段のセクションで作成した位置とノーマルの GBuffer から取得しています。

また、ライトマップはその解像度によって計算処理にかかる時間が異なります。

低解像度であれば毎フレーム全ピクセル計算しても大丈夫ですが、高解像度の場合はそうもいきません。

4k x 4kの解像度の場合は1677万ピクセルになるわけで、60フレーム換算したとしても10GRays/sの1/10くらいのレイを飛ばさなければいけないわけで、現実とは言い難い本数です。

なので今回は1フレームあたりに1k x 1kのブロック単位でレイトレをするようにしました。

サンプリング数は512サンプル、1フレームにつき1ピクセルあたり1本のレイを飛ばすので、4x4x512で8192フレームでベイクが完了することになります。

60fps動作するアプリなら2分ちょっとでベイク完了して、しかもリアルタイムにベイクされているのをチェックできる、と考えると悪くない速度かと思います。

もちろんシーンやマテリアル、ライト環境が複雑になったりするとベイク時間は延びますが、そのへんはブロックサイズを適切なサイズにすることで対応できるかなと。

まあ、実際にエンジンに組み込むとなると考えなきゃいけないことは多いのですが、D3D12で作られているエンジンであれば組み込みは比較的やりやすいのではないかと思います。

デノイズ

しかしライトマップをベイクしただけではさすがにクオリティ的に厳しいのも事実です。

ライトマップの解像度を4k x 4kにしてみたものの、それでもだいぶノイジーです。

なのでデノイズしてみました。

デノイズのアルゴリズムは単純で、ライトマップの各ピクセルにつき5x5のボックスフィルタを用いています。

この際にそれぞれのピクセルの位置とノーマルを取得し、距離が近い、ノーマルの方向が似ているという空間的な情報から各ピクセルの重みを計算するようにしました。

このフィルタ用のパラメータは固定してしまっていますが、エンジンに組み込むときはちゃんとしたパラメータを用いるか、アルゴリズム自体を見直したほうがいいかと思っています。

デノイズのON/OFFはボタンで切り替えられるようにしたので、そちらでチェックしてもらうと効果がわかりやすいです。

ただし、デノイズによってライトリークが発生してしまう箇所もあります。

ぶっ刺しで作成された部分などはライトリークしやすいのでデノイズには注意が必要でしょう。

下の図はわかりにくいですが外壁にでている部分の明るさがリークしている状態です。

問題点

習作として作ったものではありますが、なかなか悪くない結果になりました。

ただ、やはり問題点も存在しています。

まず UVAtlas が重すぎる点。正直時間がかかりすぎます。

Sponza全体を対応しているからというのもあるのでしょうけど、出来ればDCCツールで予め作成してほしいところです。

加えて UVAtlas はそこまで品質が高くないので、DCCツールからユニークUVを持ってくる仕組みを確立する必要がありそうです。

まあ、これはFBX使えば問題ないといえばないんですが…

デノイズによるライトリークも問題です。

特にぶっ刺しで作られている部分はどうしてもリークしやすいですし、座標空間的にもUV空間的にも近いので対応が難しいです。

Substanceのベイカーでもぶっ刺しは基本的に良くないとされているので、ある程度デノイズの範囲を変更するなどで対応するしかないかなと。

最後にベイクされないピクセルの問題。

Conservative Rasterization によってポリゴンエッジ部分のライトベイクもある程度行われますが、実際にレンダリングするとベイクされていない、つまり位置とノーマルが書き込まれなかったピクセルも色を拾ってきてしまったりします。

これに対応するにはデノイズ後にVoronoi的にライトの色を周辺に広げる必要がありそうです。

まあ、これはやろうと思えば出来ますし、デノイズの際に少しだけやってたりします。

最終的にはデノイズ後に対応するようにすれば問題ないでしょう。

次回は何しよう?

というわけで今回の話はこれにて終了。

だいぶDXRにも慣れてきましたが、まだまだ使ってない機能もあるので試してみたいところですね。

次回はその辺りを試してみるか、それとも別のことをやるか…