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);
最后实现的效果如下:
如果你觉得我的文章有帮助,欢迎关注我的微信公众号(大龄社畜的游戏开发之路)