Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试

代码工程地址:

https://github.com/jiabaodan/Direct12BookReadingNotes



模板缓冲(stencil buffer)状态是通过配置一个D3D12_DEPTH_STENCIL_DESC实例,并且赋值到PSO中的D3D12_GRAPHICS_PIPELINE_STATE_DESC::DepthStencilState



学习目标

  1. 学习如何通过配置一个D3D12_DEPTH_STENCIL_DESC对象在PSO中控制模板和深度缓冲状态;
  2. 学习如何通过模板测试来实现一个镜子;
  3. 能够鉴定双混合并且理解如何使用模板缓冲来防止它的发生;
  4. 解释深度复杂,描述两种在场景中测量深度复杂的方法。


1 深度/模板格式和清空

回顾深度/模板缓冲是一个纹理,所以它必须使用特定的格式,它们可以使用的格式如下:

  1. DXGI_FORMAT_D32_FLOAT_S8X24_UINT: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,
	FLOAT Depth,
	UINT8 Stencil,
	UINT NumRects,
	const D3D12_RECT *pRects);
  1. DepthStencilView:深度/模板缓冲的描述;
  2. ClearFlags:指定要清空的对象(深度还是模板);
  3. Depth:设置到每个像素的深度缓冲的浮点数值,范围是0到1;
  4. Stencil:设置到每个像素的模板缓冲的整数值,范围是0到255;
  5. NumRects:数组指针pRects中的矩形的数量;
  6. pRects:D3D12_RECTs数组,用以指定要清空的矩形区间,设置为nullptr代表清空整个深度/模板缓冲。

我们已经在我们的Demo中每帧都调用一次:

mCommandList->ClearDepthStencilView(DepthStencilView(),
	D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL,
	1.0f, 0, 0, nullptr);


2 模板测试

作为预先的状态,我们可以使用模板缓冲来阻止渲染到后置缓冲的特定部分:

if( StencilRef & StencilReadMask CF Value & StencilReadMask )
	accept pixel
else
	reject pixel

模板测试是在像素光栅化后进行的(在输出合并阶段),如果模板测试是开启的,那么会进行2个操作:

  1. 一个左手运算符,通过程序定义的模板引用值(StencilRef)和程序定义的遮罩值(StencilReadMask)的与运算来定义。
  2. 一个右手运算符,通过在整个模板缓冲中特定的像素的值(Value)和程序定义的遮罩值(StencilReadMask)的与运算来定义。

StencilReadMask对于LHS和RHS是一样的。模板测试通过程序选择的比较函数来比较LHS和RHS,然后返回True或者False。如果返回为false,就不会将像素写入后置缓冲中。(也不会写入深度缓冲中)。
比较函数在D3D12_COMPARISON_FUNC枚举类型中:

typedef enum D3D12_COMPARISON_FUNC
{
	D3D12_COMPARISON_NEVER = 1,
	D3D12_COMPARISON_LESS = 2,
	D3D12_COMPARISON_EQUAL = 3,
	D3D12_COMPARISON_LESS_EQUAL = 4,
	D3D12_COMPARISON_GREATER = 5,
	D3D12_COMPARISON_NOT_EQUAL = 6,
	D3D12_COMPARISON_GREATER_EQUAL = 7,
	D3D12_COMPARISON_ALWAYS = 8,
} D3D12_COMPARISON_FUNC;
  1. D3D12_COMPARISON_NEVER :直接返回false;
  2. D3D12_COMPARISON_LESS :<;
  3. D3D12_COMPARISON_EQUAL :==;
  4. D3D12_COMPARISON_LESS_EQUAL :<=;
  5. D3D12_COMPARISON_GREATER :>;
  6. D3D12_COMPARISON_NOT_EQUAL :!=;
  7. D3D12_COMPARISON_GREATER_EQUAL :>=;
  8. D3D12_COMPARISON_ALWAYS :直接返回true。


3 描述深度/模板状态

