Shadow Volume(3)

前回エッジ検出アルゴリズムを整理したので、今回は残りのアルゴリズムを整理。

  • Shadowポリゴン(Shadow Volume)

Shadowポリゴンの作り方だけど、前回のエッジ検出アルゴリズムで「エッジ辺」と決めた辺から四角形(triangle * 2)を引き延ばす。辺を構成する2頂点から光源と逆側に向けて別の2頂点を設定して四角形を作る。(下の図を参照)

具体的には元となる頂点を原点として、光線ベクトルの長さをk倍した先を新しく追加する頂点位置とする。図中の1から光線をk倍して3ができ、2からk’倍して4の頂点ができる。

問題はこの「k」をいくつにするのかってところ。
3つ方式を考えてみた。

  1. far-clipまたはnear-clip面のうち光源から遠い方まで引き延ばす
  2. 視錐台の大きさと影を作りたい物体の位置関係から適切な固定値を導き出す
  3. 1と2のハイブリッド型

1はZ座標だけ比較して大小関係でfar/nearのZ値と光源のZ値との距離が大きい方を選ぶ。ただし、このとき光源からclip面へ向かうベクトルと逆光線ベクトル(光線ベクトルの逆向き版)の向きが違う場合は対象のclip面は光源の裏側にいるので比較対象外とする。

どっちのclip面まで引き延ばすか決まればZ座標で目標点までの距離と光線ベクトルの割合を出せば良い。k=(目標点のZ値−頂点のZ値)÷光線ベクトルのZ成分で求まる。
この方式の問題点は、光線ベクトルがZ軸に平行な状態(もしくは限りなく平行に近い状態)だとZ座標的にk≒無限大になってkを求められないって点(光線ベクトルのZ成分が0に近くなるので)。

2の方式は必ず視錐台の外まで引き延ばせそうなくらい巨大な定数値をkとして与えてやるって方法。直感的で良いんだけど、floatの範囲ぎりぎりまで座標値として使うような巨大マップとかだと引き延ばそうとして範囲外に出ちゃわないか気にする必要があるかもしれない。あと、真面目に"ちょうど良い大きさ"のkを算出するのは面倒だと思う。マップが小さいなら巨大な数値を決め打ちで入れちゃっても大丈夫な気はする。

3の方式はハイブリッドで光線ベクトルがZ軸に平行な(光線ベクトルのZ成分が0に近い、閾値より小さい)ときだけあらかじめ決めておいた巨大な定数を使う方法。

まぁ、正直どの方式もアレなんだけど。超まじめにやるなら視錐台の6面と衝突する位置を算出してそこまで引き延ばせば良いんだけど面倒だよね…
んで、kさえ求まれば後は逆光線ベクトルをk倍して原点と足し算すれば目指す位置のX、Y、Z座標になるはず。

あと、頂点の列挙順は元となる三角形の面の向きと同じなるようにする。

図の1〜3が元になる三角形とすると、

    • 辺1−2がエッジ辺なら、2−1−四、2−四−五
    • 辺2−3がエッジ辺なら、3−2−IV、3−IV−V
    • 辺3−1がエッジ辺なら、1−3−iv、1−iv−v

の順で頂点を列挙する。
これで、もともとの三角形が表面ならShadow Volumeも表を向くし、裏面なら裏を向いてちょうどいい感じになるはず。きっと。
[2011/12/29 18:45追記]
実際にコーディングしてみたけど、うまく三角形の裏表が制御できてない。上記の方法だとダメかも。何がまずいのか判明したら更新予定(すでに1年近く進展ないから怪しいけど…)

  • 影のシェーディング

影色については、西川さんの本*1からたくさんのインスピレーション?を頂きましたm(_ _)m
とりあえず影色は黒(0.0f, 0.0f, 0.0f)とする。影の明るさはα値で調整する。暗い影の領域では影のα値を1.0fに近づけて、影が薄い領域ではα値を小さな値にする。ただし、あまり明るくすると影に見えなくなるのである程度下限値は設ける。
α合成でくっつける色はもともとのColor Buffer値を採用する。たぶん環境光に反射光とかそんな感じのが入ってるはず。正確にやるなら着目している光源"以外"からの光と間接照明で飛んできた光の色で塗るのがベストだけど、あまり変わらなそうだし保留(^^;
さらに、光源からの距離が遠くなるにつれ影を薄くする。これもα値を小さくして表現する。
まとめるとこんな感じだろうか。(疑似コード)

cbuffer ShadowPSParameter{ float SomeConst; };	// 視錐台のサイズ(と光源からの距離?)に合わせて調整?
const float Bias = 0.5f;
alpha = min(SomeConst / dist2 + Bias, 1.0f);	// 1.0f でサチってもらう
return float4(0.0f, 0.0f, 0.0f, alpha);

SomeConstは適切に決めた定数で、ちょうど良い感じに影の濃さが出るような定数。Biasも定数で影が薄くなりすぎないようにするための下限値。dist2は光源からの距離(の2乗)。

あと、影の端っこも薄くしたいけど「端っこ」を判定するのが難しい。影処理を終えたstencil bufferをもとにぼかしをかけたりできると良いけど結構面倒そうなんだよね。
1つのアイデアとしては、Shadow Volumeでは直接Color Bufferに影を書き込まずに、縮小(例えば画面サイズの縦横半分)したテクスチャに影(もしくは光源からの距離)を書き込んでおいてから、後で例えばSSAOとかのPostProcessをやるときにバイリニアフィルタ付きで影テクスチャを合成すると良い感じにボケるかもしれない。

それにしても、改めてアルゴリズムを見返すと半透明影だとVRAMアクセス量が大きそうだなぁ…画面全体が陰に入ったりすると大変なことになるかも。あと、光源の数が増えると厳しいかも。色の付け方はα値合成じゃなくて環境光との合成色にした方が良いかも。あんまり変わらないかな。

アルゴリズム的に光源の位置と物体の位置にのみ依存するのでカメラが微妙に動いても影がピクピク動いたりしないよね?
仮にピクピクしちゃう場合は

    • 影をぼかしまくってごまかす(^^;
    • いったんStreamOutとかでShadow Volumeの頂点をBufferに保存してカメラや光源が大きく移動するまで使いまわす

とかで逃げるしかない?

*1:西川 善司「3Dゲームファンのためのグラフィックス講座」インプレスジャパン(2010/11/25) pp.15-16,46,89-90,160,298-300