技術

Shadow caster mapによる高速・高品質な影計算

ソフトシャドウの計算の処理速度がかなり改善された。
GPU_softshadow_SM512_DV0.1_C163_6sec
これは光源数163の場合でトータルの処理時間は6秒。
以前実装したものは、これとほぼ同じ条件で30秒以上掛かっていた。
最新のハイエンドGPUなら、この手法でも少し条件に制限を設けることで面光源によるソフトシャドウのリアルタイム処理も可能かもしれない。

独自に考えた方法だけど、シンプルな方法なので、すでに誰かが実装したり論文にしたりしてるかもしれない。
以下説明。

1. 概要

面光源を多数の点光源で近似する場合に問題になるのが、品質を上げるために点光源を増やすと、処理時間が点光源数に比例して増加する点である。
今回の手法でひとつの点光源の処理にかかる時間を短縮することで、全体として処理の時間が大幅に短縮される。
今回の手法は大雑把には、シャドウマッピング技法とレイトレースによる光源の可視性チェックの合わせ技。
シャドウマッピングの高速性とレイトレースの質の高さのいいとこ取りな方法。
これによってシャドウマップの解像度にあまり依存しない(!)高品質な影が高速に処理可能になる。

普通のシャドウマップでは光源から見たときの深度値を保持するが、今回使うShadow caster map(勝手に命名)では光源から見たとき一番手前にあるポリゴンのIDを保持する。
これによって最終的に影光線を飛ばして光源の可視性をチェックする際に、Shadow caster mapを1ピクセル参照するだけで、交差判定をすべきポリゴンを決定することが可能。

空間分割したりして交差判定をするポリゴンを絞りこむような方法よりかなり速く処理が可能だと思われる。
さらに実装難度も比較的低い。
その代わり可視性のチェックぐらいにしか使えないけど。

ただし、これだけでは普通にシャドウマップを利用する方法より質の点で劣る。
レイトレースの利点を活かすようにするために、シャドウマップをちょっと調整する(詳細は3で説明)ようにする。

この手法はシャドウマップの生成・調整・参照の3つのフェーズから成る。

2. シャドウマップの生成フェーズ

これは普通のシャドウマップの生成フェーズとほとんど同じ処理。
光源から見た深度値の代わりにポリゴンのIDを保存するだけである。

今回は光源から見たシーンの座標変換に以下のような半球投影変換(正しい名称はなんだろう)を利用した。
ラスタライザによって半球座標が線形補間されてしまうと誤差になってしまうため、実際はこのフェーズの前にシーンのポリゴンを一定以下まで微細化する処理が入っている。

// positionにワールド座標、lightViewMatrixに光源のビュー行列がセットされてるとして
vec3 p = (lightViewMatrix * position).xyz;
float len = length(p);
p /= len;
p.z = (len - near) / (far - near); // near, far は前方半球面と後方半球面の距離

普通の透視投影変換でもいいと思うが、拡散面光源を近似する場合はこういう半球投影変換のほうがいいと思う。
ちなみにパラボロイド座標変換も試してみたが、Shadow casterとShadow receiverの距離が離れると誤差が発生し、最終的にポリゴンの継ぎ目から光が漏れるような現象が起き、補正が難しそうだったので利用を諦めた。
(普通に深度値を利用する方法なら問題ない。)

3. シャドウマップの調整フェーズ

Shadow caster mapを直接利用すると、ポリゴンの継ぎ目からかすかに光が漏れてるような見た目になってしまう。
(この問題はパラボロイド座標変換を使った場合のものに似てるけど原因は別。)
これはシャドウマップ作成時のラスタ化ルールに起因する問題である。

以下の図はオレンジと水色の2つのポリゴンがラスタ化された結果を表している。
(格子はシャドウマップ上のピクセルを表している。)
raster
シャドウマップ参照フェーズのあるピクセルの処理で、図の赤枠で囲ったシャドウマップ上のピクセルをサンプルすることになったとしよう。
その場合水色のポリゴンのIDが得られるわけだけど、しかし厳密なサンプル点はピクセル中のオレンジ寄りの部分にある可能性もある。
その場合、サンプルしたまま水色のポリゴンのIDを使って交差判定すると”影光線は何にも遮蔽されない”、つまり”光が漏れる”事になる。
(これが深度値を利用する普通のシャドウマッピングの場合なら、どちらのポリゴンも同じような深度値を持つはずなので漏れは起きない。)

これを回避するには、シャドウマップ参照フェーズでシャドウマップを1ピクセルだけサンプルするのではなく、例えば周囲8ピクセル合わせて9ピクセルサンプルして処理するのが簡単。

しかしこうすると9回も影光線とポリゴンの交差判定を行わなければいけなくなる。
CPUで処理するならこういう場合、if文などを使って交差判定の回数を必要な分まで減らすことが出来るけど、GPUでそれをやるとかえって遅くなってしまう。(最新のGPUだとどうなんでしょう。)

これでは遅いので、シャドウマップの調整フェーズでGPU処理に適した形にシャドウマップを調整する。
といってもやってることは単純で、9ピクセルサンプルして最小ポリゴンIDと最大ポリゴンIDを求め、中心のポリゴンIDと合わせて3つ分のポリゴンIDをピクセルごとに出力するだけである。
ポリゴンID収集処理といった感じ。

このフェーズでは交差判定を行わないので、もう少し処理を複雑化して4つ以上のポリゴンIDを収集するようにしてもいいかもしれない。
3つでもだいたい大丈夫だけど、光の漏れが目立つ場合もある。

