日別アーカイブ: 2009年5月17日

Direct3D11 影の設定 ShadowMap

久しぶりに影を扱ったのでメモです。

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