DirectX12 3D 游戏开发与实战第十一章内容
仅供个人学习使用,请勿转载。谢谢!
11、模板
模板缓冲区(stencil buffer)是一种“离屏”(off-screen)缓冲区,我们可以利用它来实现一些效果。模板缓冲区、后台缓冲区以及深度缓冲区都拥有相同的分辨率,这三者相同位置上的像素可以一一对应。由4.1.5节可知,模板缓冲区要和一个深度缓冲区配合使用。模板缓冲区的作用就如同印刷过程中所使用的模板一样,我们可以用它阻止特定的像素片段渲染到后台缓冲区。(比如绘制一面镜子的时候,我们可以使用模板缓冲区来组织镜子范围之外的镜像部分的绘制操作)
如果要设置模板缓冲区,我们需要填写D3D12_DEPTH_STENCIL_DESC结构体实例,并将其赋予流水线状态对象PSO的D3D12_GRAPHCI_PIPELINE_STATE_DESC::DepthStencilState字段。
学习目标
- 探究如何通过填写流水线状态对象中的D3D12_DEPTH_STENCIL_DESC DepthStencilState字段来控制深度缓冲区和模板缓冲区
- 学习如何通过模板缓冲区来防止镜像被绘制在镜子之外的区域,以此来实现正确的镜像效果
- 了解双重混合(double blending)的机制。从而利用模板缓冲区来有效的杜绝这一情况的发生
- 了解深度复杂性(depth complexity)的概念,并介绍两种方法来度量场景的深度复杂性
11.1、深度/模板缓冲区的格式及其资源数据的清理
由前文的知识可知,深度/模板缓冲区其实也是一种纹理, 因此必须使用特定的数据格式来创建。深度/模板缓冲可用的格式如下:
- DXGI_FORMAT_D32_FLOAT_S8X24_UINT:此格式使用一个32位浮点数来指定深度缓冲区,并使用另一个32位无符号整数来指定模板缓冲区。其中,无符号整数里的8位用于将模板缓冲区映射到范围[0, 255]区间,另外24位不可用,仅用作填充占位
- DXGI_FORMAT_D24_UNORM_S8_UINT:指定一个无符号整数的24位深度缓冲区,并将其映射到范围[0, 1]中,另外8位用于令模板缓冲区映射到范围[0, 255]中
我们可以在绘制每一帧画面的开始处,使用下列方法来重置模板缓冲区中的局部数据(也可以用于清理深度缓冲区)
void STDMETHODCALLTYPE ClearDepthStencilView(
D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView,
D3D12_CLEAR_FLAGS ClearFlags,
FLOAT Depth,
UINT8 Stencil,
UINT NumRects,
const D3D12_RECT *pRects);
我们会在每一帧调用此方法:例如:
mCommandList->ClearDepthStencilView(DepthStencilView(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
11.2、模板测试
我们会通过模板缓冲区来阻止对后台缓冲区特定区域的绘制行为,而这项操作的实质是由模板测试完成的,它的处理过程如下:
if(StencilRef & StencilReadMask 比较函数 Value & StencilReadMask)
{
accept pixel;
}
else
{
reject pixel;
}
模板测试会随着像素的光栅化过程而执行(即在输出合并阶段),如果开启了模板功能,则需要经过下面两则运算:
- 左运算数(left-hand_side)由程序中定义的模板参考值(stencil reference value)StencilRef和程序内定义的掩码值(masking value)StencilReadMask通过AND(与)运算来加以确定
- 右运算数(right-hand-side)由正在接受模板测试的特定像素位于模板缓冲区中的对应值value和程序中定义的掩码值StencilReadMask经过AND运算加以确定
接下来,模板测试用程序中所选定的比较函数对左运算数和右运算数进行比较,如果返回的布尔类型为true,则将当前接受模板测试的像素写入后台缓冲区中,否则则禁止该像素写入后台缓冲区中。
比较函数是下列D3D12_COMPARISON_FUNC枚举类型所定义的比较函数其中之一:
typedef 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;
11.3、描述深度/模板状态
要描述深度/模板状态,就要填写D3D12_DEPTH_STENCIL_DESC实例:
typedef struct D3D12_DEPTH_STENCIL_DESC
{
//深度信息
BOOL DepthEnable;
D3D12_DEPTH_WRITE_MASK DepthWriteMask;
D3D12_COMPARISON_FUNC DepthFunc;
BOOL StencilEnable;
//模板信息
UINT8 StencilReadMask;
UINT8 StencilWriteMask;
D3D12_DEPTH_STENCILOP_DESC FrontFace;
D3D12_DEPTH_STENCILOP_DESC BackFace;
} D3D12_DEPTH_STENCIL_DESC;
11.3.1、深度信息的相关设置
- DepthEnable:设置为true,则开启深度测试,否则禁用深度测试。如果禁用深度测试,则深度缓冲区中的元素不会被更新,DepthWriteMask项的设置也不会起作用
- DepthWriteMask:可以将该参数设置为D3D12_DEPTH_WRITE_MASK_ZERO或者D3D12_DEPTH_WRITE_MASK_ALL,如果为D3D12_DEPTH_WRITE_MASK_ZERO,便会禁止对深度缓冲区的写操作,但是仍然可以执行深度测试。如果将该项设置为D3D12_DEPTH_WRITE_MASK_ALL,则通过深度测试和模板测试的深度数据将会被写入深度缓冲区中。简单而言,这个设置是用来控制深度数据读写的能力。
- DepthFunc:该参数为枚举类型D3D12_COMPARISON_FUNC的成员之一,用来定义深度测试的比较函数
11.3.2、模板信息的相关设置
- StencilEnable:是否开启模板测试
- StencilReadMask:程序内定义的掩码值,由于模板测试(详情看11.2节)
- StencilWriteMask:当模板缓冲区被更新时,我们可以通过写掩码来屏蔽特定位的写入操作
- FrontFace:填写一个D3D12_DEPTH_STENCILOP_DESC结构体实例,以此指出根据深度测试和模板测试的结果,应对正面朝向的三角形要进行何种运算
- BackFace:同上,只不过该参数是讨论应对背面朝向的三角形要进行何种运算。
这里提一下结构体D3D12_DEPTH_STENCILOP_DESC结构体和D3D12_STENCIL_OP枚举类型
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_STENCIL_OP_KEEP
} D3D12_DEPTH_STENCILOP_DESC;
11.3.3、创建和绑定深度/模板状态
一旦将描述深度/模板状态的D3D12_DEPTH_STENCIL_DESC实例填写完整之后,我们就可以将其赋予PSO的D3D12_GRAPHICS_PIPELINE_STATE_DESC::DepthStencilState字段了。而使用该PSO绘制的几何体,都会根据上述深度/模板设置来进行渲染。
这里忘记提模板参考值的设置了,我们可以用ID3D12GraphicsCommandList::OMSetStencilRef方法来实现:
// 将模板参考值设置为1。
mConnandList->OMSetStencilRef(1);
11.4、实现平面镜效果
实现平面镜效果主要有两个问题:
- 了解任意平面反射物体的相关原理,以此来正确的绘制镜像
- 一定要将镜像绘制在镜子中
第一个问题可以利用解析几何学的知识解决,这里不过多介绍,第二个问题可以用模板缓冲区解决。
11.4.1、镜像概述
- 将地板,墙壁以及骷髅头渲染到后台缓冲区中(不包括镜子,此步骤不修改模板缓冲区)。
- 清理模板缓冲区,将其整体置为0。
- 仅将镜面渲染到模板缓冲区中。在绘制镜面的时候,我们会将模板测试设置为每次都成功,并且通过测试时使用1(模板参考值,StencilRef)来替换模板缓冲区元素,由于仅向模板缓冲区中绘制了镜面,因此在模板缓冲区中,除了镜面对应的像素为1之外,其他像素均为0.
- 现在我们可以将骷髅头的镜像渲染到后台缓冲区和和模板缓冲区了,前面曾提到,只有通过模板测试的像素才可以渲染到后台缓冲区中,所以我们只需要设置为当模板缓冲区中的值为1时,才可以通过模板测试即可。这样便可以将骷髅头的镜像限制在镜子区域内
- 最后,我们需要将镜面渲染到后台缓冲区中。但是,为了能透过镜面观察到骷髅头,我们需要将镜子材质的alpha设置为0.3,即镜子的不透明度为30%。(因为骷髅头像的镜像深度值小于镜子的深度值,不这样设置的话镜子会挡住骷髅头镜像)
11.4.2、定义镜像的深度/模板状态
为了实现上述算法,我们需要使用两个PSO对象,第一个用于绘制镜面时标记模板缓冲区内镜面部分的像素(模板参考值为1的像素点),第二个用于绘制镜面可见部分内的骷髅头镜像
//
// 用于标记模板缓冲区中镜面部分的像素
//
// 禁止对渲染目标的写操作
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;
// 我们不渲染背面朝向的三角形,因此对这些参数的设置可以随意
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"])));
11.4.3、绘制场景
以下代码概述了场景的绘制流程,为了清晰和简略,我们这里省略了许多细节。
// 绘制透明的物体———地板、骷髅头等
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]);
// 只绘制镜子范围内的镜像(即仅绘制模板缓冲区中标志为1的像素)
// 注意,这里我们需要使用两个单独的渲染过程常量缓冲区来完成这个工作,一个储存物体镜像,一个储存光照镜像
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]);
这里还有一个小问题:即在绘制RenderLayer::Reflected层的时候如何来修改其渲染过程常量缓冲区。因为在绘制物体镜像的时候,我们还需要考虑场景中光照的镜像。光源本来就存在于渲染过程常量缓冲区中,因此我们可以再创建一个渲染过程常量缓冲区,用于储存镜像中的光源。该常量缓冲区的设置方法如下:
PassConstants StencilApp::mMainPassCB;
PassConstants StencilAPP::mReflectedPassCB;
void StencilApp::UpdateReflectedPassCB(const GameTimer& gt)
{
mReflectedPassCB = mMainPassCB;
// xy平面
XMVECTOR mirrorPlane = XMVectorSet(0.0f,1.0f,0.0f);
XMMATRIX R = XMMatrixReflect(mirrorPlane);
//光照映像
for(int i = 0;i < 3;++i)
{
XMVECTOR lightDir = XMLoadFloat3(&mMainPassCB.Light[i].Direction);
XMVECTOR reflectedLightDir = XMVector3TransformNormal(lightDir,R);
XMStoreFloat3(&mReflectedPassCB.Light[i].Direction,reflectedLightDir);
}
// 将光照镜像的渲染过程常量数据存储再渲染过程常量缓冲区中索引1的位置
auto currPassCB = mCurrFrameResource->PassCB.get();
currPassCB->CopyData(1,mReflectionPassCB);
}
11.4.4、绕序和镜像
三角形镜像的绕序并不会发生改变,因此其平面法线的方向也同样保持不变。所以,实际物体的外向法线再镜像中则变成了内向法线。为了纠正这一点,我们需要告知Direct3D将镜像中的三角形绕序为逆时针的视为正面朝向,顺时针绕序为背面朝向。我们可以通过光栅化属性来改变绕序的约定:
drawReflectionPsoDesc.RasterizerState.FrontCounterClockwise = true
11.5、实现平面阴影
我们必须借助几何建模的方式来找到物体经光照投向平面的镜像,从而渲染处平面阴影效果。这可以用一些3D数学知识来实现。然后我们需要运用表示阴影的50%透明度黑色材质来渲染阴影区域中的三角形即可。
11.5.1、平行光阴影
略
11.5.2、点光阴影
略
11.5.3、通用阴影矩阵
我们可以通过齐次坐标创建出一个可以同时应用于方向光和点光的通用阴影矩阵:
- 如果L = 0;则L表示指向无穷远处的光源的方向向量(即与方向光源传播方向相反的向量)
- 如果L = 1;则L表示点光源的位置
具体的阴影矩阵这里不展示了。
DirectX的数学库提供了以下函数,用以构建在特定平面内投射阴影所用到的相应的阴影矩阵。(若w = 0表示平行光,w = 1表示点光)
inline XMMATRIX XM_CALLCONV XMMatrixShadow(
FXMVECTOR ShadowPlane,
FXMVECTOR LightPosition);
11.5.4、使用模板缓冲区防止双重混合
将物体的几何形状投影到平面而形成阴影的时候,可能会有两个甚至更多的平面阴影三角形相互重叠。若此时采用透明度混合这一技术来渲染阴影,则这些三角形的重叠部分会混合多次,使之看起来更暗。这个问题可以用模板缓冲区解决:
- 保证参与渲染阴影的模板缓冲区中的阴影范围像素都已经被清理为0
- 设置模板测试。使之只接受模板缓冲区中元素为0的像素,如果通过模板测试,则将相应模板缓冲区设置为1
在第一次渲染阴影像素时,由于模板缓冲区元素为0,阴影像素可以通过模板测试。但是重叠的像素下一次想要覆写该像素时将无法通过模板测试,因为模板缓冲区元素已经在第一次成功之后被设置为1了。
11.5.5、编写阴影部分的代码
我们把用于绘制阴影的材质定义为具有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->FrenseIRo = XMFLOAT3(0.001f,0.001f,0.001f);
shadowMat->Roughness = 0.0f;
为了防止双重混合,我们用下列的深度/模板状态来设置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.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_QEUAL;
D3D12_GRAPHICS_PIPELINE_STATE_DESC shadowPsoDesc = transparentPsoDesc;
shadowPsoDesc.DepthStencilState = shadowDSS;
ThrowIfFailed(md3dDevice->CreateGraphicsPipeLineState(
&shadowPsoDesc,IID_PPV_ARGS(&mPSOs["shadow"])));
接着用StencilRef值为0的阴影PSO来绘制骷髅头阴影
// 绘制阴影
mCommandList->OMSetStencilRef(0);
mCommandList->SetPipelineState(mPSOs["shadow".Get()]);
DrawRenderItems(mCommandList.Get(),mRitemLayer[(int)RenderLayer::Shadow]);
在这里,骷髅头阴影渲染项的世界矩阵是这样计算的:
XMVECTOR shadowPlane = SMVectorSet(0.0f,1.0f,0.0f,0.0f); // xz平面
XMVECTOR toMainLight = -XMLoadFloat3(&mMainPassCB.Light[0].Direction);
XMMATRIX S = XMMatrixShadow(shadowPlane,toMainLight);
XMMATRIX shadowOffsetY = XMMatrixTranslation(0.0f,0.001f,0.0f);
XMStoreFloat4x4(&mShadowedSkullRitem->World,skullWorld*S*shadowOffsetY);
注意点:我们将会把投影网格沿着y轴做了一点偏移,以防止发生深度冲突,所以阴影网格会比地板略高。如果阴影网格和地板网格相交,则由于深度缓冲区的精度限制,将导致地板和阴影的网格像素为了各自的完全显现而出现闪烁想象。
11.5.6、示例效果
11.6、小结
1、模板缓冲区是一种离屏缓冲区,我们可以通过它来阻止特定像素片段向后台缓冲区的渲染操作
2、是否可以向特定的像素执行写操作取决于模板测试,该测试的具体过程如下:
if(StencilRef & StencilReadMask 比较函数 Value & StencilReadMask)
{
accept pixel;
}
else
{
reject pixel;
}
其中,比较函数是枚举类型D3D12_COMPARISON_FUNC中定义的函数之一。StencilRef、StencilReadMask和比较运算符都是程序中定义且以Direct深度/模板API来设置的数值。Value即当前模板缓冲区的像素值(模板参考值)。
3、深度/模板状态时PSO描述的一部分,通过填写D3D12_GRAPHCIS_PIPELINE_STATE_DESC::DepthStencilState字段即可配置深度/模板状态。而DepthStencilState的类型为D3D12_DEPTH_STENCIL_DESC。
4、模板参考值(模板缓冲区中的像素值)由ID3D12GraphicsCommandList::OMSetStencilRef方法来设置,如:
// 将模板参考值设置为1
mCommandList->OMSetStencilRef(1);