


























本章是 Direct3D 12 渲染管线的核心进阶章节,系统讲解模板缓冲(Stencil Buffer)技术—— 通过与深度缓冲绑定的 8 位模板缓冲,在输出合并(OM)阶段执行模板测试,实现对像素渲染的精准遮罩控制,解决了平面镜面反射、平面阴影等特殊效果的渲染难题。本章内容覆盖模板缓冲的格式规范、模板测试原理、深度 / 模板状态的 PSO 配置,两大核心工程化应用(平面镜面、平面阴影),以及基于模板缓冲的场景深度复杂度测量,是实时渲染中遮罩效果、特殊光影实现的必备技术。
模板缓冲与深度缓冲作为一个资源(纹理)共享显存,二者分辨率完全一致,每个像素的模板值与深度值一一对应,随深度缓冲一同创建、一同绑定到管线。
模板/深度缓冲有两种格式
| 格式 | 深度缓冲规格 | 模板缓冲规格 | 适用场景 |
|---|---|---|---|
| DXGI_FORMAT_D24_UNORM_S8_UINT | 24 位 UNORM 深度值(映射到 [0,1]) | 8 位无符号整数模板值(0~255) | 通用场景 |
| DXGI_FORMAT_D32_FLOAT_S8X24_UINT | 32 位浮点深度值 | 8 位无符号整数模板值,24 位填充位 | 高精度深度场景,避免 Z-fighting |
既然模板/深度缓冲是一个资源,则它可以通过第六章“资源的创建流程”中提到的CreateResource方法创建。
ID3D12Resource* DSVResource = CreateResource(device
, inResourceWidth//纹理宽
, inResourceHeight//纹理高
, D3D12_HEAP_TYPE_DEFAULT
, D3D12_RESOURCE_STATE_COMMON
, D3D12_RESOURCE_DIMENSION_TEXTURE2D
, inFormat//纹理格式
, D3D12_TEXTURE_LAYOUT_UNKNOWN);
inFormat参数一般选择上面两种格式。但也有一些特别情况,即龙书附属代码中提到的,某些特定情况下,模板/深度缓冲既要作为DSV输出,又需要作为SRV使用的话,需要特殊处理格式。
// Correction 11/12/2016: SSAO chapter requires an SRV to the depth buffer to read from
// the depth buffer. Therefore, because we need to create two views to the same resource:
// 1. SRV format: DXGI_FORMAT_R24_UNORM_X8_TYPELESS
// 2. DSV Format: DXGI_FORMAT_D24_UNORM_S8_UINT
// we need to create the depth buffer resource with a typeless format.
depthStencilDesc.Format = DXGI_FORMAT_R24G8_TYPELESS;
资源创建后,就需要通过深度模板描述符堆,绑定给管线
//创建深度模板描述符堆
ID3D12DescriptorHeap* DSVHeap = CreateDescriptorHeaps(inDevice
, D3D12_DESCRIPTOR_HEAP_TYPE_DSV
, UINT inDescriptorInHeap
, D3D12_DESCRIPTOR_HEAP_FLAG_NONE);
......
//绑定给管线DepthStencilView
D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc;
dsvDesc.Flags = D3D12_DSV_FLAG_NONE;
dsvDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
//DXGI_FORMAT_D24_UNORM_S8_UINT or DXGI_FORMAT_D32_FLOAT_S8X24_UINT
dsvDesc.Format = mDepthStencilFormat;
dsvDesc.Texture2D.MipSlice = 0;
device->CreateDepthStencilView(DSVResource
, &dsvDesc
, DSVHeap->GetCPUDescriptorHandleForHeapStart());
每帧渲染前必须清除深度/模板缓冲,否则上一帧的残留数据会导致当前帧渲染错误。使用的清理函数:ID3D12GraphicsCommandList::ClearDepthStencilView,其关键参数:
ClearFlags:可单独清除深度(D3D12_CLEAR_FLAG_DEPTH)、单独清除模板(D3D12_CLEAR_FLAG_STENCIL),或同时清除二者;
Depth:深度缓冲清除值,默认 1.0f(最远深度);
Stencil:模板缓冲清除值,默认 0,范围 0~255;
传入nullptr可清除整个缓冲,也可指定多个矩形区域清除。
模板测试在渲染管线的输出合并(OM)阶段执行,与深度测试串行执行;仅当模板测试、深度测试全部通过时,像素才会被写入后台缓冲与深度缓冲。
模板测试伪代码
if( 深度测试 不通过 )
拒绝当前像素,不写入任何缓冲
return
if( (StencilRef & StencilReadMask) 【比较运算符】 (Value & StencilReadMask) )
接受当前像素,进入深度测试
else
拒绝当前像素,不写入任何缓冲
公式核心参数
StencilRef:应用程序设置的模板参考值,全局固定,可通过 API 动态修改;
StencilReadMask:读掩码,对参考值和模板缓冲当前值做按位与运算,默认0xff(不屏蔽任何位);
Value:当前像素在模板缓冲中已存储的数值;
比较运算符:D3D12_COMPARISON_FUNC枚举,决定测试通过的条件。
| 枚举值 | 测试通过条件 | 典型用途 |
|---|---|---|
| D3D12_COMPARISON_NEVER | 永远不通过 | 完全屏蔽绘制 |
| D3D12_COMPARISON_LESS | 左值 < 右值 | 深度测试默认规则 |
| D3D12_COMPARISON_EQUAL | 左值 == 右值 | 镜面 / 阴影的遮罩绘制 |
| D3D12_COMPARISON_LESS_EQUAL | 左值 ≤ 右值 | 通用深度测试 |
| D3D12_COMPARISON_GREATER | 左值 > 右值 | 反向深度测试 |
| D3D12_COMPARISON_NOT_EQUAL | 左值!= 右值 | 特殊遮罩效果 |
| D3D12_COMPARISON_GREATER_EQUAL | 左值 ≥ 右值 | 反向深度测试 |
| D3D12_COMPARISON_ALWAYS | 永远通过 | 模板缓冲标记写入 |
模板测试失败的像素,会被直接丢弃,不会写入后台颜色缓冲,也不会写入深度缓冲。
深度/模板状态通过D3D12_DEPTH_STENCIL_DESC结构体定义,是图形管线状态对象(PSO)的核心成员,必须在 PSO 创建时预定义,运行时无法单独修改,仅能通过切换 PSO 更换状态。
D3D12_DEPTH_STENCIL_DESC的字段。
深度相关设置
DepthEnable:布尔值,开启/关闭深度缓冲;关闭后深度测试完全失效,像素绘制由绘制顺序决定,且无论写入掩码如何设置,都不会修改深度缓冲。
DepthWriteMask:深度写入掩码,仅两种取值:
D3D12_DEPTH_WRITE_MASK_ALL:开启深度写入,深度/模板测试通过后,将像素深度写入深度缓冲(默认值);
D3D12_DEPTH_WRITE_MASK_ZERO:禁用深度写入,仅执行深度测试,不修改深度缓冲,是半透明渲染、模板标记的核心配置。
DepthFunc:深度测试的比较函数,使用D3D12_COMPARISON_FUNC枚举,默认D3D12_COMPARISON_LESS(像素深度比缓冲中已有深度更近时,测试通过)。
模板相关设置
StencilEnable:布尔值,开启/关闭模板测试,默认关闭。
StencilReadMask:模板读掩码,用于模板测试的按位与运算,默认0xff(8 位全部启用)。
StencilWriteMask:模板写掩码,控制模板缓冲的哪些位可被修改,默认0xff(8 位全部可写)。
FrontFace/BackFace:D3D12_DEPTH_STENCILOP_DESC结构体,分别定义正面三角形、背面三角形(绕序决定正背)的独立模板行为;若开启背面剔除,BackFace配置完全无效。
D3D12_DEPTH_STENCILOP_DESC定义模板缓冲的更新规则
StencilFailOp,模板测试失败时,模板缓冲的更新操作
StencilDepthFailOp,模板测试通过但深度测试失败时,模板缓冲的更新操作
StencilPassOp,模板测试和深度测试都通过时,模板缓冲的更新操作
StencilFunc,定义模板测试的比较函数
模板测试操作枚举D3D12_STENCIL_OP
| 枚举值 | 操作效果 | 典型用途 |
|---|---|---|
| D3D12_STENCIL_OP_KEEP | 保持模板缓冲当前值不变(默认) | 仅做测试,不修改模板值 |
| D3D12_STENCIL_OP_ZERO | 将模板值置 0 | 重置模板区域 |
| D3D12_STENCIL_OP_REPLACE | 用StencilRef替换模板当前值 | 标记镜面 / 阴影的可绘制区域 |
| D3D12_STENCIL_OP_INCR_SAT | 模板值 + 1,超过 255 则钳位在 255(饱和递增) | 深度复杂度计数、防重复绘制 |
| D3D12_STENCIL_OP_DECR_SAT | 模板值 - 1,小于 0 则钳位在 0(饱和递减) | 特殊遮罩效果 |
| D3D12_STENCIL_OP_INVERT | 模板值按位取反 | 特效遮罩 |
| D3D12_STENCIL_OP_INCR | 模板值 + 1,超过 255 则回卷到 0(循环递增) | 多层遮罩计数 |
| D3D12_STENCIL_OP_DECR | 模板值 - 1,小于 0 则回卷到 255(循环递减) | 多层遮罩计数 |
创建和绑定深度/模板状态
填充完D3D12_DEPTH_STENCIL_DESC后,该变量再设置给D3D12_GRAPHICS_PIPELINE_STATE_DESC的DepthStencilState字段,与PSO绑定,在 PSO 创建时完成定义。
运行时无法单独修改深度/模板状态,只能通过SetPipelineState切换完整 PSO。
唯一可在运行时动态修改的是模板的参考值,通过ID3D12GraphicsCommandList::OMSetStencilRef(UINT StencilRef)。
在工程实践上,不同的模板效果需创建独立的 PSO,例如标记镜面的 PSO、绘制反射的 PSO、绘制阴影的 PSO,分别配置对应的深度 / 模板状态,在应用初始化时预创建所有 PSO。
镜面渲染的有两大问题:
几何问题:如何将物体沿镜面平面做反射变换R,得到正确的反射几何体;

