線形な深度を使ったシャドウマッピング

Depth-ShadowMappingのページでは深度(Depth)を利用して影を描画しました
しかし、実はこの描画方法は問題点を孕んでいます
それはOpenGLでは深度が線形ではないということです
(DirectX系でも多分同じ)

とりあえず、問題点の解説に入る前にどんな問題が生じるのかを体験してもらいます
問題点が確認しやすいようスポットライトにてデプスシャドウを実装した例を用意しました
様々にいじってみてください
サンプルページ
スポットライトは常にオブジェクト中心位置に向かい、スポットライトの内径は外径に合わせて変化します

例えばですが、オブジェクトの表示位置をX軸方向にずらしてカメラから離して表示してみてください
すると根元辺りの影が消えてしまうのが確認できると思います
しかし、カメラに近い部分に置くと根元辺りでも影が出ていますね
シャドウの問題点画像
こんな感じで影が消えてしまう

これは、以前の解説で言うと量子化誤差をごまかすための上げ底のせいで起こります
ですが物体の位置によって上げ底が変わっているわけではありません
変わっているのは深度の方なのです

OpenGLの深度

深度とはどういうものかもう一度
深度とは、画面に描かれた物体が画面上でどれくらい奥に描かれているか、ということを示す指標です

3DCGでは画面に描かれる範囲をクリッピング空間として定義します
ここでは、クリッピング空間の一番手前の面をニアクリップ平面、一番奥をファークリップ平面と呼ぶことにします
ニアクリップをn=0.1、ファークリップをf=10.0と定義したとします
画面に描かれる一番手前の物体は、カメラの向きに距離0.1の位置にあります
逆に一番奥の物体は距離10.0ですね
距離nより近いものは描かれず、fより遠いものは描かれない

と、こういったものがクリッピング平面の性質です
そして、深度とはn~fの間を0~1の数値で表した奥行き情報となります

では深度0.5の物体はどこにあるのでしょうか?
普通に直感で考えると、nとfの中間なんじゃないの?って考えてしまうんですが、実は違います
座標変換の都合上、クリッピング空間の奥行き(Z軸)は非線形な形に変換されてしまうのです

具体的に深度をグラフにしてみると、n=0.1、f=10.0の場合の深度値は次のグラフのように変化します
深度のグラフ
横軸はカメラ空間のZ座標、縦軸が深度値です
対数みたいなグラフになってますね
深度0.5の位置はかなり手前にあります
全然中間じゃないですね…

これの何が問題かというと、シャドウマッピングではこの深度情報をテクスチャに書き込んで扱うため0~255の数値で扱います
(うちのプログラムでは0~1023で扱うことが出来ますが)
カメラ空間でのZ座標が1.0の時大体深度が0.91です
0.91を256段階のカラー値に変換すると232になります
つまりカメラからの距離0.1~1.0の間は233段階で表されて、1.0~10.0の範囲は残りの23段階で深度が表現されます
ということは256段階使っているにもかかわらず、1.0から10.0の範囲は23段階と1/10以下の精度に落ちているわけです
(というか5.0以降はもう変化がないと言っていいレベルですね…)
カメラから離れるほど精度が落ちていくので上げ底0.005が悪さをするようになります
(うちのプログラムだと上げ底0.0015だけど、それでもあのサンプルの体たらくです)
これでは影が非常に浮いてしまうのでできれば何とかしたいですね

言葉だけだと少し分かりにくいので深度値を視覚化してみました
0~1をグレースケールで表現して、クリッピングによって描かれない部分(といっても視野角は考慮せず)は赤色で表現しました
サンプルページ
深度の視覚化サンプル
サンプルではライト設定にシャドウマッピング用カメラが連動して変化するのでスポットライトの距離減衰設定をいじってみてください
どんな深度値が計算されるのかなんとなくわかると思います

実はこの非線形なデプスシャドウは手法的には古く、固定機能パイプラインのOpenGL2.0以下の頃からあるシロモノです
プログラマブルシェーダが使えないので深度値をそのまま読み取って比較しなくてはいけないためこの問題を根本的に克服することは出来ませんでした
が、今はプログラマブルシェーダで深度を自由に計算できるので何とかなります
この都合の悪い深度情報ではなく、もっと計算に都合のいい深度を作ってしまえばいいのです

