久しぶりに影を扱ったのでメモです。
Direct3D 10 以降、ShaderModel 4.0 以降はハードウエアシャドウマップが当たり前に
使えるようになっているので比較的簡単になりました。
Direct3D8 ~ Direct3D9、ShaderModel 1.0 ~ 3.0 までは NVIDIA GeForce
独自の拡張機能でした。ハードウエアシャドウマップは
・DepthBuffer を直接参照するため他にバッファを用意することがないこと
・通常のサンプリングと違い、比較した結果の Bool 値 1.0 or 0.0 を返すこと
・比較結果をバイリニア補間するため、追加コストなしにハードウエアでフィルタリング可能なこと
等の利点があります。
ShaderModel 2.0 以降なら同等の機能をシェーダーで記述することができます。
旧 RADEON 向けシェーダーでは必須となりますが、追加バッファが必要でバイリニア
用サンプリングのコストがゼロにならないので完全に同じとはいえませんでした。
Direct3D 10 では標準機能の一部になったので、ShaderModel 4.0 に対応した
RADEON HD 2900XT 以降はハードウエアシャドウマップが使えます。
下位の ShaderModel でも利用可能で、RADEON HD 2900XT では
ShaderModel 3.0 を使っても GeForce 向けシェーダーがそのまま動作しました。
最初試したときは感動。
ShaderModel 4.0 以降は値を比較しながらサンプリングできる汎用機能として用意されて
いるので、GeForce 用、RADEON 用とシェーダーやプログラムを分ける必要がなくなりました。
バッファの作成
// depth buffer 用リソースの作成 CD3D11_TEXTURE2D_DESC ddesc( DXGI_FORMAT_R24G8_TYPELESS, width, height, 1, // array 1, // mip D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_DEPTH_STENCIL, D3D11_USAGE_DEFAULT, 0, // cpu 1, // sample count 0, // quality 0 // misc ); iDevice->CreateTexture2D( &ddesc, NULL, &iShadowDepth ); // Depth Buffer 用 View の作成 // レンダリング時はこちら CD3D11_DEPTH_STENCIL_VIEW_DESC dsdesc( D3D11_DSV_DIMENSION_TEXTURE2D, DXGI_FORMAT_D24_UNORM_S8_UINT, 0, // mipSlice 0, // firstArraySlice 1, // arraySize 0 // flags ); iDevice->CreateDepthStencilView( iShadowDepth, &dsdesc, &iShadowDepthView ); // Shader Resource 用 View の作成 // サンプリング時はこちら CD3D11_SHADER_RESOURCE_VIEW_DESC sdesc( D3D11_SRV_DIMENSION_TEXTURE2D, DXGI_FORMAT_R24_UNORM_X8_TYPELESS, 0, // mostDetailedMip 1, // mipLevels 0, // firstArraySlice 1 // arraySize ); iDevice->CreateShaderResourceView( iShadowDepth, &sdesc, &iShadowView );
使用している CD3D11_TEXTURE2D_DESC や CD3D11_DEPTH_STENCIL_VIEW_DESC
等は DirectX SDK March 2009 以降使えるようになった新機能です。
各種構造体を初期化してくれるヘルパーで、D3D11.h で定義されています。
ヘッダを見て発見しました。
サンプラーの作成
CD3D11_SAMPLER_DESC desc( D3D11_DEFAULT ); desc.AddressU= D3D11_TEXTURE_ADDRESS_CLAMP; desc.AddressV= D3D11_TEXTURE_ADDRESS_CLAMP; desc.AddressW= D3D11_TEXTURE_ADDRESS_CLAMP; desc.Filter= D3D11_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT; desc.ComparisonFunc= D3D11_COMPARISON_LESS; iDevice->CreateSamplerState( &desc, &iShadowSampler );
比較結果のフィルタリングなので、D3D11_FILTER_COMPARISON_~ と
COMPARISON 付きのフィルタを指定します。
CD3D11_SAMPLER_DESC に渡している D3D11_DEFAULT は、デフォルト値で構造体を
初期化することを意味しています。
クリアと RenderTarget の設定
void __fastcall ShadowBuffer::Clear( ID3D11DeviceContext* iContext ) { iContext->ClearDepthStencilView( iShadowDepthView, D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL, DVF(1), 0 ); } void __fastcall ShadowBuffer::SetRenderTarget( ID3D11DeviceContext* iContext ) { ID3D11RenderTargetView* irtv= NULL; iContext->OMSetRenderTargets( 1, &irtv, iShadowDepthView ); CD3D11_VIEWPORT view( DVF(0), DVF(0), ShadowWidth, ShadowHeight ); iContext->RSSetViewports( 1, &view ); }
ShadowBuffer にレンダリングする場合の前処理です。
OMSetRenderTargets() では DepthBuffer のみ指定します。
RenderTarget が NULL の場合書き込みが行われません。
リソースの設定
void __fastcall ShadowBuffer::SetResources( ID3D11DeviceContext* iContext ) { iContext->PSSetShaderResources( SHADOW_REGISTER_ID, 1, &iShadowView ); iContext->PSSetSamplers( SHADOW_REGISTER_ID, 1, &iShadowSampler ); }
作成した ShadowBuffer を用いて影描画する場合の前設定です。
シェーダーとのバインドを簡略化するために、ShadowBuffer 用のレジスタ番号を
決めうちにしています。
シェーダー
struct VS_INPUT { float3 vPos : POSITION; float3 vNormal : NORMAL; float2 vTex0 : TEXCOORD; #if CF_WEIGHT float4 vWeight : WEIGHT; uint4 vIndex : INDEX; #endif }; struct VS_OUTPUT { float4 vPos : SV_Position; float2 vTex0 : TEXCOORD; }; VS_OUTPUT vmain( VS_INPUT idata ) { VS_OUTPUT odata; float4x4 world; #if CF_WEIGHT world= World[idata.vIndex.x] * idata.vWeight.x; world+= World[idata.vIndex.y] * idata.vWeight.y; world+= World[idata.vIndex.z] * idata.vWeight.z; world+= World[idata.vIndex.w] * idata.vWeight.w; #else world= World[0]; #endif float4 wpos= mul( float4( idata.vPos.xyz, 1 ), world ); odata.vPos= mul( wpos, PV ); odata.vTex0= idata.vTex0; return odata; } typedef VS_OUTPUT PS_INPUT; Texture2D tex0 : register( t0 ); SamplerState sample0 : register( s0 ); float pmain( PS_INPUT idata ) : SV_Target { #if CF_ALPHA1BIT float alpha= tex0.Sample( sample0, idata.vTex0.xy ).w; if( alpha < 0.8f ){ clip( -1 ); } #endif return 0; }
ShadowBuffer を生成する場合は特別なことが何もありません。
カラーを書かないので不要に見えますが、テクスチャの Alpah 抜きに対応する場合は
テクスチャも読み込まなければならなくなります。
今回は特別なことを何もしてないので大丈夫ですが、影の合成を効率化しようと
凝ったことをし出すとテクスチャの扱いが結構負担になってきます。
パス毎にフィルタリングや Mip 指定、uv の計算等を厳密にあわせておかないと、
ちょっとしたレンダーステートの差がテクスチャの隙間を生み出してしまうからです。
例えば DepthBuffer の事前生成など。
描画用シェーダー
struct VS_INPUT { float3 vPos : POSITION; float3 vNormal : NORMAL; float2 vTex0 : TEXCOORD; #if CF_WEIGHT float4 vWeight : WEIGHT; uint4 vIndex : INDEX; #endif }; struct VS_OUTPUT { float4 vPos : SV_Position; float3 vNormal : NORMAL; float2 vTex0 : TEXCOORD; float4 vShadow : SHADOW; }; VS_OUTPUT vmain( VS_INPUT idata ) { VS_OUTPUT odata; float4x4 world; #if CF_WEIGHT world= World[idata.vIndex.x] * idata.vWeight.x; world+= World[idata.vIndex.y] * idata.vWeight.y; world+= World[idata.vIndex.z] * idata.vWeight.z; world+= World[idata.vIndex.w] * idata.vWeight.w; #else world= World[0]; #endif float4 wpos= mul( float4( idata.vPos.xyz, 1 ), world ); odata.vPos= mul( wpos, PV ); odata.vNormal.xyz= mul( idata.vNormal, (float3x3)world ); odata.vTex0= idata.vTex0; odata.vShadow= mul( wpos, ShadowMatrix ); return odata; } typedef VS_OUTPUT PS_INPUT; Texture2D tex0 : register( t0 ); SamplerState sample0 : register( s0 ); Texture2D shadowTex0 : register( t8 ); SamplerComparisonState shadowSample0 : register( s8 ); float4 pmain( PS_INPUT idata ) : SV_Target { float3 diffuse= Ambient.xyz; float3 specular= float3( 0, 0, 0 ); float4 color= float4( 0, 0, 0, 0 ); float3 sh_uv= idata.vShadow.xyz; sh_uv.xyz/= idata.vShadow.w; sh_uv.x= sh_uv.x * 0.5f + 0.5f; sh_uv.y= -sh_uv.y * 0.5f + 0.5f; sh_uv.z-= 0.000004f; // 適当 float shdepth= shadowTex0.SampleCmpLevelZero( shadowSample0, sh_uv.xy, sh_uv.z ); // float shdepth= shadow_pcf( sh_uv ); lightMask= saturate( shdepth + 0.0f ); float3 normal= normalize( idata.vNormal.xyz ); diffuse.xyz+= saturate( dot( normal, _LightDir ) ) * Diffuse.xyz * lightMask; specular.xyz+= pow( saturate( dot( normal, normalize(idata.vEyeVec + _LightDir ) )), Specular.w ) * Specular.xyz * lightMask; float4 texcol= tex0.Sample( sample0, idata.vTex0.xy ); color.xyz+= texcol.xyz * diffuse.xyz + specular.xyz; color.w= texcol.w * Ambient.w; #if CF_ALPHA1BIT if( color.w < 0.8f ){ clip( -1 ); } #endif return color; }
VertexShader では描画用の Matrix と影用の Matrix、2つの座標系へ変換します。
実際の PixelShader は非常に長いので光源演算を大幅に削りました。
影のサンプリングは SampleCmpLevelZero() を使っています。
Depth を読み込み、指定した z 値と比較した結果をフィルタリングして返すので、
対応する光源に対して影の中かどうかわかります。
影用 uv への変換は効率化するなら Matrix に埋め込んでおくことができます。
埋め込むと影を落とすときと完全に同じ Matrix にならないことが難点。
基本的な描画手順
// Shadow Buffer の作成 ShadowBuffer->Clear( iContext ); ShadowBuffer->SetRenderTarget( iContext ); iContext->OMSetDepthStencilState( iDSS_ZEnable, 0 ); // shadow レンダリング用 matrix の設定など // shadow buffer 生成のシェーダーで描画 // 影をサンプリングしながらレンダリング // 本来の RenderTarget の設定 // レンダリング用 matrix の設定 ShadowBuffer->SetResources(); // 描画用シェーダーで描画
影、不透明、半透明 といった順番になります。
DepthBuffer の事前生成を行う場合や、複数の光源の影を落とす場合は描画パスが増加していきます。
alpha 抜きを使う場合は半透明と同じように、さらに別グループに分類した方が良いかもしれません。
関連エントリ
・DirectX SDK March 2009
・Direct3D 10 DXGI_FORMAT の機能対応一覧
・D3D10/DX10 D3D10_FILTER の新機能
・D3D10/DX10 RADEON HD2900XT