D3DBook:Shadow Maps

From GDWiki
Jump to: navigation, search

Shadow mapping

Image:Pvgps3_7_lamppost_smooth.png

An another approach to shadowing, bitmapped shadow (or shadow map) techniques attempt to emulate the light occlusion by using a discrete grid of “photons”, in the form of texels of a depth texture that is projected from the light source to the occlusion geometry and to the scene. This is in contrast to the volumetric shadow technique which explicitly processes the geometry of the shadow casters, in that bitmapped shadow techniques are very much independent from the geometric complexity of the said shadow casters.

As compared to volumetric shadows, they can consume far less gpu resources as it isn’t nearly always needed to match the shadow map resolution to the full viewport resolution, thus saving fill rate and memory bandwidth at the cost of the video memory for the shadow maps themselves. Shadow maps will also work more readily with various types of filtering, because as at heart they are textures, shadow data adjacent to currently evaluated "photon" is always available.

Theory of depth map shadows

Bitmapped shadows generally work by first projecting the depth of the scene to a texture from the light source’s perspective. Then, during the main rasterization of the scene, the texture is projected from the lamp space to the camera space and its depth value is compared to the current pixel’s depth value.

The following image illustrates the depth of the scene – on the left, from camera space and on right, from the light’s space:

Image:Pvgps3_8_cam_light_depth_compare.png

The scene needs to be projected into the same space in which the light source’s depth texture was rendered in order for the depth values to be comparable. This can be accomplished by transforming the scene’s geometry into the light space in a special vertex shader that, in addition to the normal duties of a vertex shader, calculates the light-space position of each vertex and outputs it into the pixel shader for the shadow testing phase.

The shadow test, in its most basic form, draws the scene as follows: The shadow depth texture rendered earlier is projected to the scene. Then, the scene is drawn in an ordinary manner. During this drawing, a pixel shader does the depth check: if the current pixel’s depth in the light space is greater than the shadow depth map’s projected depth value, the current pixel is in shadow as it is behind the shadow caster. This knowledge can be used to modulate the color of the pixel being drawn, so as to simulate the occlusion of the photons from the light source. As the depth values are compared, a depth bias value can be used to reduce the artifacts that result from the inevitable inaccuracy in depth buffer values. Without it, the shadows could reach to faces that they don’t belong to, or the shadows might not show on surfaces that are supposed to be shadowed.

A filter can be applied to the depth testing procedure to smooth the transition from shadowed to non-shadowed pixels and reduce the staircase effect that is inherent in bitmap-based techniques. Another way of increasing the fidelity of bitmapped shadows is to simply make the texture rendered from the light space depth larger in dimensions.

In the following image, the shadow on the left is rendered without filtering, and the one on the right using 4-tap percentage-close filtering: Image:Pvgps3_9_filt_nofilt_comp.png

This code listing shows how to render the scene depth from the light’s perspective:

cbuffer cb1
{
//ordinary wvp transformation:
matrix worldViewProj;
//light-space equivalent:
matrix lightWVP;

//other buffer members omitted for clarity.
}

//shadow pixel shader input structure:
struct SHADOWPS_INPUT
{
    float4 Pos : SV_POSITION; //ordinary position
    float3 Pos2D : TEXCOORD0; //Unclamped position
};

// Bitmapped shadow vertex shader. 
SHADOWPS_INPUT ShadowVS( VS_INPUT input )
{
    SHADOWPS_INPUT output = (SHADOWPS_INPUT)0;

	//Here, we apply a geometric bias so as
// to reduce self-shadowing:
    input.Pos += input.Norm * 0.1; 

    output.Pos = mul( float4(input.Pos*100,1), ShadowWVP);

	//In order to preserve the full range of coordinates,
// we copy the position to a texture coordinate too.
	output.Pos2D = output.Pos.xyz;
	//we don't need other components at the shadow drawing.
// Depth is relayed to the pixel shader already.

    return output;
}


