个人学习用,请勿转载
第11章 模板
词汇
模板缓冲:stencil buffer 离屏:off-screen 双重混合:double blending 深度复杂性:depth complexity
模板参考值:stencil reference value 掩码值:masking value 比较函数:comparison function
递增:increment 递减:decrement 绕序:winding order
平面阴影:planar shadow 深度冲突:z-fighting
重复绘制:overdraw
内容
模板缓冲区、后台缓冲区和深度缓冲区有着相同的分辨率,用于将三者相同位置上的像素一一对应。
在指定一个模板缓冲区时,需要将它与一个深度缓冲区配合使用,模板缓冲区的作用是阻止特定的像素片段渲染至后台缓冲区。

当需要物体反射到镜子所在的平面上时,只应该绘制出镜子中的镜像部分,这时就可以用过模板缓冲区阻止范围外镜像部分的绘制操作。
- 要设置模板缓冲去状态,就要填写D3D12_DEPTH_STENCIL_DESC结构体示例,并将其赋予流水线状态对象(PSO)的D3D12_GRAPHICS_PIPELINE_DESC::DepthStencilState字段。学习模板缓冲区用法的最佳方式就是从现存的示例程序着手,一旦对实例中的模板缓冲区部分有了感性的认识,就可以更加得心应手的运用它了(?)。
学习目标:
- 探究如何通过填写流水线状态对象中的D3D12_DEPTH_STENCIL_DESC DepthStencilState字段控制深度缓冲区以及模板缓冲区。
- 学习通过模板缓冲去防止镜像被绘制到镜子以外的区域,以此实现正确的镜像效果。
- 了解双重混合的机制,从而利用模板缓冲去来有效杜绝这一情况的发生。
- 知晓深度复杂性概念,并介绍两种方法来度量场景的深度复杂性。
11.1深度/模板缓冲区的格式以及资源数据的清理
深度/模板缓冲其实也是一种纹理,因此必须用下列特定的数据格式创建:
1. DXGI_FORMAT_D32_FLOAT_S8X24_UINT:用一个32位浮点数来指定深度缓冲区,并以另一个32位无符号整数来指定模板缓冲区。其中无符号整数里的8位用于将模板缓冲区映射到范围[0,255],另外24位不可用,仅作填充占位。
2. DXGI_FORMAT_D24_UNORM_S8_UINT:指定一个无符号24位深度缓冲区,并将其映射到范围[0,1]内,另外8位无符号整数用于将模板缓冲区映射至范围[0,255]
D3Dapp应用框架中,当要创建深度缓冲区时就要如下指定它的格式:
DXGI_FORMAT mDepthStencilFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;
depthStencilDesc.Format = mDepthStencilFormat;
可以在每帧绘制画面之初,用下列方法重置缓冲区的局部数据
void ID3D12GraphicsCommandList::ClearDepthStencilVIew(
D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView,
D3D12_CLEAR_FLAGS ClearFlags,
UINT8 Stencil,
UINT NumRects,
const D3D12_RECT *pRects);
- DepthStencilView:待清理的深度/模板缓冲区视图的描述符
- ClearFlags:指定为D3D12_CLEAR_FLAG_DEPTH 仅清理深度缓冲区,D3D12_CLEAR_FLAG_STENCIL则同时清理两种缓冲区。
- Depth:将此浮点值设置到深度缓冲区中的每一个像素。此浮点数x务必满足0⩽x⩽10\leqslant x\leqslant 10⩽x⩽1
- Stencil:将此整数值设置到模板缓冲区的每一个像素,n满足0⩽n⩽2550\leqslant n \leqslant 2550⩽n⩽255
- NumRects:数组pRects所指引的矩形数量。
- pRects:一个D3D12_RECT类型数组,它标定了一系列深度/模板缓冲区内要清理的区域。若指定nullptr则清理整个深度/模板缓冲区。演示程序中的每一帧都已经调用此方法:
mCommandList->ClearDepthStencilView(DepthStencilView(),
D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL,
1.0f,0,0,nullptr);
11.2模板测试
通过模板缓冲区阻止对后台缓冲区特定区域的绘制行为的操作名为模板测试。
if(StencilRef & StencilReadMask ⊴\trianglelefteq⊴ Value & StencilReadMask)
accept pixel
else
reject pixel
模板测试会随着像素的光栅化过程而进行(输出合并阶段)。若模板功能为开启状态,则需要经过下面两处运算。
1.左运算数(left-hand-side,LHS)由程序定义的模板参考值StencilRef与程序内定义的掩码值StencilReadMask通过AND运算来加以确定。
2.右运算数(right-hand-side,RHS)由正在接受模板测试的特定像素位于模板缓冲区内对应值VValue与程序中定义的掩码值StencilReadMask经过AND运算加以确定。
左运算与右运算中的StencilReadMask是同一个值。接下来模板测试所选定的比较函数对左运算数和右运算数进行比对,从而得到布尔型的返回值,如果测试结果为true,则将当前接受检测的像素写入后台缓冲区,如果结果为false,则禁止此像素向后台缓冲区的写操作。
运算符⊴\trianglelefteq⊴ 是D3D12_COMPARISON_FUNC枚举类型所定义的下列比较函数之一:
enum D3D12_COMPARISON_FUNC
{
D3D12_COMPARISON_FUNC_NEVER = 1,
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
} D3D12_COMPARISON_FUNC;
- D3D12_COMPARISON_FUNC_NEVER:总是返回false
- D3D12_COMPARISON_FUNC_LESS:运算符<
- D3D12_COMPARISON_FUNC_EQUAL:运算符==
- D3D12_COMPARISON_FUNC_LESS_EQUAL:运算符<=
- D3D12_COMPARISON_FUNC_GREATER:运算符>
- D3D12_COMPARISON_FUNC_NOT_EQUAL:运算符!=
- D3D12_COMPARISON_FUNC_GREATER_EQUAL:运算符>=
- D3D12_COMPARISON_FUNC_ALWAYS 永远返回true
11.3描述深度/模板状态
填写D3D12_DEPTH_STENCIL_DESC实例:
typedef struct D3D12_DEPTH_STENCIL_DESC
{
BOOL DepthEnable;//默认值为true
D3D12_DEPTH_WRITE_MASK DepthWriteMask;
D3D12_COMPARISON_FUNC DepthFunc;
BOOL StencilEnable;//默认值为false
UINT8 StencilReadMask;//默认值为0xff,即D3D12_DEFAULT_STENCIL_WRITE_MASK
UINT8 StencilWriteMask;//默认值为0xff,即D3D12_DEFAULT_STENCIL_WRITE_MASK
D3D12_DEPTH_STENCILOP_DESC FrontFace;
D3D12_DEPTH_STENCILOP_DESC BackFace;
} D3D12_DEPTH_STENCIL_DESC;
11.3.1深度信息的相关设置
- DepthEnable:设置为true,则开启深度缓冲,若设为false则禁用,当深度缓冲被禁止时。物体的描绘顺序就尤为重要。并且如果深度缓冲被禁用,DepthWriteMask项也不会生效。
- DepthWriteMask:可设置为D3D12_DEPTH_WRITE_MASK_ZERO或者D3D12_DEPTH_WRITE_MASK_ALL,但两者不能共存,当DepthEnable为true,此参数为D3D12_DEPTH_WRITE_MASK_ZERO,则会禁止对深度缓冲区的写操作,但仍可执行深度测试,如果 D3D12_DEPTH_WRITE_MASK_ALL,则通过深度测试与模板测试的深度数据将被写入深度缓冲区,这种控制深度数据读写的能力,为某些特效的实现提供了良好的契机。
3.DepthFunc:该参数指定为枚举类型D3D12_COMPARISON_FUNC的成员之一,以此来定义深度测试所用的比较函数,此项一般设为D3D12_COMPARISON_FUNC_LESS,但Direct3D也允许用户根据需求来自定义深度测试。
11.3.2模板信息的相关设置
- StencilEnable:设置为true,则开启模板测试,fale则禁用
- StencilReadMask:用于模板测试,如果采用该项的默认值,则不会屏蔽任何一位模板值
#define D3D12_DEFAULT_STENCIL_READ_MASK(0xff) - StencilWriteMask:当模板缓冲区被更新时,我们可以通过掩码来屏蔽特定位的写入操作,例如,我们希望防止前4位被改写,便可以将掩码设置为0x0f,而默认配置不会屏蔽任何一位模板值
#define D3D12_DEFAULT_STENCIL_WRITE_MASK(0xff)、 - FrontFace:填写一个D3D12_DEPTH_STENCILOP_DESC结构体实例,以指出根据模板测试与深度测试的结果,应对正面朝向的三角形进行何种模板运算
- BackFace:填写一个D3D12_DEPTH_STENCILOP_DESC结构体实例,以指出根据模板测试与深度测试的结果,应对背面朝向 的三角形进行何种模板运算。
typedef struct D3D12_DEPTH_STENCILOP_DESC
{
D3D12_STENCIL_OP StencilFailOp;//默认值为D3D12_STENCIL_OP_KEEP
D3D12_STENCIL_OP StencilDepthFailOp;//默认值为D3D12_STENCIL_OP_KEEP
D3D12_STENCIL_OP StencilPassOp;//默认值为D3D12_STENCIL_OP_KEEP
D3D12_COMPARISON_FUNC StencilFunc;//默认值为D3D12_COMPARISON_FUNC_ALWAYS
} D3D12_DEPTH_STENCILOP_DESC;
- StencilFailOp:枚举类型D3D12_STENCIL_OP的成员之一,描述了当像素片段在模板测试失败时,应该如何更新模板缓冲区。
- StencilDepthFailOp:枚举类型D3D12_STENCIL_OP的成员之一,描述了当像素片段通过模板测试,在深度测试失败时如何更新模板缓冲区
- StencilPassOp:枚举类型D3D12_STENCIL_OP成员之一,描述了当像素通过模板测试与深度测试时该如何更新模板缓冲区。
- StencilFunc:枚举类型D3D12_COMPARISON_FUNC的成员之一,定义了模板测试所用的比较函数。
typedef
enum D3D12_STENCIL_OP
{
D3D12_STENCIL_OP_KEEP = 1,
D3D12_STENCIL_OP_ZERO = 2,
D3D12_STENCIL_OP_REPLACE = 3,
D3D12_STENCIL_OP_INCR_SAT = 4,
D3D12_STENCIL_OP_DECR_SAT = 5,
D3D12_STENCIL_OP_INVERT = 6,
D3D12_STENCIL_OP_INCR = 7,
D3D12_STENCIL_OP_DECR = 8
} D3D12_STENCIL_OP;
- D3D12_STENCIL_OP_KEEP :不修改模板缓冲区,保持当前数据
- D3D12_STENCIL_OP_ZERO:将模板缓冲区中的元素设置为0
- D3D12_STENCIL_OP_REPLACE:将模板缓冲区内的元素替换为用于模板测试的参考值,只有将深度/模板缓冲区状态绑定到渲染流水线时,才能够设定StencilRef值
- D3D12_STENCIL_OP_INCR_SAT:对模板缓冲区中的元素进行递增操作(increment),若递增值超出最大值,则将此模板缓冲区元素限定为最大值
- D3D12_STENCIL_OP_DECR_SAT:对模板缓冲区中的元素进行递减操作,递减值小于0,则将该模板缓冲区元素限定为0
- D3D12_STENCIL_OP_INVERT:对模板缓冲区的元素数据按二进制进行反转
- D3D12_STENCIL_OP_INCR:对模板缓冲区中的元素进行递增操作,如果递增值超出最大值,则循环回0。
- D3D12_STENCIL_OP_DECR:对模板缓冲区中元素进行递减操作,如果递减值超出最小值,则循环回最大值。
对正面朝向和背面朝向的三角形所进行的模板运算是可以互不相同的。有时候需要针对特定的图形学算法或透明几何体的处理来渲染背面朝向的多边形,此时的BackFace的设置则非常重要。
11.3.3创建和绑定深度/模板状态
将描述深度/模板状态的D3D12_DEPTH_STENCIL_DESC实例填写完整,就可以将其赋予PSO的D3D12_GRAPHICS_PIPELINE_DESC::DepthStencilState字段,使用此PSO绘制的几何体,都将根据上述的深度/模板设置进行渲染。
设置模板参考值的方法:由ID3D12GraphicsCommandList::OMSetStencilRef方法来实现,它以一个无符号整数作为参数:mCommandList->OMSetStencilRef(1);
11.4实现平面镜效果
实现镜像变成需解决的问题:
1. 必须了解平面反射物体的相关原理,以此来完整的绘制镜像
2. 需要标记处表面属于平面镜的部分
也需要将光源反映到镜子所在的平面内,否则会造成镜像中的光照不够精准

