Implementing shadow buffers for Direct3D feature level 9 (DirectX and C++)
[ This article is for Windows 8.x and Windows Phone 8.x developers writing Windows Runtime apps. If you’re developing for Windows 10, see the latest documentation ]
To develop a Windows Runtime app using DirectX with C++ that renders shadows effects and that uses a Direct3D feature level of 9_1, 9_2, or 9_3, you must use a specific subset of Direct3D 10_0+ shadow buffer features to correctly implement shadow effects. Here we talk about implementing shadow buffers for Direct3D feature level 9.
What's a shadow buffer?
Shadows are an important element in creating a realistic 3-D scene and for portraying a convincing illusion of depth. The shadow buffer technique is a common approach for rendering shadows using graphics hardware; however, it's also relatively computationally expensive. Shadow buffers are implemented by using a depth buffer to store the scene depth information from the perspective of the light source, and then comparing each point rendered in the scene with the depth buffer to determine if the point is in shadow.
Implementing shadows on Direct3D feature level 9_1, 9_2, or 9_3
When your app starts, call ID3D11Device::CheckFeatureSupport by using the D3D11_FEATURE_D3D9_SHADOW_SUPPORT Direct3D feature enumeration value to determine if the installed graphics driver supports shadow buffers. If the data (the corresponding D3D11_FEATURE_DATA_D3D9_SHADOW_SUPPORT structure) received by the pFeatureSupportData parameter of CheckFeatureSupport has the SupportsDepthAsTextureWithLessEqualComparisonFilter field set to false, shadow buffers won't work in your Windows Runtime app using DirectX when running on the user's computer. To fix this, users must upgrade their driver.
D3D11_FEATURE_DATA_D3D9_SHADOW_SUPPORT d3d9ShadowSupportResults;
ZeroMemory(&d3d9ShadowSupportResults, sizeof(D3D11_FEATURE_DATA_D3D9_SHADOW_SUPPORT));
m_device->CheckFeatureSupport(
D3D11_FEATURE_D3D9_SHADOW_SUPPORT,
&d3d9ShadowSupportResults,
sizeof(D3D11_FEATURE_DATA_D3D9_SHADOW_SUPPORT)
);
This technique works with either 16-bit or 24-bit depth buffers, so both are discussed in the remarks section. Be aware that some driver implementations might only support 16-bit depth buffers.
Note For more info, read Common techniques to improve shadow depth maps.
Create a Texture2D surface resource (using ID3D11Device::CreateTexture2D) with the Format field of D3D11_TEXTURE2D_DESC set to DXGI_FORMAT_R24G8_TYPELESS if 24-bit depth buffers are supported, or DXGI_FORMAT_R16_TYPELESS if they aren't. Specify the BIND_SHADER_RESOURCE flag and the BIND_DEPTH_STENCIL flag values in the BindFlags field as well. Doing so allows the surface to be used either as a depth buffer or as a texture on separate rendering passes.
Note Don't rely on CheckFormatSupport for discovering if the graphics driver supports these specific formats. If you use the Direct3D 11 CheckFeatureSupport and CheckFormatSupport APIs to query DXGI_FORMAT support for feature level 9_* , the DirectX runtime won't report support for any shadow buffer features.
Using a depth format such as DXGI_FORMAT_D24_UNORM_S8_UINT for 24-bit shadow buffer depth, or DXGI_FORMAT_D16_UNORM for 16-bit shadow buffer depth, create a ID3D11DepthStencilView (using ID3D11Device::CreateDepthStencilView) onto the Texture2D resource allocated in step 1.
Load the shadow buffer with shadow information. First, set up a temporary camera at the position of the light source and aimed in the light source direction. Then, render all the objects in the scene that can cast shadows onto themselves or onto other objects. For this pass, don't set a pixel shader, and only bind the ID3D11DepthStencilView for output.
To render objects into the scene with shadows, create sampler state objects that have comparison filtering set. The comparison mode must be set to LessEqual for feature level 9_* devices. BorderColor addressing is also allowed on this depth sampler, even though BorderColor isn't normally supported on Direct3D feature level 9_1 and 9_2 by most graphics drivers. Using the border color, it's possible to control whether the regions outside shadow map appear to be always in shadow or never in shadow by setting the BorderColor value to 0.0 or 1.0, respectively.
Shadow filter quality is controlled by the Mag and Min filter options in the comparison sampler. Point sampling produces shadows that have aliased edges. Linear filter sampler settings result in higher quality shadow edges, but might affect performance on some power-optimized devices.
Note A few points to be aware of. First, using a separate setting for Mag and Min filter options produce an undefined result. Also, anisotropic filtering is not supported. Lastly, Direct3D feature level 9_* doesn't support MIP-mapped depth buffers.
Create a ID3D11ShaderResourceView (using ID3D11Device::CreateShaderResourceView) for the Texture2D resource allocated in step 1, and bind it with the comparison sampler described previously. As in step 1, you must set the pixel format for this view to DXGI_FORMAT_R24_UNORM_G8_TYPELESS for 24-bit depth, or DXGI_FORMAT_R16_UNORM for 16-bit depth.
Create and set a ps_4_0_level_9_* pixel shader for the shadow application pass. It should compute the position of each pixel relative to the light source position, and then compare the z value of the pixel with the value in the shadow buffer to determine whether that pixel is in shadow. You accomplish this by writing the shader code for the following (with the hardware doing the comparison step):
- First, the texture coordinates and z value are obtained by projecting current position into the same world, view, and projection space used to render the shadow, and then remapping the x-y coordinates from [-1..1] space to [0..1] space for sampling the shadow map.
- Next, the texture coordinates and z value are obtained by projecting current position into the same world, view, and projection space used to render the shadow, and then remapping the x-y coordinates from [-1..1] space to [0..1] space for sampling the shadow map.
- And then, to find out the amount of shadow for the current pixel, use the SampleCmp and SampleCmpLevelZero Texture2D methods to sample from the ID3D11ShaderResourceView created in step 5, just as you would with Direct3D feature level 10_0 and higher. The result is the fraction of the samples not in shadow for linear filtering, specified as values in the range [0..1]. For point sampling, a value of either 0.0 or 1.0 is returned.
Note On Direct3D feature levels 9_1, 9_2, and 9_3, attempts to compile a shader with these SampleCmp* intrinsics using an older HLSL compiler (such as the Windows 7 version of the ID3D11Device::CreatePixelShader method) will fail. The SampleCmp* intrinsics are only supported in the fxc compiler and the associated DLL that ships starting with Windows 8. These HLSL intrinsics are also present in shader models for Direct3D feature levels higher than 9.
Render the objects in the scene that will have shadows cast onto them.
Remember to clean up API state after rendering with shadows. On a device that has a configured feature level of 9_1, mismatched API state results in the ID3D11DeviceContext::Draw call being skipped. For example, if your renderer binds a texture (depth or otherwise) while the corresponding sampler remains configured for comparison sampling, the ID3D11DeviceContext::Draw is skipped. If this happens, you have set the API state incorrectly.
Implementing shadows on Direct3D feature level 10_0 or higher
The previous steps for shadow mapping on feature level 9 devices works just as well on feature level 10 or higher devices, and with no code change required. However, higher feature levels support a broader range of options, including advanced comparison modes beyond, and additional depth surface formats.