線形な深度

これを解決するには深度値の変化が次のグラフのように距離に比例した変化をしてほしいわけです
線形な深度のグラフ
直線的な変化をする深度になるので線形な深度、リニアデプス(linear depth)とでも呼びましょう
今回の目的はこの線形な深度を得ることにあります

それでは線形な深度値、どうやって求めるのか?となるわけですが
こちらのpdfを参考にさせてもらいました
さらなる大元のソースは404でしたがきちんとした出どころなので大丈夫でしょう

線形な深度の計算は、実はすごく簡単です
バーテックスシェーダでgl_Positionに変換行列を掛けた座標値を渡しますよね?
その時渡す座標のW値がカメラ空間でのZ座標になってます
なんとも都合がいいですね

これは飽くまでZ座標に過ぎません
というわけで深度値への変換は、線形な深度値をLとし、シャドウマッピング用カメラの座標変換行列mvpSと頂点座標vPositionを用いて、 ニアクリップをn、ファークリップをfとすると

 L = ( ( mvpS * vPosition ).w - n ) / ( f - n )

になります
ZじゃなくてWを使うのがミソです
なんだか不思議な感じですね

さて、これで得られる深度を視覚化すると次のようになります
サンプルページ
線形な深度の視覚化サンプル

比較してもらえば一目瞭然ですが、非線形の深度はほぼ真っ白に描かれてしまうのに対して、線形の深度は綺麗なグラデーションが出ますね
これなら影も綺麗に描けそうです

深度の補間

以前作ったデプスシャドウですが、解説したとおり深度が非線形です
これは精度以外にもある問題があります
それは頂点間の深度値の補間が行えないということです

バーテックスシェーダで深度値を計算したとします
それをvarying変数に入れてフラグメントシェーダに渡したとします
この場合、面上で参照される深度値は頂点で求めた深度値を線形補間したものになります
深度値は非線形に変化するのに補間が線形なので誤差がでてしまいますね
面の大きさが大きいほど、面の位置がカメラに近いほど誤差が大きく出ます

対して今回の線形な深度は線形であるが故、このバーテックスシェーダからフラグメントシェーダへの橋渡しができるのです
線形補間で深度が計算できるので高速化にも繋がるわけですね
いいコト尽くめだ

ただ、サンプルでは都合によりフラグメントシェーダで逐次計算します
線形の深度と非線形の深度を比較したくて…分けるの面倒くさくて…

というわけで最後に線形・非線形のデプスによるシャドウマッピングを使ったサンプルを御覧ください
サンプルページ
ライトからオブジェクトを離しても全く影が浮かないと思います
非常にきれいな影になりましたね
これくらいできれば浮動小数点数テクスチャも必要ないんじゃないかなあ
どうかな?
(設定如何ではフルハイビジョンだと2,3ピクセル分影が表示されなかったりはするかも?まあ実用範囲だろう)

あとがき

線形な深度の計算いかがでしょうか?
計算部は拍子抜けするほど簡単だったと思います
これだけで精度が10倍以上になります(部分的、限定的な条件では別ですが)
まあ、実はこの計算方法に辿り着くまでに色々思考錯誤して大変だったんだけどね
出来上がってみれば凄くシンプル
非線形の深度より簡単とは…ビックリ

ちなみにこの線形な深度はカメラからの距離として扱いやすく、距離フォグとか別なことにも使いやすいと思います
まあ厳密に言えばカメラからの距離ではなく、ニアクリップ平面からの距離となるのですが

実はサンプルをいじると分かるかと思うのですが、距離減衰を短く設定すると鏡面反射光が不自然になります
これはファークリップ以降の部分が影として判定されるためです
ファークリップより遠くを影を描かないようにすると今度は鏡面反射光のせいで影が途切れていることが露呈します…
これを防ぐには鏡面反射光を消してしまうか、ファークリップを距離減衰と連動せずにある程度大きく取るしか無いかな
ゲームグラフィックなんかだと鏡面反射光の取り扱いって結構難しいので、ライトの鏡面反射は消して環境マッピングだったりしますね
うーん、鏡面反射光にも距離減衰つけよかなぁ?

2014/01/28掲載

FC2カウンター
inserted by FC2 system