Shadow map is a common means of achieving shadow effects, which can be divided into two stages: generating shadow maps and sampling shadow maps. To generate a shadow map, the first step is to prepare the resources and bound views:
D3D12_RESOURCE_DESC texDesc;
ZeroMemory(&texDesc, sizeof(D3D12_RESOURCE_DESC));
texDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
texDesc.Alignment = 0;
texDesc.Width = mWidth;
texDesc.Height = mHeight;
texDesc.DepthOrArraySize = 1;
texDesc.MipLevels = 1;
texDesc.Format = mFormat;
texDesc.SampleDesc.Count = 1;
texDesc.SampleDesc.Quality = 0;
texDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
texDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
D3D12_CLEAR_VALUE optClear;
optClear.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
optClear.DepthStencil.Depth = 1.0f;
optClear.DepthStencil.Stencil = 0;
ThrowIfFailed(md3dDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&texDesc,
D3D12_RESOURCE_STATE_GENERIC_READ,
&optClear,
IID_PPV_ARGS(&mShadowMap)));
A shadow map is essentially a depth cache of a light source space, so the way resources are created is similar to a depth buffer. Due to the use of shadow maps in both the generation and sampling processes, two views, one dsv and one srv, are required to bind them:
D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc;
dsvDesc.Flags = D3D12_DSV_FLAG_NONE;
dsvDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
dsvDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
dsvDesc.Texture2D.MipSlice = 0;
md3dDevice->CreateDepthStencilView(mShadowMap.Get(), &dsvDesc, mhCpuDsv);
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.Format = DXGI_FORMAT_R24_UNORM_X8_TYPELESS;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MostDetailedMip = 0;
srvDesc.Texture2D.MipLevels = 1;
srvDesc.Texture2D.ResourceMinLODClamp = 0.0f;
srvDesc.Texture2D.PlaneSlice = 0;
md3dDevice->CreateShaderResourceView(mShadowMap.Get(), &srvDesc, mhCpuSrv);
In the process of drawing a shadow map, we only need to record the pixel depth and do not actually need to write pixels. Here, we can modify the pixel shader to not output color:
void PS(VertexOut pin)
{
...
}
Correspondingly, it is also necessary to add a pipeline state object to control the drawing state for the shadow map:
D3D12_GRAPHICS_PIPELINE_STATE_DESC smapPsoDesc = opaquePsoDesc;
smapPsoDesc.RasterizerState.DepthBias = 100000;
smapPsoDesc.RasterizerState.DepthBiasClamp = 0.0f;
smapPsoDesc.RasterizerState.SlopeScaledDepthBias = 1.0f;
smapPsoDesc.pRootSignature = mRootSignature.Get();
smapPsoDesc.VS =
{
reinterpret_cast<BYTE*>(vs->GetBufferPointer()),
vs->GetBufferSize()
};
smapPsoDesc.PS =
{
reinterpret_cast<BYTE*>(ps->GetBufferPointer()),
ps->GetBufferSize()
};
smapPsoDesc.RTVFormats[0] = DXGI_FORMAT_UNKNOWN;
smapPsoDesc.NumRenderTargets = 0;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&smapPsoDesc, IID_PPV_ARGS(&mShadowPso)));
Note that NumRenderTargets is set to 0 here.
Before proceeding with the formal drawing, we need to transfer the information required by the shader from the CPU. Drawing a shadow map requires transforming all objects in the scene into the light source space, so we need the camera matrix and projection matrix in the light source space:
XMStoreFloat4x4(&mShadowPassCB.View, XMMatrixTranspose(view));
XMStoreFloat4x4(&mShadowPassCB.InvView, XMMatrixTranspose(invView));
XMStoreFloat4x4(&mShadowPassCB.Proj, XMMatrixTranspose(proj));
XMStoreFloat4x4(&mShadowPassCB.InvProj, XMMatrixTranspose(invProj));
XMStoreFloat4x4(&mShadowPassCB.ViewProj, XMMatrixTranspose(viewProj));
XMStoreFloat4x4(&mShadowPassCB.InvViewProj, XMMatrixTranspose(invViewProj));
currPassCB->CopyData(1, mShadowPassCB);
When drawing, use the const buffer, pso, and dsv corresponding to the shadow map:
mCommandList->RSSetViewports(1, &mShadowMap->Viewport());
mCommandList->RSSetScissorRects(1, &mShadowMap->ScissorRect());
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(mShadowMap->Resource(),
D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_RESOURCE_STATE_DEPTH_WRITE));
mCommandList->ClearDepthStencilView(mShadowMap->Dsv(),
D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
mCommandList->OMSetRenderTargets(0, nullptr, false, &mShadowMap->Dsv());
mCommandList->SetGraphicsRootConstantBufferView(1, passCBAddress);
mCommandList->SetPipelineState(mShadowPso);
DrawScene();
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(mShadowMap->Resource(),
D3D12_RESOURCE_STATE_DEPTH_WRITE, D3D12_RESOURCE_STATE_GENERIC_READ));
After obtaining the shadow map, the next stage is to sample it. The sampling stage is relatively simple, just pass the transformation matrix of the light source space to the shader, and then set the corresponding SRV for the shadow map:
XMStoreFloat4x4(&mMainPassCB.ShadowTransform, XMMatrixTranspose(shadowTransform));
mCommandList->SetGraphicsRootDescriptorTable(3, mShadowSrv);
Then, the shadow map can be sampled in the shader, and the sampled values can be compared with the depth values to determine whether the pixel is in the shadow. Many modern hardware already natively support sampling and comparison operations, which can be achieved through the SampleCmpLevelZero
API. Correspondingly, a sampler needs to be passed in outside the CPU layer:
const CD3DX12_STATIC_SAMPLER_DESC shadow(
1, // shaderRegister
D3D12_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT, // filter
D3D12_TEXTURE_ADDRESS_MODE_BORDER, // addressU
D3D12_TEXTURE_ADDRESS_MODE_BORDER, // addressV
D3D12_TEXTURE_ADDRESS_MODE_BORDER, // addressW
0.0f, // mipLODBias
16, // maxAnisotropy
D3D12_COMPARISON_FUNC_LESS_EQUAL,
D3D12_STATIC_BORDER_COLOR_OPAQUE_BLACK);
The final achieved effect is as follows:
.