//a pixel shader for the bitmap shadow generation. 
float4 ShadowPS( SHADOWPS_INPUT input) : SV_Target
{
	float4 cDiffuse = (float4)0;
	cDiffuse.rgba = input.Pos2D.z / maxDepth;
    return cDiffuse;
}

 Here’s how to test the projected scene depth against the results of the shadow rendering:

struct VS_INPUT
{
    float3 Pos          : POSITION;         
    float3 Norm         : NORMAL;           
    float2 Tex          : TEXCOORD0;        
};


struct ORDPS_INPUT
{
float4 Pos : SV_POSITION; //ordinary position
float4 ShadowSamplePos : TEXCOORD0; //light-space position
float3 Norm : TEXCOORD1; // Normal for per-pixel light calculation
float RealDist : TEXCOORD2; //Real distance from the eye to the scene
float3 Pos3D : TEXCOORD3; //Position in object space
};

// This is the depth map shadow test vertex shader.
// In addition to usual vertex shader tasks, it
// outputs a light-space position too so that the
// pixel shader can compare the scene's depth values
// with the previously rendered depth map. 
ORDPS_INPUT VS( VS_INPUT input )
{
    ORDPS_INPUT output = (ORDPS_INPUT)0;
    
    output.Pos = mul( float4(input.Pos*100,1), World);
	output.Pos = mul( output.Pos, View);
	output.Pos = mul( output.Pos, Projection);

	//We calculate the shadow-space vertex position by 
//transforming the input with the shadow matrix. 
	output.ShadowSamplePos = mul( float4(input.Pos*100,1), ShadowWVP);

	//we need to accommodate the max depth here if we 
//are using fixed function depth maps.
	output.RealDist = output.ShadowSamplePos.z / maxDepth;
	output.Pos3D = input.Pos;

    output.Norm = mul( input.Norm, (float3x3)World );

    return output;
}


// This is the depth test pixel shader. It determines
// whether the current pixel is in shadow or not,
// and outputs color accordingly.
float4 PS( ORDPS_INPUT input) : SV_Target
{
    // Calculate lighting assuming light color is <1,1,1,1>
    float fLighting = saturate( dot( input.Norm, vLightDir ) );
    float4 cDiffuse = (float4)fLighting;
    cDiffuse.a = 1;
	
	//We'll continue a little bit with the shadow mapping. 
	
float2 projTexCoord;

//Project the shadow-space position to screen xy by dividing the values with w.
projTexCoord.x = input.ShadowSamplePos.x / input.ShadowSamplePos.w;
projTexCoord.y = -input.ShadowSamplePos.y / input.ShadowSamplePos.w;

//Then bias the coordinates to texture space so that the projected range transforms from -1...1 to 0...1.
projTexCoord += 1.0f;
projTexCoord *= 0.5f;

//Sample the shadow-space depth from the scene rendered
// in shadow space:
float currentDepth = g_txShadowDepth.Sample(samLinearClamp, projTexCoord);

//We apply a little bias to the depth values so that our geometry
// doesn't, for the most part, shadow itself.
if (!((input.RealDist -1.0f/20.0f) <= currentDepth))
{
	cDiffuse.rgb *= 0.2f;
}

// NOTE: The texture object also has a method "SampleCmp" which
// can take in a comparison operation,
// perform depth comparing given reference and return a value
// between 0 and 1 depending on the portion of the four PCF
// (percentage closer filtering) sub-samples that pass the
// comparison. 

// SampleCmp uses hardware coverage filtering so it results in
// better quality shadow edges with essentially no extra fillrate
// cost.

// Please check the recent DirectX SDK documentation for up-to- 
// date info about this feature.

// The SampleCmp method is very sensitive wrt. the format of the
// depth buffer (internal format must be untyped) and the
// the choice of view formats is very restricted.

    return cDiffuse;
}


Cubic shadow maps

As the depth from the light is projected with a perspective matrix, it is unsuitable for point light sources that cast light and shadows in all directions; instead, it is best used with a spotlight or a directional light source as the direction of the light (and therefore of the shadow) from the source is somewhat uniform and possible to accurately map to a 2D texture.