11.4.1镜像概述
我们需要将它反射到镜面的表面,超出镜面范围的骷髅头镜像绘制操作需要被模板缓冲区阻止。
1. 将地板、墙壁、以及骷髅头实物照常渲染到后台缓冲区内,此步骤不修改模板缓冲区
2. 清理模板缓冲区,将其整体置零

3. 仅将镜面渲染到模板缓冲区中。若要阻止其它颜色数据写入到后台缓冲区,可用下列设置所创建的混合状态:
D3D12_RENDER_TARGET_BLEND_DESC::RenderTargetWriteMask = 0;
再通过以下配置来禁止向深度缓冲区的写操作:
D3D12_DEPTH_STENCIL_DESC::DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;
在向模板缓冲区渲染镜面的时候,我们将模板测试设置为每次都成功(D3D12_COMPARISON_ALWAYS),并且在通过测试时用1来替换模板缓冲区元素,如果深度测试失败,则采用枚举项D3D12_STENCIL_OP_KEEP,时模板缓冲区中的对应像素保持不变,由于仅向模板缓冲区绘制了镜面,因此在模板缓冲区内,除了镜面可见部分像素的对应像素为1,其它像素皆为0。

需要保证先绘制出骷髅头实物,再将镜面渲染至模板缓冲区中
4. 将骷髅头的镜像渲染至后台缓冲区及模板缓冲区中。只有通过模板测试的像素才能渲染到后台缓冲区,所以将其设置为仅当模板缓冲内的值为1时才能通过模板测试,可以通过令StencilRef为1,且模板运算符为D3D12_COMPARISON_FUNC_EQUAL来实现,如此只有模板缓冲区中元素为1的骷髅头像部分才得以渲染,由于只有镜面可见部分所对应的模板缓冲区中元素数组为1,所以仅有这一范围内的骷髅头镜像才能被渲染出来。
5.将镜面渲染到后台缓冲区中。为了解决透过镜面观察骷髅头的镜像,骷髅头会被镜子挡住的 问题,需要用混合技术渲染镜面。因此需要为镜面定义一个新的材质配置实例,将其漫反射alpha值设为0.3,并按透明混合状态来渲染镜面。
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;
11.4.2定义镜像的深度/模板状态
为实现上述算法,需要用到两个PSO对象,第一个用于在绘制镜面时标记模板缓冲区内镜面部分的镜像,第二个用于绘制镜面可见部分内的骷髅头镜像
// 用于标记模板缓冲区中镜面部分的PSO
//禁止对渲染目标的写操作
CD3DX12_BLEND_DESC mirrorBlendState(D3D12_DEFAULT);
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;
//不渲染背面朝向的多边形,因而对这些参数的设置并不关心
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 for stencil reflections.
//用于渲染模板缓冲区中反射镜像的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;
// We are not rendering backfacing polygons, so these settings do not matter.
//并不关心背面朝向的多边形渲染
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"])));
11.4.3绘制场景
以下代码概述了场景的绘制流程
// Draw opaque items--floors, walls, skull.
//绘制不透明的物体——地板、墙壁、骷髅头
auto passCB = mCurrFrameResource->PassCB->Resource();
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Opaque]);
// Mark the visible mirror pixels in the stencil buffer with the value 1
//将模板缓冲区可见的镜面像素标记为1
mCommandList->OMSetStencilRef(1);
mCommandList->SetPipelineState(mPSOs["markStencilMirrors"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Mirrors]);
// Draw the reflection into the mirror only (only for pixels where the stencil buffer is 1).
// Note that we must supply a different per-pass constant buffer--one with the lights reflected.
//只绘制镜子范围内模板缓冲区中标记为1的像素
//必须使用两个单独渲染过程常量缓冲区来完成工作,一个存储物体镜像,一个存储光照镜像
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress() + 1 * passCBByteSize);
mCommandList->SetPipelineState(mPSOs["drawStencilReflections"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Reflected]);
// Restore main pass constants and stencil ref.
//恢复主渲染过程常量数据以及模板参考值
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress());
mCommandList->OMSetStencilRef(0);
// Draw mirror with transparency so reflection blends through.
//绘制透明镜面使镜像与之混合。
mCommandList->SetPipelineState(mPSOs["transparent"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Transparent]);
绘制RenderLayer::Reflected层的时候如修改其渲染过程常量缓冲区,在绘制物体镜像的同事,还设计场景中光照的镜像(即物体的镜像也要有与之对应的光照)。光源本存于渲染过程常量缓冲区中,因此可以再额外创建一个渲染过程常量缓冲区,泳衣存储场景中的光照镜像,该常量缓冲的设置方法:
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);
// Reflect the lighting.
//光照镜像
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);
}
// Reflected pass stored in index 1
//将光照镜像的渲染过程常量数据存于渲染过程常量缓冲区中索引1的位置
auto currPassCB = mCurrFrameResource->PassCB.get();
currPassCB->CopyData(1, mReflectedPassCB);
}
11.4.4绕序与镜像
当一个三角形被反射到某平面时,其绕序并不会发生改变,因此其平面法线的方向同样保持不变,所以实际物体的外向法线在镜像中则变为了内向法线,为了纠正这一点,需要告知Direct3D将逆时针绕序的三角形看做是正面朝向,而将顺时针绕序的三角形看作背面朝向,实际上是对法线的方向进行了反射,以此使镜像称为外向朝向。可以通过设置下列PSO的光栅化属性来改变绕序的约定
drawReflectionsPsoDesc.RasterizerState.FrontCounterClockWise = true;
11.5实现平面阴影
需要借助几何建模的方式找到物体经光照投向平面的阴影,再运用表示阴影的50%透明度黑色材质来渲染阴影区域中的三角形,这有可能会导致“双重混合”的问题。
11.5.1平行光阴影

给定方向为L的平行光源,并用r(t)=p+tLr(t)=p+tLr(t)=p+tL来标识途径点P的光线。光线r(t)与平面(n,d)交点为s。以此光源射出的光线照射到物体的各个顶点,用这些映射到平面上的交点集合便可以定义几何体所投射出的阴影形状。对点p来说,它的阴影投射可由下列公式求出:
s=r(ts)=p−n⋅p+dn⋅Ls=r(t_s)=p-\frac{n·p+d}{n·L}s=r(ts)=p−n⋅Ln⋅p+d
光线与平面的相交测试细节在书中附录C中。
矩阵则为
[n⋅L−Lxnx−Lynx−Lznx0−Lxnyn⋅L−Lyny−Lzny0−Lxnz−Lynzn⋅L−Lznz0−Lxd−Lyd−Lzdn⋅L] \begin{bmatrix}
n·L-L_xn_x & -L_yn_x & -L_zn_x&0\\
-L_xn_y & n·L-L_yn_y & -L_zn_y&0 \\
-L_xn_z&-L_yn_z& n·L-L_zn_z&0\\
-L_xd&-L_yd&-L_zd&n·L
\end{bmatrix}⎣⎢⎢⎡n⋅L−Lxnx−Lxny−Lxnz−Lxd−Lynxn⋅L−Lyny−Lynz−Lyd−Lznx−Lznyn⋅L−Lznz−Lzd000n⋅L⎦⎥⎥⎤
上述矩阵称为方向光阴影矩阵(directional shadow matrix)用sdirs_dirsdir表示。

