DX 深度模板缓冲区
前言
本篇为DX12个人学习笔记,会介绍深度缓冲区、深度冲突、模板缓冲区、如何使用模板缓冲区
深度缓冲区
-
定义:深度缓冲就像颜色缓冲(Color Buffer)(储存所有的片段颜色)一样,在每个片段中储存深度值信息,且和颜色缓冲有着一样的宽度和高度。深度缓冲是由窗口系统自动创建的,它会以16、24或32位float的形式储存它的深度值。在大部分的系统中,深度缓冲的精度都是24位的
-
深度测试:若启用深度测试(Depth Testing),会将一个片段的深度值与深度缓冲的内容进行对比,
若深度测试通过,深度缓冲将会更新为新的深度值。若深度测试失败,片段将会被丢弃 -
执行阶段:深度缓冲在运行在PS中(模板测试(Stencil Testing)运行之后)且对象在屏幕空间中
Early Depth Testing(early-z)
- 定义:现在大部分的GPU都提供一个叫做提前深度测试(Early Depth Testing)的硬件特性。提前深度测试允许深度测试在片段着色器之前运行。只要我们清楚一个片段永远不会是可见的(它在其他物体之后),我们就能提前丢弃这个片段
- 为什么需要?片段着色器通常开销都是很大的,所以我们应该尽可能避免运行它们
- 限制:当使用提前深度测试时,片段着色器的一个限制是你不能写入片段的深度值。D3D不能提前知晓深度值
深度冲突(Z-fighting)
-
定义当两个平面非常紧密地平行排列在一起时,会导致
深度缓冲没有足够的精度来决定两个形状哪个在前面。结果就是这两个形状不断地在切换前后顺序,这会导致很奇怪的花纹。这个现象叫做深度冲突(Z-fighting),因为它看起来像是这两个形状在争夺谁该处于顶端,当物体在远处时效果会更明显如下图,箱子被放置在地板的同一高度上,这也就意味着箱子的底面和地板是共面的,可以看到箱子的底部不断地在箱子底面与地板之间切换,形成了花纹

-
深度冲突不能够被完全避免,但一些技巧有助于减轻/避免深度冲突
-
不要把多个物体摆得太靠近——在两个物体之间设置一个玩家无法注意到的offset -
尽可能
将近平面设置远一些.不过,将近平面设置太远将会导致近处的物体被裁剪掉,所以这通常需要实验来决定最适合场景的近平面距离如下图所示,当近平面靠近相机时视锥体只有靠近相机的那部分精度是很高的,因此我们可以让近平面远离相机,使得视锥体整体都有较高的精度

-
使用更高精度的深度缓冲.大部分深度缓冲的精度都是24位的,但现在大部分的显卡都支持32位的深度缓冲,这将会极大地提高精度,但也会牺牲一些性能
-
模板(stencil)缓冲区
-
什么是模板缓冲区?
我们知道深度缓冲区存储每个像素的深度值z,而模板缓冲区是一个额外的buffer,它的分辨率大小和深度缓冲区相同,其中模板缓冲区的一个像素点占用8个bit,而这8bit的作用是
控制颜色缓冲区和z缓冲区的渲染,比如在一个像素的模板缓冲区中存放1,表示该像素对应的空间点处于阴影体中.也就是说,模板起到的作用和印刷中所用的模板相同注意:模板缓冲区需要搭配深度缓冲区一起工作
-
为什么需要模板缓冲区?通过使用模板,我们可以实现许许多多的特效
如下图所示,中间是一面镜子且镜子四周有一些砖块,按理说我们不应看到左图中露出来的一截,因此借助模板缓冲区,即可避免这一错误

又或者,若在模板缓冲区中绘制了一个空心矩形,模板缓冲首先会被清除为0,之后在模板缓冲中使用1填充了一个空心矩形,场景中的片段将会只在片段的模板值为1的时候会被渲染,这就如同mask操作

