2012年07月14日

Cascaded Shadow Mapsのテスト その2

続きです。

N分割した視錐台のシャドウマップがN枚できますので、シェーダ側でどのシャドウマップを使用するのか決める必要があります。
参考文献のサンプルプログラムでは複数枚使用しているようでしたが、とりあえず対応する距離のシャドウマップを1枚使ってやってみます。

視錐台の情報を元に分割し、N個の遠クリップ面までの距離がありますので、フラグメントシェーダでのz値で使用するシャドウマップを選択することができます。

// f[i].fardは視点から見た空間の遠クリップ面の距離です。
// カメラの座標系を使用してz値を計算することができます。
// cam_proj * (0, 0, f[i].fard, 1)^t 左の結果を[0; 1]の範囲に正規化します。(0.5倍+0.5)
far_bound[i] = 0.5f*(-f[i].fard*cam_proj[10]+cam_proj[14])/f[i].fard + 0.5f;

この計算結果をフラグメントシェーダに渡して比較を行います。

あとはシャドウマッピングを行います。
まず、フラグメントシェーダにワールド座標をバーテックスシェーダから渡しておきます。
このワールド座標を、対応する視錐台のModelViewProjectionCrop行列を掛けて[0; 1]に正規化すれば、光源座標系のシャドウマップでのx,y,zを求めることができます。
後は求めたz値とシャドウマップのz値を比較して、そこが影であるかどうかを判定します。

実行結果

fovy=45.0, aspect=1.33, zNear=5.0, zFar=200.0という視錐台に対して、1,2,4分割した結果です。
サンプルプログラムでかけていたので、少しPercentage Closer Filteringを掛けています。

csm_001.png csm_002.png csm_003.png

1分割(分割なし)の場合は、解像度が荒くなっています。静止画だとそうでもないですが、視点を動かすと影の揺らぎがひどいです。
2分割にした段階でかなり改善されていることがわかりました。
まぁ色々怪しいですが一応できたということにしたいと思います。

以下参考文献の適当和訳です。
Cascaded Shadow Maps[1]

シャドウマップは、ゲームエンジンでリアルな影を得るためのとても一般的なテクニックです。
広い空間にそれを使用しようとしたとき、シャドウマップは、調整が難しくなり、エイリアスなどが表示されやすい傾向にあります。
Cascaded Shadow Maps(CSM)は、視点から近い深度テクスチャの解像度を高め、始点から遠い所は解像度を下げることによって、
エイリアスの問題を解決することを助ける良く知られたアプローチです。これは、スクリーン誤差を一定にするために、カメラのビュー視錘台を分割し、
分割されたそれぞれの領域に対してデプスマップを作成します。

CSMは通常、大きい地形に太陽の光があたることによって出来る影に使用されます。
ひとつのシャドウマップで全てをキャプチャしようとした場合、実用的でないとても高い解像度が要求されます。
従って、下記のようないくつかのシャドウマップを使用します。

  • オブジェクトの近くのみをカバーする、詳細な影を記録するシャドウマップ
  • 全体をキャプチャする荒い解像度のシャドウマップ
  • 必要に応じてその中間のシャドウマップ

このわけ方は合理的です。なぜなら、遠く離れた所にあるオブジェクトが作る影は、
スクリーン空間でたったの2,3ピクセルしか占めず、近いオブジェクトが作る影はスクリーンのかなりの部分を占めるからです。

図1-1は、平行分割CSMの概略図を示しています。それぞれの分割平面は近クリップ面と遠クリップ面に平行で、それぞれのスライス自体が錘台になっています。
太陽は方向光源なので、関連付けられる光源錘台は(赤と青で示されている)ボックスになります。

アルゴリズムは、次のように進みます:

  • それぞれの光源錘台のために、光源位置から見たシーンの深度をレンダリングします。
  • カメラの位置からシーンをレンダリングします。フラグメントのz値に依存して、適切なシャドウマップを選んで参照します。


