DirectXの話 第112回

スクリーン空間ソフトシャドウ

今回はGPU Proに掲載されていたスクリーン空間ソフトシャドウ (Screen Space Soft Shadow) をやってみました。

個人的な感想としては割と微妙で、少なくとも現世代ならVSMやESMを使った方がいいのではないかと思います。

さて、いわゆるソフトシャドウ技術は前述のVSMやESMなどのシャドウを描画するときにソフト加工するものが一般的ですが、今回は『バーチャファイター5』 で使われていたものの発展系です。

『バーチャ5』では白いキャンバスにハードシャドウのみを描画し、これにフィルタをかけることで実現していました。

今回の技術はそれに2つほど工夫を重ねているものですが、基本は同じです。

PS3の『God of War 3』も同じような技術を使っていて、こちらでは影のみを描画した白いキャンバスを “ホワイトバッファ” と呼んでいます。

こちらも今回はその名前を使用することにします。

影のエッジがソフトになる理由は完全に光が届かない部分と完全に光が届く部分の他に、いくらかの光が届く部分が存在するためです。

光の一部が届くことで影が薄くなっている部分を半影 (penumbra) と言います。

これはどんな光に対しても一定とはなりませんが、同じ光でも光源とキャスタの距離、およびキャスタとレシーバの距離でも変わってきます。

下の図の (a) は光源とキャスタの距離による違い、(b) はキャスタとレシーバの距離による違いを示しています。

見ての通り、距離の違いで半影、本影のサイズは変わってきます。このほか、光源の大きさ、光源の形状によっても変化してきます。

今回の技術は光源の形状は無視しています。どのオブジェクトから見ても平行な板状光源と考えています。

では、実装を見ていきます。描画パスは以下のようになります。

    1. シャドウマップ描画

    2. シャドウマップの最小フィルタX

    3. シャドウマップの最小フィルタY

    4. 法線・深度描画、ホワイトバッファ描画、距離マップ描画

    5. ホワイトバッファのガウスフィルタX

    6. ホワイトバッファのガウスフィルタY

    7. Light Pre-Pass

    8. 最終描画

第1パスは通常のシャドウマップです。説明は不要ですね。

第2,3パスはシャドウマップに対する最小フィルタをかけます。ライトサイズを元にしてシャドウマップを一様に"太らせ"ます (下図参照)。

この際にサンプリングしたピクセルの最小値 (つまり、周辺の最小深度) を求めてシャドウマップとは別に保存しておきます。

この“太らせた”シャドウマップの範囲が半影になる部分です。このマップを投影して白くなった部分は本影も半影も出来ません。

第4パスはMRTを用いて3枚のテクスチャを描画します。

法線・深度マップはDeferred Renderingでおなじみなので説明不要でしょうし、ホワイトバッファもスクリーン空間でハードシャドウのみを白い背景に描き込むだけです。

問題は距離マップです。ここにはキャスタとレシーバの距離から求めた半影の強さを描画します。参照するキャスタはもちろん“太らせた”シャドウマップの方です。

半影の強さは以下の計算式で求めています。

penumbra = (receiver - caster) * light_size / caster;

キャスタとレシーバの距離、光源からキャスタの距離、光源のサイズを考慮しています。

第4パスで描画した3つのバッファは第5,6パスで使用されます。

『バーチャ5』の技術を以前に実装したときは何も考えずにガウスフィルタをかけていましたが、今回はここに2つの工夫を重ねています。

まず、スクリーンからの深度を利用してオブジェクトの分断状況をチェックしています。

以前の実装ではやらなかったのですが、そのために本来影にならない部分に影が“滲む”形となってしまい、不自然さが出てきてしまっていました。

今回の実装ではそのようなことがないように深度が一定以上離れている部分はサンプリングしないようにしています。

もう1つの工夫が今回のソフトシャドウに必要な部分で、フィルタの範囲を距離マップに保存した半影の強さから求めるようにしています。

正確には、フィルタの範囲は半影の強さ、ライトサイズ、スクリーンからの距離、スクリーンに対する法線の向きを参照しています。

求めるピクセルのスクリーンからの距離が離れていれば、当然半影の範囲が変化します。

また、スクリーンに対する法線の向きがスクリーン面に対して垂直に向けば向くほど、サンプリングする範囲が広くなっては困るという状況になります。

この辺の計算式は割と適当です。GPU Proのサンプルを参照していますが、この計算式自体を調整項目に入れてもいいと思います。

第6パスで半影付きのホワイトバッファを作成したら、後はそれと最終描画を組み合わせて終了となります。

さて、この技術が微妙な理由としては、まず描画パスの数が問題となります。

とはいっても、通常のシャドウマップ付きシーンを描画するより4パス多いだけですし、しかもそれぞれ全画面描画なのでコスト見積もりはしやすいと思います。

しかしながら全画面描画。フィルが弱いマシンだと厳しいと言わざるを得ないでしょう。

また、SSAO と同じように影となる部分がオブジェクトで隠される場合に半影が弱くなったり消えたりすることがあります。

隣のピクセルに影があるはずだけど、手前のオブジェクトで隠されてしまうという現象のためです。

これを避けるため、GPU Proのサンプルではデプスピーリングの要領で一番手前とその次の深度に対してハードシャドウを求めています。

この一番手前のレイヤーでは影が隠されていても、その次のレイヤーではしっかりと影になっていることを期待しているわけです。

多分、たいていのシーンではそれで問題ないんじゃないかと思います。今回のサンプルでは第2レイヤーを見ていないにもかかわらず、ライトサイズを大きくしなければそれほど問題となりません (少なくとも、自分の目で見て)。

しかし、ライトサイズが大きければ問題ですし、第2レイヤーのハードシャドウを描画するにはオブジェクトをもう1度描画しないといけないので微妙です。

複雑なシーンだとレイヤー2つでも十分かどうかは微妙でしょう。以前にやった OIT と組み合わせる方法もありですが、現行のPC用GPUでも速度面で厳しいと思います。

それだけ苦労しても高品質にするには結構な調整が必要そうと感じました。その辺も含めてコスト的に合うかどうかは微妙です。

ただ、もうちょっと工夫すれば使い物になるかもしれないとは思っています。アイデアが浮かんだら試してみようかな。

では、サンプルは添付ファイルから落としてください。

テンキーの1,2でソフトシャドウとハードシャドウを切り替えられます。

テンキーの7~9でライトサイズを変更できます。ライトサイズが大きすぎると映像として破綻しますので注意してください。

次回はやっぱり未定。

何か面白そうなもの (特にDeferred Rendering関連) がないかなぁ…。