-
如何使用模板缓冲区?
步骤
- 填写struct D3D12_DEPTH_STENCIL_DESC来设置模板、深度缓冲区状态
- 将D3D12_DEPTH_STENCIL_DESC赋给D3D12_GRAPHICS_PIPELINE_STATE_DESC中的DepthStencilState
深度/模板缓冲区的格式及其资源数据的重置
-
深度/模板缓冲区的格式:深度/模板缓冲区是一种纹理
- DXGI_FORMAT_D32_FLOAT_S8X24_UINT:深度缓冲区的格式为一个32位float,模板缓冲区的格式为一个32位UINT,其中8位UINT用于将模板缓冲区映射至范围[0,255],剩余24位不可使用仅用于填充占位
- DXGI_FORMAT_D24_UNORM_S8_UINT:深度缓冲区的格式为一个24位的无符号类型,并将其映射至范围[0,1];模板缓冲区的格式为8位UINT,映射至范围[0,255]
-
重置深度/模板缓冲区的数据:调用ID3D12GraphicsCommandList::ClearDepthStencilView()
-
定义
void ClearDepthStencilView( [in] D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView, //待重置的深度/模板缓冲区视图的描述符 [in] D3D12_CLEAR_FLAGS ClearFlags, //标志位,指定清理哪个缓冲区.若为D3D12_CLEAR_FLAG_DEPTH清理深度缓冲区,若为D3D12_CLEAR_FLAG_STENCIL清理模板缓冲区 [in] FLOAT Depth, //此float值会赋值给深度缓冲区的每个像素,范围为[0,1] [in] UINT8 Stencil, //此UINT值会赋值给模板缓冲区的每个像素,范围为[0,255] [in] UINT NumRects, //数组pRects中的数量 [in] const D3D12_RECT *pRects //D3D12_RECT类型的数组,指定深度/模板缓冲区中要重置的区域,nullptr表示整个区域 ); typedef enum D3D12_CLEAR_FLAGS { D3D12_CLEAR_FLAG_DEPTH = 0x1, D3D12_CLEAR_FLAG_STENCIL = 0x2 } ; -
示例
//每一帧都需要调用 mCommandList->ClearDepthStencilView(DepthStencilView(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
-
模板测试
-
定义:允许我们在渲染片段时将模板缓冲设定为一个特定的值.通过在渲染时修改模板缓冲的内容,我们写入模板缓冲。在接下来的渲染迭代中,我们可以读取这些值,来决定丢弃/保留某个片段
-
执行阶段:输出合并阶段。当片段着色器处理完一个片段之后,模板测试(Stencil Test)会开始执行
-
大致步骤
-
启用模板缓冲的写入
-
渲染物体,更新模板缓冲的内容
-
禁用模板缓冲的写入
-
渲染(其它)物体,这次根据模板缓冲的内容丢弃特定的片段
-
-
处理过程
if( StencilRef & StencilReadMask \(\unlhd\) Value & StencilReadMask )
accept pixel
else
reject pixel- 定义的模板参考值StencilRef 与 定义的掩码值StencilReadMask进行按位AND(&)运算,此计算结果称为左运算数
- 正在接受模板测试的特定像素在模板缓冲区中的对应值Value 和 程序内定义的掩码值StencilReadMask进行按位AND(&)运算,此计算结果称为右运算数
- 采样选定的
比较函数\(\unlhd\)对左运算数、右运算数进行对比,若结果位true则写入,否则禁止该像素向后台缓冲区的写操作
-
比较函数
该比较函数也可用于深度测试中
-
定义
以下我们称\(\unlhd\)运算符为比较运算符。默认为D3D12_COMPARISON_FUNC_LESS——丢弃深度值≥当前模板缓冲区的片段值
//以下比较运算符成立则通过测试 typedef enum D3D12_COMPARISON_FUNC { D3D12_COMPARISON_FUNC_NEVER = 1, //只返回false.永远不通过深度测试 D3D12_COMPARISON_FUNC_LESS = 2, //"<".片段深度值小于缓冲区的深度值 D3D12_COMPARISON_FUNC_EQUAL = 3, //"==".片段深度值等于缓冲区的深度值 D3D12_COMPARISON_FUNC_LESS_EQUAL = 4, //"≤".片段深度值小于等于缓冲区的深度值 D3D12_COMPARISON_FUNC_GREATER = 5, //">".片段深度值大于缓冲区的深度值 D3D12_COMPARISON_FUNC_NOT_EQUAL = 6, //"!=".片段深度值不等于缓冲区的深度值 D3D12_COMPARISON_FUNC_GREATER_EQUAL = 7, //"≥".片段深度值大于等于缓冲区的深度值 D3D12_COMPARISON_FUNC_ALWAYS = 8 //只返回true.永远通过深度测试 } ;
-
创建绑定深度/模板状态
-
创建:通过填写struct D3D12_DEPTH_STENCIL_DESC来描述深度/模板状态
-
定义
typedef struct D3D12_DEPTH_STENCIL_DESC { BOOL DepthEnable; //是否开启深度测试,TRUE则开启.默认值为TRUE D3D12_DEPTH_WRITE_MASK DepthWriteMask; //是否禁止深度写入,若为D3D12_DEPTH_WRITE_MASK_ZERO则不可进行深度写入,若为D3D12_DEPTH_WRITE_MASK_ALL则可以.默认值为D3D12_DEPTH_WRITE_MASK_ALL D3D12_COMPARISON_FUNC DepthFunc; //指定比较函数。指定为比较函数中的一员 BOOL StencilEnable; //是否开启模板测试,若为true则开启.默认值为FALSE UINT8 StencilReadMask; //定义的掩码值,用于模板测试 UINT8 StencilWriteMask; //写掩码值,用于屏蔽对应位的写入操作 D3D12_DEPTH_STENCILOP_DESC FrontFace; //指示根据测试和深度测试的结果,对正面朝向的三角形进行什么模板运算 D3D12_DEPTH_STENCILOP_DESC BackFace; //指示根据测试和深度测试的结果,对背面朝向的三角形进行什么模板运算 } D3D12_DEPTH_STENCIL_DESC; //是否禁止深度写入 typedef enum D3D12_DEPTH_WRITE_MASK { D3D12_DEPTH_WRITE_MASK_ZERO = 0, D3D12_DEPTH_WRITE_MASK_ALL = 1 } ; //用于模板测试的掩码值的默认值——不会屏蔽任何一位模板值 #define D3D12_DEFAULT_STENCIL_READ_MASK (0xff) //写掩码值的默认值——不会屏蔽任何一位模板值 #define D3D12_DEFAULT_STENCIL_WRITE_MASK (0xff) //描述基于模板测试的结果被执行的模板运算符 typedef struct D3D12_DEPTH_STENCILOP_DESC { D3D12_STENCIL_OP StencilFailOp; //描述当片元在模板测试失败时,应如何更新模板缓冲区 D3D12_STENCIL_OP StencilDepthFailOp; //描述当片元通过模板测试,但在深度测试失败时,应如何更新模板缓冲区 D3D12_STENCIL_OP StencilPassOp; //描述当片元通过模板测试、深度测试时,应如何更新模板缓冲区 D3D12_COMPARISON_FUNC StencilFunc; //模板测试中所用的比较函数 } D3D12_DEPTH_STENCILOP_DESC; //指定在深度/模板测试期间被执行的模板运算符 typedef enum D3D12_STENCIL_OP { D3D12_STENCIL_OP_KEEP = 1, //不修改模板缓冲区 D3D12_STENCIL_OP_ZERO = 2, //将模板缓冲区中的元素置为0 D3D12_STENCIL_OP_REPLACE = 3, //将模板缓冲区中的元素替换为用于模板测试的模板参考值(StencilRef).只有当深度/模板缓冲区状态块绑定至管线时,才能设定该值 D3D12_STENCIL_OP_INCR_SAT = 4,//对模板缓冲区中的元素进行递增.若超出范围会进行钳制 D3D12_STENCIL_OP_DECR_SAT = 5,//对模板缓冲区中的元素进行递减.若超出范围会进行钳制 D3D12_STENCIL_OP_INVERT = 6, //对模板缓冲区中的元素按二进制位进行反转 D3D12_STENCIL_OP_INCR = 7, //对模板缓冲区中的元素进行递增.若超出范围会回到最小值0 D3D12_STENCIL_OP_DECR = 8 //对模板缓冲区中的元素进行递增.若超出范围会回到最大值255 } ; -
绑定:填写完D3D12_DEPTH_STENCIL_DESC后,即可将其赋值给D3D12_GRAPHICS_PIPELINE_STATE_DESC::DepthStencilState
-
设置模板参考值(StencilRef):通过调用ID3D12GraphicsCommandList::OMSetStencilRef()
-
定义
void OMSetStencilRef( [in] UINT StencilRef );
-
示例
实现平面镜
为了简易性,此处仅完成平面上的镜面
- 实现平面镜需要解决两个问题
- 平面反射物体的原理
- 要将镜像显示在镜子中,必须对表面的镜面部分做标记
实现镜像
存在的问题:下图描述了镜像的渲染过程——反射至镜面的背面,但这存在一个问题——现实中只有透过镜子才能看到镜像,但此处的镜像却是另一个实物,若没有东西遮挡他,我们同样可以看到他,这并不是我们想要的效果

解决方法:模板缓冲区来制止超出镜面范围的镜像绘制操作
重点
-
将除镜子外的物体按正常流程渲染至后台缓冲区中
-
清除模板缓冲区,将其整体置零
-
仅将镜面渲染至模板缓冲区
过程如下:将模板测试设为每次都通过(D3D12_COMPARISON_FUNC_ALWAYS),且在通过测试时以1(模板参考值)来更新(D3D12_STENCIL_OP_REPLACE)模板缓冲区元素。若深度测试失败(如物体挡住部分镜子),则采用D3D12_STENCIL_OP_KEEP(不修改模板缓冲区),使得模板缓冲区对应的像素保持不变。由于仅仅向模板缓冲区绘制镜面,因此只有镜面对应的像素为1,而其他像素都为0

采用如下设置来禁止其他颜色数据写入至后台缓冲区
D3D12_RENDER_TARGET_BLEND_DESC::RenderTargetWriteMask = 0; //禁止写入后台缓冲区中的任何颜色通道 D3D12_DEPTH_STENCIL_DESC::DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO; //禁止深度写入 -
将镜像渲染至后台缓冲区、模板缓冲区
只有通过模板测试的像素才能渲染至后台缓冲区,如此只有在镜面中的部分物体才会被渲染
-
以正常方式结合blend将镜面渲染至后台缓冲区
由于要呈现镜像效果,我们需要运用blend来渲染镜面。假设不使用blend,会导致物体的z值小于镜面的z值,镜像被镜子挡住
-
定义镜面材质:将漫反射alpha设为0.3,,再设置blend状态——\(C = 0.3 · C_{src} + 0.7 · C_{dst}\)
auto icemirror = std::make_unique<Material>(); icemirror->Name = "icemirror"; icemirror->MatCBIndex = 2; icemirror->DiffuseSrvHeapIndex = 2; icemirror->DiffuseAlbedo = XMFLOAT4(1.0f, 1.0f, 1.0f, 0.3f); icemirror->FresnelR0 = XMFLOAT3(0.1f, 0.1f, 0.1f); icemirror->Roughness = 0.5f;
定义镜像的深度/模板状态
为了实现以上效果,需要用到两个PSO,一个用于在绘制镜面时标记模板缓冲区内镜面部分的像素,而另一个用于绘制镜面的镜像
-
用于标记模板缓冲区内镜面部分的像素的PSO
CD3DX12_BLEND_DESC mirrorBlendState(D3D12_DEFAULT); mirrorBlendState.RenderTarget[0].RenderTargetWriteMask = 0; //禁止对渲染目标进行写操作 //定义深度模板状态描述符 D3D12_DEPTH_STENCIL_DESC mirrorDSS; mirrorDSS.DepthEnable = true; //开启深度测试 mirrorDSS.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO; //禁止深度写入 mirrorDSS.DepthFunc = D3D12_COMPARISON_FUNC_LESS; //比较运算符为小于 mirrorDSS.StencilEnable = true; //开启模板测试 mirrorDSS.StencilReadMask = 0xff; //模板参考值 mirrorDSS.StencilWriteMask = 0xff; //模板全部可写 //正面朝向的多边形 mirrorDSS.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP; //模板测试失败时,不修改模板缓冲区 mirrorDSS.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP; //通过模板测试,但在深度测试失败时,不修改模板缓冲区 mirrorDSS.FrontFace.StencilPassOp = D3D12_STENCIL_OP_REPLACE; //通过模板测试、深度测试时,将模板缓冲区中的元素替换为用于模板测试的模板参考值 mirrorDSS.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS; //比较函数只返回TRUE //不渲染背面朝向的多边形 mirrorDSS.BackFace.StencilFailOp = D3D12_STENCIL_OP_KEEP; mirrorDSS.BackFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP; mirrorDSS.BackFace.StencilPassOp = D3D12_STENCIL_OP_REPLACE; mirrorDSS.BackFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS; D3D12_GRAPHICS_PIPELINE_STATE_DESC markMirrorsPsoDesc = opaquePsoDesc; markMirrorsPsoDesc.BlendState = mirrorBlendState; markMirrorsPsoDesc.DepthStencilState = mirrorDSS; ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&markMirrorsPsoDesc, IID_PPV_ARGS(&mPSOs["markStencilMirrors"]))); -
用于绘制镜面的镜像的PSO
D3D12_DEPTH_STENCIL_DESC reflectionsDSS; reflectionsDSS.DepthEnable = true; reflectionsDSS.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL; //开启深度写入 reflectionsDSS.DepthFunc = D3D12_COMPARISON_FUNC_LESS; reflectionsDSS.StencilEnable = true; reflectionsDSS.StencilReadMask = 0xff; reflectionsDSS.StencilWriteMask = 0xff; reflectionsDSS.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP; reflectionsDSS.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP; reflectionsDSS.FrontFace.StencilPassOp = D3D12_STENCIL_OP_KEEP; //通过模板测试、深度测试时,不修改模板缓冲区 reflectionsDSS.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_EQUAL; //比较运算符为"==" reflectionsDSS.BackFace.StencilFailOp = D3D12_STENCIL_OP_KEEP; reflectionsDSS.BackFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP; reflectionsDSS.BackFace.StencilPassOp = D3D12_STENCIL_OP_KEEP; reflectionsDSS.BackFace.StencilFunc = D3D12_COMPARISON_FUNC_EQUAL; D3D12_GRAPHICS_PIPELINE_STATE_DESC drawReflectionsPsoDesc = opaquePsoDesc; drawReflectionsPsoDesc.DepthStencilState = reflectionsDSS; drawReflectionsPsoDesc.RasterizerState.CullMode = D3D12_CULL_MODE_BACK; drawReflectionsPsoDesc.RasterizerState.FrontCounterClockwise = true; ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&drawReflectionsPsoDesc, IID_PPV_ARGS(&mPSOs["drawStencilReflections"])));
绘制场景
// 绘制不透明的物体
auto passCB = mCurrFrameResource->PassCB->Resource();
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Opaque]);
// 将模板缓冲区中可见的镜面像素标记为1
mCommandList->OMSetStencilRef(1);
mCommandList->SetPipelineState(mPSOs["markStencilMirrors"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Mirrors]);
// 只绘制镜子范围内的镜像
//使用两个单独的渲染过程常量缓冲区,一个存储物体镜像,另一个存储光照镜像
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress() + 1 * passCBByteSize);
mCommandList->SetPipelineState(mPSOs["drawStencilReflections"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Reflected]);
// 恢复主渲染过程常量数据和模板参考值
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress());
mCommandList->OMSetStencilRef(0);
// 绘制透明的镜面
mCommandList->SetPipelineState(mPSOs["transparent"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Transparent]);
在绘制镜像时,镜像也需要有光照效果,因此使用两个单独的渲染过程常量缓冲区,一个存储物体镜像,另一个存储光照镜像
PassConstants mMainPassCB;
PassConstants mReflectedPassCB;
void StencilApp::UpdateReflectedPassCB(const GameTimer& gt)
{
mReflectedPassCB = mMainPassCB;
XMVECTOR mirrorPlane = XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f); // xy plane
XMMATRIX R = XMMatrixReflect(mirrorPlane);
// 光照镜像
for(int i = 0; i < 3; ++i)
{
XMVECTOR lightDir = XMLoadFloat3(&mMainPassCB.Lights[i].Direction);
XMVECTOR reflectedLightDir = XMVector3TransformNormal(lightDir, R);
XMStoreFloat3(&mReflectedPassCB.Lights[i].Direction, reflectedLightDir);
}
auto currPassCB = mCurrFrameResource->PassCB.get();
currPassCB->CopyData(1, mReflectedPassCB);
}
镜像的绕序
当三角形反射到镜像时,由于该绕序不会发生改变,从而使得它的平面法线方向也没有变化,因此这会造成它的法线为内向法线,而原本物体为外向法线。为了纠正这个问题,需告知D3D将逆时针绕序的三角形看作是正面朝向,顺时针绕序的三角形看作背面朝向 (原本规定的是若我们看到的是三角形的正面,则此三角形为正面朝向且顶点顺序为顺时针;看到的是三角形反面,则此三角形为背面朝向且定点顺序为逆时针)
drawReflectionsPsoDesc.RasterizerState.FrontCounterClockwise = true;