関連研究
和訳省略。Perspective Shadow Maps(PSM)やLight Space Perspective Shadow Maps(LiPSM)、Trapezoidal Shadow Maps (TSM)などがあります。
それぞれ特徴があるのでどれが一番いいとはいえません。

詳細な概要

次の詳解は、OpenGL SDKデモのcascaded shadow mapsに基づいており、その詳細な手順について説明していきます。

シャドウマップは、分割されたシャドウマップを保持するそれぞれのレイヤを持つテクスチャアレイに格納するのがベストです。
これは、ピクセルシェーダ内での効果的なアドレス割当を可能にし、全てのレイヤを同じ方法で扱うことが出来るので合理的です。

シャドウマップの生成

図1-1を見て、全ての影の投影とカメラ錘台が現在の光源錘台(ボックス)に含まれているのであれば、その外側は何もレンダリングされるべきとは限らないことに注意してください。
この方法では、光源の錘台はカメラ錘台スライスのバウンディングボックスであり、近い側は全ての遮蔽を起こす可能性がある物体をキャプチャするのに十分な拡張になります。
もし遮蔽物B(例えば鳥)が木の上にあった場合、ボックスは適切に拡大されるか、Bが影を落とさないようにするべきです。

アルゴリズムの最初のステップは、カメラの視空間で、分割された視錘台のZ値を計算することです。
シャドウマップのひとつのピクセルが、dsという辺長を持っていると仮定します。
それが投影する影は、影になっているオブジェクトの法線と位置に依存するスクリーンのごく僅かなdpを遮蔽します。
図表2-1を参照してください。
(式1)で、nは視錘台の近クリップ距離です。

理論では、スクリーン上の誤差を完全に等しくするためには、dp/dsが定数であるべきです。
加えて、私たちは、定数を決める依存する要因として、コサインも扱うことが出来ます。
なぜなら、私たちは視錘台の誤差を最小化し、それがプロジェクションの誤差に関与するからです。
従って、
(式2)
ここで、ρは0から1の値をとるsにより強制的に決まります。

上の式をzについて解いて、離散化すると、(分割数Nの数が非常に大きいと仮定する)、
分割点は、次式で指数関数的に分配されるべきです。
(式3)
ここで、Nは分割の総数です。より詳細な導出は、[2]を参照してください。

しかしながら、典型的なNは1から4なので、影の解像度の変化がはっきりと変わるためにこの式では分割点が見えてしまいます。
図2-2は、この相違の理由を示しています:ある領域は視錘台の外側ですが、光源錘台の内側であり、見えないので使用されません。
しかしながら、N→∞にすれば、この領域は0になります。

この効果を打ち消すために、線形項iを加えることで、違いを見えづらくさせます。
(式4)
ここでλは、修正する強さをコントロールする値です。

zで分割した後は分かっているので、現在の錘台スライスのコーナー点は、スクリーンのFOVとアスペクト比から計算することが出来ます。
詳細については[4]を参照してください。
その一方で、光源のモデルビュー行列Mは光源の方向を見たものに設定され、一般的な正射影プロジェクション行列P=Iが設定されます。
そして、カメラの錘台スライスのそれぞれのコーナー点pは、光源の同次空間内でph=PMpへ投影されます。
バウンディングボックスのそれぞれの方向の形の最小値miと最大値Miは、光源錘台(ボックス)に沿っていて、
私たちは一般的な光源錘台を実際のものに合わせるためにスケーリングとオフセット移動を定義します。
この効果は、zの最も良い精度を得ることが出来、xとyの可能性を少し失い、crop行列Cを構築することによって達成されます。
最終的に、光源のプロジェクション行列Pは、P=CPzに修正されます。Pzは正射影行列で、近クリップ面と遠クリップ面であるmzとMzと
(式5)です。
私たちは光源の錘台を錘台スライスと完全に一致させることも出来ますが、これは、光原の方向と種類をパースペクティブシャドウマップに変更します。

