在DirectX 12中使用SSAO
SSAO是常用的实现环境光遮蔽,提高物体真实性的一种技术,它可以分为生成AO贴图和使用AO贴图两个阶段。在生成阶段,我们需要一张view space下的normal texture,和一张depth texture作为输入。depth texture是已有的,这里意味着需要新增normal texture资源:
D3D12_RESOURCE_DESC texDesc;
ZeroMemory(&texDesc, sizeof(D3D12_RESOURCE_DESC));
texDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
texDesc.Alignment = 0;
texDesc.Width = mRenderTargetWidth;
texDesc.Height = mRenderTargetHeight;
texDesc.DepthOrArraySize = 1;
texDesc.MipLevels = 1;
texDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
texDesc.SampleDesc.Count = 1;
texDesc.SampleDesc.Quality = 0;
texDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
texDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET;
float normalClearColor[] = { 0.0f, 0.0f, 1.0f, 0.0f };
CD3DX12_CLEAR_VALUE optClear(DXGI_FORMAT_R16G16B16A16_FLOAT, normalClearColor);
ThrowIfFailed(md3dDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&texDesc,
D3D12_RESOURCE_STATE_GENERIC_READ,
&optClear,
IID_PPV_ARGS(&mNormalMap)));
这个normal texture有两个用处,首先它要作为render target,保存view space下的normal;其次它要作为绘制AO贴图的输入,因此它需要两个绑定的view:
D3D12_RENDER_TARGET_VIEW_DESC rtvDesc = {};
rtvDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D;
rtvDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
rtvDesc.Texture2D.MipSlice = 0;
rtvDesc.Texture2D.PlaneSlice = 0;
md3dDevice->CreateRenderTargetView(mNormalMap.Get(), &rtvDesc, mhNormalMapCpuRtv);
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
srvDesc.Texture2D.MostDetailedMip = 0;
srvDesc.Texture2D.MipLevels = 1;
md3dDevice->CreateShaderResourceView(mNormalMap.Get(), &srvDesc, mhNormalMapCpuSrv);
在绘制normal之前,还需要准备相应的pipeline state object:
D3D12_GRAPHICS_PIPELINE_STATE_DESC drawNormalsPsoDesc = basePsoDesc;
drawNormalsPsoDesc.VS =
{
reinterpret_cast<BYTE*>(vs->GetBufferPointer()),
vs->GetBufferSize()
};
drawNormalsPsoDesc.PS =
{
reinterpret_cast<BYTE*>(ps->GetBufferPointer()),
ps->GetBufferSize()
};
drawNormalsPsoDesc.RTVFormats[0] = mNormalMapFormat;
drawNormalsPsoDesc.SampleDesc.Count = 1;
drawNormalsPsoDesc.SampleDesc.Quality = 0;
drawNormalsPsoDesc.DSVFormat = mDepthStencilFormat;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&drawNormalsPsoDesc, IID_PPV_ARGS(&mDrawNormalsPso)));
有了normal texture之后,就可以着手绘制AO贴图了。类似地,我们需要新增资源,同时该资源需要作为AO pass的输出和正常场景pass的输入,因此也需要两个绑定的view:
texDesc.Width = mRenderTargetWidth;
texDesc.Height = mRenderTargetHeight;
texDesc.Format = DXGI_FORMAT_R16_UNORM;
float ambientClearColor[] = { 1.0f, 1.0f, 1.0f, 1.0f };
optClear = CD3DX12_CLEAR_VALUE(DXGI_FORMAT_R16_UNORM, ambientClearColor);
ThrowIfFailed(md3dDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&texDesc,
D3D12_RESOURCE_STATE_GENERIC_READ,
&optClear,
IID_PPV_ARGS(&mAmbientMap)));
md3dDevice->CreateShaderResourceView(mAmbientMap.Get(), &srvDesc, mhAmbientMapCpuSrv);
md3dDevice->CreateRenderTargetView(mAmbientMap.Get(), &rtvDesc, mhAmbientMapCpuRtv);
类似地,在绘制AO之前,还需要准备相应的pipeline state object。因为上一步骤已经有了depth buffer了,所以这里不需要进行深度写入:
D3D12_GRAPHICS_PIPELINE_STATE_DESC ssaoPsoDesc = basePsoDesc;
ssaoPsoDesc.InputLayout = { nullptr, 0 };
ssaoPsoDesc.pRootSignature = mSsaoRootSignature.Get();
ssaoPsoDesc.VS =
{
reinterpret_cast<BYTE*>(vs->GetBufferPointer()),
vs->GetBufferSize()
};
ssaoPsoDesc.PS =
{
reinterpret_cast<BYTE*>(ps->GetBufferPointer()),
ps->GetBufferSize()
};
ssaoPsoDesc.DepthStencilState.DepthEnable = false;
ssaoPsoDesc.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;
ssaoPsoDesc.RTVFormats[0] = ambientMapFormat;
ssaoPsoDesc.SampleDesc.Count = 1;
ssaoPsoDesc.SampleDesc.Quality = 0;
ssaoPsoDesc.DSVFormat = DXGI_FORMAT_UNKNOWN;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&ssaoPsoDesc, IID_PPV_ARGS(&mSsaoPso)));
SSAO的原理主要是根据采样点的坐标,随机采样若干个一定距离范围内的点,然后逐一判断每个点和采样点的遮挡关系,最后计算出一个遮挡的权重值。在shader代码中可以使用类似如下的方式判断两个点的遮挡关系:
float4 PS(VertexOut pin) : SV_Target
{
...
// Test whether r occludes p.
float distZ = p.z - r.z;
float dp = max(dot(n, normalize(r - p)), 0.0f);
float occlusion = dp*OcclusionFunction(distZ);
...
}
float OcclusionFunction(float distZ)
{
float occlusion = 0.0f;
if(distZ > gSurfaceEpsilon)
{
float fadeLength = gOcclusionFadeEnd - gOcclusionFadeStart;
occlusion = saturate( (gOcclusionFadeEnd-distZ)/fadeLength );
}
return occlusion;
}
由于它实际上是一个screen space的pass,因此绘制时并不需要传入vertex buffer和index buffer:
cmdList->IASetVertexBuffers(0, 0, nullptr);
cmdList->IASetIndexBuffer(nullptr);
cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
cmdList->DrawInstanced(6, 1, 0, 0);
得到AO贴图之后,就可以进入正式的绘制物体阶段,根据pixel的坐标去采样AO贴图中的值即可。
如果你觉得我的文章有帮助,欢迎关注我的微信公众号(大龄社畜的游戏开发之路)