# DX11-ShadowMap
本文为 DX11 阴影专栏第一篇。主要讲述 ShadowMap 的实现。
前置知识 Render To Texture
ShadowMap 的算法非常简单。实现上唯一对于初学者的难点就是怎么采样渲染出来的纹理。DX11 中我们只需要创建 Texture2D 的 RenderTargetView 并在渲染时绑定即可渲染在纹理上,而后再使用 ShaderResourceView 绑定到着色器上进行采样即可。类似与 GL 中的帧缓冲。
此处沿用了 DX11 教程中封装好的 Texture2D 类。
此外,本文会省略一些基础过程,诸如设置缓冲区。
# 初始场景
我的初始场景如下,白色的点代表我的假定的光源位置。
# ShadowMap (以方向光为例)
ShadowMap 主要思想如下:
- 以光源为视角,渲染场景获取到离光源最近的深度图;
- 以相机为视角,计算着色点离光源的深度,判断是否被遮挡。
# 渲染 ShadowMap
- 创建所需的 Render 纹理并初始化
其实 ShadowMap 仅仅需要深度即可,并不需要输出颜色。但是这里为了可视化我使用 Shader 输出了深度值作为颜色。
std::shared_ptr<Texture2D> m_pTexture_ShadowMap; | |
std::shared_ptr<Depth2D> m_pTexture_ShadowMapDepth; | |
m_pTexture_ShadowMap = std::make_shared<Texture2D>(m_pd3dDevice.Get(), 2048, 2048, DXGI_FORMAT_R8G8B8A8_UNORM); | |
m_pTexture_ShadowMapDepth = std::make_shared<Depth2D>(m_pd3dDevice.Get(), 2048, 2048); |
- 确定光源位置,得到光源坐标矩阵
由于使用方向光,我们需要假定构建一个点作为光源点。此处直接使用 ShadowMap,因此必须保证选取的点和正交投影范围囊括所有需要渲染的物体。之后我们会提到 CSM 可以解决这个问题。
此处,由于我的场景使用了一个 50x50,以坐标原点为中心的地板,我将假定光源设置在 (50,10,50), 光源方向为 (-1.0f,-1.0f,-1.0f);\
// 设置光源坐标以及朝向 | |
camera->LookTo(m_DirLight.position, XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT3(0.0f, 1.0f, 0.0f)); | |
// 设置视锥 - 由于我们使用正交投影,前两个参数没用, 只需要关心 nearZ,farZ 能够涵盖整个场景 | |
camera->SetFrustum(XM_PI / 3, 1, 10.0f, 1000.0f); | |
// 拿到 view,pro 矩阵 设置在备用缓冲区中等待提交更新 | |
m_ShadowConstantBuffer.view = XMMatrixTranspose(camera->GetViewXM()); | |
m_ShadowConstantBuffer.pro = XMMatrixTranspose(camera->GetOrthoProjXM()); |
- 渲染阴影深度图
Shader 完整代码如下
// ShadowMapBasic.hlsl | |
#include "LightHelper.hlsl" | |
cbuffer VSConstantBufferEveryDrawing : register(b0) { | |
matrix g_World; | |
matrix g_WorldInvTranspose; | |
Material g_Material; | |
}; | |
cbuffer LightBuffer : register(b1) { | |
matrix g_View; | |
matrix g_Projection; | |
}; | |
// Input Type | |
// VertexPosNormalTex | |
// In | |
struct VertexPosNormalTex { | |
float3 posL : POSITION; | |
float3 normalL : NORMAL; | |
float2 tex : TEXCOORD; | |
}; |
// VS | |
#include "ShadowMapBasic.hlsl" | |
float4 VS(VertexPosNormalTex input) : SV_POSITION | |
{ | |
float4 posW = mul(float4(input.posL, 1.0f), g_World); | |
float4 posV = mul(posW, g_View); | |
float4 posH = mul(posV,g_Projection); | |
return posH; | |
} |
// PS | |
#include "ShadowMapBasic.hlsl" | |
float4 PS(float4 posH : SV_POSITION) : SV_TARGET | |
{ | |
float depth = posH.z / posH.w; | |
return float4(depth,depth,depth,1.0f); | |
} |
随后,我们需要在 DrawScene 过程中加入对 ShadowMap 的渲染。这里 RTT 的代码忽略。
完成后我们使用 RenderDoc 截帧可以看到刚开始新增了 Color Pass。
这里的图是我后面写 CSM 的 ShadowMap 渲染示意图。前面写 ShadowMap 的时候忘记截图了。但是大致就是这样的一张深度图。
- 渲染阴影
接下来我们需要在常规渲染 Shader 中加入对阴影的计算。
首先先来看常量缓冲区,我们需要添加一个光源的坐标转换矩阵来让我们计算光源空间下的着色点坐标。其次我们还需要一个浮点值来控制阴影偏移。
cbuffer xxxCB { | |
// ... Other Params | |
MATRIX g_LightMatrix; | |
float g_bias; | |
float3 g_pad; // 填充用于字节对齐 | |
} |
其次我们需要将我们的 ShadowMap 作为着色器资源进行导入
Texture2D g_ShadowMap : register(t1) |
接下来就是阴影的计算。
怎么样才算阴影呢?
ShadowMap 中存储了某个方向上里光源的最小距离。我们计算某着色点离光源的距离,即该点坐标转换到光源坐标后的深度 depthCal,来与最小距离比较。
如果 depthCal > depthTex, 说明他并不是能被光源照亮的表面,我们就认为他在阴影中。
对于着色,ambient 是环境光照,理应不受某一特定光照的阴影控制;而 diffuse 和 specular 应该谁产生就乘上谁的阴影因子。
float ShadowCalculate(float4 lightPosH) | |
{ | |
lightPosH = lightPosH / lightPosH.w; | |
float depth = lightPosH.z; | |
float texCoordx = 0.5f * lightPosH.x + 0.5f; | |
float texCoordy = -0.5f * lightPosH.y + 0.5f; | |
float depthTex = SampleShadowMap(float2(texCoordx,texCoordy)); | |
float shadow = 0.0f; | |
shadow += depth>depthTex+g_bias?1.0f:0.0f; | |
return shadow; | |
} | |
//main 中 | |
float4 lightPosH = mul(posW, g_LightMatrix); | |
float shadow = ShadowCalculate(lightPosH); | |
... | |
//shadow 作为阴影因子控制阴影颜色。 | |
diffuse *= 1-shadow; | |
specular *= 1-shadow; | |
... |
最终结果如下:
可以见到虽然使用了 2048x2048 这对于场景大小来说不太小的纹理尺寸,但是阴影边缘还是走样严重并且对于如此小的场景,很难调节 bias 使得漏光现象和自遮挡现象同时消失。而且 ShadowMap 无法渲染软化的阴影。
种种这些向我们提出了对更好算法的需求。