DirectXの話 第140回
Real-Time Local Reflection 13/09/10 up
今回は『Crysis 2』で実装されていたReal-Time Local Reflection (RLR) を実装してみました。
サンプルとしての質は高くないですので、実装する際は参考程度にしておいていただけると幸いです。
昨今の環境マップは静的なキューブマップを利用している場合が多いのではないでしょうか。
もちろん、レースゲームなどではバックミラーや車への映り込みなどで動的に処理しているものもあります。
しかし、動的な映り込みを実装しようとするとどうしてもコストが高くなってしまいます。
例えば、動的なキューブマップを用意しようとするなら、そのキューブマップで描画される範囲を6面分描画する必要があります。
DX10世代以降ならキューブマップの6面を1回のDrawCallで処理することが可能ですが、DX9世代ではそうはいきません。
平面環境マップやデュアルパラボロイド環境マップなら1回、もしくは2回で済みますが、DrawCallが増えることがネックになりやすいマシンではこれを増やすのも大変です。
通常のレンダリングに加えて、シャドウマップ(カスケード4枚なら4回)、そして環境マップ用に1,2回のDrawCallがモデルごとに増えるとなると馬鹿に出来ません。
また、Deferred Renderingで大量の点光源に対応した場合、環境マップ描画にもそれらを適用するかが問題になってくるはずです。
環境マップにも適用するとなると、GPUの計算量はかなり増えてしまうでしょう。
Deferred Renderingにおいては、環境マップ用に複数回描画は割とネックになるのではないでしょうか。
もちろん、環境マップに関しては点光源を諦める、という手段もありますが…
『Crysis 2』ではこの問題に対応するため、スクリーンスペースで反射ベクトルをレイマーチングする方法が実装されています。
この手法を Real-Time Local Reflection と呼んでいますが、リアルタイムにある程度ローカルな範囲内だけで反射を取り上げられる技術はRLRと呼んでも差し支えないでしょう。
実装方法の解説をしようと思いますが、実際にはそれほど特殊なことはしていません。
本当にただ反射ベクトルをレイマーチングするだけです。
ただ、少しわかりにくいかな、と思う部分もあるので、そこら辺だけ解説します。
まず、FadeFactor についてです。
今回の技術はいくつかの問題があるため、それをごまかすためにフェードを入れられるようにしています。
上のスクリーンショットからわかると思いますが、画面端に近い部分ではフェードが強くなります。
これはスクリーンスペース技術が画面外には弱いという問題をごまかすためです。
画面端に近づけば近づくほどFadeFactor が強くなり、床面の色が見えやすくなります。
他にも、視線ベクトルと法線ベクトルが正対している場合に強くなる ViewFadeFactor 、
レイマーチングのサンプリング数が増えるほど強くなる SampleFadeFactor、
反射ベクトルがオブジェクトの後ろに回り込んだ場合に対応する DepthFadeFactor の3つがあります。
この3つについてはON/OFF機能がついていますので、サンプルを動かしてどのような効果があるか確認出来ます。
わかりやすいのはSampleFadeFactor でしょう。
ViewFadeFactor はカメラを傾けるとわかりやすくなるかと思います。
DepthFadeFactor は実装してみたはいいのですが、イマイチ効果を実感出来ませんね。
次にレイの進行速度ですが、これは各ループの先頭で約1ピクセル分程度進行するように長さを調整しています。
PSLocalReflection.hlslの78行目付近ですね。
現在の深度から1ピクセル分程度と思われる反射ベクトルの長さを概算しています。
かなり適当なので実際には1ピクセル分ではないのですが、さほど問題は無い…と思います。
他に解説する部分はやはりその大きな欠点でしょうか。
スクリーンスペース系の技術はどうしても見えない部分で対応出来ないという問題を抱えます。
SSAOの場合は画面端で影が消えていったり、オブジェクトで遮蔽された部分の影が消えていったりすることがよくあります。
RLRでも同様で、画面端や他のオブジェクトによる遮蔽で見えなくなっている部分が反射される可能性が大いにあります。
上のスクリーンショットでは球の付近で球の背後の赤い壁が反射すべきところがしていなかったりします。
白くなってしまっているのは、反射ベクトルの深度チェックにおいてある程度の深度差では衝突判定を取らないようにしているためです。
サンプルではDepthThreshold のパラメータで指定出来ますが、この値を大きくしていくと緑の球が衝突していると判定されてやはりおかしな絵面になります。
下の図はDepthThreshold の値による違いで、左が0.003、右が0.133の場合の結果です。
明らかにおかしな感じで伸びてます。
対処方法として最も良さそうな方法は、見えない部分をキューブマップで補間してやることです。
このサンプルシーンの場合、部屋は赤い壁と天井で囲まれています。
なので、そのようなキューブマップを用意しておき、衝突判定が取られなかった場合にキューブマップを参照する、
衝突判定が取られたときでもキューブマップのサンプリング結果との間でフェードするというのはいい手だと思います。
サンプルシーンのような狭い部屋の場合、Parallax-Corrected Cubemap を使うのがいいんじゃないかと思いますし、
オープンフィールドなら無限遠のグローバルキューブマップを使用するのもありでしょう。
何となく何かが反射している、という程度の反射であれば無視してもかまわないかもしれませんが、
それなりに鏡面っぽく反射させるような材質の場合はキューブマップとの併用も考えなければなりません。
この辺りは今後の課題ですね。
ではサンプルです。
Download:Sample131.zip
一応パラメータはいくつか用意しているので、ソースコードを眺めながら色々調整してみてください。
また、実際にゲームに使うのであれば解像度を下げてやった方がいいと思います。
はっきりした鏡面反射をやらないのであれば特に解像度を下げても困らないでしょうし、それでいて速度面もアップするはずです。
出来れば縦横それぞれ1/4くらいにしたいところですね。
次回は果てしなく未定ですが、このサンプルを元にParallax-Corrected Cubemapでもやってみようかと思ったり思わなかったり…