HLSLで影の描画 -シャドウマップ-

シェーダをさらに使ってみよう、ということで影を描画してみます。

影を描画する方法はいろいろありますが、今回はシャドウマップを使います。

参考サイト

シャドウマップの実装方法は上記の「マルペケつくろーどっとコム」さんで詳しく解説されているので、「シャドウマップってなんぞ?」という方は最初に見ておくと良いかと。

手順

  • 1.レンダーターゲットの作成
  • 2.光源から見たシーンのZ値をテクスチャに描き込む
  • 3.カメラから見た実際のシーンを、1のテクスチャを参照して影かどうか判定しながら描画

1.レンダーターゲットの作成

  • 3Dの描画を行うのでZバッファも作成。
  • width、heightで影のクオリティが変わる。大きい方がキレイ。
  • DepthFormatは適当なものを。
Texture texture = new Texture(device,
    width, height, 1,
    Usage.RenderTarget, Format.A8R8G8B8, Pool.Default);
Surface zbuffer = device.CreateDepthStencilSurface(
    width, height, DepthFormat.D24S8, MultiSampleType.None, 0, false);
Surface surface = texture.GetSurfaceLevel(0);

/*...*/


if (surface != null)
{
    surface.Dispose();
    surface = null;
}
if (texture != null)
{
    texture.Dispose();
    texture = null;
}
if (zbuffer != null)
{
    zbuffer.Dispose();
    zbuffer = null;
}

2.光源から見たシーンのZ値をテクスチャに描き込む


  • 座標変換後の頂点座標をそのままテクスチャ座標としてピクセルシェーダに渡す
  • ピクセルシェーダでテクスチャ座標のzをwで割って、グレースケールで描画
シェーダ
/*...*/

//--------------------------------------------------------------------
//頂点シェーダ
//--------------------------------------------------------------------
struct VertexOutput
{
    float4 pos : POSITION;
    float4 shadowmap : TEXCOORD;
};

VertexOutput VS( float4 pos : POSITION )
{
    VertexOutput output;
    float4 p = mul( pos, M_world );
    p = mul( p, M_lightview_proj );

    output.pos = p;
    //座標変換後の頂点座標をそのままテクスチャ座標としてピクセルシェーダに渡す
    output.shadowmap = p;
	
    return output;
}

//--------------------------------------------------------------------
//ピクセルシェーダ
//--------------------------------------------------------------------
float4 PS( float4 shadowmap : TEXCOORD ) : COLOR
{
    //0〜1の深度を黒〜白で描画
    return shadowmap.z / shadowmap.w;
}
プログラム
Viewport viewport = new Viewport();
viewport.X = 0;
viewport.Y = 0;
viewport.Width = width;
viewport.Height = height;
viewport.MinZ = 0.0f;
viewport.MaxZ = 1.0f;

//レンダーターゲットを切り替える
device.SetRenderTarget(0, surface);
device.DepthStencilSurface = zbuffer;
device.Viewport = viewport;

//白で塗りつぶす
device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.White, 1.0f, 0);

シーンの描画;

//レンダーターゲットを元に戻す
;

3.カメラから見た実際のシーンを、1のテクスチャを参照して影かどうか判定しながら描画

シェーダ
/*...*/

//--------------------------------------------------------------------
//頂点シェーダ
//--------------------------------------------------------------------
struct VertexOutput
{
    float4 pos : POSITION;
    float4 color : COLOR0;
    float4 shadowmap : TEXCOORD;
};

VertexOutput VS( float4 pos : POSITION, float3 normal : NORMAL )
{
    VertexOutput output = ( VertexOutput )0;
    float4 p = mul( pos, M_world );
    output.pos = mul( p, M_view_proj );

    //lambert
    float3 n = mul( normal, M_world );
    n = normalize( n );
    float k = max( dot( light_dir.xyz, n ), 0.0f );
    float4 color = k * light_diffuse * material_diffuse;
    color += light_ambient * material_ambient;
    output.color = color;

    p = mul( pos, M_world );
    p = mul( p, M_lightview_proj );
    output.shadowmap = p;

    return output;
}

//--------------------------------------------------------------------
//ピクセルシェーダ
//--------------------------------------------------------------------
sampler shadow_sampler = sampler_state
{
	Texture = <shadow_texture>;
	MinFilter = LINEAR;
	MagFilter = LINEAR;
	MipFilter = LINEAR;
	AddressU = Clamp;
	AddressV = Clamp;
	AddressW = Clamp;
};

float4 PS( VertexOutput input ) : COLOR
{
    float w = 1.0f / input.depthmap.w;

    //シャドウマップのZ値を参照
    float2 tex;
    tex.x = ( 1.0f + input.shadowmap.x * w ) * 0.5f;
    tex.y = ( 1.0f - input.shadowmap.y * w ) * 0.5f;
    float z = tex2D( shadow_sampler, tex ).x;

    float color = input.color;
    if( input.shadowmap.z * w > z + 0.005f )// + バイアス
        color = color * 0.5f;//暗くする

    return color;
}

問題点

ライトが当たっていない部分、面の法線とライト方向のベクトルが90度に近い部分が汚くなってしまいます。バイアス、Zバッファの解像度だけでは対応できないようです。


次回は、全体のプログラムをまとめつつ、上記の問題点を解決しようと思います。