惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
SegmentFault 最新的问题
S
Secure Thoughts
Google DeepMind News
Google DeepMind News
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
Security Affairs
TaoSecurity Blog
TaoSecurity Blog
Cloudbric
Cloudbric
Cisco Talos Blog
Cisco Talos Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
H
Heimdal Security Blog
The Last Watchdog
The Last Watchdog
T
Threatpost
Hacker News: Ask HN
Hacker News: Ask HN
Security Latest
Security Latest
Know Your Adversary
Know Your Adversary
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Securelist
Microsoft Azure Blog
Microsoft Azure Blog
The GitHub Blog
The GitHub Blog
阮一峰的网络日志
阮一峰的网络日志
D
Docker
V
Vulnerabilities – Threatpost
Attack and Defense Labs
Attack and Defense Labs
Hugging Face - Blog
Hugging Face - Blog
W
WeLiveSecurity
Engineering at Meta
Engineering at Meta
aimingoo的专栏
aimingoo的专栏
Last Week in AI
Last Week in AI
L
LINUX DO - 热门话题
NISL@THU
NISL@THU
D
Darknet – Hacking Tools, Hacker News & Cyber Security
The Cloudflare Blog
博客园_首页
P
Privacy International News Feed
Scott Helme
Scott Helme
N
News and Events Feed by Topic
WordPress大学
WordPress大学
宝玉的分享
宝玉的分享
T
Tenable Blog
H
Hacker News: Front Page
N
News and Events Feed by Topic
罗磊的独立博客
Google Online Security Blog
Google Online Security Blog
S
Security @ Cisco Blogs
Hacker News - Newest:
Hacker News - Newest: "LLM"
A
About on SuperTechFans
有赞技术团队
有赞技术团队
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
G
GRAHAM CLULEY
Application and Cybersecurity Blog
Application and Cybersecurity Blog

博客园 - 斌伯

顺序无关半透明资料 【DX12龙书】第十章 混合 【DX12龙书】第九章 纹理贴图 【DX12龙书】第八章 光照 【DX12龙书】第七章 利用Direct3D绘制几何体(续) 【DX12龙书】第六章 利用Direct3D绘制几何体 【DX12龙书】第五章 渲染管线 【DX12龙书】第四章 Direct3D的初始化 Godot开发环境与学习资源 源码开放的引擎研究 海龟交易法操作商品期货 重新开始 使用node_redis进行redis数据库crud操作 ACTGame项目 我所理解的网络游戏<?>:战斗逻辑设计 我所理解的网络游戏<一>:网游的顶层设计 《Real Time Rendering》第四章 图形变换 《程序员的自我修养》学习笔记(一):简介 《Real Time Rendering》第三章 图形处理单元 《TCP/IP协议详解》学习笔记(一):概述 《Windows via C/C++》学习笔记(二):Working with Characters and String 《Real Time Rendering》第二章 图形渲染管线 《Real Time Rendering》第三版 翻译 《Windows via C/C++》学习笔记(一):Error handling
【DX12龙书】第十一章 模板
斌伯 · 2026-06-13 · via 博客园 - 斌伯

本章是 Direct3D 12 渲染管线的核心进阶章节,系统讲解模板缓冲(Stencil Buffer)技术—— 通过与深度缓冲绑定的 8 位模板缓冲,在输出合并(OM)阶段执行模板测试,实现对像素渲染的精准遮罩控制,解决了平面镜面反射、平面阴影等特殊效果的渲染难题。本章内容覆盖模板缓冲的格式规范、模板测试原理、深度 / 模板状态的 PSO 配置,两大核心工程化应用(平面镜面、平面阴影),以及基于模板缓冲的场景深度复杂度测量,是实时渲染中遮罩效果、特殊光影实现的必备技术。


各小节内容概况

11.1 深度/模板缓冲区的格式与清理

模板缓冲与深度缓冲作为一个资源(纹理)共享显存,二者分辨率完全一致,每个像素的模板值与深度值一一对应,随深度缓冲一同创建、一同绑定到管线。

模板/深度缓冲有两种格式

格式 深度缓冲规格 模板缓冲规格 适用场景
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可清除整个缓冲,也可指定多个矩形区域清除。