实现平面阴影
思路:借助几何建模的方式找到物体经光照投向平面的阴影
平行光阴影
给定方向为L的平行光源,以\(r(t) = p + tL\)表示经过顶点p的光线,光线r与阴影平面(n,d)的交点为s,使用这些光线和阴影平面的交点即可定义几何体所的阴影形状,投影公式:\(r(t_s) = p - \frac{n·p + d}{n·L}L\)

投影公式推导:此处实则是求解投影平面与光线求交

- 表示射线:\(p = p_0 + tu\),其中\(p_0\)为该射线的起点,\(u\)为与该射线平行的向量,\(t\)用于获得该射线上任意一点
- 表示平面:\(n · (p - p_0) = n · p - n·p_0 = n · p + d = 0\),其中$d = -n · p_0 \(.将平面视为一个xyz轴上的长度都是无限长的平面,其中\)n\(为平面的法向量,\)p_0,p\(为平面内一点,当且仅当\)(p - p_0)\(正交于\)n$时才可表示此平面
- 解得射线与平面求交的t:\(n·p(t) + d = 0 \rightarrow n(p_0 + tu) +d = 0 \rightarrow n · p_0 + tn·u + d = 0 \rightarrow t = \frac{-n·p_0 - d}{n·u}\)
- 代入射线公式:\(p = p_0 + \frac{-n·p_0 - d}{n·u}u \rightarrow p_0 - \frac{n·p_0 + d}{n·u}u\)
将投影公式转换为矩阵:\(r(t_s) = [p_x, p_y, p_z, 1] \left[\begin {array} {c} n·L - L_x n_x & -L_y n_x & -L_z n_x & 0 \\
-L_x n_y & n·L - L_y n_y & -L_z n_y & 0 \\
-L_x n_z & -L_y n_z & n · L - L_zn_z & 0 \\ -L_x d & -L_y d & -L_z d & n·L \end {array} \right]\)。该矩阵被称为方向光阴影矩阵
该矩阵有个注意点:为了引用该阴影矩阵,我们应当将其进行MVP变换,但在进行世界变换后,由于透视除法并未执行,因此几何体的阴影还未进行投射,这边出现了一个问题——若\(n·L < 0\),则投影矩阵w坐标为负值,但在将投影矩阵乘以透视投影矩阵时,计算过程中会使得w的值等于z的值,造成w的坐标为负,而w坐标为负说明该点位于视锥体外部,会对其进行裁剪,结果便是这段阴影无法显示
解决:用指向无穷远的光源的方向向量\(-L\)取代L,采用适当的t值也可以得到与之前的效果,如此即可使得\(n·L > 0\)

