DirectXの話 第149回

Screen Space Planar Reflection 18/01/21 up

滑らかな物体表面は周囲の環境を反射します。

それは鏡であったり水であったり金属であったり様々ですが、リアルタイムレンダリングではいろいろと問題になりやすい部分でもあります。

今回紹介するのは UBI から発売された『Ghost Recon : Wildlands』で使用された Screen Space Planar Reflection (SSPR) を実装してみたので紹介します。

ちなみに、元ネタのサイトはこちら。

Screen Space Planar Reflections in Ghost Recon Wildlands

作成したサンプルはいつも通りにGitHubにアップしています。

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

D3D12Samples

各反射手法の利点と欠点

リアルタイムレンダリングにおける反射手法はいくつか方法がありますが、どの手法にも利点と欠点が存在します。

環境マップ

球面、もしくはキューブマップを用いた環境マップと呼ばれる手法は昔から使われています。

予め用意しておけば比較的コストが安い手法で、今のハードウェアであればたいていどれでも使用可能な手法です。

反射の美しさは環境マップの解像度に依存しますが、動的に実装する場合は複数枚のバッファに対してシーンをレンダリングしなければなりません。

ライティングもそれぞれで行わなければいけませんし、動的な場合は解像度が落ちるのが一般的です。

また、反射する環境は無限遠にあるものと考えられるため、レイの反射位置はほとんど考慮されません。

そのため、被反射物体の近くの物体反射は正確になりません。

Parallax Corrected Cubemap

環境マップによるキューブマップと違い、反射したレイとキューブマップを定義するボックスの衝突点を取り、その座標に合わせた反射情報をキューブマップから取得する方法です。

詳しくは第141回の記事をご覧ください。

この手法は環境マップと比べて比較的正確な反射位置を求めることができますが、キューブマップを定義するボックスの境界面から離れれば歪みは大きくなり、特にボックス内部の反射オブジェクトの結果はかなりひどいものになります。

また、動的な反射を行う場合は環境マップと同様の問題をはらみます。

Screen Space Reflection

昨今の多くのリアルタイムレンダリングエンジンで実装されている手法です。

描画された画面に対して被反射マテリアルに対してレイを飛ばし、その反射レイをレイマーチして深度バッファとの衝突をチェック、衝突したらその位置のカラーを取得します。

この方法は深度マップ、シーンの各ピクセルの法線情報が必要となるため、Deferred Renderingとは比較的相性がいい手法です。

利点としては追加のシーン描画が不要なことです。つまり、同じメッシュを複数回描画しなくて済むわけです。

あくまでもスクリーン空間ですでに描画されているカラーの値を参照するだけです。

欠点としてはスクリーンに描画されない部分は反射しない点。

そして、綺麗に反射させようとするとレイマーチのサンプリング数を多くしなければならず、処理が重くなるわりにエラーはやはり発生するという点でしょう。

特に水面のような綺麗な鏡面反射では、下の画像のように反射したオブジェクトが伸びたりしてしまう現象をよく見ることでしょう。

Planar Reflection

UE4に実装されている、平面専用の反射手法です。

特定平面に対して反転させたオブジェクトを描画することでかなり正確な反射をレンダリングすることができます。

SSR のように見えない場所の反射も正しく処理されます。

欠点としてはやはり処理速度で、シーンのレンダリングを1回追加しなければなりません。

また、特定の無限平面に対してのみ使用可能で、複数の無限平面に対応するのであればその分シーンのレンダリングを増やさなければいけません。

UE4の場合はどうしても正確な反射が欲しい場合、例えば水面のようなもののために使用されます。

Screen Space Planar Reflection の概要

今回実装した Screen Space Planar Reflection は SSR と Planar Reflection の欠点を併せ持ったような手法です。

利点じゃないのかよ!と思われるかもしれませんが、残念ながら欠点で正しいです。

ただ、速度面では他の手法と比べてかなり高速に処理されるはずです。計測してないので正確な速度は何ともですが。

どのような手法か、というのを簡単に解説します。