11.2 模板测试

模板测试在渲染管线的输出合并(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 永远通过 模板缓冲标记写入

模板测试失败的像素,会被直接丢弃,不会写入后台颜色缓冲,也不会写入深度缓冲。

11.3 设置深度/模板状态

深度/模板状态通过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/BackFaceD3D12_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_DESCDepthStencilState字段,与PSO绑定,在 PSO 创建时完成定义。

运行时无法单独修改深度/模板状态,只能通过SetPipelineState切换完整 PSO。

唯一可在运行时动态修改的是模板的参考值,通过ID3D12GraphicsCommandList::OMSetStencilRef(UINT StencilRef)

在工程实践上,不同的模板效果需创建独立的 PSO,例如标记镜面的 PSO、绘制反射的 PSO、绘制阴影的 PSO,分别配置对应的深度 / 模板状态,在应用初始化时预创建所有 PSO。

11.4 实现平面镜效果

镜面渲染的有两大问题:

几何问题:如何将物体沿镜面平面做反射变换R,得到正确的反射几何体;

image 346x252

渲染问题:如何保证反射仅在镜面区域显示,不会穿透墙面出现在场景其他位置(模板缓冲的核心解决场景)。

  • 镜面渲染的标准 5 步流程
    • 正常渲染场景中所有不透明物体(地面、墙面、原物体),不修改模板缓冲;

    • 清除模板缓冲为 0;

    • 切换镜面PSO,标记镜面区域:

      • 禁用颜色写入(D3D12_RENDER_TARGET_BLEND_DESCRenderTargetWriteMask为0);

      • 禁用深度写入(D3D12_DEPTH_SEENCIL_DESCDepthWriteMaskD3D12_DEPTH_WRITE_MASK_ZERO);

      • 模板测试永远通过(D3D12_DEPTH_SEENCIL_DESCStencilFuncD3D12_COMPARISON_ALWAYS),测试通过(只有深度测试)后用1替换模板值(D3D12_STENCIL_OP_REPLACEOMSetStencilRef为1)。由于先绘制原物体,再标记镜面,这样被原物体遮挡的镜面区域会因深度测试失败,不会被标记到模板缓冲中,避免反射穿透原物体显示;

      最终仅镜面的可见区域模板值为 1,其他区域为 0;

      image 376x133
    • 绘制反射物体:模板测试仅当模板值等于1时通过,保证反射仅绘制在镜面区域:

      • OMSetStencilRef(1)模板参考值设置为1

      • 切换绘制反射PSO,D3D12_DEPTH_STENCILOP_DESCStencilFunc设置为D3D12_COMPARISON_EQUAL

      • 通过反射矩阵变换要在镜面上绘制的物体,经过绘制出来

      • 当反射变换后,外法线变成内法线,需要更改光栅化时三角形的绕序约定(D3D12_RASTERIZER_DESCFrontCounterClockwise为true),以保持三角形是正面不会被背面剔除

      • 绘制反射物体时,场景光源也需要沿镜面平面做反射,保证反射物体的光照计算与原物体一致,效果真实。

    • 通过半透明混合渲染镜面本身,让反射透过镜面显示。例如镜面材质 Alpha值设为 0.3,混合公式为C=0.3·\(C_{src} \)+ 0.7·\(C_{dst} \),实现 30% 不透明的镜面效果,让反射透过镜面显示。

11.5 实现平面阴影

平面阴影核心原理是从光源方向,将物体的每个顶点投影到接收阴影的平面上,生成阴影的扁平几何体,再用半透明黑色渲染该几何体,实现平面阴影效果。

平行光阴影投影公式:对于平行光方向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

diagram

从而推导出阴影矩阵:\(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) \)

diagram

从而推导出阴影矩阵:\(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的顶点,经过局部→世界→阴影,变成了阴影面片的顶点,后续还需要转到视图→投影,最终转到齐次裁剪空间。

diagram

但针对平行光源的阴影,出现一个问题,要形成阴影,平面的法线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),让阴影网格略高于地面,避免深度冲突。