渲染问题:如何保证反射仅在镜面区域显示,不会穿透墙面出现在场景其他位置(模板缓冲的核心解决场景)。
正常渲染场景中所有不透明物体(地面、墙面、原物体),不修改模板缓冲;
清除模板缓冲为 0;
切换镜面PSO,标记镜面区域:
禁用颜色写入(D3D12_RENDER_TARGET_BLEND_DESC的RenderTargetWriteMask为0);
禁用深度写入(D3D12_DEPTH_SEENCIL_DESC的DepthWriteMask为D3D12_DEPTH_WRITE_MASK_ZERO);
模板测试永远通过(D3D12_DEPTH_SEENCIL_DESC的StencilFunc为D3D12_COMPARISON_ALWAYS),测试通过(只有深度测试)后用1替换模板值(D3D12_STENCIL_OP_REPLACE,OMSetStencilRef为1)。由于先绘制原物体,再标记镜面,这样被原物体遮挡的镜面区域会因深度测试失败,不会被标记到模板缓冲中,避免反射穿透原物体显示;
最终仅镜面的可见区域模板值为 1,其他区域为 0;
绘制反射物体:模板测试仅当模板值等于1时通过,保证反射仅绘制在镜面区域:
OMSetStencilRef(1)模板参考值设置为1
切换绘制反射PSO,D3D12_DEPTH_STENCILOP_DESC的StencilFunc设置为D3D12_COMPARISON_EQUAL
通过反射矩阵变换要在镜面上绘制的物体,经过绘制出来
当反射变换后,外法线变成内法线,需要更改光栅化时三角形的绕序约定(D3D12_RASTERIZER_DESC的FrontCounterClockwise为true),以保持三角形是正面不会被背面剔除
绘制反射物体时,场景光源也需要沿镜面平面做反射,保证反射物体的光照计算与原物体一致,效果真实。
通过半透明混合渲染镜面本身,让反射透过镜面显示。例如镜面材质 Alpha值设为 0.3,混合公式为C=0.3·\(C_{src} \)+ 0.7·\(C_{dst} \),实现 30% 不透明的镜面效果,让反射透过镜面显示。
平面阴影核心原理是从光源方向,将物体的每个顶点投影到接收阴影的平面上,生成阴影的扁平几何体,再用半透明黑色渲染该几何体,实现平面阴影效果。
平行光阴影投影公式:对于平行光方向L、发出的光线r(t)、阴影平面(n,d)、顶点p,阴影投影点为:\(s=r(t_s)=p+t_s·L=p-\frac{n·p+d}{n·L}L=p+\frac{n·p+d}{n·L}L̃
\),其中L̃=-L

