在DirectX 12中使用shadow map
shadow map是常见的实现阴影效果的手段,可以分为生成shadow map和采样shadow map两个阶段。生成shadow map首先需要准备好资源,和绑定的view:
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)));
shadow map本质上是一张光源空间的深度缓存,因此创建资源的方式类似depth buffer。由于shadow map在生成过程和采样过程都要用到,这里需要两个view,一个dsv,一个srv对其进行绑定:
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);
在绘制shadow map的过程中,我们只需记录像素深度,并不需要实际进行像素写入,这里可以修改pixel shader,使其不输出color:
void PS(VertexOut pin)
{
...
}
相应地,也需要为shadow map添加控制绘制状态的pipeline state object:
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)));
注意这里NumRenderTargets设置为0。
在进行正式绘制之前,我们要把shader所需要的信息从CPU中传递过来。绘制shadow map需要将场景中的所有物体变换到光源空间,因此我们需要光源空间下的相机矩阵和投影矩阵:
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);
绘制时,要使用shadow map对应的const buffer,pso和dsv:
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));
得到shadow map之后,下一阶段就是对其采样。采样阶段相对简单,只需将光源空间的变换矩阵传给shader,然后设置好shadow map对应的srv:
XMStoreFloat4x4(&mMainPassCB.ShadowTransform, XMMatrixTranspose(shadowTransform));
mCommandList->SetGraphicsRootDescriptorTable(3, mShadowSrv);
然后就可以在shader中对shadow map进行采样,将采样值与深度值进行比较得到pixel是否在阴影中。很多现代硬件中已经原生支持采样比较操作,通过SampleCmpLevelZero
这个API即可。相应地,要在CPU层额外传入一个sampler:
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);
最后实现的效果如下:
如果你觉得我的文章有帮助,欢迎关注我的微信公众号(大龄社畜的游戏开发之路)