深度/模板状态通过赋值一个D3D12_DEPTH_STENCIL_DESC结构对象来定义:

typedef struct D3D12_DEPTH_STENCIL_DESC {
	BOOL DepthEnable; // Default True
	// Default: D3D11_DEPTH_WRITE_MASK_ALL
	D3D12_DEPTH_WRITE_MASK DepthWriteMask;
	// Default: D3D11_COMPARISON_LESS
	D3D12_COMPARISON_FUNC DepthFunc;
	BOOL StencilEnable; // Default: False
	UINT8 StencilReadMask; // Default: 0xff
	UINT8 StencilWriteMask; // Default: 0xff
	D3D12_DEPTH_STENCILOP_DESC FrontFace;
	D3D12_DEPTH_STENCILOP_DESC BackFace;
} D3D12_DEPTH_STENCIL_DESC;

3.1 深度设置

  1. DepthEnable:深度缓冲开关,如果关闭,深度缓冲的值也不会再更新,并且无视DepthWriteMask的设置;
  2. DepthWriteMask:可以是D3D12_DEPTH_WRITE_MASK_ZERO或者D3D12_DEPTH_WRITE_MASK_ALL,如果设置的是true,该值设置的是D3D12_DEPTH_WRITE_MASK_ZERO,那么深度信息不会写入深度缓冲,但是还是会做深度测试;
  3. DepthFunc:D3D12_COMPARISON_FUNC枚举中的一个类型;一般情况下使用D3D12_COMPARISON_LESS。

3.2 模板设置

  1. StencilEnable:模板测试开关;
  2. StencilReadMask:用以模板测试:
if( StencilRef & StencilReadMask CF Value & StencilReadMask )
	accept pixel
else
	reject pixel

默认值不遮挡任何位:

#define D3D12_DEFAULT_STENCIL_READ_MASK ( 0xff )
  1. StencilWriteMask:模板缓冲被更新的时候,可以遮挡对应位被写入,比如你不想写入前4位,你可以设置为 0x0f。默认值为不遮挡任何位:
#define D3D12_DEFAULT_STENCIL_WRITE_MASK ( 0xff )
  1. FrontFace:填充一个D3D12_DEPTH_STENCILOP_DESC对象,用以指定模板缓冲如何对前向三角面工作;
  2. BackFace:填充一个D3D12_DEPTH_STENCILOP_DESC对象,用以指定模板缓冲如何对后向三角面工作;
typedef struct D3D12_DEPTH_STENCILOP_DESC {
	D3D12_STENCIL_OP StencilFailOp; // Default: D3D12_STENCIL_OP_KEEP
	D3D12_STENCIL_OP StencilDepthFailOp; // Default: D3D12_STENCIL_OP_KEEP
	D3D12_STENCIL_OP StencilPassOp; // Default: D3D12_STENCIL_OP_KEEP
	D3D12_COMPARISON_FUNC StencilFunc; // Default: D3D12_COMPARISON_ALWAYS
} D3D12_DEPTH_STENCILOP_DESC;
  1. StencilFailOp:D3D12_STENCIL_OP枚举类型的值,用来描述当模板测试失败的时候,模板缓冲中的像素如何被更新;
  2. StencilDepthFailOp:D3D12_STENCIL_OP枚举类型的值,用来描述当模板测试通过,但是深度测试失败的时候,模板缓冲中的像素如何被更新;
  3. StencilPassOp:D3D12_STENCIL_OP枚举类型的值,用来描述当模板和深度测试都通过的时候,模板缓冲中的像素如何被更新;
  4. 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;
  1. D3D12_STENCIL_OP_KEEP :不改变模板缓冲,保持现有的值;
  2. D3D12_STENCIL_OP_ZERO :写为0;
  3. D3D12_STENCIL_OP_REPLACE :将整个模板缓冲替换为StencilRef的值,StencilRef值是在绑定深度/模板状态到渲染管线时设置的;
  4. D3D12_STENCIL_OP_INCR_SAT :增加整个模板缓冲中的值,如果达到了最大值,就保持在最大值;
  5. D3D12_STENCIL_OP_DECR_SAT :减小整个模板缓冲中的值,如果达到了0,就保持在0;
  6. D3D12_STENCIL_OP_INVERT :将整个模板缓冲中的值按位非;
  7. D3D12_STENCIL_OP_INCR :增加整个模板缓冲的值,如果达到了最大值,就设置为0;
  8. D3D12_STENCIL_OP_DECR :减少整个模板缓冲的值,如果达到了0,就设置为最大值;