点光阴影
下图描述的是点光源L所投射出的物体阴影

其中,点光源投射的光线公式:\(r(t) = p + t(p-L)\),投影公式:\(r(t_s) = p - \frac{n·p + d}{n·(p-L)}(p-L)\),投影矩阵为:$s_{point} = \left[ \begin {array} {c} n·L + d - L_x n_x & -L_y n_x & -L_z n_x & -n_x \
-L_x n_y & n·L + d -L_y n_y & -L_z n_y & -n_y \
-L_x n_z & -L_y n_z & n·L + d - L_z n_z & -n_z \
-L_x d & -L_y d & -L_z d & n·L \end {array} \right] $
通用阴影矩阵
通用阴影矩阵的表示:\(S = \left[ \begin {array} {c} n·L + dL_w - L_x n_x & -L_y n_x & -L_z n_x & -L_w n_x \\ -L_xn_y & n·L + d L_w - L_y n_y & -L_z n_y & -L_w n_y \\ -L_x n_z & -L_y n_z & n·L + d L_w - L_z n_z & -L_w n_z \\ -L_x d & -L_y d & -L_z d & n·L \end {array} \right]\)
对于通用阴影矩阵有以下两个规定:
- 若\(L_w = 0\),L为指向方向光光源的向量
- 若\(L_w = 1\),L为点光源的位置
DirectX数学库提供了对应函数用于计算阴影矩阵:
inline XMMATRIX XM_CALLCONV XMMatrixShadow(
FXMVECTOR ShadowPlane,
FXMVECTOR LightPosition //w = 0为方向光,w = 1为点光源
);
防止双重混合
问题:对物体进行投射形成阴影时,经常会出现有两个以上的平面阴影三角形相互重叠,此时使用透明渲染来渲染阴影,会使得这些三角形blend多次,最终结果会更暗
如下图,左边重叠后的透明渲染,右边是正确结果