4. シャドウマップ参照フェーズ

ここではシャドウマップを参照しながら3つのポリゴンと影光線の交差判定を行う。

1回でも影光線が遮蔽されたらそのピクセルは影である。
あとは普通にリアルタイムレンダリングで行うようなパーピクセルライティングを行い、その結果に影なら0、それ以外なら1を掛けて出力するだけである。

5. 結果

実行したマシンのCPU/GPUはCore2Duo 2GHz/Geforce 9400M。
いつもの箱のシーンは上に載せたので、他のシーンを。

GPU_softshadow_SM512_DV0.1_C1495_47sec
1500弱の点光源を47秒で処理。
以前のものはほぼ同じ条件で800秒を超えていたので17倍ほど高速化したことになる。
(しかも以前のものは影領域の計算を微妙に間違ってたので少し暗くなっている。)

GPU_softshadow_SM512_DV0.1_C1345_29sec
1350弱の点光源を29秒で処理。
平面の光源ばかりじゃあれなので、スザンヌ型光源にしてみた。

これまで載せた結果は全て512×512の解像度のシャドウマップを利用していたが、以下は大胆にも64×64のシャドウマップを使ったもの。

GPU_softshadow_SM64_DV0.1_C163_5sec
163個の点光源を5秒で処理。
多少影が欠けてる部分があるけど、トップの画像と比べてもシャドウマップの解像度ほどの差は見られない。
素晴らしい。
これが普通のシャドウマップなら見るに耐えない結果になると思う。

ただ、512×512のシャドウマップの作成がそもそも軽いからか、64×64にしても解像度の差ほどは処理時間は短縮されなかった。
(1500弱点光源47秒の場合で7秒程高速化)

6. 課題

微細なポリゴンが多数あるシーンでは、Shadow caster mapにおけるポリゴンIDの”抜け”が発生しやすく、その結果影領域に本来ないはずの輝点が現れる場合がある。

この原因は2つあり、それぞれ別の対処法が考えられる。
ひとつはShadow caster mapの解像度不足。
解像度が不足すると、ラスタライズされないポリゴンが発生する。
その結果、そのポリゴンに関する情報は一切Shadow caster mapに保存されない。
この場合は単に解像度を上げればいいが、限界がある。

もうひとつは近隣ポリゴンIDの収集不足。
Shadow caster map上のあるピクセルを中心とした3×3の領域には、最大で9種類のポリゴンIDが存在し得るが、現状ではサンプル自体は9ピクセル分行ってるが、そのうち3種類のポリゴンIDしか利用していない。
ポリゴンID収集フェーズでより多くのIDを収集し保持するようにすればいいが、その分コストは高くなる。

どちらにしろ、問題は影領域における輝点、つまり高周波成分となって可視化されるので、影のボカシのテクニックと合わせれば軽減されるかもしれない。

あと、これは今回の手法特有というより、面光源を多数の点光源で近似するやり方そのものの問題だと思うが、有限の点光源数で近似してる関係で、影の濃さが段階的に変わることによる模様が、半影領域において目立つ場合がある。

こちらも、リアルタイムレンダリングで用いられる影のボカシ手法を適用すればかなり軽減されると思う。
その代わり正確性は犠牲になるかもしれないが、VSM法なら自然な感じでぼかせるかもしれない。

追記

最初にこの記事を上げてから色々実験してたけど、もう一個デカイ問題を発見した。
シャドウマップの生成フェーズのところでも書いたけど、半球座標がラスタライザで線形補間されると誤差が発生する。
で、その誤差を出来るだけ小さくするためにあらかじめポリゴンを細分化する処理を通してるんだけど、遮蔽物が光源にくっ付くぐらい近くにあると、生半可な細分化では間に合わなくなる。
その結果、影領域に結構大きめの半月型の光の漏れが発生する。

これは例えば光源に傘をつけてスポットライトにしたときに問題が起きると思う。
この問題って、ただの遮蔽物だけじゃなく、光源自体によっても引き起こされるからちょっと無視するわけにもいかない。
例えばスザンヌ型光源の場合、耳とか顎の部分が出っ張ってるから、それが遮蔽物になりうるわけで、しかも光源の一部だから、光源の他の部分に対する”超近距離遮蔽物”になってしまう。

光源はシャドウマップの作成時に除外するというのも一つの手かなと思うけど、それだと物理的に正しくなくなってしまう。
(2枚の同じサイズの面光源が同じ方向を向いて微妙に間をあけて重なってる状態を想像すると、光源を遮蔽の判定から省いた場合の問題がわかるとおもう。)

あと、光源に近いポリゴンだけ多めに細分化する方法とか考えたけど、光源はシーンの至る所に複数存在し得るので、シーン全体で一意に細分化具合を決めることも難しい。

座標変換をGPUフレンドリーな透視投影変換にするのがいいと思う。
ただfovを180°近くにするとシャドウマップの有効解像度の問題やらで誤差の問題やらが起きるので、1光源で複数枚シャドウマップを作る必要があるだろう。

ラスタライザで半球座標も正しく補間できればいいのに。
多分その辺りはGPUにハードウェア実装されてる部分だろうからソフトウェアではどうしようもないと思う。
(将来インターポレーションシェーダステージとか増設されませんかねw)

一瞬CUDAでラスタライザとか考えたけど大変そうだしなぁ。
今回の問題に対処するだけなら別にラスタライザ全体を実装する必要はないと思うけど。

やはりガチのGPUレイトレを実装したほうが品質面では一番問題なさそう。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です



※画像をクリックして別の画像を表示

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください