シャドウマッピングの基本


 1.はじめに…
とりあえず,作りました。





 2.シャドウマップについて
 シャドウマッピングは,ライト方向の深度値を格納したシャドウマップと呼ばれるテクスチャを使用してシーンに影を付加して描画を行う手法です。
シャドウマップの長所としては物体の形によらず影がかけることです。短所としては,テクスチャを使用するためテクスチャの解像度が低いとジャギーがでてしまい影が汚くなってしまうという点です。
まずは,シャドウマッピングの原理について説明していきます。
シャドウマッピングでどのように影をつけるかについてですが,これはライトからみた視点と比較して隠れているかどうかを判定します。この判定に使用するが深度値です。
そのために,まずは深度値を格納するシャドウマップを作成する必要があります。これは単純にライト視点でシーンを描画すればよいです。


深度値さえ描ければよいので,深度ステンシルビューだけをアウトプットマネージャに設定すればよいです。レンダーターゲットビューの設定やピクセルシェーダの設定はいらず,頂点シェーダと深度ステンシルビューのみの設定で良いです。
頂点シェーダは下記のように見慣れた処理となります。
00043:  VSOutput VSFunc( VSInput input )
00044:  {
00045:      VSOutput output = (VSOutput)0;
00046:  
00047:      float4 localPos = float4( input.Position, 1.0f );
00048:      float4 worldPos = mul( World, localPos );
00049:      float4 viewPos  = mul( View, worldPos );
00050:      float4 projPos  = mul( Proj, viewPos );
00051:  
00052:      output.Position = projPos;
00053:  
00054:      return output;
00055:  }
シャドウマップの作成ができたら次は影を描画する処理です。影を描画する方法ですが下図を基に説明します。


影の描画するやり方ですが,上の図を基に説明します。 まず,紫色の点を視点と仮定します。ある点を描画するときにライトまでの距離とシャドウマップに格納されている深度値を比較します。比較した結果,シャドウマップに格納された深度値よりもライトまでの距離が大きい場合は,影を描画します。小さいあるいは等しい場合は影ではない部分です。 たとえば,点P1の場合は,ライトまでの距離の方がシャドウマップに格納された深度値よりも大きいです。このような場合は影を描画します。 点P2の場合は,ライトまでの距離とシャドウマップに格納された深度が等しくなります。この場合は影は描画されません。 この処理をどうのようにするかということですが,深度値を射影テクスチャリングを使用してオブジェクトにマッピングして比較を行えるようにします。
射影テクスチャリングに使用する行列はCPU側で用意しておき,GPU側に転送する行列は下記のコードのようになります。
00017:  static const asdx::Matrix SHADOW_BIAS = asdx::Matrix(
00018:      0.5f,  0.0f, 0.0f, 0.0f,
00019:      0.0f, -0.5f, 0.0f, 0.0f,
00020:      0.0f,  0.0f, 1.0f, 0.0f,
00021:      0.5f,  0.5f, 0.0f, 1.0f );

00566:      cbParam.Shadow    = m_SdwMgr.GetLightViewProj() * SHADOW_BIAS;
一応上記コードで何をしているかというと,ライトのビュー行列でライト視点にしています。次の射影行列でシャドウマップの横幅と縦幅を決めるような処理になります。ライトのビュー行列とライトの射影行列を掛けた結果の値の範囲は(-1, -1, 0) ~(1, 1, 1)になります。一方,テクスチャを通常張る場合は(0, 0)~(1, 1)の範囲がテクスチャ座標となりますので,(x, y)と(u, v)の値の範囲が異なります。
(-1, -1)~(1, 1)の範囲を(0, 0)~(1, 1)にすれば,そのままテクスチャ座標として使用することができますので,0.5を掛けて0.5を足してやれば(0, 0)~(1, 1)となります。このような変換を行う行列が一番後ろの行列になっています。ただ一点気を付けたいのは,Direct3Dのテクスチャ座標空間はOpenGLと異なりv方向が逆になっています。そのため,v成分は逆にする必要があります。この処理は行列の2行目2列の成分として表しています。
見るとわかるように0.5ではなく,-0.5とマイナスを付けているのがミソです。この計算をすると...
 -1 * -0.5 + 0.5 = 1
  1 * -0.5 + 0.5 = 0
となり上下逆転するようにできます。
あとは,この3つの行列を掛けてできたシャドウマップ行列と頂点座標を乗算してやれば,射影テクスチャリングにつかうテクスチャ座標が求まるので,このテクスチャ座標を使ってシャドウマップテクスチャーをフェッチして深度比較をすれば影であるかどうかの判定ができます。
深度判定ですが,実はDirect3D 10/11からはHLSL側で比較用のSampleCmp()/SampleCmpLevelZero()という関数が用意されています。さらにこの比較関数ですがPCFも計算してくれるというオマケ付きなので,できればこちらの関数を使った方が良いです。
さて,このSampleCmp()関数ですが,普通によく使うSample()関数とは違って第1引数がSamplerComparisonStateとなっていることに注意しましょう。SamplerComparisionStateとするためには,ID3D11SamplerStateを下記のような感じでつくる必要があります。
00235:      D3D11_SAMPLER_DESC desc;
00236:      ZeroMemory( &desc, sizeof( desc ) );
00237:      desc.AddressU       = D3D11_TEXTURE_ADDRESS_BORDER;
00238:      desc.AddressV       = D3D11_TEXTURE_ADDRESS_BORDER;
00239:      desc.AddressW       = D3D11_TEXTURE_ADDRESS_BORDER;
00240:      desc.BorderColor[0] = 1.0f;
00241:      desc.BorderColor[1] = 1.0f;
00242:      desc.BorderColor[2] = 1.0f;
00243:      desc.BorderColor[3] = 1.0f;
00244:      desc.ComparisonFunc = D3D11_COMPARISON_LESS_EQUAL;
00245:      desc.Filter         = D3D11_FILTER_COMPARISON_MIN_MAG_MIP_LINEAR;
00246:      desc.MaxAnisotropy  = 1;
00247:      desc.MipLODBias     = 0;
00248:      desc.MinLOD         = -FLT_MAX;
00249:      desc.MaxLOD         = +FLT_MAX;
00250:  
00251:      // サンプラーステートを生成.
00252:      HRESULT hr = pDevice->CreateSamplerState( &desc, &m_pSmp );
00253:      if ( FAILED( hr ) )
00254:      {
00255:          ELOG( "Error : ID3D11Device::CreateSamplerState() Failed." );
00256:          return false;
00257:      }
見るとわかるように,設定するフィルタ値がD3D11_FILTER_COMPARISON_XXXX系になっていたりします。このようにして作ったID3D11SamplerSateをID3D11DeviceContext::PSSetSamplers()などで設定してあれば,HLSL側でSamplerComparisonStateとして使うことができます。
最後に影をつける方法ですが,SampleCmp()/SampleCmpLevelZero()で返ってきた結果でカラーを乗算してあげれば,シャドウを付けることができます。シャドウが黒すぎて嫌な場合は,関数の返り値をつかってうまく制御してあげてください。例として,ここでは線形補間をつかって色を付ける方法をあげておきます。
00117:      float  shadowThreshold = 1.0f;      // シャドウにするかどうかの閾値です.
00118:      float  shadowBias      = 0.01f;     // シャドウバイアスです.
00119:      float3 shadowColor     = float3( 0.25f, 0.25f, 0.25f );
00120:      shadowThreshold = ShadowMap.SampleCmpLevelZero( ShadowSmp, shadowCoord.xy, shadowCoord.z - shadowBias );
00121:      shadowColor     = lerp( shadowColor, float3( 1.0f, 1.0f, 1.0f ), shadowThreshold );
上記のコードのshadowColorをfloat3( 0.0f, 0.0f, 1.0f )にすれば,青色のシャドウ。float3( 1.0f, 0.0f, 0.6f )にすれば下記のようなピンク色のシャドウになります。





 3.バイアスについて
 実際にシャドウマップを実装してみると,下記のようにシャドウアクネ(Shadow Acne)と呼ばれる現象がでたりなど綺麗にいかないケースが出てきます。この問題について少し説明をしておきます。



何故このような変な縞模様がでてくるのか?についてですが,これはシャドウマップのテクセルによって発生するものです。
下図を見るとわかるように本来はなめらかポリゴンは,シャドウマップのテクセルによって離散化された値になってしまいます。これはシャドウマップの原理上避けることができません。



シャドウマップを使ってシャドウを付けるということはこの離散化されたポリゴンとの比較によって決定されます。つまり,下図のようになります。



上図のようにシャドウマップに書かれている値よりも,奥にあるものは隠れているということになるので,緑色の部分がシャドウになる部分です。見て分かるように本来はシャドウにならない部分にシャドウが付いてしまいます。緑色の部分だけをみると縞模様になっており,これがシャドウアクネの原因になります。
では,これを低減するためにどうすればよいかということですが,一番単純なのは1テクセルあたりの大きさを小さくすることです。つまりシャドウマップの解像度を無限大にすれば理論上はマッハバンドが発生しなくなるはずです。…が当然コンピュータのメモリには限りがあるわけで,無限大にすることはできません。また,シャドウマップの解像度があがると,ピクセルが増えるので,ピクセルシェーダが走る回数も増えるのでパフォーマンスが悪くなるという副作用も出てきます。
それじゃどうするの?という話になるのですが,シャドウアクネ対策としてよく使うのは,バイアスをかける方法です。



