DirectXの話 第164回

Raytracing AOの調整とか

19/07/05 up

”とか”ってなんだ?

DXRのようなリアルタイムレイトレーシングは今後普及するの?って話はたまに聞かれます。

個人的な意見としては、来年はまだ無理だと思うけど、5年後は可能性高いってくらいかなと思ってます。

ただ、5年後になってもリアルタイムレイトレのみで絵作りされるってことはないと思いますし、レイトレするだけじゃなくてデノイズやフィルタリングなどのテクニックを駆使して使われることになると思います。

なので、レイトレできたやったー!だけでは済まない状態がだいぶ長く続くでしょう。

そうなってくるとデノイズとか Temporal Reprojection とかは相も変わらず重要になってくると思いますので、そういう方面も勉強しなくちゃならなくなります。

そんなわけで、今回は少し実践寄りという感じで、DXRを実際に使うならまずはここ!という印象のあるシャドウとAOの調整について実装してわかったこと、試したこと、今後の課題などを書いていこうと思います。

シーンの構築にどれほど時間がかかる?

これまでのサンプルではあまり気にしてなかった内容ではありますが、ゲーム開発を考慮するのであればシーンの更新は必須条件となります。

以前の記事でも書いたとおり、DXRでは各メッシュ単位で構築する Bottom Acceleration Structure (BottomAS) とインスタンス情報から生成される Top Acceleration Structure (TopAS) の2つから成立します。

トライアングル情報を取り扱うのは BottomAS の方で、構築も BottomAS の方が重い傾向にあります。

TopAS はインスタンス情報を取り扱うので、一般的には BottomAS より軽くなるはずです。

あくまでも一般的であって、特殊なシーンの場合は TopAS の構築が重くなるかもしれませんが、まあそこはそれってことで。

BottomAS はいわゆるメッシュ情報なので、静的なメッシュであればリソースロード時などに生成しておけば再生性の必要はありません。

スケルタルメッシュのような骨による変形があるメッシュ、骨じゃなくても風で揺れる草などのメッシュを使う場合は変形のたびに BottomAS を生成し直す必要があります。

これはかなりネックになります。

どの程度のプリミティブ数でどの程度の処理負荷なのかはパターンによってマチマチかもしれませんが、今回使った Sponza と、Blenderのマスコットキャラ Suzanne さんは合わせて6.7万ちょっとのプリミティブ数です。

そのうち Suzanne さんは200程度なので、ほとんどが Sponza ですね。

また、Sponza は27個のサブメッシュに分かれているので、それも処理不可に響く可能性があります。

さて、実際に計測する際にはレイトレを高速化するか構築を高速化するかを選択することが出来ます。

ビルド時のオプションで以下のどちらを選択するかで優先される処理が決まります。

D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PREFER_FAST_TRACE

D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PREFER_FAST_BUILD

今回のシーンで試したところ、レイトレを高速化する設定の前者で 3.7ms、構築を高速化する後者で 2.5ms と明確な差が出ました。

もちろん、構築速度優先にした場合はレイトレのコストが上がっていました。

毎フレーム更新するようなものでなければレイトレ優先の方が良いでしょう。

対して TopAS の構築ですが、残念ながら今回はインスタンスが2つだけなので速度検証としては微妙です。

重くても 0.05ms もかかっていないので、TopAS の構築については巨大なシーンになるまではそこまで気にしなくてもいいのかもしれませんね。

時間軸方向でサンプリングを増やす場合のあれこれ

これまでのサンプルでは時間軸方向にブルートフォースでレイ飛ばして対応してましたが、実際のゲームではそんなわけにもいかないので Temporal Reprojection が必要になります。

Temporal Reprojection を行う場合、完全に静的なシーンであれば前回のスクリーン変換行列があれば対応できるわけですが、Suzannne さんが動いてる今回のようなサンプルの場合は移動方向と量を示す Velocity Map が必要になります。

これは最初の Z pre pass で行っています。

まあ、Z pre pass といいつつほぼ GBuffer に書き込むベースパス的なパスですが。

このパスでは深度バッファと一緒に Normal と Velocity、そして Linear Depth も書き込んでいます。

Linear Depth は深度バッファがあればそこから再構築可能なのですが、ちょっと面倒だったのと Linear Depth の変化量が欲しかったのでこのパスで描画しています。

Linear Depth 用のバッファは16ビットFloatの4チャンネルバッファで、RGBAそれぞれに別々の情報を書き込んでいます。

コードはこんな感じ。

R はそのまま Linear Depth が格納されます。

G は Linear Depth の ddx, ddy の絶対値の最大値です。このピクセルでどの程度の深度の変化量があるか保存します。

B はこのピクセルの前回の Linear Depth です。この Linear Depth が前回のバッファに保存されているかどうかは保証されませんが。

A は Normal の ddx, ddy を取得して長さを求めています。こちらも、どの程度周辺ピクセルと比べて変化しているかという情報ですね。

これらの情報は Temporal Reprojection をする際に前回情報を使用していいかどうかの目安に使います。

