DirectXの話 第104回
全方位シャドウマップ
新しいサイトになってから初めての新規更新です。
今回は点光源のシャドウイングとして用いられる全方位シャドウマップをやってみました。
通常のシャドウマップは平行光源やスポットライトのような指向性のあるライトに用いられる技術です。
点光源のように全方向に光を放射する光源の場合、通常のシャドウマップでは表現がしづらくなります。
これが格闘ゲームのようにキャラが2体しかいないというのであれば、点光源から各キャラの方向に対してシャドウマップを行えばOKです。
しかし、数百体のキャラが出てくる場合でも同様に行えるかというと、まあ無理です。
そこで全方位シャドウマップが登場します。
全方位シャドウマップは環境マップと同様にキューブマップで実装が可能です。
この場合は6面の描画が必要になるので、描画能力がそれなりに高いハードでなければ厳しいかと思います。
今回は双方物曲面を利用しています。この技術の場合、2面の描画だけで済みます。
双方物曲面についての詳しい記事はImagireさんのこちらの記事を参照してください。
なお、今回もDeferred Renderingを行っています。
左上の画像は影の判定のみを行った画像です。これとLight Pre-Pass Renderingを組み合わせています。
では、簡単にプログラムの解説を行います。
まずは全方位シャドウマップを描画します。当然、影を落とすオブジェクト(キャスタ)のみを描画します。
サンプル内にある"DPShadow.fx"を見てみましょう。
VS_OUT_CASTER vsCaster( const VS_IN_CASTER v )
{
VS_OUT_CASTER o = (VS_OUT_CASTER)0;
// 座標変換
o.vPos = mul( v.vPos, mtxWV );
o.vPos.z *= zSign;
float len = length( o.vPos.xyz );
o.vPos /= len;
o.z = o.vPos.z;
float rz = 1.0f / (o.vPos.z + 1.0f);
o.vPos.xy *= rz;
o.vPos.z = (len - fNear) / (fFar - fNear);
o.vPos.w = 1.0f;
o.depth = o.vPos.zw;
return o;
}
float4 psCaster( const VS_OUT_CASTER v ) : COLOR
{
clip( v.z );
float zz = v.depth.x / v.depth.y;
zz += fOffset;
return float4( zz, zz, zz, zz );
}
頂点をスクリーン座標に変換し、ピクセルシェーダで深度値を書き込む、という点では普通のシャドウマップと変わりません。
ただ、座標変換がちょっと特殊です。これについては前述したImagireさんの記事を参照してください。
説明しておいた方が良さそうなのは頂点シェーダ内のzSignというパラメータくらいでしょうか。
これは双方物曲面を鏡合わせにするためのパラメータで、1なら表面、-1なら裏面になります(どっちが表でどっちが裏かはビュー行列次第です)。
mtxWVが同一でも、zSignの符号を変更すれば2枚のテクスチャに描画可能と言うことです。
実際のシーンの描画はいつも通り法線・深度マップの描画から始まります。そしてライトバッファを描画するのも同様です。
これとは別に、影の描画を行います。最初の画像の左上を描画するわけです。
これはライトバッファを描画するのと同じように球を描画します。影を落とされるオブジェクト(レシーバ)を描画するわけではありません。
この球の範囲内はライトの影響範囲であり、点光源が影を生成できる距離でもあります。
球の外部には影は落ちませんが、元々ライトの影響もないので暗くなるので問題はないはずです。環境光との兼ね合いで不自然に見えてしまうことがあったりはするかもしれませんが、多分大丈夫。
頂点シェーダは頂点をスクリーン座標に変換しているだけなので省略しますが、ピクセルシェーダは以下に提示しておきます。
float4 psReceiver( const VS_OUT_RECEIVER v ) : COLOR
{
float2 uv = v.vPos2.xy / v.vPos2.w;
float3 view = { vFrustumCorner.x * uv.x, vFrustumCorner.y * uv.y, vFrustumCorner.z };
uv = uv * float2( 0.5, -0.5 ) + float2( 0.5, 0.5 ) + vUvOffset;
float3 normal;
float depth;
UnpackDepthNormal( depth, normal, tex2D( sampleDepthNormal, uv ) );
float3 pos = depth * view;
// ライトビュー空間の座標を求める
float4 lv_pos = mul( float4( pos, 1 ), mtxShadow );
// UVと距離を求める
float len = length( lv_pos.xyz );
lv_pos /= len;
float z = abs( lv_pos.z );
float rz = 1.0f / (z + 1.0f);
float2 shadowUV = (lv_pos.xy * rz) * float2( 0.5f, -0.5f ) + float2( 0.5, 0.5 );
float mesh_depth = (len - fNear) / (fFar - fNear);
// 影マップと比較する
float front_depth = tex2D( sampleShadowFront, shadowUV ).r;
float back_depth = tex2D( sampleShadowBack, shadowUV ).r;
float shadowFactor;
if( lv_pos.z > 0.0f ) { shadowFactor = front_depth > mesh_depth; }
else { shadowFactor = back_depth > mesh_depth; }
return float4( shadowFactor, shadowFactor, shadowFactor, 1 );
}
最初にスクリーン座標からビュー空間中の座標を求めます。いつも通りですね。
この座標をライトビュー空間に変換します。mtxShadowはビュー行列の逆数とキャスタ描画で使用したライトビュー行列を積算したものです。
ライトビュー空間中の座標からシャドウマップ用のUV値と光源からの深度値を求めます。
UV値を利用して2枚のシャドウマップから深度値をフェッチします。
ライトビュー空間中のZ値が正なら表側のシャドウマップを、負なら裏側のシャドウマップを利用します。
シャドウマップから持ってきた深度値とこの座標の深度値を比較し、その結果をバッファに出力するのはいつも通りの手法になります。
こうしてできたバッファとライトバッファ、法線・深度バッファを用いて最終描画を行います。こちらは特に説明の必要がないでしょう。
海外のサイトでは全方位シャドウマップのVSMを実装している例もありましたが、2枚のテクスチャの境界付近が少々怪しかったです。
これは双放物曲面を利用してもキューブマップを利用しても発生する問題だと思います。のりしろを大きくすることである程度の対応はできるとは思いますが。
どうしてもソフトシャドウをやりたい場合はスクリーン空間ソフトシャドウを利用する方がいいのではないかと思います。こっちの方が何も考えなくて実装できるから簡単なんですよね。
全方位シャドウマップで使用できるPSM系の技術については聞いたことがありません。
シャドウマップの表面を常にカメラ向きにすることで、表面の解像度を高く、裏面の解像度を低くすることは可能です。
裏面の解像度を1/4(縦横それぞれ1/2)にしてみたところ、サンプルではそれほどの影響は見られませんでした。
シーンが複雑になればなるほど問題は出てくると思うので、シーンによって変更できるようにしておくといいかもしれません。
サンプルは下に置いてあります。
次回もDeferred Rendering関係をやってみる予定です。
そろそろWindows7も発売されるし、DirectX11対応GPUも発売されたことですし、そのうちPCを買い換えてDX11解説に移行するかもしれません。