解决方案:模板缓冲区
- 将模板缓冲区中的阴影范围的像素清为0
- 设置模板测试,仅接受模板缓冲区中元素为0的像素。若通过模板测试,将相应像素点的值赋为1,使得若覆写已通过的区域,模板测试会失效
实现阴影
将阴影视为具有50%透明度的黑色材质
//材质
auto shadowMat = std::make_unique<Material>();
shadowMat->Name = "shadowMat";
shadowMat->MatCBIndex = 4;
shadowMat->DiffuseSrvHeapIndex = 3;
shadowMat->DiffuseAlbedo = XMFLOAT4(0.0f, 0.0f, 0.0f, 0.5f);
shadowMat->FresnelR0 = XMFLOAT3(0.001f, 0.001f, 0.001f);
shadowMat->Roughness = 0.0f;
//防止双重混合
D3D12_DEPTH_STENCIL_DESC shadowDSS;
shadowDSS.DepthEnable = true;
shadowDSS.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;
shadowDSS.DepthFunc = D3D12_COMPARISON_FUNC_LESS;
shadowDSS.StencilEnable = true;
shadowDSS.StencilReadMask = 0xff;
shadowDSS.StencilWriteMask = 0xff;
shadowDSS.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
shadowDSS.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
shadowDSS.FrontFace.StencilPassOp = D3D12_STENCIL_OP_INCR;
shadowDSS.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_EQUAL;
// 不渲染背面朝向的三角形
shadowDSS.BackFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
shadowDSS.BackFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
shadowDSS.BackFace.StencilPassOp = D3D12_STENCIL_OP_INCR;
shadowDSS.BackFace.StencilFunc = D3D12_COMPARISON_FUNC_EQUAL;
D3D12_GRAPHICS_PIPELINE_STATE_DESC shadowPsoDesc = transparentPsoDesc;
shadowPsoDesc.DepthStencilState = shadowDSS;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&shadowPsoDesc, IID_PPV_ARGS(&mPSOs["shadow"])));
//绘制阴影
mCommandList->OMSetStencilRef(0);
mCommandList->SetPipelineState(mPSOs["shadow"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Shadow]);
//更新阴影的矩阵
XMVECTOR shadowPlane = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f); // xz plane
XMVECTOR toMainLight = -XMLoadFloat3(&mMainPassCB.Lights[0].Direction);
XMMATRIX S = XMMatrixShadow(shadowPlane, toMainLight);
XMMATRIX shadowOffsetY = XMMatrixTranslation(0.0f, 0.001f, 0.0f); //将投影网格沿y轴做少量offset,来防止发生深度冲突,使得阴影略高于地面
XMStoreFloat4x4(&mShadowedSkullRitem->World, skullWorld * S * shadowOffsetY);
reference
Directx12 3D 游戏开发实战
https://github.com/QianMo/Game-Programmer-Study-Notes
[深度测试 - LearnOpenGL CN (learnopengl-cn.github.io)](https://learnopengl-cn.github.io/04 Advanced OpenGL/01 Depth testing/#_1)

浙公网安备 33010602011771号