从而推导出阴影矩阵:\(s=p·s'_{d} \)
\(s'_{d}=\begin{bmatrix} p_{x}& p_{y}& p_{z}&1\end{bmatrix} \begin{bmatrix} 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_{z}n_{z}&0\\-L_{x}d&-L_{y}d&-L_{z}d&n·L\end{bmatrix} \)
点光源阴影投影公式:对于点光源位置L、阴影平面(n,d)、顶点p,阴影投影点为:\(s=r(t_s)=p-\frac{n·p+d}{n·L}(p-L)
\)

从而推导出阴影矩阵:\(s=p·s'_{p} \)
\(s'_{p}=\begin{bmatrix} p_{x}& p_{y}& p_{z}&1\end{bmatrix} \begin{bmatrix} 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{bmatrix} \)
上面的阴影矩阵,是与局部转世界矩阵结合使用的。即mesh的顶点,经过局部→世界→阴影,变成了阴影面片的顶点,后续还需要转到视图→投影,最终转到齐次裁剪空间。

但针对平行光源的阴影,出现一个问题,要形成阴影,平面的法线n和光方向L夹角会大于90度,导致n·L小于0,导致经过阴影矩阵变换后的齐次坐标的w分量<0。D3D 渲染管线中,透视除法之前会触发齐次裁剪,GPU 会对每个顶点的齐次坐标(x,y,z,w)做如下判断,只有全部满足的顶点才会被保留:\(\begin{cases} -w ≤ x ≤ w \\ -w ≤ y ≤ w \\ 0 ≤ z ≤ w \end{cases}
\),由于w<0,上述不等式完全反转,不能成立(正数≤x/y/z≤ 负数 )。解决方案是使用朝向光源的反方向向量L̃=-L,保证点积为正,投影结果不变。而对于点光源的阴影,经过阴影矩阵变换后w分量为\(w=-p_xn_x-p_yn_y-p_zn_z+n·L=-p·n+n·L=-n(p-L)
\),在需要渲染阴影的情况下,这个结果都是>0,不需要特殊处理L。
观察上述两种光源的阴影矩阵,可以综合得出通用阴影矩阵:可通过齐次坐标的 w 分量区分光源类型(\(L_w=0 \)为平行光,\(L_w=1\)为点光源)
\(s'=\begin{bmatrix} n·L+dL_w-L_{x}n_{x}&-L_{y}n_{x}&-L_{z}n_{x}&-L_wn_x\\-L_{x}n_{y}&n·L+dL_w-L_{y}n_{y}&-L_{z}n_{y}&-L_wn_y\\-L_{x}n_{z}&-L_{y}n_{z}&n·L+dL_w-L_{z}n_{z}&-L_wn_z\\-L_{x}d&-L_{y}d&-L_{z}d&n·L\end{bmatrix} \)
DirectX数学库提供内置函数XMMatrixShadow(FXMVECTOR ShadowPlane, FXMVECTOR LightPosition)直接构建阴影矩阵。
阴影几何体的三角形会出现重叠,半透明混合渲染时,重叠区域会被多次混合,导致颜色过深,出现阴影粉刺(acne)。这需要使用模板缓冲区处理。具体是:模板测试仅当模板值为 0 时通过,测试通过后将模板值递增为 1;保证每个像素仅被绘制一次,重叠区域的重复绘制会被模板测试拒绝,避免双重混合。
代码实现:
阴影材质配置:50% 透明的纯黑色材质,Alpha=0.5,用于半透明混合渲染阴影。
阴影 PSO 核心配置:开启深度测试与写入,模板测试函数EQUAL、参考值 0,测试通过后执行INCR操作,所有失败操作均为KEEP。
Z-fighting 问题与解决:阴影网格与地面网格共面,会因深度缓冲精度问题出现画面闪烁;解决方案是将阴影网格沿平面法向做微小偏移(如 Y 轴 + 0.001f),让阴影网格略高于地面,避免深度冲突。
模板缓冲与深度缓冲绑定,分辨率一致,标准格式为D24_UNORM_S8_UINT和D32_FLOAT_S8X24_UINT,通过 8 位模板值实现像素级的渲染遮罩控制;
模板测试通过参考值、读掩码、比较函数决定像素是否被接受,测试失败的像素会被直接丢弃;
深度/模板状态是 PSO 的一部分,必须在初始化时通过D3D12_DEPTH_STENCIL_DESC预创建,运行时仅能通过OMSetStencilRef修改模板参考值;
模板缓冲的核心应用:通过标记可绘制区域实现平面镜面反射,通过防重复绘制解决平面阴影的双重混合问题;
模板缓冲可用于测量场景的深度复杂度(Overdraw),辅助渲染性能优化。
数学推导:验证通用阴影矩阵对平行光、点光源的适配性;
效果调试:复现无模板缓冲时的反射穿透、阴影双重混合问题,理解模板缓冲的作用;
深度缓冲原理:验证深度缓冲开启/关闭对物体遮挡的影响,理解深度测试的核心规则;
环绕序验证:测试反射物体不反转环绕序的渲染错误,理解法线与环绕序的关联;
深度复杂度:是指通过深度测试,向后台缓冲区某一特定元素写入像素片段的次数。它的测量可以利用模板缓冲的D3D12_STENCIL_OP_INCR操作,这样可以逐像素统计场景的Overdraw(过度绘制),并通过颜色编码可视化深度复杂度;
另一种深度复杂性的实现方式是通过加法混合D3D12_BLEND_OP_ADD,源混合因子与目标混合因子都为D3D12_BLEND_ONE。
Early Z优化:理解从前到后绘制不透明物体对Early Z的优化作用,以及像素着色器通过语言为SV_Depth的变量,修改深度值会导致Early Z完全失效;
进阶应用:实现镜面中地面的反射、复现 Z-fighting 问题并验证解决方案。
模板缓冲本质:8 位整型离屏缓冲,与深度缓冲绑定,通过模板测试实现像素级渲染遮罩,是 OM 阶段的核心控制工具。
模板测试核心:通过「参考值 & 读掩码 比较运算符 缓冲值 & 读掩码」决定像素是否被接受,测试失败直接丢弃像素。
状态绑定规则:深度 / 模板状态是 PSO 的核心成员,运行时无法单独修改,仅模板参考值可通过 API 动态调整。
模板操作核心:可针对「模板失败、深度失败、全部通过」三种场景,独立定义模板缓冲的更新规则(替换、递增、置零等)。
平面镜面实现:先通过模板缓冲标记镜面可见区域,再仅在标记区域内绘制反射物体,解决反射穿透问题。
平面阴影实现:通过阴影矩阵将物体投影到平面,利用模板缓冲保证每个像素仅绘制一次,解决三角形重叠导致的双重混合问题。
深度复杂度测量:利用模板缓冲的递增计数,统计每个像素的 Overdraw 次数,可视化场景的过度绘制情况,辅助性能优化。
正反面独立配置:可对正面 / 背面三角形设置不同的模板规则,适配透明物体、双面渲染等特殊场景。
Z-fighting 解决:共面几何体通过微小的空间偏移,避免深度缓冲精度不足导致的画面闪烁。
反射环绕序修正:物体反射后需反转正面三角形的判定规则,修正法线方向,保证光照计算正确。
在学习11.4时,书中提到反射变换R,并提到因为这个反射变换导致法线内翻。所以跟进查看附录C中相关内容。
首先,先看到平面是通过平面方程定义的:\(0=n·(p-p_0)=n·p-n·p_0=n·p+d=ax+by+cz+d \)

通过方程,可以得到\(\begin{cases} n·p+d>0 && p在n指向一侧\\ n·p+d=0 && p在平面上 \\ n·p+d<0 && p在n指向相反的一侧 \end{cases} \)
平面外的点在平面上的投影到该点的向量,\(proj_n(p-p_0)=[(p-p_0)·n]n=(p·n-p_0·n)n=(p·n+d)n \)

对一个点p相对于一个平面进行反射,得到q这个变换是一个平移,公式为\(q = p - 2(n·p + d)\,n \)
\(p-2(n·p+d)n=\begin{bmatrix} p_x\\p_y\\p_z\\1\end{bmatrix}^T+\begin{bmatrix} -2n_x(n·p+d)\\-2n_y(n·p+d)\\-2n_z(n·p+d)\\0\end{bmatrix}^T=\begin{bmatrix} p_x\\p_y\\p_z\\1\end{bmatrix}^T+\begin{bmatrix} -2n_x(p_xn_x+p_yn_y+p_zn_z+d)\\-2n_y(p_xn_x+p_yn_y+p_zn_z+d)\\-2n_z(p_xn_x+p_yn_y+p_zn_z+d)\\0\end{bmatrix}^T \)
\(=\begin{bmatrix} p_x-2n_x(p_xn_x+p_yn_y+p_zn_z+d)\\p_y-2n_y(p_xn_x+p_yn_y+p_zn_z+d)\\p_z-2n_z(p_xn_x+p_yn_y+p_zn_z+d)\\1\end{bmatrix}^T=\begin{bmatrix} p_x-2n_xp_xn_x-2n_xp_yn_y-2n_xp_zn_z-2n_xd\\-2n_yp_xn_x+p_y-2n_yp_yn_y-2n_yp_zn_z-2n_yd\\-2n_zp_xn_x-2n_zp_yn_y+p_z-2n_zp_zn_z-2n_zd\\1\end{bmatrix}^T \)
\(=\begin{bmatrix} p_x(1-2n_xn_x)+p_y(-2n_xn)_y+p_z(-2n_xn_z)+(-2n_xd)\\p_x(-2n_yn_x)+p_y(1-2n_yn_y)+p_z(-2n_yn_z)+(-2n_yd)\\p_x(-2n_zn_x)+p_y(-2n_zn_y)+p_z(1-2n_zn_z)+(-2n_zd)\\1\end{bmatrix}^T \)
\(=\begin{bmatrix} p_x&p_y&p_z&1\end{bmatrix}\begin{bmatrix}(1-2n_xn_x)&(-2n_yn_x)&(-2n_zn_x)&0\\(-2n_xn_y)&(1-2n_yn_y)&(-2n_zn_y)&0\\(-2n_xn_z)&(-2n_yn_z)&(1-2n_zn_z)&0\\(-2n_xd)&(-2n_yd)&(-2n_zd)&1\end{bmatrix} \)
由此推导出反射矩阵:\(R=\begin{bmatrix}1-2n_xn_x&-2n_yn_x&-2n_zn_x&0\\-2n_xn_y&1-2n_yn_y&-2n_zn_y&0\\-2n_xn_z&-2n_yn_z&1-2n_zn_z&0\\-2n_xd&-2n_yd&-2n_zd&1\end{bmatrix} \)
在书的11.7习题部分的第八题,提到重复绘制,可以通过模板进行统计,对此挺感兴趣,尝试实现这一题。我是在第十一章配套代码,StencilDemo,的基础上去做这个题目的,程序启动是正常渲染,按下F5可视化Overdraw情况。
题目中提到通过D3D12_STENCIL_OP_INCR将渲染记录到模板缓冲区。我的第一个想法是先渲染一遍,到得缓存了Overdraw计数的Stencil Buffer。第二遍是渲染一个全屏三角形,将这个buffer作为其SRV采样,实现Overdraw的可视化。
刚好也想尝试viber coding,所以将这个思路丢给了Trae去实现,Trae调用MiniMax2.7生成代码,最开始是各种各样的问题,程序一跑起来就崩溃。最后还说Depth Stencil Buffer是禁止用于SRV的,搞得我也有点怀疑自己的思路。于是将题目丢给豆包,豆包给了我两个思路:纯模板测试筛选法(无SRV),和SRV直接采样法。它们之间的差异主要是在可视化如何实现。
shader方面:
需要进行一个Overdraw计数pass的渲染,将Overdraw记录到stencil buffer中。渲染的shader可以极简,vs只需要进行mvp变换,ps直接输出黑色。
#include "LightingUtil.hlsl"
cbuffer cbPerObject : register(b0)
{
float4x4 gWorld;
float4x4 gTexTransform;
};
cbuffer cbPass : register(b1)
{
float4x4 gView;
float4x4 gInvView;
float4x4 gProj;
float4x4 gInvProj;
float4x4 gViewProj;
float4x4 gInvViewProj;
float3 gEyePosW;
float cbPerObjectPad1;
float2 gRenderTargetSize;
float2 gInvRenderTargetSize;
float gNearZ;
float gFarZ;
float gTotalTime;
float gDeltaTime;
float4 gAmbientLight;
float4 gFogColor;
float gFogStart;
float gFogRange;
float2 cbPerObjectPad2;
Light gLights[MaxLights];
};
struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 TexC : TEXCOORD;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout = (VertexOut)0.0f;
float4 posW = mul(float4(vin.PosL, 1.0f), gWorld);
vout.PosH = mul(posW, gViewProj);
return vout;
}
float4 PS(VertexOut pin) : SV_Target
{
return float4(0.0f, 0.0f, 0.0f, 0.0f);
}
接着是可视化pass的渲染,核心原理简单粗暴,就是不断调整模板值进行模板测试,想画多少层Overdraw,就进行多少次全屏绘制的模板测试。vs实现全屏三角面,ps从常量缓冲区中取颜色进行着色。
cbuffer cbVisualize : register(b2)
{
float4 gColor;
};
float4 VS(uint id : SV_VertexID) : SV_POSITION
{
// 实例绘制3个点,id为实例编号,注意三个点顺序是左手定则的顺时针
float x = (id == 2) ? 3.0f : -1.0f;
float y = (id == 1) ? 3.0f : -1.0f;
return float4(x, y, 0.0f, 1.0f);
}
float4 PS(float4 posH : SV_POSITION) : SV_Target
{
return gColor;
}
C++代码方面:
StencilApp成员变量需要增加
class StencilApp : public D3DApp
{
...
private:
...
bool mShowOverdraw = false;
// 通过常量缓冲区预先记录不同overdraw的颜色
ComPtr<ID3D12Resource> mOverdrawVisualizeCBs[8];
}
接下来就是在StencilApp的初始化过程中,对Overdraw计数pass和可视化pass的相关shader编译、创建其相应的PSO和资源。
void StencilApp::BuildShadersAndInputLayout()
{
...
mShaders["overdrawCountVS"] = d3dUtil::CompileShader(L"Shaders\\Default.hlsl", nullptr, "VS", "vs_5_0");
mShaders["overdrawCountPS"] = d3dUtil::CompileShader(L"Shaders\\OverdrawCount.hlsl", nullptr, "PS", "ps_5_0");
mShaders["overdrawVisualizeVS"] = d3dUtil::CompileShader(L"Shaders\\OverdrawVisualize.hlsl", nullptr, "VS", "vs_5_0");
mShaders["overdrawVisualizePS"] = d3dUtil::CompileShader(L"Shaders\\OverdrawVisualize.hlsl", nullptr, "PS", "ps_5_0");
...
}
void StencilApp::BuildPSO()
{
...
// Overdraw计数Pass的PSO创建
D3D12_GRAPHICS_PIPELINE_STATE_DESC overdrawCountPsoDesc = opaquePsoDesc;
overdrawCountPsoDesc.VS =
{
reinterpret_cast<BYTE*>(mShaders["overdrawCountVS"]->GetBufferPointer()),
mShaders["overdrawCountVS"]->GetBufferSize()
};
overdrawCountPsoDesc.PS =
{
reinterpret_cast<BYTE*>(mShaders["overdrawCountPS"]->GetBufferPointer()),
mShaders["overdrawCountPS"]->GetBufferSize()
};
CD3DX12_BLEND_DESC overdrawBlendState(D3D12_DEFAULT);
overdrawBlendState.RenderTarget[0].RenderTargetWriteMask = 0;
D3D12_DEPTH_STENCIL_DESC overdrawCountDSS = {};
overdrawCountDSS.DepthEnable = TRUE;
overdrawCountDSS.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;
overdrawCountDSS.DepthFunc = D3D12_COMPARISON_FUNC_LESS;
overdrawCountDSS.StencilEnable = TRUE;
overdrawCountDSS.StencilReadMask = 0xFF;
overdrawCountDSS.StencilWriteMask = 0xFF;
overdrawCountDSS.FrontFace.StencilFailOp = D3D12_STENCIL_OP_INCR;
overdrawCountDSS.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_INCR;
overdrawCountDSS.FrontFace.StencilPassOp = D3D12_STENCIL_OP_INCR;
overdrawCountDSS.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS;
overdrawCountDSS.BackFace.StencilFailOp = D3D12_STENCIL_OP_INCR;
overdrawCountDSS.BackFace.StencilDepthFailOp = D3D12_STENCIL_OP_INCR;
overdrawCountDSS.BackFace.StencilPassOp = D3D12_STENCIL_OP_INCR;
overdrawCountDSS.BackFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS;
overdrawCountPsoDesc.BlendState = overdrawBlendState;
overdrawCountPsoDesc.DepthStencilState = overdrawCountDSS;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&overdrawCountPsoDesc, IID_PPV_ARGS(&mPSOs["overdrawCount"])));
// Overdraw可视化Pass的PSO创建
D3D12_GRAPHICS_PIPELINE_STATE_DESC overdrawVisualizePsoDesc = {};
overdrawVisualizePsoDesc.pRootSignature = mRootSignature.Get();
overdrawVisualizePsoDesc.VS =
{
reinterpret_cast<BYTE*>(mShaders["overdrawVisualizeVS"]->GetBufferPointer()),
mShaders["overdrawVisualizeVS"]->GetBufferSize()
};
overdrawVisualizePsoDesc.PS =
{
reinterpret_cast<BYTE*>(mShaders["overdrawVisualizePS"]->GetBufferPointer()),
mShaders["overdrawVisualizePS"]->GetBufferSize()
};
overdrawVisualizePsoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
overdrawVisualizePsoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
overdrawVisualizePsoDesc.DepthStencilState.DepthEnable = FALSE;
overdrawVisualizePsoDesc.DepthStencilState.StencilEnable = TRUE;
overdrawVisualizePsoDesc.DepthStencilState.StencilReadMask = 0xFF;
overdrawVisualizePsoDesc.DepthStencilState.StencilWriteMask = 0x00;
overdrawVisualizePsoDesc.DepthStencilState.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
overdrawVisualizePsoDesc.DepthStencilState.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
overdrawVisualizePsoDesc.DepthStencilState.FrontFace.StencilPassOp = D3D12_STENCIL_OP_KEEP;
overdrawVisualizePsoDesc.DepthStencilState.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_EQUAL;
overdrawVisualizePsoDesc.DepthStencilState.BackFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
overdrawVisualizePsoDesc.DepthStencilState.BackFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
overdrawVisualizePsoDesc.DepthStencilState.BackFace.StencilPassOp = D3D12_STENCIL_OP_KEEP;
overdrawVisualizePsoDesc.DepthStencilState.BackFace.StencilFunc = D3D12_COMPARISON_FUNC_EQUAL;
overdrawVisualizePsoDesc.SampleMask = UINT_MAX;
overdrawVisualizePsoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
overdrawVisualizePsoDesc.NumRenderTargets = 1;
overdrawVisualizePsoDesc.RTVFormats[0] = mBackBufferFormat;
overdrawVisualizePsoDesc.DSVFormat = mDepthStencilFormat;
overdrawVisualizePsoDesc.SampleDesc.Count = 1;
overdrawVisualizePsoDesc.SampleDesc.Quality = 0;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&overdrawVisualizePsoDesc, IID_PPV_ARGS(&mPSOs["overdrawVisualize"])));
// Overdraw可视化pass需要用的资源
// 将8种颜色存入常量缓冲区
static const float colors[8][4] = {
{0.0f, 0.0f, 1.0f, 1.0f},
{0.0f, 1.0f, 0.0f, 1.0f},
{1.0f, 1.0f, 0.0f, 1.0f},
{1.0f, 0.5f, 0.0f, 1.0f},
{1.0f, 0.0f, 0.0f, 1.0f},
{1.0f, 0.0f, 1.0f, 1.0f},
{0.0f, 1.0f, 1.0f, 1.0f},
{1.0f, 1.0f, 1.0f, 1.0f}
};
UINT visualizeCBSize = d3dUtil::CalcConstantBufferByteSize(sizeof(float) * 4);
for(int i = 0; i < 8; ++i)
{
ThrowIfFailed(md3dDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(visualizeCBSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&mOverdrawVisualizeCBs[i])));
float* colorData;
mOverdrawVisualizeCBs[i]->Map(0, nullptr, reinterpret_cast<void**>(&colorData));
colorData[0] = colors[i][0];
colorData[1] = colors[i][1];
colorData[2] = colors[i][2];
colorData[3] = colors[i][3];
mOverdrawVisualizeCBs[i]->Unmap(0, nullptr);
}
}
最后,在绘制函数进行计数Pass和可视化pass的渲染
void StencilApp::OnKeyboardInput(const GameTimer& gt)
{
if(GetAsyncKeyState(VK_F5) & 0x8000)
mShowOverdraw = !mShowOverdraw;
...
}
void StencilApp::Draw(const GameTimer& gt)
{
if(mShowOverdraw)
{
DrawOverdraw(gt);
return;
}
...
}
void StencilApp::DrawOverdraw(const GameTimer& gt)
{
auto cmdListAlloc = mCurrFrameResource->CmdListAlloc;
ID3D12DescriptorHeap* descriptorHeaps[] = { mSrvDescriptorHeap.Get() };
ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
float black[] = { 0.0f, 0.0f, 0.0f, 1.0f };
//
// Pass 1: Overdraw counting using stencil buffer
//
ThrowIfFailed(cmdListAlloc->Reset());
ThrowIfFailed(mCommandList->Reset(cmdListAlloc.Get(), mPSOs["overdrawCount"].Get()));
mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->RSSetScissorRects(1, &mScissorRect);
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));
mCommandList->ClearRenderTargetView(CurrentBackBufferView(), black, 0, nullptr);
mCommandList->ClearDepthStencilView(DepthStencilView(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
mCommandList->OMSetRenderTargets(1, &CurrentBackBufferView(), true, &DepthStencilView());
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
mCommandList->SetPipelineState(mPSOs["overdrawCount"].Get());
UINT passCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(PassConstants));
mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps);
auto passCB = mCurrFrameResource->PassCB->Resource();
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Opaque]);
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Mirrors]);
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress() + 1 * passCBByteSize);
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Reflected]);
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Transparent]);
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Shadow]);
ThrowIfFailed(mCommandList->Close());
mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
mCurrFrameResource->Fence = ++mCurrentFence;
HANDLE eventHandle = CreateEvent(nullptr, FALSE, FALSE, nullptr);
mCommandQueue->Signal(mFence.Get(), mCurrentFence);
mFence->SetEventOnCompletion(mCurrentFence, eventHandle);
WaitForSingleObject(eventHandle, INFINITE);
CloseHandle(eventHandle);
//
// Pass 2: Visualization - loop through depth complexity levels 1 to 8
//
ThrowIfFailed(cmdListAlloc->Reset());
ThrowIfFailed(mCommandList->Reset(cmdListAlloc.Get(), mPSOs["overdrawVisualize"].Get()));
mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->RSSetScissorRects(1, &mScissorRect);
mCommandList->ClearRenderTargetView(CurrentBackBufferView(), black, 0, nullptr);
mCommandList->ClearDepthStencilView(DepthStencilView()
, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);//只清理深度缓冲
mCommandList->OMSetRenderTargets(1, &CurrentBackBufferView(), true, &DepthStencilView());
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps);
mCommandList->SetPipelineState(mPSOs["overdrawVisualize"].Get());
mCommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
//重复绘制8遍进行染色
for(int k = 1; k <= 8; ++k)
{
mCommandList->OMSetStencilRef(k);
mCommandList->SetGraphicsRootConstantBufferView(3
, mOverdrawVisualizeCBs[k - 1]->GetGPUVirtualAddress());
mCommandList->DrawInstanced(3, 1, 0, 0);//实例绘制3个点,由vs设置顶点
}
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));
ThrowIfFailed(mCommandList->Close());
cmdsLists[0] = mCommandList.Get();
mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
ThrowIfFailed(mSwapChain->Present(0, 0));
mCurrBackBuffer = (mCurrBackBuffer + 1) % SwapChainBufferCount;
mCurrFrameResource->Fence = ++mCurrentFence;
mCommandQueue->Signal(mFence.Get(), mCurrentFence);
}
我们可以为同一个深度/模板资源创建两种不同用途的视图:
深度/模板视图(DSV):绑定到输出合并阶段,用于渲染时的深度/模板测试和写入
着色器资源视图(SRV):绑定到像素着色器,用于后处理阶段直接采样模板平面的数值
这样就可以通过Overdraw可视化Pass改为一次渲染,采样深度/模板缓冲,就能可视化Overdraw。
Overdraw计数Pass的Shader保持不变,可视化pass的Shader修改为从贴图中取样后渲染。
Texture2D<uint> gStencilBuffer : register(t0);
cbuffer cbVisualize : register(b2)
{
float4 gDepthColors[8];
};
float4 VS(uint vid : SV_VertexID) : SV_POSITION
{
float x = (vid == 2) ? 3.0f : -1.0f;
float y = (vid == 1) ? 3.0f : -1.0f;
return float4(x, y, 0.0f, 1.0f);
}
float4 PS(float4 posH : SV_POSITION) : SV_Target
{
// When sampling Plane 1 (stencil plane) of an X24_TYPELESS_G8_UINT
// depth/stencil resource, the .x channel of the returned uint holds
// the 8-bit stencil value directly (not packed as RGBA).
uint stencilValue = gStencilBuffer.Load(int3(posH.xy, 0));
if(stencilValue == 0)
return float4(0.0f, 0.0f, 0.0f, 1.0f);
int colorIndex = min(int(stencilValue) - 1, 7);
return gDepthColors[colorIndex];
}
需要注意的是:
必须用Texture2D<uint>采样模板值,不能用Texture2D<float>,否则会得到 0
使用Load函数而不是Sample:因为我们是按精确的像素坐标读取,不需要纹理过滤,性能更高,也不需要绑定采样器
模板值为 0 表示该像素没有被任何片元覆盖(通常是屏幕外或天空盒区域),可以用黑色表示
StencilApp成员变量需要增加
class StencilApp : public D3DApp
{
...
private:
...
bool mShowOverdraw = false;
ComPtr<ID3D12Resource> mOverdrawColorCB = nullptr;
UINT mStencilSrvDescriptorIndex = 4;
}
描述符要增加一个,以指向作为SRV的深度/模板缓冲,窗口大小变化时要更新它
void StencilApp::BuildDescriptorHeaps()
{
...
srvHeapDesc.NumDescriptors = 5;
...
...
// next descriptor
hDescriptor.Offset(1, mCbvSrvDescriptorSize);
D3D12_SHADER_RESOURCE_VIEW_DESC stencilSrvDesc = {};
stencilSrvDesc.Format = DXGI_FORMAT_X24_TYPELESS_G8_UINT;
stencilSrvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
// X24_TYPELESS_G8_UINT has two components: R=X24(typeless,0) G=G8(stencil).
// We must map G (component 1) to the shader's R channel so Texture2D<uint>
// returns the stencil value instead of the typeless depth bits.
stencilSrvDesc.Shader4ComponentMapping = D3D12_ENCODE_SHADER_4_COMPONENT_MAPPING(
D3D12_SHADER_COMPONENT_MAPPING_FROM_MEMORY_COMPONENT_1,
D3D12_SHADER_COMPONENT_MAPPING_FORCE_VALUE_0,
D3D12_SHADER_COMPONENT_MAPPING_FORCE_VALUE_0,
D3D12_SHADER_COMPONENT_MAPPING_FORCE_VALUE_1);
stencilSrvDesc.Texture2D.MostDetailedMip = 0;
stencilSrvDesc.Texture2D.MipLevels = 1;
stencilSrvDesc.Texture2D.PlaneSlice = 1;
md3dDevice->CreateShaderResourceView(mDepthStencilBuffer.Get(), &stencilSrvDesc, hDescriptor);
}
void StencilApp::OnResize()
{
...
// Rebuild stencil SRV — OnResize recreates mDepthStencilBuffer,
// so the old SRV descriptor becomes stale.
// Guard: only rebuild after BuildDescriptorHeaps has run.
if(mSrvDescriptorHeap != nullptr)
{
CD3DX12_CPU_DESCRIPTOR_HANDLE hDescriptor(
mSrvDescriptorHeap->GetCPUDescriptorHandleForHeapStart(),
mStencilSrvDescriptorIndex,
mCbvSrvDescriptorSize);
D3D12_SHADER_RESOURCE_VIEW_DESC stencilSrvDesc = {};
stencilSrvDesc.Format = DXGI_FORMAT_X24_TYPELESS_G8_UINT;
stencilSrvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
stencilSrvDesc.Shader4ComponentMapping = D3D12_ENCODE_SHADER_4_COMPONENT_MAPPING(
D3D12_SHADER_COMPONENT_MAPPING_FROM_MEMORY_COMPONENT_1,
D3D12_SHADER_COMPONENT_MAPPING_FORCE_VALUE_0,
D3D12_SHADER_COMPONENT_MAPPING_FORCE_VALUE_0,
D3D12_SHADER_COMPONENT_MAPPING_FORCE_VALUE_1);
stencilSrvDesc.Texture2D.MostDetailedMip = 0;
stencilSrvDesc.Texture2D.MipLevels = 1;
stencilSrvDesc.Texture2D.PlaneSlice = 1;
md3dDevice->CreateShaderResourceView(mDepthStencilBuffer.Get(), &stencilSrvDesc, hDescriptor);
}
}
深度/模板缓冲本质是一个多平面 2D 纹理:
平面 0:存储深度值(24 位 UNORM 或 32 位 FLOAT)
平面 1:存储模板值(8 位 UINT)
SRV 法最容易踩坑的地方:必须使用 TYPELESS 格式创建深度/模板资源,否则无法同时创建 DSV 和 SRV。
D3D12 提供两种常用的深度/模板格式组合,必须严格对应:
| 资源格式(TYPELESS) | DSV 格式(深度写入用) | SRV 格式(模板采样用) | 适用场景 |
|---|---|---|---|
| DXGI_FORMAT_R24G8_TYPELESS | DXGI_FORMAT_D24_UNORM_S8_UINT | DXGI_FORMAT_X24_TYPELESS_G8_UINT | 通用场景,教材默认格式 |
| DXGI_FORMAT_R32G8X24_TYPELESS | DXGI_FORMAT_D32_FLOAT_S8X24_UINT | DXGI_FORMAT_X32_TYPELESS_G8X24_UINT | 高精度深度场景,避免 Z-fighting |
PlaneSlice = 1是最容易出错的地方:如果设为 0,会采样到深度值,得到完全错误的结果;
必须添加D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL标志,否则无法创建 DSV;
描述符堆必须是D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV类型,且至少预留一个位置用于模板SRV。
而我们最终使用的DXGI_FORMAT_X24_TYPELESS_G8_UINT 是双通道格式,即
| 内存分量 | 含义 | Load 返回值 |
|---|---|---|
| Component 0 (R) | X24 — 深度位,TYPELESS | 0(不可读) |
| Component 1 (G) | G8 — 模板值,UINT | 正确的 stencil 值 |
为了在Shader中可以读取模板值,需要 D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING,修改 Shader4ComponentMapping,把 Component 1(G/模板值)映射到着色器的 R 通道,使得Shader可以通过Texture2D<uint>.Load()读取模板值。
初始化过程中,对Overdraw计数pass和可视化pass的相关shader编译、创建其相应的PSO和资源。
void StencilApp::BuildShadersAndInputLayout()
{
...
mShaders["overdrawCountVS"] = d3dUtil::CompileShader(L"Shaders\\Default.hlsl", nullptr, "VS", "vs_5_0");
mShaders["overdrawCountPS"] = d3dUtil::CompileShader(L"Shaders\\OverdrawCount.hlsl", nullptr, "PS", "ps_5_0");
mShaders["overdrawVisualizeSRVVS"] = d3dUtil::CompileShader(L"Shaders\\OverdrawVisualizeSRV.hlsl", nullptr, "VS", "vs_5_0");
mShaders["overdrawVisualizeSRVPS"] = d3dUtil::CompileShader(L"Shaders\\OverdrawVisualizeSRV.hlsl", nullptr, "PS", "ps_5_0");
...
}
void StencilApp::BuildPSOs()
{
...
// Overdraw计数Pass的PSO创建
D3D12_GRAPHICS_PIPELINE_STATE_DESC overdrawCountPsoDesc = opaquePsoDesc;
overdrawCountPsoDesc.VS =
{
reinterpret_cast<BYTE*>(mShaders["overdrawCountVS"]->GetBufferPointer()),
mShaders["overdrawCountVS"]->GetBufferSize()
};
overdrawCountPsoDesc.PS =
{
reinterpret_cast<BYTE*>(mShaders["overdrawCountPS"]->GetBufferPointer()),
mShaders["overdrawCountPS"]->GetBufferSize()
};
CD3DX12_BLEND_DESC overdrawBlendState(D3D12_DEFAULT);
overdrawBlendState.RenderTarget[0].RenderTargetWriteMask = 0;
D3D12_DEPTH_STENCIL_DESC overdrawCountDSS = {};
overdrawCountDSS.DepthEnable = TRUE;
overdrawCountDSS.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;
overdrawCountDSS.DepthFunc = D3D12_COMPARISON_FUNC_LESS;
overdrawCountDSS.StencilEnable = TRUE;
overdrawCountDSS.StencilReadMask = 0xFF;
overdrawCountDSS.StencilWriteMask = 0xFF;
overdrawCountDSS.FrontFace.StencilFailOp = D3D12_STENCIL_OP_INCR;
overdrawCountDSS.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_INCR;
overdrawCountDSS.FrontFace.StencilPassOp = D3D12_STENCIL_OP_INCR;
overdrawCountDSS.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS;
overdrawCountDSS.BackFace.StencilFailOp = D3D12_STENCIL_OP_INCR;
overdrawCountDSS.BackFace.StencilDepthFailOp = D3D12_STENCIL_OP_INCR;
overdrawCountDSS.BackFace.StencilPassOp = D3D12_STENCIL_OP_INCR;
overdrawCountDSS.BackFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS;
overdrawCountPsoDesc.BlendState = overdrawBlendState;
overdrawCountPsoDesc.DepthStencilState = overdrawCountDSS;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&overdrawCountPsoDesc, IID_PPV_ARGS(&mPSOs["overdrawCount"])));
// Overdraw可视化Pass的PSO创建
D3D12_GRAPHICS_PIPELINE_STATE_DESC overdrawVisualizePsoDesc = {};
overdrawVisualizePsoDesc.pRootSignature = mRootSignature.Get();
overdrawVisualizePsoDesc.VS =
{
reinterpret_cast<BYTE*>(mShaders["overdrawVisualizeSRVVS"]->GetBufferPointer()),
mShaders["overdrawVisualizeSRVVS"]->GetBufferSize()
};
overdrawVisualizePsoDesc.PS =
{
reinterpret_cast<BYTE*>(mShaders["overdrawVisualizeSRVPS"]->GetBufferPointer()),
mShaders["overdrawVisualizeSRVPS"]->GetBufferSize()
};
overdrawVisualizePsoDesc.InputLayout = { nullptr, 0 };
overdrawVisualizePsoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
overdrawVisualizePsoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
overdrawVisualizePsoDesc.DepthStencilState.DepthEnable = FALSE;
overdrawVisualizePsoDesc.DepthStencilState.StencilEnable = FALSE;
overdrawVisualizePsoDesc.SampleMask = UINT_MAX;
overdrawVisualizePsoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
overdrawVisualizePsoDesc.NumRenderTargets = 1;
overdrawVisualizePsoDesc.RTVFormats[0] = mBackBufferFormat;
overdrawVisualizePsoDesc.SampleDesc.Count = 1;
overdrawVisualizePsoDesc.SampleDesc.Quality = 0;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&overdrawVisualizePsoDesc, IID_PPV_ARGS(&mPSOs["overdrawVisualizeSRV"])));
UINT cbSize = d3dUtil::CalcConstantBufferByteSize(sizeof(XMFLOAT4) * 8);
auto heapProps = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
auto bufferDesc = CD3DX12_RESOURCE_DESC::Buffer(cbSize);
ThrowIfFailed(md3dDevice->CreateCommittedResource(
&heapProps,
D3D12_HEAP_FLAG_NONE,
&bufferDesc,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&mOverdrawColorCB)));
// Overdraw的颜色存入常量缓冲区
XMFLOAT4* colorData;
mOverdrawColorCB->Map(0, nullptr, reinterpret_cast<void**>(&colorData));
colorData[0] = { 0.0f, 0.0f, 1.0f, 1.0f };
colorData[1] = { 0.0f, 1.0f, 0.0f, 1.0f };
colorData[2] = { 1.0f, 1.0f, 0.0f, 1.0f };
colorData[3] = { 1.0f, 0.5f, 0.0f, 1.0f };
colorData[4] = { 1.0f, 0.0f, 0.0f, 1.0f };
colorData[5] = { 1.0f, 0.0f, 1.0f, 1.0f };
colorData[6] = { 0.0f, 1.0f, 1.0f, 1.0f };
colorData[7] = { 1.0f, 1.0f, 1.0f, 1.0f };
mOverdrawColorCB->Unmap(0, nullptr);
}
最后,在绘制函数进行计数Pass和可视化pass的渲染
void StencilApp::DrawOverdraw(const GameTimer& gt)
{
auto cmdListAlloc = mCurrFrameResource->CmdListAlloc;
ThrowIfFailed(cmdListAlloc->Reset());
ThrowIfFailed(mCommandList->Reset(cmdListAlloc.Get(), mPSOs["overdrawCount"].Get()));
mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->RSSetScissorRects(1, &mScissorRect);
auto transition = CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET);
mCommandList->ResourceBarrier(1, &transition);
float black[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
mCommandList->ClearRenderTargetView(CurrentBackBufferView(), black, 0, nullptr);
mCommandList->ClearDepthStencilView(DepthStencilView(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
auto cbbv = CurrentBackBufferView();
auto dsv = DepthStencilView();
mCommandList->OMSetRenderTargets(1, &cbbv, true, &dsv);
ID3D12DescriptorHeap* descriptorHeaps[] = { mSrvDescriptorHeap.Get() };
mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps);
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
UINT passCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(PassConstants));
auto passCB = mCurrFrameResource->PassCB->Resource();
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress());
mCommandList->SetPipelineState(mPSOs["overdrawCount"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Opaque]);
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Mirrors]);
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress() + 1 * passCBByteSize);
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Reflected]);
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Transparent]);
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Shadow]);
transition = CD3DX12_RESOURCE_BARRIER::Transition(mDepthStencilBuffer.Get(),
D3D12_RESOURCE_STATE_DEPTH_WRITE, D3D12_RESOURCE_STATE_GENERIC_READ);
mCommandList->ResourceBarrier(1, &transition);
mCommandList->SetPipelineState(mPSOs["overdrawVisualizeSRV"].Get());
// Unbind DSV — depth/stencil buffer is in GENERIC_READ state for SRV sampling,
// so we must not have a DSV bound. Only bind the RTV.
mCommandList->OMSetRenderTargets(1, &cbbv, true, nullptr);
CD3DX12_GPU_DESCRIPTOR_HANDLE stencilSrv(mSrvDescriptorHeap->GetGPUDescriptorHandleForHeapStart());
stencilSrv.Offset(mStencilSrvDescriptorIndex, mCbvSrvDescriptorSize);
mCommandList->SetGraphicsRootDescriptorTable(0, stencilSrv);
mCommandList->SetGraphicsRootConstantBufferView(3, mOverdrawColorCB->GetGPUVirtualAddress());
mCommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
mCommandList->DrawInstanced(3, 1, 0, 0);
transition = CD3DX12_RESOURCE_BARRIER::Transition(mDepthStencilBuffer.Get(),
D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_RESOURCE_STATE_DEPTH_WRITE);
mCommandList->ResourceBarrier(1, &transition);
transition = CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT);
mCommandList->ResourceBarrier(1, &transition);
ThrowIfFailed(mCommandList->Close());
ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
ThrowIfFailed(mSwapChain->Present(0, 0));
mCurrBackBuffer = (mCurrBackBuffer + 1) % SwapChainBufferCount;
mCurrFrameResource->Fence = ++mCurrentFence;
mCommandQueue->Signal(mFence.Get(), mCurrentFence);
}
正常渲染:

渲染Overdraw:
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。