DirectXの話 第159回

DXRを用いたハイブリッドレンダリング

19/02/10 up

リアルタイムレイトレーシングへの道

UE4がDXR対応を予定している中、リアルタイムレイトレーシングへのいろいろな思惑があることでしょう。

しかし、フルレイトレーシングでリアルタイムアプリケーション、特にゲームアプリケーションを実装するのは現実的とは言えません。

ゲームの映像、内容をいろいろと絞ってみればなんとかなる場合もあるかとは思いますが、例えば普通のアクションゲームやRPGをレイトレーシングのみで実現するのはまだまだ無理でしょう。

ですが、フルにレイトレーシングしなければいいのでは?という意見もあります。

そこで登場するのがハイブリッドレンダリングです。

レイトレとラスタライザを組み合わせた全く新しい(ry

うぉぉぉぉ!!!

じゃない。

まあ、全く新しくもなんともないわけですが、要は基本をラスタライザで、一部の処理をレイトレでやってしまおうという話です。

レイトレはレイを飛ばして衝突判定を取り、そこから様々な情報を取得、計算して処理する手法です。

ここで重要なのは衝突判定を取る、というところで、これはレイトレであれば必ず処理することになります。

衝突判定後に法線見て、テクスチャサンプリングして、ライティング計算して、2次反射のために再びレイを飛ばして…という感じにやっていくと処理速度的には厳しいわけです。

しかし、衝突判定だけならそれほどではない、というか、絶対やることなんだからそれだけでなんとかなる部分があるだろうかと考えます。

それで思いつくのはシャドウとAOです。

シャドウはライト方向にレイを飛ばして、レイが衝突していたら影、そうでなければ光が届くと考えられます。

また、AOも似たような感じで、法線に対して半球方向に複数のレイを飛ばし、そのレイが衝突していたら遮蔽されている、衝突しなければ遮蔽されていないと判断してアンビエントの遮蔽情報とします。

今回のサンプルではシャドウもAOも複数フレームでサンプリングし、シャドウはソフトシャドウを実現、AOも遮蔽情報を0 or 1ではなく、0~1の浮動小数点になるようにしています。

今回やっていること

今回のサンプルはいつもどおりにGitHubにコミット済みです。

D3D12Samples

Sample012が対象のサンプルです。

このサンプルは以下の処理を行っています。

    • ラスタライザによる Z pre passレンダリング(法線も描画してるのでほぼGBufferのレンダリング)

    • レイトレによるシャドウとAOのレンダリング

    • ラスタライザでライティングとレイトレ結果の合成

    • 時間計測

描画パス

今回の描画パスは以下の順番で行っています。

    1. Z pre pass

    2. レイトレによるシャドウ、AOレンダリング

    3. ライティングパス

    4. GUIレンダリング

Z pre pass はなくてもいけますが、現在のレンダリングパスにおいて Z pre pass を使わないという選択肢はほぼないと思います。

これは3のライティングパスで無駄なピクセルシェーダを起動しないためにも有効な処理ですので、せっかくだからと追加しました。

また、これによってレイ1本分を節約しようという目的でもあります。

Z pre pass

ここでやっているのは深度バッファへの書き込みと、法線のGBufferへの書き込みです。

通常であれば深度バッファのみではありますが、次のレイトレの処理の関係で法線の出力も行っています。

Sample012/src/main.cpp の630~676行目が実際のレンダリング処理です。

特に難しいことはしていないので、処理内容やシェーダコードは割愛します。

レイトレパス

レイトレパスでは平行光源に対するシャドウの計算とAOの計算を行っています。

CloseHitShader、MissShaderは遮蔽されているかという情報を 0 or 1 で返すだけなので割愛。

今回のサンプルではこれらは重要ではなく、RayGenerationShaderが重要になってきます。

これまでのサンプルではカメラから各ピクセル用のレイを飛ばす方法を採用していましたが、今回は予め深度バッファとGBufferに法線が出力されています。

つまり、最初のカメラからのレイトレを行う必要がなく、カメラからレイを飛ばして当たってからどうする?という処理をそのまま RayGenerationShader に書くことが出来るわけです。

上記のコードはシャドウ判定用のレイを生成する部分を抜き出していますが、AOもほぼ同様です。

ただ、シャドウのレイトレは1フレームに付き1回に対して、AO用のレイトレは1フレームに付き1~8回のレイトレを行います。

レイを飛ばす回数はデバッグUIから変更可能になっています。

当然、回数を増やせば早い段階でキレイになりますが、その分時間もかかります。

最終的にシャドウもAOも複数フレームのレンダリング結果を合成して作成していますが、いわゆるテンポラルリプロジェクションは行っていません。

カメラやAOのレイの長さ、AOのレイのサンプリング回数を変化させると最初からレンダリングし直します。

実際にハイブリッドレンダリングを行う場合はこのような処理はせず、テンポラルリプロジェクションによってカメラ等の変更に柔軟な対応を行う必要があります。

ライティングパス

ライティングパスもラスタライザで行っています。

ライティングは単純なランバートライティングと、法線方向から空の色を取ってくる半球ライトに近いライティングを行っています。

空の色については Raytracing in a weekend の処理をそのまま持ってきています。

この際、平行光源によるランバートライティングはレイトレ時のシャドウの結果を、空の色はアンビエントとして扱ってレイトレのAOの結果をそれぞれ参照しています。

やろうと思えば点光源に対してもシャドウ計算ができそうですが、今回はやってません。そのうちやります。

特にこのパスも難しい処理はしていないので割愛。

今後の展望

今回は前回までの処理をそのまま流用する形で実装してしまったわけですが、当然ここには問題があります。

大きな問題としてはカメラ等が動いた場合にレイトレ結果がリセットされてしまう部分です。

本来であればこれらはカメラやオブジェクトの移動量から前回の情報をサンプリング、それをマージするという形が良いかと思います。

いわゆる普通のテンポラルリプロジェクションですね。

また、レイトレ部分だけ解像度を変えられるようにしたほうが良いでしょう。

この場合、中途半端な低解像度処理を入れるのであれば深度バッファからの読み取りは難しくなってくるので、カメラからレイを飛ばしたほうがいいかもしれませんね。

ポイントライトへの対応もやってみたいところです。

ポイントライトやエリアライトのように大きさのある光源による影は見た目にも良いものになります。

シャドウマップでの実装は割と面倒ですし解像度の問題も出てきますが、レイトレなら実装自体は簡単です。

まあ、処理速度面では問題出るかと思いますが。

次は?

そういえば AnyHitShader を試していなかったので試そうかと思います。