11.6 总结

  • 模板缓冲与深度缓冲绑定,分辨率一致,标准格式为D24_UNORM_S8_UINTD32_FLOAT_S8X24_UINT,通过 8 位模板值实现像素级的渲染遮罩控制;

  • 模板测试通过参考值、读掩码、比较函数决定像素是否被接受,测试失败的像素会被直接丢弃;

  • 深度/模板状态是 PSO 的一部分,必须在初始化时通过D3D12_DEPTH_STENCIL_DESC预创建,运行时仅能通过OMSetStencilRef修改模板参考值;

  • 模板缓冲的核心应用:通过标记可绘制区域实现平面镜面反射,通过防重复绘制解决平面阴影的双重混合问题;

  • 模板缓冲可用于测量场景的深度复杂度(Overdraw),辅助渲染性能优化。

11.7 练习

  • 数学推导:验证通用阴影矩阵对平行光、点光源的适配性;

  • 效果调试:复现无模板缓冲时的反射穿透、阴影双重混合问题,理解模板缓冲的作用;

  • 深度缓冲原理:验证深度缓冲开启/关闭对物体遮挡的影响,理解深度测试的核心规则;

  • 环绕序验证:测试反射物体不反转环绕序的渲染错误,理解法线与环绕序的关联;

  • 深度复杂度:是指通过深度测试,向后台缓冲区某一特定元素写入像素片段的次数。它的测量可以利用模板缓冲的D3D12_STENCIL_OP_INCR操作,这样可以逐像素统计场景的Overdraw(过度绘制),并通过颜色编码可视化深度复杂度;
    另一种深度复杂性的实现方式是通过加法混合D3D12_BLEND_OP_ADD,源混合因子与目标混合因子都为D3D12_BLEND_ONE

  • Early Z优化:理解从前到后绘制不透明物体对Early Z的优化作用,以及像素着色器通过语言为SV_Depth的变量,修改深度值会导致Early Z完全失效;

  • 进阶应用:实现镜面中地面的反射、复现 Z-fighting 问题并验证解决方案。


核心知识点列表

  1. 模板缓冲本质:8 位整型离屏缓冲,与深度缓冲绑定,通过模板测试实现像素级渲染遮罩,是 OM 阶段的核心控制工具。

  2. 模板测试核心:通过「参考值 & 读掩码 比较运算符 缓冲值 & 读掩码」决定像素是否被接受,测试失败直接丢弃像素。

  3. 状态绑定规则:深度 / 模板状态是 PSO 的核心成员,运行时无法单独修改,仅模板参考值可通过 API 动态调整。

  4. 模板操作核心:可针对「模板失败、深度失败、全部通过」三种场景,独立定义模板缓冲的更新规则(替换、递增、置零等)。

  5. 平面镜面实现:先通过模板缓冲标记镜面可见区域,再仅在标记区域内绘制反射物体,解决反射穿透问题。

  6. 平面阴影实现:通过阴影矩阵将物体投影到平面,利用模板缓冲保证每个像素仅绘制一次,解决三角形重叠导致的双重混合问题。

  7. 深度复杂度测量:利用模板缓冲的递增计数,统计每个像素的 Overdraw 次数,可视化场景的过度绘制情况,辅助性能优化。

  8. 正反面独立配置:可对正面 / 背面三角形设置不同的模板规则,适配透明物体、双面渲染等特殊场景。

  9. Z-fighting 解决:共面几何体通过微小的空间偏移,避免深度缓冲精度不足导致的画面闪烁。

  10. 反射环绕序修正:物体反射后需反转正面三角形的判定规则,修正法线方向,保证光照计算正确。


难点笔记

反射变换的推导

在学习11.4时,书中提到反射变换R,并提到因为这个反射变换导致法线内翻。所以跟进查看附录C中相关内容。

首先,先看到平面是通过平面方程定义的:\(0=n·(p-p_0)=n·p-n·p_0=n·p+d=ax+by+cz+d \)

image 343x121

通过方程,可以得到\(\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 \)

image 308x300

对一个点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} \)

Overdraw的可视化实现

在书的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);
}

SRV采样法

我们可以为同一个深度/模板资源创建两种不同用途的视图

  • 深度/模板视图(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);
}

最终结果

正常渲染:

image 802x632

渲染Overdraw:

image 802x632