在模板缓冲中,前向面和后向面的设置是可以不同的;如果我们设置了背面裁切,那后向面的设置就是无效的。


3.3 创建和绑定深度/模板状态

当我们填充好D3D12_DEPTH_STENCIL_DESC应用来描述我们的深度/模板状态后,我们可以把它指定到PSO中的D3D12_GRAPHICS_PIPELINE_STATE_DESC::DepthStencilState。
模板引用值可以通过方法ID3D12GraphicsCommandList::OMSetStencilRef来设置(一个单独的无符号整形):

mCommandList->OMSetStencilRef(1);


4 实现平面镜


4.1 镜面概述

当我们绘制反射的时候,我们将灯光也反射过去,否则光照会不准确。
如下图所示,被反射的物体就是场景中的另一个物体,如果它没有被任何遮挡,那么可以直接看到它;我们通过模板测试让它只显示在镜子上。主要步骤如下:
Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第1张图片

  1. 正常渲染墙,地面和骷髅到后置缓冲中,这个步骤不修改模板缓冲;
  2. 清空模板缓冲为0,下图为当前后置缓冲和模板缓冲的状态;
    Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第2张图片
  3. 将镜子只渲染到模板缓冲中,我们可以通过禁用写到后置缓冲的颜色通道:
D3D12_RENDER_TARGET_BLEND_DESC::RenderTargetWriteMask = 0;

然后禁用写到深度缓冲:

D3D12_DEPTH_STENCIL_DESC::DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;

当我们渲染镜子到模板缓冲后,我们设置模板测试为总是成功(D3D12_COMPARISON_ALWAYS)然后指定模板缓冲如果测试通过,替换(D3D12_STENCIL_OP_REPLACE)为值1(StencilRef)。如果深度测试失败,我们指定D3D12_STENCIL_OP_KEEP,这样模板缓冲在深度测试失败的时候就不会被改变。这个时候模板缓冲中除了可见的镜子部分以外,其他部分都是不可见的(值为0),如下图:
Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第3张图片
4. 现在我们渲染反射的骷髅头到后置缓冲和模板缓冲中,但是当且仅当模板测试通过后才渲染到后置缓冲中。我们使用只有模板测试值为1时,模板测试才成功,可以设置StencilRef为1,然后模板测试运算为D3D12_COMPARISON_EQUAL。
5. 最后我们正常渲染镜子到后置缓冲中,为了不遮挡到反射的骷髅,我们需要为镜子添加透明混合。为了实现这个方案,我们为镜子添加一个新的材质,将透明度设置为30%,然后用上章中提到的透明混合状态类渲染:

auto icemirror = std::make_unique();
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;

使用下面的混合公式:
在这里插入图片描述


4.2 定义界面的深度/模板状态

为了实现之前描述的算法,我们需要2个PSO,一个用来绘制镜子到模板缓冲中;另一个用来只在标记了镜面的地方绘制反射的骷髅:

//
// PSO for marking stencil mirrors.
//
// Turn off render target writes.
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;

// We are not rendering backfacing polygons, so these settings do not
// matter.
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.
//
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"])));

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
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.
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一层的时候,我们如何改变per-pass的常量缓冲,因为灯光也是需要反射过去的。我们另外创建了一个per-pass的常量缓冲,来保存反射的灯光,这个常量缓冲设置代码如下:

PassConstants StencilApp::mMainPassCB;
PassConstants StencilApp::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
	auto currPassCB = mCurrFrameResource->PassCB.get();
	currPassCB->CopyData(1, mReflectedPassCB);
}

4.4 缠绕顺序和反射

当三角形反射过平面后,它的缠绕顺序并没有反向,所以它的法向量并没有反向。导致向外的面变成了向里的面。为了修正这个问题,我们让D3D使用逆时针防线来缠绕表示三角形的正面,顺时针表示背面。可以通过对PSO属性设置来实现:

drawReflectionsPsoDesc.RasterizerState.FrontCounterClockwise = true;

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第4张图片



5 实现平面阴影

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第5张图片
为了实现平面阴影,我们需要先找到在阴影在几何算法上是如何投射到平面上的,这个可以通过3D数学简单的得到。然后我们渲染描述阴影的三角形,使用黑色,50%半透明的材质。这种渲染阴影的方法可以介绍一种技术叫做“双混合”(“double blending”),我们利用模板缓冲来防止双混合的出现。


5.1 平行光阴影

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第6张图片
如上图所示,给出一个具有方向L的平行光,光线经过点P(r(t) = p + tL),射线r(t)和阴影平面(n, d)的交点是s。所以这组通过射线经过物体每个顶点射向阴影平面的交点就可以组成阴影。对于点P,阴影投射公式:(更多射线和平面相交的内容可以查看本书的附录C)
在这里插入图片描述
上述公式可以写成下面矩阵:
Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第7张图片
我们管这个4x4矩阵叫方向阴影矩阵并通过Sdir来表示。为了证明他们是相等的,我们可以执行乘法运算:
Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第8张图片
我们使用阴影矩阵的时候会合并到我们的世界矩阵中。执行完世界转换后,几何体还没有变成阴影,因为还没有执行透视除法。
(还没有完结,公式看得头大,以后再写 ^ @ ^)


5.2 顶点阴影

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第9张图片
(公式看得头大,本小节以后再写 ^ @ ^)


5.3 生成阴影矩阵

使用其次坐标系,可以创建一个通用的可以应用于点光源和平行光的阴影矩阵。

  1. 如果Lw = 0,那么L就描述了一个指向无限远的一个光源(平行光)
  2. 如果Lw = 1,L就描述了一个点光源。

那么阴影矩阵就可以写为:
Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第10张图片
DirectX数学库中提供了下面的函数来通过给出的平面和一个描述光方向向量(w = 0平行光;w = 1点光源)来创建阴影矩阵:

inline XMMATRIX XM_CALLCONV XMMatrixShadow(
	FXMVECTOR ShadowPlane,
	FXMVECTOR LightPosition);

如果要进一步阅读平面阴影,可以查看[Blinn96] 和 [Möller02]。


5.4 使用模板缓冲来防止双混合(Double Blending)

当我们把几何体压平到平面上的时候,可以会有多个三角形重叠到一起。这时候我们使用透明混合渲染阴影,就会出现有些地方被透明计算了多次,导致出现渲染错误。
Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第11张图片
我们可以通过模板缓冲来解决这个问题:

  1. 假设阴影需要绘制的地方的模板缓冲被重置为0;
  2. 当有阴影绘制后(模板测试成功),就把模板缓冲的值设置为1;

5.5 阴影代码:

定义阴影材质为50%透明的黑色:

auto shadowMat = std::make_unique();
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;

为了防止双混合,我们新添加一个PSO:

// We are going to draw shadows with transparency, so base it off
// the transparency description.
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"])));

然后我们使用阴影PSO并设置StencilRef为0来绘制骷髅阴影:

// Draw shadows
mCommandList->OMSetStencilRef(0);
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)。如果网格和地面交叉了,由于深度缓冲精度的限制,我们会看到一些闪烁的像素点。