为运用阴影矩阵,需要将它和世界矩阵结合。但在世界变换之后,由于透视除法尚未执行,因而几何体的阴影还没有被投射到阴影平面上,此时便出现了一个问题:若sw=n⋅L<0s_w=n·L<0sw=n⋅L<0,则w的坐标会成为负值。在透视过程中,一般将z坐标复制到w坐标,若w坐标为负值则表明这个点位于视野范围外而应该被裁减掉(裁减操作会在透视除法之前的齐次空间执行),除了计算透视除法外,还要用w坐标来实现阴影效果。
为了纠正这个问题,需要一个指向无穷远处的光源的方向向量来取代光线的方向向量L。
11.5.2点光投影

从点光源发出经任意点p的光线可由r(t)=p+t(p−L)r(t)=p+t(p-L)r(t)=p+t(p−L)来表示,其阴影投影可表示为:
s=r(ts)=p−n⋅p+dn⋅(p−L)(p−L)s=r(t_s)=p-\frac{n·p+d}{n·(p-L)}(p-L)s=r(ts)=p−n⋅(p−L)n⋅p+d(p−L)
[n⋅L+d−Lxnx−Lynx−Lznx−nx−Lxnyn⋅L+d−Lyny−Lzny−ny−Lxnz−Lynzn⋅L+d−Lznz−nz−Lxd−Lyd−Lzdn⋅L] \begin{bmatrix}
n·L+d-L_xn_x & -L_yn_x & -L_zn_x&-n_x\\
-L_xn_y & n·L+d-L_yn_y & -L_zn_y&-n_y \\
-L_xn_z&-L_yn_z& n·L+d-L_zn_z&-n_z\\
-L_xd&-L_yd&-L_zd&n·L
\end{bmatrix}⎣⎢⎢⎡n⋅L+d−Lxnx−Lxny−Lxnz−Lxd−Lynxn⋅L+d−Lyny−Lynz−Lyd−Lznx−Lznyn⋅L+d−Lznz−Lzd−nx−ny−nzn⋅L⎦⎥⎥⎤
对于点光和平行光L充当不同角色,使用点光时L定义了光源位置,在使用平行光时,L用来定义指向无穷远处光源的方向向量(与平行光光源方向相反的向量)
11.5.3通用阴影矩阵
可以通过齐次坐标创建一个同事应用于点光与方向光的通用阴影矩阵。
[n⋅L+dLw−Lxnx−Lynx−Lznx−nx−Lxnyn⋅L+dLw−Lyny−Lzny−ny−Lxnz−Lynzn⋅L+dLw−Lznz−nz−Lxd−Lyd−Lzdn⋅L] \begin{bmatrix}
n·L+dL_w-L_xn_x & -L_yn_x & -L_zn_x&-n_x\\
-L_xn_y & n·L+dL_w-L_yn_y & -L_zn_y&-n_y \\
-L_xn_z&-L_yn_z& n·L+dL_w-L_zn_z&-n_z\\
-L_xd&-L_yd&-L_zd&n·L
\end{bmatrix}⎣⎢⎢⎡n⋅L+dLw−Lxnx−Lxny−Lxnz−Lxd−Lynxn⋅L+dLw−Lyny−Lynz−Lyd−Lznx−Lznyn⋅L+dLw−Lznz−Lzd−nx−ny−nzn⋅L⎦⎥⎥⎤
若Lw=0L_w=0Lw=0就是的平行光阴影矩阵,Lw=1L_w=1Lw=1则是点光源阴影
DirectX的数学库提供了以下函数,w=0表示平行光,w=1表示点光
inline XMMATRIX XM_CALLCONV XMMatrixShadow(
FXMVECTOR ShadowPlane,
FXMVECTOR LightPosition);
11.5.4使用模板缓冲防止双重混合
将物体的几何形状投影到平面阴影时可能会出现两个甚至更多平面阴影三角形之间相互重叠,如果使用透明度这一混合技术渲染阴影,则这些三角形的重叠部分会被混合多次,使之看起来更暗,该问题可以通过模板缓冲区来解决。
1. 首先保证参与渲染阴影的模板缓冲区中的阴影范围像素都已经被清0
2. 设置模板测试,使之仅接受模板缓冲区元素为9的像素,如果通过模板测试,则相应模板缓冲区值增为1。
在第一次渲染阴影像素时,由于模板缓冲区元素为0,因而模板测试成功,渲染该像素的同时,会把模板缓冲区元素的值增为1,这样一来,如果想要覆写已经被渲染过的区域,模板测试会失败,就可以防止同一个像素被多次绘制,阻止双重混合的发生。
11.5.5编写阴影部分的代码
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;
// We are going to draw shadows with transparency, so base it off the transparency description.
//为了防止双重混合,用下列的深度/模板缓冲区来设置PSO
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;
// We are not rendering backfacing polygons, so these settings do not matter.
//不渲染背面朝向的多边形,因此这些设置无关紧要
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"])));
// Draw shadows
//绘制阴影
mCommandList->SetPipelineState(mPSOs["shadow"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Shadow]);
// Update shadow world matrix.
//更新阴影矩阵
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);
XMStoreFloat4x4(&mShadowedSkullRitem->World, skullWorld * S * shadowOffsetY);
我们将投影网格沿y轴做了少量的偏移,以防止发生深度冲突(z-fighting),阴影网格不会与地面网格相交,而是略高于地面,如果这两个网格相交,会因为深度缓冲区的精度限制而地板与阴影的网格像素会为了各自的显示而发生闪烁现象。
11.6 小结
- 模板缓冲区是一种离屏缓冲区,可以通过它来阻止特定像素片段向后台缓冲区的渲染操作。
- 是否可以向特定像素执行写操作取决于模板测试。
- 深度/模板状态是PSO的一部分。
- 需要传入一个无符号整数来作为模板参考值。
深度复杂性
深度复杂性是指通过深度测试,向后台缓冲区中某一像素特定元素写入像素片段的次数。例如一个本已绘制好的像素可能会被更靠近摄像机的像素覆写。