微小な定数オフセットを加えることで,シャドウアクネを低減することができます。
もちろん,この手法も万能ではなく試してみるとわかると思うのですが,うまくいかないケースが出てきます。
アクネを消すためにバイアスを強めにかけるとアクネは消えたんだけど影が離れてしまい,いわゆるピーターパン現象が出てきてしまうなどの問題があります。


『シャドウ深度マップの品質向上のための一般的な技法』
http://msdn.microsoft.com/ja-jp/library/ee416324(v=vs.85).aspxより図を引用

上図を見るとわかるようにシャドウが離れている状態が発生します。
そこで定数オフセットでなく,もう少しマシな方法として傾斜を考慮したSlope Scaled Depth Biasという方法があります。
この手法は隣接ピクセルとの勾配(Gradient)を見て,その勾配が大きいところに合わせてバイアスを掛けるという手法のようです。
ID3D11RasterizerStateで深度傾斜バイアスを設定できる口があります。(参照:http://msdn.microsoft.com/en-us/library/windows/desktop/ff476198(v=vs.85).aspx) しかし,困ったことにID3D11RasterizerStateでは,動的に変化させることが難しいので,ここではHLSLで実装して動的に変化できるようにしてみます。深度傾斜バイアスは勾配が一番大きいところを基準にしてバイアスを掛けた方が良いでしょう。…となると,勾配はHLSLのddx(), ddy()で計算でき,勾配が一番大きい所は計算結果のmax()をとれば良さそうです。
また,Microsoftの深度バイアスの説明(http://msdn.microsoft.com/ja-jp/library/cc308048(v=vs.85).aspx)によると,勾配が一番大きい所に対して,スケール倍するというようなコードがあるので,これをそのまま実装すれば良さそうです。
Microsoftの深度バイアスの説明の最後の方にこんな一文が載っています。
ただし、DepthBias と SlopeScaledDepthBias の使用によって、ポリゴンを極端な鋭角で表示すると、
バイアスの式によって非常に大きい z 値が生成されるという新たなレンダリングの問題が生じる可能性があります。
この結果、実質的に、シャドウ マップ内の元のサーフェスから極端に離れた位置にポリゴンが押し出されます。
この特有の問題を軽減するのに役立つ方法の 1 つとして、DepthBiasClamp を使用する方法があります。
これによって、計算される z バイアスの大きさに関する (正または負の) 上限が指定されます。
 確かに,極端に勾配が強い所があると異常にポリゴンが押し出されてしまう可能性があるので,これは良くない気がします。
説明に沿って,計算した値をクランプするようにしましょう。実装コードは下記のようになります。
00115:      float3 shadowCoord = input.SdwCoord.xyz / input.SdwCoord.w;
00116:  
00117:      // 最大深度傾斜を求める.
00118:      float  maxDepthSlope = max( abs( ddx( shadowCoord.z ) ), abs( ddy( shadowCoord.z ) ) );
00119:  
00120:      float  shadowThreshold = 1.0f;      // シャドウにするかどうかの閾値です.
00121:      float  bias            = 0.01f;     // 固定バイアスです.
00122:      float  slopeScaledBias = 0.01f;     // 深度傾斜.
00123:      float  depthBiasClamp  = 0.1f;      // バイアスクランプ値.
00124:  
00125:      float  shadowBias = bias + slopeScaledBias * maxDepthSlope;
00126:      shadowBias = min( shadowBias, depthBiasClamp );
00127:  
00128:      float3 shadowColor     = float3( 0.25f, 0.25f, 0.25f );
00129:      shadowThreshold = ShadowMap.SampleCmpLevelZero( ShadowSmp, shadowCoord.xy, shadowCoord.z - shadowBias );
00130:      shadowColor     = lerp( shadowColor, float3( 1.0f, 1.0f, 1.0f ), shadowThreshold );
とりあえず,上記コードはサンプルのためシェーダ内で固定値を設定するようになっていますが,定数バッファ経由で値を設定するようにすれば動的に変更することが可能です。ご自分のプログラムに組み込みたい方は定数バッファにしておくのをおすすめします。


 4.最後に
 シャドウマップの基本について説明してみました。
今回バイアスについては,定数バイアスと傾斜を考慮したバイアスについて説明しましたが,比較的最近発表された論文で"Adaptive Depth Bias for Shadow Maps"というものがあり,竹重さんのブログで
 おそらくですが,現状で最も最適化されたShadowMapのBiasと思われます。
従って,shadow acne, shadow detachmentの問題が最も少ない結果が期待できます。
また,他のWarp系の手法(Dual Paraboloid, Perspective SM)にも正しく応用することができ,
良好なBias値の導出が可能です。
実行時間に関しても,slope scale depth biasを使用している状況下と比較するならば,
軽微な増加と考えることができると思います。
…と紹介されているので,そのうち取り上げようかと思います。


 Download
本ソースコードおよびプログラムはMIT Licenseに準じます。
プログラムの作成にはMicrosoft Visual Studio 2012 Express Editionを用いています。