シーンは、それぞれの錘台スライスiのためにカリングされた錘台でもあり、全てが、モデルビュープロジェクション行列である(CPfM)iを使用することでデプスレイヤにレンダリングされ、
全体の手続きは全ての錘台分割で繰り返されます。

最後のシーンレンダリング

前のステップで、1〜Nのシャドウマップが生成され、オブジェクトが影の中にあるかどうかを決定するために使用されます。
全てのピクセルのレンダリングで、そのZ値がN個の事前に計算されたZ範囲と比較するべきです。次に、それがi番目の範囲に落ちると仮定します。
ピクセルシェーダは、この値を視線空間で最初に計算されたプロジェクション後の空間で受け取ることに注意してください。

その後、カメラのモデルビュー行列の逆行列Mc-1(それは完全な逆行列である必要はありません。拡大縮小が使用されない場合は、
3x3の部分を転置するだけでかまいません)を使用することで、フラグメントの位置はワールド空間に変換されます。
後で、それにスライスiのための光源の行列を掛けます。
変換は次の複合行列(CPfM)iMc-1によってキャプチャされます。
最後に、射影された点は、[-1; 1]から[0; 1]まで線形的にスケーリングされます。
この変換の全てが終わった後、フラグメントの(x,y)座標は、実際にi番目の深度マップのテクスチャ座標系で、また、Z座標は光源から
粒子?への距離を示します。
検査を行うことによって、私たちは光源から最も近い遮蔽物までの距離を、同じ方角で知ることができます。
これらの2つの値の比較により、そのフラグメントが影の中にあるかどうかがわかります。

コードの概要

OpenGL SDKのサンプルは、次のソースファイルを含みます:

  • terrain.cpp - 環境を読込んでレンダリングする関数とその定義を持っています。シャドウマッピングアルゴリズムに必要なメソッドは、
    Draw()で、、Load()とGetDim()は初期化中に読込みとワールド属性のバウンディングボックスの設定のために呼ばれます。
  • utility.cpp - メインコードを読みやすくするためのたくさんのヘルパー関数があります。シェーダの読込み、カメラ操作、メニュー、
    マウス及びキーボード操作なども含まれます。
  • shadowmapping.cpp - このファイルは、ここに示したアルゴリズムの核となるコードを含んでおり、シャドウマップとスクリーンに
    表示される最終画像の生成と描画を含む全てのコードがあります。

Roughly、terrain.cppとutility.cppは、実際のゲームではゲームエンジンによって提供されるサンプルを実行するために必要なフレームワークを提供します。、
このanalogyでは、display()はレンダリングループの一部で、このサンプルではmakeShadowMap()とrenderScene()をその中で呼んでいます。

renderSceneは、シェーダのuniform(リスト3-2をみてください)の設定と、CSMなしのシーンのレンダリングを行います。
このコード断片でCSMにとって重要な部分は、ピクセルシェーダ内でこのパスが適用されることです。

結果

省略

結論

Cascaded Shadow Mapsは、広大な環境の影のための有望なアプローチです。
他の曲げる手法と比較して、たくさんの特殊なケースや困難に苦しまずにすみ、スクリーン空間で比較的一定のエラーを得ることになります。
従って、シャドウマップの解像度をただ上げるだけで、視点からの距離に殆ど依存することなく、シーン内の全てのオブジェクトの影のジャギーの大部分を減らすことができます。


[1] Rouslan Dimitrov: "Cascaded Shadow Maps", NVIDIA Corporation, 2007
posted by シンドラー at 12:31 | Comment(0) | TrackBack(0) | OpenGL & Metasequoia | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。
※ブログオーナーが承認したコメントのみ表示されます。
この記事へのトラックバックURL
http://blog.seesaa.jp/tb/280964095
※ブログオーナーが承認したトラックバックのみ表示されます。

この記事へのトラックバック