Fortunately, we can simulate shadows from a point light by using 6 different spotlight-like shadow buffers for the light, arranged in a cube fashion around the light source.

Direct3D 8 and 9 provide cube maps, a special type of texture with 6 separate faces for up, down, back, forward, left and right directions for this kind of purposes. However, with Direct3D 8 and 9, each face of the cube has to be rendered separately, each with own matrices and draw calls. As draw calls and state changes cause bottlenecks in the Direct3D system due to the physical time it takes to make those calls as well as causing the graphics system to stall due to over-serialization of the commands, it is beneficial if we can minimize the number of these calls when rendering anything.

Efficient drawing of cubic shadow maps in Direct3D 10

Direct3D 10 introduces the concept of resource views; that is, video memory can be mapped to almost arbitrary representation. In the case of a cube map, the data for the cube faces can be represented to the shaders as an array of 2D textures and be indexed in that fashion in the geometry shader, to be used again in a cube map form for depth testing against the scene after the rendering of the depth values from the light’s point of view.

In the geometry shader, we can direct our primitives to a given render target by using an integer component having the system-defined semantic SV_RenderTargetArrayIndex on our output vertex structure. As we process each triangle, we copy it to all the render targets in the array by iterating through the render targets array and using the index of each render target to fetch the correct transformation matrix - for each given render target direction as mapped to a cube – from the effect parameters.

The following example code shows how to create a view for a 6-face render target array:

D3D10_RENDER_TARGET_VIEW_DESC DescRT;
DescRT.Format = cubeDepthTexture.Format;
DescRT.ViewDimension = D3D10_RTV_DIMENSION_TEXTURE2DARRAY;
DescRT.Texture2DArray.FirstArraySlice = 0;
DescRT.Texture2DArray.ArraySize = 6;
DescRT.Texture2DArray.MipSlice = 0;
pd3dDevice->CreateRenderTargetView( cubeDepthRes, &DescRT, &pCubeDepthRTV);

We can then use the rendered cube texture in the shadow comparison function by projecting it to the scene, much like how we processed the single spotlight shadow.

There are some things to keep in mind though: As of Direct3D 10, you can only set one depth stencil surface to the device at any given time. This means that you need to store the depth values in the color data of the render targets. Fortunately, D3D10 pixel shaders let us to handle arbitrary values (including colors) as depth via views to typeless resources, so this isn’t a problem.

Here’s a basic geometry shader which uses the render target index semantic to direct output:

[maxvertexcount(18)]
void GS_CubeMap( triangle GS_CUBEMAP_IN input[3], inout TriangleStream<PS_CUBEMAP_IN> CubeMapStream )
{
//iterate all 6 directions of the cube map:
    for( int f = 0; f < 6; ++f )
    {
        // Compute screen coordinates
        PS_CUBEMAP_IN output;
//output.RTIndex is an integer declared with the 
//render target index semantic.
        output.RTIndex = f;
//output a whole triangle for each cube face. The rasterizer
//will determine if they actually get drawn or not:
        for( int v = 0; v < 3; v++ )
        {
//lightViewMtxs is an array of matrices that determine the light
//positions and orientations for each of the main directions:
            output.Pos = mul( input[v].Pos, lightViewMtxs[f] );
//mProj is a projection matrix that contains a 90-degree
//perspective. 90 * 4 = 360 degrees so all directions around
//the cube are considered.
            output.Pos = mul( output.Pos, mProj );
            output.Tex = input[v].Tex;
            CubeMapStream.Append( output );
        }
        CubeMapStream.RestartStrip();
    }
}

Note that you need to declare a pixel shader input with the render target index semantic too, so that the index goes all the way to the rasterizer.

The rest of the shadow map rendering procedure is the same as in single-target shadow map rendering. In the depth compare pixel shader, you need to loop thru all the faces of the cube map and handle the texture colors as depths, but otherwise this part is exactly the same as in the single shadow map technique too. This image shows a simple test scene for the technique:

Image:Pvgps_3_10_cubicshadowmap.png

Personal tools