0. 画有遮挡关系的三角形
根据之前AI建议的短期内的学习路线,上一篇已经画了一个3D三角形了。下一篇就是要引入深度缓冲了,同时需要画两个有遮挡关系的三角形来验证深度缓冲是有效的。

和AI说过后,Claude Opus 4.6 再次一遍就完成了代码的编写,效果没问题:

下面根据代码的修改来问AI一些问题。
1. 顶点数据变为两个三角形
首先是顶点数据变成了两个有遮挡关系的三角形,没啥好问的。

只需要注意之后Draw的时候是画俩三角形即可:

2. 创建深度纹理
渲染的时候,在Render_Learning函数中首先创建了一张深度纹理。然后Clear掉:

其实我注意到在SceneTextures应该已经有深度纹理了,于是问AI它为什么要再创建一次,它的回答:(仅供参考)
SceneTextures里确实有Depth:
77:77:Engine/Source/Runtime/Renderer/Internal/SceneTextures.hFRDGTextureMSAA Depth{};和
Color一样是FRDGTextureMSAA(你屏蔽了 MSAA 后Target == Resolve,等于一张普通纹理)。
我没用它,是因为SceneTextures.Depth需要在InitializeViewFamily之后额外调用特定的 setup 流程才会被真正创建和分配 。在你当前的Render_Learning里,只做了InitializeViewFamily+ 取SceneTextures.Color,如果直接用SceneTextures.Depth.Target很可能是nullptr(未被创建),因为深度纹理的创建取决于SetupMode里是否包含了SceneDepth等标志。不过,如果你希望和原版更一致地使用
SceneTextures.Depth,也是可以的------需要在Render_Learning里确保SceneTextures的 depth 被正确初始化。我选择自己CreateTexture是为了最小化依赖 :不需要了解SceneTextures的深度初始化流程,只要知道"创建一张深度纹理、清除、绑到 pass"就能学到深度缓冲的概念。等你后续学习阶段需要用到
SceneTextures.Depth(比如实际的 base pass),再切换过去会更自然。
看起来是这样更有利于学习,那我就想正好学习下这里创建纹理的接口了。
总的来说,就是FRDGTextureDesc用来描述纹理,接下来用 GraphBuilder.CreateTexture 实际创建。SceneColor的Color也是如此,只是描述参数不一样:

让AI为我介绍下并对比下:(仅供参考)
| 成员 | 类型 | 基础含义 | SceneColor 的值 | 你的 Depth 的值 |
|---|---|---|---|---|
| Dimension | ETextureDimension |
纹理维度(2D / 3D / Cube 等) | Texture2D |
Texture2D |
| Extent | FIntPoint |
纹理宽高(像素) | Config.Extent(视口分辨率,如 1920×1080) |
SceneTextures.Color.Target->Desc.Extent(和 Color 相同) |
| Format | EPixelFormat |
像素格式 | Config.ColorFormat(通常 PF_FloatRGBA,每像素 4 个 16 位浮点通道) |
PF_DepthStencil(深度 24 位 + 模板 8 位,或平台等价格式) |
| ClearValue | FClearValueBinding |
GPU 快速清除时的默认值 | Config.ColorClearValue(通常是黑色透明) |
FClearValueBinding::DepthFar(深度最远值,Reversed-Z 下为 0) |
| Flags | ETextureCreateFlags |
纹理用途标志 | 包含 TexCreate_RenderTargetable(可作为颜色 RT) |
TexCreate_DepthStencilTargetable(可作为深度 RT)` |
| NumSamples | uint8 |
MSAA 采样数,1 = 无 MSAA | Config.NumSamples(你强制设为 1) |
1(默认值,未指定) |
| NumMips | uint8 |
Mip 链层数 | 1(默认值) |
1(默认值) |
| Depth | uint16 |
3D 纹理的深度,2D 时固定为 1 | 1 |
1 |
| ArraySize | uint16 |
纹理数组的层数,非数组时为 1 | 1 |
1 |
3. 把深度纹理绑定RT

这段代码是把深度纹理绑定为当前 pass 的深度渲染目标 ,和颜色 RT 的绑定(RenderTargets[0] = FRenderTargetBinding(...))是对应关系。逐参数看让AI解释下参数:
| 参数 | 值 | 含义 |
|---|---|---|
| DepthTexture | 你创建的那张 PF_DepthStencil 纹理 |
绑哪张纹理作为深度目标 |
| DepthLoadAction | ERenderTargetLoadAction::ELoad |
pass 开始时加载这张纹理里已有的深度值(你之前清除到了最远值,所以加载的就是"全部最远") |
| StencilLoadAction | ERenderTargetLoadAction::ENoAction |
模板通道不关心,不加载也不清除 |
| DepthStencilAccess | FExclusiveDepthStencil::DepthWrite_StencilNop |
这个 pass 对深度是可写 的,对模板不操作 |
这里我多嘴问了下,为啥 "深度(Depth)" 和 "模板(Stencil)" 总是一起出现?
AI的回答:(仅供参考)
因为GPU 硬件上,深度和模板就是存在同一张纹理里的 。
典型的深度/模板格式(比如你用的
PF_DepthStencil)在显存里的布局是:每个像素 32 位,其中 24 位存深度,8 位存模板 。它们共享一块显存、共享一个 DSV(Depth Stencil View),是硬件层面的绑定关系。所以在 API 层面(D3D、Vulkan 等),绑定深度目标时必然同时绑上了模板------因为它们物理上就是同一张纹理的不同通道。UE 的
FDepthStencilBinding需要你分别指定两者的 LoadAction 和访问权限,就是因为虽然共享一张纹理,但逻辑上是两个独立功能,你可能只用深度不用模板(你当前的情况),也可能两个都用。
4. PSO:启用深度写入和深度测试
PSO的DepthStencilState发生如下变化:

让AI解释一下:(仅供参考)
第一个参数 true:bEnableDepthWrite
控制是否往深度缓冲写入新的深度值。
| 值 | 效果 |
|---|---|
true |
像素通过深度测试后,把它的深度值写进深度缓冲,后续像素会和这个新值比较 |
false |
只做深度测试(比较),但不更新深度缓冲 |
你之前画全屏四边形时用的是 false------因为不需要深度,只是拷贝纹理。现在画 3D 三角形需要 true,这样先画的三角形会把深度写进去,后画的三角形再和它比较,才能实现遮挡。
第二个参数 CF_DepthNearOrEqual:深度测试的比较函数
决定**"新像素的深度值和深度缓冲里已有的值怎么比,才算通过测试"**。
UE 使用 Reversed-Z (近处 z=1,远处 z=0),所以"离相机更近"意味着"z 值更大"。CF_DepthNearOrEqual 会自动适配这个约定:
| 比较函数 | 含义 | 通过条件(Reversed-Z 下实际操作) |
|---|---|---|
CF_DepthNearOrEqual |
比已有的更近或等距才通过 | 新z >= 旧z |
CF_DepthNear |
比已有的严格更近才通过 | 新z > 旧z |
CF_DepthFartherOrEqual |
比已有的更远或等距才通过 | 新z <= 旧z |
CF_DepthFarther |
比已有的严格更远才通过 | 新z < 旧z |
CF_Always |
永远通过,不做比较 | 无条件通过 |
CF_Never |
永远不通过 | 无条件拒绝 |
你用 CF_DepthNearOrEqual 就是标准的"近处覆盖远处"------这是绝大多数 3D 渲染的默认选择,原版 base pass 用的也是这个。
和之前的对比
| 旧(全屏四边形) | 新(3D 三角形) | |
|---|---|---|
| 写深度 | false |
true |
| 比较函数 | CF_Always(不做测试) |
CF_DepthNearOrEqual(近处通过) |
5. 让AI解释下GPU相关的行为
问AI::
目前我已经看完了新加的所有的C++代码了,其实只是增加了深度缓冲纹理并且在PSO上指定了。
那实际的深度比较逻辑,应该是GPU去做的吧。请帮我详细解释下这里GPU的行为,如果能结合目前的代码改动那最好了。
AI回答:(仅供参考)
是的:深度比较和(在你配置下)深度写入,都是 GPU 固定功能管线里做的,不是你在 C++ 里手写循环。你这边做的是:准备好深度纹理、告诉 RDG「这个 pass 要绑深度」、在 PSO 里打开「测深度 + 写深度」。下面按数据流把 GPU 在干什么说清楚,并和你现在的代码对上。
1) 深度值从哪来(和你 shader 的关系)
顶点着色器里:
16:16:Engine/Shaders/Private/Tools/SceneLearning.usf
OutPosition = mul(float4(InPosition, 1.0f), ViewProjectionMatrix);
输出的是 裁剪空间 的 float4(x, y, z, w)(SV_POSITION)。
之后 GPU 自动做:
- 透视除法:得到 NDC 里的 ((x', y', z'))(以及用于插值的 (w) 等)。
- 视口变换 :把 NDC 映射到当前
SetViewport里的像素坐标。
用于深度测试的"片元深度" 来自这一步之后管线里的深度值(在 UE 的 Reversed-Z 投影下,通常离相机越近,这个值越大 )。你没有在 PS 里改深度,所以用的就是光栅化阶段插值后的这个深度。
也就是说:深度比较用的数,间接来自你的 ViewProjectionMatrix × 世界坐标,但比较本身不在 shader 里写逻辑。
2) 你 C++ 里做的事,在 GPU 上对应什么
(1)FDepthStencilBinding + DepthWrite_StencilNop
含义是:这个 pass 把 DepthTexture 绑成当前渲染目标的深度/模板表面 ,并且声明「要写深度、不动模板」。
GPU 侧会:在光栅化/输出合并阶段,读写这张纹理里每个像素对应的深度存储。
(2)DepthLoadAction::ELoad
Pass 开始时,深度缓冲里已经是你在 AddClearDepthStencilPass 里清成的「最远」值。
GPU 不会 在深度测试前把整张图清成别的值(除非你用的是 Clear);每个像素会和「当前缓冲里已有的深度」比较。
(3)TStaticDepthStencilState<true, CF_DepthNearOrEqual>
这是 深度测试 + 深度写入 的总开关:
true:深度测试通过 的片元,把新深度写回深度缓冲。CF_DepthNearOrEqual:在 UE 的 Reversed-Z 下,等价于「新片元不比已有深度更远」才通过(更近或相等都可以通过)。
若未通过 :该片元被丢弃,通常不会执行 PS(也不会写颜色、不写深度)。
3) 对你这次 DrawPrimitive(0, 2, 1) 的具体行为
一次 draw 会画 两个三角形、共 6 个顶点(两个在空间里交叉)。
对每一个 最终被光栅化覆盖到的子像素/片元(可理解成「一个像素里可能被三角形盖住的一个采样点」),GPU 大致做:
- 用插值得到该片元的 深度 (znewz_{new}znew)。
- 读当前绑定的深度纹理在该像素处的 (zoldz_{old}zold)(最初是清除后的最远值)。
- 用 PSO 里的比较函数判断:在 Reversed-Z +
CF_DepthNearOrEqual下,相当于「(znewz_{new}znew) 是否 ≥ (zoldz_{old}zold)」(语义是「更近或一样近」)。 - 若失败:丢弃,不写颜色、不写深度。
- 若成功 :执行 PS(你的
MainPS只输出颜色),然后把 颜色 写到RenderTargets[0],把 (z_{new}) 写到深度缓冲(因为开启了深度写入)。
绘制顺序 :两个三角形在同一个 draw 里按顶点顺序光栅化,本质上是按图元顺序处理。交叉区域里,后光栅化、且深度更近的那一侧会覆盖先画的那一侧的颜色和深度------这就是你在画面上看到的遮挡。
4) 和「只有颜色 RT、CF_Always」时对比
| 项目 | 之前(无深度) | 现在 |
|---|---|---|
| 深度纹理 | 未绑定 | 绑定 DepthTexture |
| 深度测试 | CF_Always(永远过) |
CF_DepthNearOrEqual |
| 深度写入 | 关 | 开 |
| 交叉三角形 | 后画的像素会直接盖颜色(画家算法式) | 由深度决定谁可见,更符合 3D 遮挡 |
5) 一句话总结
CPU/RDG/PSO 只负责「绑哪张深度图、清成什么初值、用什么比较、过不过就写不写」;每个片元拿自己的插值深度和缓冲里已有的深度比一比,是 GPU 固定管线在光栅化之后、像素着色器前后(实现因硬件略异,但语义如此)自动完成的。 你的 SceneLearning.usf 只负责给出裁剪空间位置(从而间接决定深度),没有写 SV_Depth 之类的覆盖,所以全程用的就是这套默认深度管线。