6 总结

  1. 模板缓冲是一个离屏缓冲,我们使用它来防止像素被绘制到后置缓冲中。它和深度缓冲共享一个缓冲,拥有相同的分辨率。可以使用的格式有DXGI_FORMAT_D32_FLOAT_S8X24_UINT和DXGI_FORMAT_D24_UNORM_S8_UINT;
  2. 觉得一个像素是否绘制是由模板测试决定:
if( StencilRef & StencilReadMask  ST Value & StencilReadMask )
	accept pixel
else
	reject pixel
  1. 模板测试的操作符可以是D3D12_COMPARISON_FUNC枚举中的任意类型。StencilRef、StencilReadMask、StencilWriteMask和比较操作符都是通过D3D API设置;
  2. 深度/模板状态是PSO的一部分。通过填充一个D3D12_GRAPHICS_PIPELINE_STATE_DESC::DepthStencilStat对象来定义,其中DepthStencilState的类型是D3D12_DEPTH_STENCIL_DESC;
  3. 模板引用值由ID3D12GraphicsCommandList::OMSetStencilRef方法设置,是一个无符号整形。


7 练习

3. 修改本章Demo,达到下面的效果:
Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第12张图片
关闭drawStencilReflections PSO模板测试即可:

//reflectionsDSS.StencilEnable = true;
reflectionsDSS.StencilEnable = false;

4. 修改本章Demo,达到下面的效果:
Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第13张图片
修改shadow PSO,关闭模板测试即可

//shadowDSS.StencilEnable = true;
shadowDSS.StencilEnable = false;

5. 修改本章Demo,根据下面的方式。首先使用下面的设置绘制墙面

depthStencilDesc.DepthEnable = false;
depthStencilDesc.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;
depthStencilDesc.DepthFunc = D3D12_COMPARISON_LESS;

然后使用下面的设置绘制骷髅:

depthStencilDesc.DepthEnable = true;
depthStencilDesc.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;
depthStencilDesc.DepthFunc = D3D12_COMPARISON_LESS;

可以遮挡住骷髅吗?如果对墙面再次替换到下面的设置呢?

depthStencilDesc.DepthEnable = true;
depthStencilDesc.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;
depthStencilDesc.DepthFunc = D3D12_COMPARISON_LESS;

修改完前两个设置后:
Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第14张图片
骷髅没有被遮挡,因为墙面的Z值没有写入Z缓冲;对墙面替换设置后就可以准确显示,因为Z值准确写入了Z缓冲中。
Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第15张图片

6. 如果不反向Demo中三角形缠绕顺序,被反射的骷髅会准确显示吗?
Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第16张图片
无法准确显示,会只显示背面,通过修改drawStencilReflections PSO设置:

//drawReflectionsPsoDesc.RasterizerState.FrontCounterClockwise = true;
drawReflectionsPsoDesc.RasterizerState.FrontCounterClockwise = false;

8. 深度复杂度是指多个像素片元通过深度测试在同一个像素上竞争写入后置缓冲。比如一个像素可能会被另一个距离相机更近的像素覆盖。下图中P的深度复杂度是3:
Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第17张图片
显卡可能会每帧在同一个点上绘制多次,这种覆盖会影响到性能,因为它在重复绘制看不到的点。所以测量深度复杂度对优化分析就很有用。
我们可以通过下面的方法测量深度复杂度:渲染场景,并且使用模板测试作为计数器;刚开始将模板缓冲初始化为0,然后每次像素绘制,我们增加对应的模板缓冲中的值(D3D12_STENCIL_OP_INCR),模板测试公式使用D3D12_COMPARISON_ALWAYS。所以如果绘制完成后,深度缓冲中的值为5,则这个像素点被绘制了5次。值得注意的是,在计算深度复杂度的时候,你只需要绘制到模板缓冲中。