こちらでは簡単に解説するため、詳しい手法は前述のブログを参考にしてみてください。

通常、SSR の場合は以下のように、カメラに映るオブジェクトにレイを飛ばし、その反射ベクトルがシーンのどこに衝突するかを求めます。

考え方はほぼレイトレースと同じです。

SSPR はそれに対して、反射するオブジェクトのピクセルが、ある平面に対して反射したら画面のどこになるか、というのを計算します。

ハッシュバッファの書き込み

まず最初に、平面より上に存在するオブジェクトのピクセルが、反射後は画面のどこに描画されるべきかを求めます。

あるピクセルのワールド空間座標は深度バッファから求めることができます。

このワールド空間座標を平面に反射させ、スクリーン空間に変換します。

ここで求められたスクリーン空間の座標に反射前の元位置を格納します。

これをハッシュバッファととりあえず呼びます。

何故最初からカラーを求めないのかというと、最初にカラーを取得してしまうと以下のような場面で困るからです。

カラーをそのまま反射先に格納してしまうと、どちらか処理が遅く終わった方に上書きされてしまいます。

つまり、黄色と赤色のどちらがピックアップされるかわからないというわけです。

そこでハッシュバッファには反射元のスクリーン座標を格納します。

この際、ハッシュバッファにすでに書き込まれている値と新しく現れた候補を比較します。

比較するのはY軸の座標です。

Y軸の座標は上を0として、下に行くほど大きくなっていきます。つまり、Y軸の座標が大きいということは、反射元ピクセルも下の方ということになります。

上の図からわかる通り、反射先にピックアップされるべきピクセルはY軸の座標が下にあるものです。つまり、Y軸の座標の大きな方を選択すればよいということになるのです。

元のブログではこの処理をピクセルシェーダでのUAVを使用していましたが、ピクセルシェーダでのUAV書き込みがなぜかうまくいかなかったのでコンピュートシェーダで実装しました。

ハッシュバッファのリゾルブ

ハッシュバッファへの描画が完了したら各ピクセルには反射元の最も近い座標が格納されているはずです。

これをカラーに変換します。

ハッシュバッファの座標にあるスクリーン描画結果のカラーをサンプリングするだけの簡単な作業です。

ギャップを埋める

ハッシュバッファの全てのピクセルに何らかの値が入っているわけではありません。

本来であればスクリーン上のカラーが反射していなければならない場所でも穴が開くようになってしまうことがあります。

これに対応するため、ハッシュバッファをリゾルブする際、値が書き込まれていないピクセルの場合は周囲4ピクセルのハッシュ値を取得、この中で有効なハッシュ値からカラーを計算し、ブレンドするようにします。

サンプルでは "Gap Bleed" フラグを入れると、この処理が行われます。

元のブログ記事では画面端のギャップを埋める手法についても言及されています。

屋外で使用する場合は必要になる可能性が高いでしょう。

Temporal Reprojection

これでも埋められない大きな穴は Temporal Reprojection で対応します。

以前のフレームの情報を使えばある程度の穴埋めはできる、というわけです。

ただし、あまりにも大きな穴はやはり埋められませんし、逆にその部分がおかしくなることもあります。

屋外であれば天球だけは鏡面反射状態でメッシュ描画する、という方法で穴をふさぐことも可能です。

屋内であってもローカルキューブマップを用いることである程度ごまかせるかもしれません。

ただ、屋外よりは良い結果とならない可能性が高いでしょう。

屋内で使う場合はフレネル反射を利用して手前側は反射がわかりにくいようにした方がいいでしょう。

最後に

この手法はメッシュ描画の回数を増やさず、レイマーチによる多数のテクスチャサンプリングを減らす効果がある高速な手法です。

実装するまではもっとひどい結果になるかと思っていたのですが、やってみると予想以上に良い結果となりました。

特に SSR 特有の伸びたような結果にならない点がとても良いと感じました。

確かに使用用途が限られる手法ではあるのですが、それなりに綺麗な水面を表現したい場合に使ってみるとよいのではないでしょうか?