Temporal Reprojection のパスでは今回のレイトレ結果と前回までのレイトレ結果を Reprojection して合成します。

どちらもデノイズ前のものを合成する形にしていますが、前回のものをデノイズ後のものにしてみたら結果が良くなかったからです。

ただ、フィルタリング→Temporal Reprojection→フィルタリングとやったほうが品質は良くなるかもしれません。

UE4はそんな感じなので。

Temporal Reprojectionのコード片はこんな感じ。

float nw = distance(normal, prevNormal) / (depth.w + 1e-2) > 16.0 ? 0 : 1;

float nd = abs(prevDepth.x - depth.x) / (depth.y + 1e-4) > 2.0 ? 0 : 1;

float weight = nw * nd; weight *= any(prevUV < 0) || any(prevUV > 1) ? 0 : 1;

depth が Linear Depth を保存しているバッファから取得したものです。

weight が 0 でなければ Reprojection に成功したとみなして前回情報とブレンド、失敗した場合は今回の情報で上書きとなります。

デノイズパスではX軸方向とY軸方向でブラーをかけています。

普通のガウシアンフィルタに Depth と Normal の変化量から求めたウェイト値を用いているというくらいですが、この部分はまだまだ工夫の余地があると思います。

デノイズ手法としては NVIDIA さんが発表されている『Spatiotemporal Variance-Guided Filtering』が結果が良さそうですが、それなりに処理不可も高そうかなと。

こちらは Temporal Reprojection と A-Torus wavelet transform を用いたフィルタリングでデノイズしています。

該当ページにサンプルコードがあったので読んでみたのですが、最後のフィルタリングはクオリティを上げる場合は複数回イテレーションするようで、イテレーション回数によっては結構厳しいかもしれませんね。

今回、こちらで実装したデノイズも最大4回まで適用できるようにしました。

さすがに4回やるとかなりノイズが弱まりますので、下手にサンプリング数を増やすより良いかもしれません。

デノイズについてはまだまだ検証の余地が大いにある感じなので、余裕があるときに上のSVGFも実装できたらいいなぁと思ったりしてます。

レイトレース時のあれこれ

まずレイトレースの速度ですが、当然ながらレイの本数を増やすと速度面で不利になります。

ただ、これ以外にもいろいろ検証したので報告しておきます。

レイの距離ですが、こちらもかなり重要な要素になります。

当然レイの距離が短くなれば枝切りも速く行われるので高速化につながるでしょう。

レイの距離、本数の計測結果は以下のようになります。

本数も距離も明らかに速度に影響を与えます。

AO の場合はそれほど距離は長くなくてもいいはずなので、できるだけ短い距離で運用するほうが良いでしょう。

ノイズについてはこれまで適当に乱数テーブルを作って引っ張ってくるようにしてましたが、そもそも処理速度面にも問題があったり乱数の質がよろしくなかったりしたのでブルーノイズも選択できるようにしました。

ブルーノイズの場合は少しパターンが見えてしまう場面もあるのですが、速度面でも良好ですしデノイズの結果も悪くないのでオススメです。

ただ、ノイズ無しは絶対的にオススメできません。

UIで選択できるようにしてみましたが、やっちゃダメなやつだとはっきり分かる結果となります。

シャドウはどうなの?

シャドウに Temporal Reprojection を行ったのですが、結果はさんざん。

AOの場合は対象点とその周辺の情報から結果を得られるものですし、シャドウよりうっすら入るものなので Temporal Repojection もうまくいってました。

しかしシャドウの場合はキャスターとレシーバーの相互関係はあまりなかったりしますので、空間的な Reprojection が成功したからと言ってシャドウの結果もそのまま使えるという保証がないわけです。

完全に静的なシーンならともかく、今回のサンプルのように動くものがあると、それが落とした影が移動したあとでも残ってしまったり、逆にそれなりの速度で移動された際に影が映らなかったりという問題が出ました。

これに対応するにはシャドウによる遮蔽情報も Reprojection の成功判定に加えないとダメかな?と思いました。

結局今回はソフトシャドウに対応するためのサンプリングは行わず、デノイズでブラーがかかるのでそれでソフトシャドウを実現するにとどめています。

平行光源のシャドウなら問題ないですが、点光源やスポットライトのことを考えるといろいろ調整しなきゃダメなんだろうなぁ…という印象です。

レイトレよりもその先での苦労が多くない?

いろいろ調整しながら思ったことですが、レイを飛ばしてアレコレするのはとても簡単なのですが、レイを飛ばし終えたあとにやっぱりいろいろ調整しなきゃならないことが多いです。

この辺はスクリーンスペース技術とほとんど変わらなくて、サンプリング数をそんなに増やせないから少ないサンプリング数でも良い結果になるようなサンプリングを行いつつデノイズしたりしましょうねってのと全然変わりません。

まあ、何がいいたいのかというと、リアルタイムレンダリングはレイトレが来たとしても泥臭い苦労は変わらずやることになります。

なんだかなぁ…って気分にもなりますが、現在の技術は応用が効くと考えればむしろ僥倖なのかもしれませんね。