为了能够图形化的显示深度复杂度,可以根据下面的步骤:
1、对每一个深度复杂度k关联一个颜色Ck,比如蓝色是复杂度1,绿色是复杂度2,红色是复杂度3等。(对于一个非常复杂的场景,可以对多个相连的复杂度关联同一个颜色);
2、设置模板缓冲比较公式D3D12_STENCIL_OP_KEEP,表示我们不再修改它了。
3、对每一个深度复杂度等级k:
a、设置模板比较函数为D3D12_COMPARISON_EQUAL并且设置模板引用值为k;
b、绘制Ck的方块覆盖到整个投射窗口。

根据上面的步骤,我们为每一个像素基于深度复杂度绘制了颜色,我们就可以很简单的分析深度复杂度。作为这个的练习,为Demo Blending绘制深度复杂度。下面是一个屏幕截图的例子:
Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第18张图片

深度测试时在输出合并阶段执行的(像素着色器之后)。所以即使像素后续会被放弃,但是还是会进行很耗时的像素着色器计算。但是现代的显卡提供了一个叫早期Z测试(early z-test)的技术,可以让Z测试在像素着色器之前进行。为了能够得到这个技术带来的重要的好处,你需要在绘制没有混合的物体的时候,进行从前往后的顺序进行绘制,这样最近的物体会先绘制,然后被遮挡的物体会被这个early z-test技术剔除,它会对具有高深度复杂度的场景带来大量的性能提升。对于early z-test技术我们无法使用D3D API进行控制,它完全是由显卡驱动判定控制的。比如如果你的像素着色器中修改了像素的Z值,那么early z-test将无法进行。

我们刚才提到在像素着色器中修改Z值,也就是说像素着色器不仅仅只能输出颜色,还可以输出整个结构:

struct PixelOut
{
	float4 color : SV_Target;
	float depth : SV_Depth;
};

PixelOut PS(VertexOut pin)
{
	PixelOut pout;
	
	// … usual pixel work
	pout.Color = float4(litColor, alpha);
	
	// set pixel depth in normalized [0, 1] range
	pout.depth = pin.PosH.z - 0.05f;
	
	return pout;
}

SV_Position的Z元素就是没有修改的Z值,SV_Depth代表修改后输出的Z值。

这道题暂时没有写,因为觉得第九题的方案更好(主要不知道如何在Shader中访问模板缓冲的数据 ^ @ ^)

9. 另一个实现图形化深度复杂度的方法是使用叠加混合。首先将后置缓冲初始化为黑色,然后将混合因子都设置为D3D12_BLEND_ONE,然后混合公式设置为D3D12_BLEND_OP_ADD,然后我们对物体使用(0.05, 0.05, 0.05)来绘制。最终绘制出的结果就是越亮的地方,深度复杂度越高。使用上一章的Blending Demo实现它:
Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第19张图片
根据提示修改,代码在https://github.com/jiabaodan/Direct12BookReadingNotes的Chapter11_Exercises_DepthComBlending工程

10. 如何计算出通过深度测试的像素的个数?如何计算没有通过深度测试像素的个数?
使用模板缓冲???

11. 修改本章Demo,增加反射地面:
添加一个反射的地面的RenderItem即可:

// 反射的地面
auto reflectedFloorRitem = std::make_unique();
*reflectedFloorRitem = *floorRitem;
reflectedFloorRitem->ObjCBIndex = 6;

XMVECTOR mirrorPlane = XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f); // xy plane
XMMATRIX R = XMMatrixReflect(mirrorPlane);
XMStoreFloat4x4(&reflectedFloorRitem->World, XMMatrixIdentity() * R);

mRitemLayer[(int)RenderLayer::Reflected].push_back(reflectedFloorRitem.get());

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第20张图片

12. 修改本章Demo,不要在Y方向偏移骷髅的阴影,观察深度冲突(z-fighting):
Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试_第21张图片
修改下面一行代码即可:

XMStoreFloat4x4(&mShadowedSkullRitem->World, skullWorld * S/* * shadowOffsetY*/);

你可能感兴趣的:(DirectX,Direct12,游戏开发)