显卡可能会每一帧将某一像素填充多次,这种重复绘制会对性能造成影响。度量场景中的深度复杂性对性能分析是很有意义的。
具体实现方法就看书吧,写得很详尽惹,大概就是用模板缓冲区来进行计数,再令颜色与深度复杂性关联,从而实现深度复杂性的可视化。
- 现代硬件还会对像素进行“提前z测试”而将之后处理过程中将被剔除的像素片段提前剔除,若想使用这种优化技术,应该按照距离摄像机由近及远的渲染非混合的游戏对象。如果处理的场景因较高的深度复杂性而需要大量的重新绘制操作,那么提前z测试可以带来很好的优化效果。并不能通过Direct3D API来控制提前z测试,负责该功能的一般是图形驱动。例如:如果像素着色器将修改像素片段的深度值,则不会执行提前z测试。
本文是关于DirectX 12中3D游戏开发的学习笔记,主要探讨了模板缓冲区的使用,包括深度/模板缓冲区的格式、模板测试、创建和绑定深度/模板状态,以及如何实现平面镜和阴影效果。通过模板缓冲区,可以阻止特定像素的绘制,实现如镜像和阴影等特效。文章还介绍了模板和深度信息的相关设置,以及如何防止双重混合和深度复杂性问题。

800

被折叠的 条评论
为什么被折叠?



