游戏中的阴影将告诉玩家光源的位置并且基于阴影玩家能更好地判断物体在场景中所处的位置。本章节将会告诉大家基本的阴影映射算法,其被广泛应用于模拟场景中的动态阴影。而本书主要作为DX12的参考书,所以作者只介绍了一些基础的阴影算法;如果各位老铁想要了解更为高级的算法,可以去美亚上买一些其他主讲渲染的书籍。
目标:
1. 了解基础的阴影映射算法。
2. 学习投影贴图的工作原理。
3. 学会正交投影。
4. 了解阴影映射中的锯齿问题以及相对应的解决办法。
20.1 绘制场景深度
阴影映射算法主要依赖于从光源视角绘制的场景深度——而这是另一种绘制到贴图(render-to-texture,在第十三章作者也有提到)。这里的“绘制场景深度”意味着我们需要在光源的视角去构建depth buffer。因此,在我们以光源所在位置作为视角绘制完场景之后,我们将知道距离光源最近的像素——显然这些像素不在阴影中。本小节,我们将熟悉类ShadowMap,其将帮助我们以光源的角度在存储场景的深度值。该类封装了一个depth/stencil buffer,一些必要的view/descriptor以及视口。而阴影映射所使用的depth/stencil buffer被称为阴影映射贴图(shadow map)。
class ShadowMap { public: ShadowMap(ID3D12Device* device, UINT width, UINT height); ShadowMap(const ShadowMap& rhs) = delete; ShadowMap& operator= (const ShadowMap& rhs) = delete; ~ShadowMap() = default; UINT Width() const; UINT Height() const; ID3D12Resource* Resource(); CD3DX12_GPU_DESCRIPTOR_HANDLE Srv() const; CD3DX12_CPU_DESCRIPTOR_HANDLE Dsv() const; D3D12_VIEWPORT Viewport() const; D3D12_RECT ScissorRect() const; void BuildDescriptors( CD3DX12_CPU_DESCRIPTOR_HANDLE hCpuSrv, CD3DX12_GPU_DESCRIPTOR_HANDLE hGpuSrv, CD3DX12_CPU_DESCRIPTOR_HANDLE hCpuDsv); void OnResize(UINT newWidth, UINT newHeight); private: void BuildDescriptors(); void BuildResource(); private: ID3D12Device* md3dDevice = nullptr; D3D12_VIEWPORT mViewport; D3D12_RECT mScissorRect; UINT mWidth = 0; UINT mHeight = 0; DXGI_FORMAT mFormat = DXGI_FORMAT_R24G8_TYPELESS; CD3DX12_CPU_DESCRIPTOR_HANDLE mhCpuSrv; CD3DX12_GPU_DESCRIPTOR_HANDLE mhGpuSrv; CD3DX12_CPU_DESCRIPTOR_HANDLE mhCpuDsv; Microsoft::WRL::ComPtr<ID3D12Resource> mShadowMap = nullptr; };
构造函数将按照我们所声明的尺寸来创建贴图与视口。而阴影映射贴图的分辨率决定了阴影的质量,同时,高分辨率的阴影映射贴图意味着渲染时更大的开销以及占用更多的空间。
ShadowMap::ShadowMap(ID3D12Device* device, UINT width, UINT height) { md3dDevice = device; mWidth = width; mHeight = height; mViewport = { 0.f, 0.f, (float)width, (float)height, 0.f, 1.f }; mScissorRect = { 0, 0, (int)width, (int)height }; BuildResource(); } void ShadowMap::BuildResource() { 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.f; 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))); }
正如我们所看见的,阴影映射算法需要两个渲染通道。在第一个渲染通道中,我们以光源的视角将场景的深度绘制到阴影映射贴图中;在第二个渲染通道中,我们按照正常流程给予玩家的camera位置想场景绘制到back buffer中,但是过程中我们将使用阴影映射贴图作为着色器的输入资源进行阴影算法。而我们也提供了读取阴影映射贴图资源以及其相应的view/descriptor的函数:
ID3D12Resource* ShadowMap::Resource() { return mShadowMap.Get(); } CD3DX12_GPU_DESCRIPTOR_HANDLE ShadowMap::Srv() const { return mhGpuSrv; } CD3DX12_CPU_DESCRIPTOR_HANDLE ShadowMap::Dsv() const { return mhCpuDsv; }