深度测试、Early-Z 与深度精度
深度测试、Early-Z 与深度精度:保留 LearnOpenGL 3.3 Core 正文机制,以 context—资源—结果合同、GPU 轨迹和章专属单故障完成可重放验收。
学习目标
- 能解释深度比较、深度写入与颜色写入的契约,以及驱动何时能提前执行 Early-Z
- 能配置并验证
GL_DEPTH_TEST、glDepthFunc、glDepthMask与每帧清除值 - 能从
gl_FragCoord.z反算线性视图空间距离,并解释 near/far 如何影响精度 - 能区分“关闭深度测试”“
GL_ALWAYS总通过”和“关闭深度写入”三种状态 - 能诊断 Z-fighting、上一帧深度残留、错误比较函数和
gl_FragDepth引起的性能问题
为什么谁先画都不该决定谁挡住谁
想象一条往画布上贴贴纸的流水线:场景里每个物体被拆成无数小色块,一块接一块地送上传送带,轮到谁就把谁贴到画布对应的位置。问题来了——如果一个近处的杯子和一面远处的墙,恰好要贴到画布同一个点上,先送到的那块会先贴,后送到的又盖上去……那最后留在画布上的,到底该是杯子还是墙?
如果完全听传送带的「谁后到谁说了算」,结果就乱了:墙明明在杯子后面,却可能因为「碰巧后送上来」而盖住了杯子,近的反被远的挡住,画面穿帮。这一章要解决的就是:怎么让流水线自动认准「近的该挡住远的」,不管它们谁先谁后送上来。
办法是在传送带末端加一道质检工序:每块色块贴之前,先报一下「我离镜头多远」,和这个位置上已经贴着的那块比一比——更近才准贴、更远就丢掉。有了这道工序,三维物体的前后遮挡就全自动正确了;没有它,你的场景就只是一堆按上色顺序胡乱叠起来的色块。
深度缓冲:每个像素偷偷存了一个「远近」
那道质检工序要能「比远近」,前提是画布上每个像素都记着「我现在贴的这块,离镜头多远」。存这份远近账本的地方,就是 ↡显存里一块和颜色缓冲一样大的内存,为每个像素额外存一个 0~1 的「深度值」(离镜头的远近)。屏幕显示的是颜色缓冲,而深度缓冲只在幕后记账,供深度测试比较用。常见精度为 24 位。(depth buffer,又叫 z-buffer)。
它和你上一篇认识的颜色缓冲是「孪生兄弟」:尺寸一样大、一一对应每个像素,只不过颜色缓冲存的是「这个像素什么颜色」,深度缓冲存的是「这个像素现在贴着的色块有多远」。屏幕显示的是颜色缓冲;深度缓冲不直接显示,只在幕后默默记账。它通常是 24 位精度,足够细地区分远近。
每个像素记的那个数,就是 ↡深度缓冲里每个像素存的窗口深度,通常范围 0~1。标准投影约定近平面映射到 0、远平面映射到 1,并配 GL_LESS;reversed-Z 会反转映射与比较方向。片段着色器可从 gl_FragCoord.z 读取当前片段的窗口深度。(depth value)。本章先采用 OpenGL 教程的标准配置:0 表示最近、1 表示最远;后文提到 reversed-Z 时会明确说明状态反转。下面这张图把「一个像素 = 颜色 + 深度两份数据」,以及「靠比深度让近物盖住远物」画在一起:
为什么要专门存这份账?因为只有记下每个像素「当前贴的有多远」,新来的色块才有得比。它在管线里的位置紧贴着颜色缓冲:每写一个像素的颜色,旁边就配一个深度,供下一块色块过来时核对。
深度测试:新色块和账本比一比,近的才准贴
有了账本,那道质检工序就能干活了。↡GPU 对每个候选片段做的深度比较:把片段最终深度和深度缓冲里同像素的旧值按 glDepthFunc 比较。通过后颜色才有资格进入后续写入,且只有 depth mask 允许时才更新深度;失败片段不改颜色或深度。驱动可在不改变结果时把比较提前成 Early-Z。(depth test)做的事,一句话:片段要写进某像素前,先拿最终深度和缓冲旧值比较;是否通过与是否写回深度由不同状态控制。
具体地,对每一个片段:读出它的深度值 → 和深度缓冲里同位置那个数比较 → 若「更近」(默认即深度值更小)就通过,把颜色写进颜色缓冲、并把这个更小的深度值更新进深度缓冲;否则不通过,整个片段被丢弃,颜色缓冲和深度缓冲都不动。这样无论物体以什么顺序送进管线,留在屏幕上的永远是每个像素处最近的那块。
从规范结果看,深度测试决定片段是否能更新帧缓冲;但硬件调度不一定等片段着色器跑完。若 shader 不会用 discard 改变覆盖、不写 gl_FragDepth 改变最终深度,也没有必须保留的相关副作用,驱动可以先比较深度,把必定失败的片段提前拒绝,这叫 Early-Z,能省掉昂贵的片段着色。反之则可能退回着色后的 Late-Z。无论调度早晚,最终可见结果必须一致。
因此更准确的管线描述是:光栅化先产生窗口深度,深度测试逻辑位于片段操作阶段、颜色写入之前;实现可以在安全时把比较提前。下面先按“着色完成后统一判定”的逻辑模型拆步骤,因为它最容易理解结果契约,再回到代码讨论状态与优化边界。
逐片段拆解:一个色块怎么过这道工序
猜一猜:场景里先画了一面远墙(深度 0.7),又画一个近杯子(深度 0.3),它们落在同一个像素上。如果把比较规则从「更近才通过」改成「更远才通过」,最后留在屏幕上的会是杯子还是墙?先想一想,再单步走一遍下面的判定。
下面用一个具体片段(深度 0.3)走完整条深度测试。每一步都画出深度缓冲那一格在这一刻长什么样——盯着那格的数字怎么变,就懂了整套逻辑:
① 逻辑模型:候选片段已有颜色 + 深度
流水线把这个片段送到质检口:它的颜色已经算好,深度值是
0.3。深度缓冲这一格当前存着上一块(远墙)留下的
0.7。还没动手,先把两个数摆出来。
走完这一遍就明白:留在屏幕上的永远是每格深度最小(最近)的那块,和谁先画无关。这就是 §2 那道「比远近」工序的全部秘密。
深度函数:那条「谁算通过」的规则可以换
第 ② 步里的「更小才通过」并不是写死的,它由 ↡决定深度比较怎样算通过的规则,用 glDepthFunc 设置。默认 GL_LESS;GL_LEQUAL 允许相等,GL_GREATER 用于相反方向的比较,GL_ALWAYS 总通过但仍属于启用的深度测试,depth mask 为 true 时仍会写入新深度。(depth function)决定,用 glDepthFunc(...) 设置。默认是 GL_LESS——「新深度严格更小才通过」,这也是标准深度配置下的“近者胜出”。
但你可以换成别的算子来玩花样。常用的几个:
| 深度函数 | 含义(新片段深度 vs 缓冲深度) |
|---|---|
GL_LESS | 更小才通过(默认,近的胜出) |
GL_LEQUAL | 更小或相等才通过 |
GL_GREATER | 更大才通过(反过来:远的胜出) |
GL_EQUAL | 相等才通过 |
GL_ALWAYS | 永远通过;若 depth mask 开启,仍写入新深度 |
GL_NEVER | 永远不通过(什么都画不出来) |
GL_ALWAYS 会让颜色表现成“谁后画谁赢”,但它不等于关闭深度测试:测试仍启用且总通过,glDepthMask(GL_TRUE) 时还会把每个片段深度写回缓冲;真正关闭 GL_DEPTH_TEST 时既不比较也不写深度。八个算子里还有 GL_NOTEQUAL / GL_GEQUAL,用法同理。
深度值的精度:为什么近处看得清、远处糊
到这儿还有个反直觉的真相要点破:深度值在 0~1 之间不是均匀分布的。它是 ↡深度缓冲里的深度值随真实距离不是均匀变化的,而是与 1/z 成正比——绝大部分精度集中在离镜头很近的范围,越远精度越粗。这是透视投影的副产物,好处是近处物体的遮挡判定特别精确。(non-linear depth):它和真实距离的倒数 1/z 成正比,结果就是绝大部分精度被堆在了离镜头很近的地方,越往远,同样一段真实距离能分到的深度值越少、越粗。
下面这张图把这件事画得很直白——把深度值 0→1 等间隔取刻度、标回真实距离轴,你会看到近平面附近刻度挤成一簇、远平面附近稀稀拉拉:
为什么要这么设计?因为我们通常更在乎近处物体之间的遮挡是否精确(它们占了画面大头),远处糊一点无所谓。这套非线性映射是透视投影「免费附送」的副产物,具体公式 §4 会拆开讲。但它也埋了个雷:远处深度值挨得太近,两个远处的面稍微一靠近就会分不出谁前谁后——这正是下一节 Z-fighting 的温床。
顺带一提,正因为深度是非线性的,你若想在着色器里直接把 gl_FragCoord.z 当灰度输出来做 ↡把片段的深度值当成灰度颜色输出到屏幕,用来「看见」深度缓冲。直接输出非线性的 gl_FragCoord.z 会发现画面几乎全白(精度堆在近处);先线性化再输出,才能看到从近到远平滑的灰度渐变。(depth visualization),会发现画面几乎一片惨白——因为非线性把值全挤到了接近 1 的高位。想看到从近到远平滑的灰阶,得先把它线性化。这两种输出的差别,就是下面 Demo 让你亲手切换的。
深度冲突:两个面贴太近,谁也压不住谁
最后一个绕不开的现象。当两个面片几乎共面(深度差小到深度缓冲分辨不出)时,会出现 ↡两个表面靠得极近、深度值小到深度缓冲分辨不出谁前谁后时,渲染出来同一片区域里两个面交替胜出、形成一道道交错闪烁的撕裂条纹的现象。又称深度冲突。摄像机一动还会闪。常见于贴在墙上的海报、地面上的贴花等共面情形。(Z-fighting):同一片区域里,哪个面通过深度测试反复横跳,渲染成一道道交错的撕裂条纹,视角一动还会闪烁。
成因正是上一节那个雷:深度值在远处(或两面太近时)精度不够,两个面的深度值被四舍五入成了同一个数,深度测试只能「随机」二选一,于是斑驳撕裂。怎么治?§7 会给三种实用修法。
公式:深度为什么非线性,以及怎么线性化回去
本节两件事:写出深度的非线性映射、给出把它线性化还原成真实距离的反算公式。先约定符号:
- :片段在视图空间里离镜头的真实距离(沿镜头朝向,正值)
- :近平面距离(视锥体最近的那张面,坐标系统章见过)
- :远平面距离
- :写进深度缓冲的那个 的非线性深度值
- :标准化设备坐标(NDC)里的深度,范围
非线性映射。透视投影会把视图空间的 按下面这种「与 成正比」的方式压进 :
这个式子在说:深度值不是直接拿真实距离 来线性缩放,而是拿它的倒数 来缩放。因为 在 小(近)时变化剧烈、在 大(远)时变化迟缓,所以近处一点点距离变化就能让 明显挪动(精度高),远处挪很大距离 才动一点(精度低)——这就是「近密远疏」的来源,也对上了上面那张精度图。
线性化反算。深度可视化时,我们想把这个非线性的 还原回正比于真实距离的线性值。第一步,把 的深度值映射回 的 NDC 深度:
这个式子在说:深度缓冲存的是 ,而 NDC 约定 ,所以乘 2 减 1 把区间拉伸平移过去——纯粹的区间换算。第二步,套透视投影的逆运算,从 NDC 深度反解出视图空间的真实距离 :
这个式子在说:把上一步得到的 代进去,就能解出这个片段在视图空间里的真实距离 (结果落在 之间)。最后若想再画成 的灰度,把 除以 归一即可。下面 Demo 里的「线性化」开关,切的就是「直接显示 」与「跑完这两步再显示」的区别。
动手:把深度画成灰度,亲眼看非线性
猜一猜:下面这块画布把三个前后交叠的圆,按各自的「深度」涂成灰度——越近越黑、越远越白。把「线性化」开关从关到开,靠近你的那个圆的灰度会变深还是变浅?非线性那档下,三个圆的灰度差为什么那么不明显?先拖一下再看。
这块画布没有真正的 3D 深度缓冲(ShaderDemo 只是跑一个全屏片段着色器)。我们在着色器里手动摆了三个交叠的圆,各给它一个 01 的「伪深度」,再用深度测试的逻辑挑出每个像素最近的那个圆,把它的深度按你选的方式涂成灰度——这正是「深度可视化」做的事。far 间均匀拉开的灰阶):uLinearize 关时显示这些圆在透视投影下的非线性深度(远处几个被挤到一起、偏白难分),开时按真实距离线性显示(即 §4 线性化想还原的那种在 near
实时演示加载中…
把「线性化」关掉时,三个圆灰度都偏白、彼此几乎一个色——因为非线性深度把它们的值全挤到了高位(远处精度差);打开后,三圆的灰阶才在 near~far 之间均匀拉开、一眼分出远近。拖 near / far 改变可视范围,能进一步看出 near 设得越小、精度越往近处堆。这就是 §4 那两条公式在「看见深度」上的直观差别。
代码对照:开启深度测试这几行
深度测试的代码出奇地短——绝大部分活 GPU 自动干了,你只要「开开关、每帧清账本」。先看最关键的两件事:启用深度测试 + 每帧清深度缓冲。
// 初始化时开一次:默认是关的,不开就没有遮挡判定
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glClearDepth(1.0); // 标准深度:1 是最远,和 GL_LESS 配套
// 渲染循环里每帧开头:颜色缓冲 + 深度缓冲一起清
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// 初始化时开一次:默认是关的,不开就没有遮挡判定
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LESS);
gl.clearDepth(1.0); // 标准深度:1 是最远,和 LESS 配套
// 渲染循环里每帧开头:颜色缓冲 + 深度缓冲一起清
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);两端逻辑一字对应:标准深度配置用 LESS + clearDepth(1),把账本重置成最远值后,新一帧较近片段才能逐步覆盖。不要只记“清成 1”:清除值必须与比较方向成对;采用 reversed-Z 时通常改成 GREATER + clearDepth(0),并配套反转投影深度映射。状态只改一半会让整帧全部失败或遮挡反转。
想换比较规则、或只读深度不写,再加这两行。glDepthFunc 换的就是 §3 那张表里的算子;glDepthMask(GL_FALSE) 让深度缓冲变只读(仍参与比较,但通过的片段不更新它)——画半透明物体时常用,避免它们互相挡掉。
glDepthFunc(GL_LESS); // 设比较规则(默认就是 GL_LESS,可改 GL_LEQUAL 等)
glDepthMask(GL_FALSE); // 深度缓冲只读:参与比较但通过的片段不写回深度
// …画完需要只读深度的物体后,记得 glDepthMask(GL_TRUE) 恢复可写…gl.depthFunc(gl.LESS); // 设比较规则(默认就是 LESS,可改 gl.LEQUAL 等)
gl.depthMask(false); // 深度缓冲只读:参与比较但通过的片段不写回深度
// …画完需要只读深度的物体后,记得 gl.depthMask(true) 恢复可写…depthMask(false) 只在深度测试已启用时才有意义。透明物体通常仍要读取已有不透明深度,却不应抢先写深度挡住后续透明层,因此常见流程是:先画不透明物体并写深度,再按远到近画透明物体且关闭深度写入,结束后恢复。它不能替代透明排序,这会在混合章完整展开。
若片段 shader 写 gl_FragDepth,驱动通常必须等它给出最终深度才能完成比较;大量 discard 也可能限制 Early-Z。性能诊断时可对比“不写自定义深度/不用 discard”的版本,观察片段着色调用或 GPU 时间是否显著下降。不要为了理论上的提前测试擅自删除正确性所需逻辑,而应先用 profiler 证明这里确实是过度绘制热点。
容易踩的坑
小结
- 深度缓冲为每个像素存窗口深度;标准配置是
DEPTH_TEST + LESS + clearDepth(1),状态与投影映射必须成套 - 深度测试决定片段最终能否写入;驱动可在结果不变时用 Early-Z 提前拒绝遮挡片段,
discard/gl_FragDepth可能限制优化 - 深度比较和深度写入独立:关闭测试、
GL_ALWAYS、depthMask(false)三者行为不同 - 深度值是非线性的(与
1/z成正比),精度堆在近处;要看清远近得先线性化还原 - 两面几乎共面会 Z-fighting(撕裂闪烁):别共面 / 拉远 near 平面 / 用更高精度深度缓冲
练习
问题 1(改 Demo 代码 / 观察题) 在上面的深度可视化 Demo 里,把「线性化」开关在关、开之间来回切几次,并把 near 拖到最小(0.1)、far 拖到最大(50)。回答:① 非线性档下三个圆为什么灰度都偏白、彼此难分?② 把 near 调到最小后,画面的灰度对比是更明显还是更糊?为什么?
问题 2(问答题) 有人说「深度测试就是按物体离镜头的远近排个序,远的先画、近的后画盖上去」。这个说法对吗?深度测试到底是不是靠「控制绘制顺序」来实现遮挡的?
问题 3(问答题) 你做了一张「墙 + 贴在墙上的海报」,海报和墙严丝合缝地共面,结果海报边缘满是闪烁的撕裂条纹。这是什么现象?给出至少两种不改硬件就能缓解的办法,并各说一句为什么管用。
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 深度缓冲
显存里一块和颜色缓冲一样大的内存,为每个像素额外存一个 0~1 的「深度值」(离镜头的远近)。它和颜色缓冲是孪生兄弟:颜色缓冲存「什么颜色」、深度缓冲存「多远」。屏幕显示颜色缓冲,深度缓冲只在幕后记账供深度测试比较,常见精度 24 位。又叫 z-buffer。详见本章「深度缓冲」一节。
- 深度值
深度缓冲里每个像素存的窗口深度,通常范围 0~1。标准投影把近平面映射到 0、远平面映射到 1,并配
GL_LESS;reversed-Z 会反转映射、清除值与比较方向。片段着色器可从gl_FragCoord.z读取当前片段深度。详见本章「深度缓冲」一节。- 深度测试
GPU 对候选片段最终深度做的逐像素比较。规范结果决定片段是否能写颜色,并在 depth mask 允许时更新深度;驱动可以在不改变结果时把比较提前成 Early-Z。它让遮挡与物体提交先后无关。详见本章「深度测试」一节。
- 深度函数
决定深度测试「怎样算通过」的比较规则,用
glDepthFunc设置。默认GL_LESS(新深度更小才通过,近的胜出);还有GL_LEQUAL(更小或相等)、GL_GREATER(反向比较)、GL_ALWAYS(总通过但仍可能写深度)等八种算子。详见本章「深度函数」一节。- 非线性深度
深度值随真实距离不是均匀变化的,而是与
1/z成正比——绝大部分精度集中在离镜头很近的范围,越远精度越粗。它是透视投影的副产物,好处是近处遮挡判定特别精确,代价是远处易出深度冲突。详见本章「深度值的精度」一节与 §4。- 线性化深度
把非线性的深度值还原成正比于真实距离的线性值的过程:先把 0
1 的深度映射回 -11 的 NDC 深度,再套透视投影的逆运算解出视图空间里的真实距离。深度可视化时必须先线性化,画面才能看出从近到远平滑的灰度渐变。详见本章「深度值的精度」一节与 §4。- 深度可视化
把片段的深度值当成灰度颜色输出到屏幕,用来「看见」深度缓冲长什么样。直接输出非线性的
gl_FragCoord.z会发现画面几乎全白(精度堆在近处);先线性化再输出,才能看到从近到远平滑的灰阶。详见本章「深度值的精度」一节。- 深度冲突
两个表面靠得极近、深度值小到深度缓冲分辨不出谁前谁后时,同一片区域里两个面交替胜出、渲染成一道道交错闪烁的撕裂条纹的现象(又称 Z-fighting)。摄像机一动还会闪。常见于贴在墙上的海报、地面贴花等共面情形。三种治法:别共面 / 拉远近平面 / 用更高精度深度缓冲。详见本章「深度冲突」一节与 §7。
版本、来源与运行边界
本章以 Joey de Vries 的 LearnOpenGL 原章 为授权改编依据,教学运行基线是 OpenGL 3.3 Core Profile。Khronos 当前发布的规范参照是 OpenGL 4.6 Core Profile;这里用 4.6 规范核查术语和状态合同,但不把 4.6 API 偷偷倒填为原教程内容。GLFW、GLAD、Assimp 与驱动版本都属于运行环境,不能拿“编译通过”替代对 context、资源和 framebuffer 结果的验证。
正式概念与状态责任
- depth test:在“深度测试、Early-Z 与深度精度”中由framebuffer 的 depth attachment 与 GL depth-test state负责解释其输入、受控状态和可观察结果;运行时以depth bits、func/mask 查询、片段 z、线性化值、draw 顺序与深度截图定位它的第一处变化。
- depth buffer:在“深度测试、Early-Z 与深度精度”中由framebuffer 的 depth attachment 与 GL depth-test state负责解释其输入、受控状态和可观察结果;运行时以depth bits、func/mask 查询、片段 z、线性化值、draw 顺序与深度截图定位它的第一处变化。
- depth function:在“深度测试、Early-Z 与深度精度”中由framebuffer 的 depth attachment 与 GL depth-test state负责解释其输入、受控状态和可观察结果;运行时以depth bits、func/mask 查询、片段 z、线性化值、draw 顺序与深度截图定位它的第一处变化。
- linearize:在“深度测试、Early-Z 与深度精度”中由framebuffer 的 depth attachment 与 GL depth-test state负责解释其输入、受控状态和可观察结果;运行时以depth bits、func/mask 查询、片段 z、线性化值、draw 顺序与深度截图定位它的第一处变化。
章专属 OpenGL 状态实验
先预测“清除 depth,启用测试/写入,绘制不透明物并按需线性化观测值”发生后,framebuffer 的 depth attachment 与 GL depth-test state应怎样改变深度格式、clear value、compare func、write mask、片段深度和 early test 条件;再操作三个实验。实验不生成变化率或正确率等虚构总分,只显示真实 GL 状态、资源、命令和可观察结果。
实验一:Context—资源—结果合同
选择任一正式概念与基线/单故障场景,核对它是否进入本章状态合同。正式概念只有同时出现在解释、可视状态和交付证据中才算覆盖。
Context · resource · observable result
深度测试、Early-Z 与深度精度:状态合同
解释 depth buffer、比较函数、写掩码、clear 与非线性精度如何共同决定可见片段
验证场景
官方教程正式概念
logl-18 · 基线帧
depth test:固定 context、资源内容与输入事件,执行“清除 depth,启用测试/写入,绘制不透明物并按需线性化观测值”
冻结输入:depth test
framebuffer 的 depth attachment 与 GL depth-test state记录深度格式、clear value、compare func、write mask、片段深度和 early test 条件
结果:得到可重复的初始 GL 状态与资源身份
观测:depth bits、func/mask 查询、片段 z、线性化值、draw 顺序与深度截图中的初始快照
预期:framebuffer 的 depth attachment 与 GL depth-test state得到可复查结果,并持续满足“每帧清除可写 depth buffer;透明/天空盒阶段改变状态后显式恢复”
实验二:CPU 命令到 GPU 结果的五段轨迹
逐段执行“清除 depth,启用测试/写入,绘制不透明物并按需线性化观测值”,在每一步记录资源身份、状态变化与第一个可观察结果,并持续核对“每帧清除可写 depth buffer;透明/天空盒阶段改变状态后显式恢复”。
CPU command · GL state · GPU result
深度测试、Early-Z 与深度精度:五段轨迹
当前观测:depth bits、func/mask 查询、片段 z、线性化值、draw 顺序与深度截图中的初始快照
不变量:每帧清除可写 depth buffer;透明/天空盒阶段改变状态后显式恢复
实验三:单故障与同输入恢复
注入“上一帧把 glDepthMask 设为 false,下一帧直接 clear,深度缓冲实际未被清除”,保存首个分岔;撤销后沿用完全相同的 context、资源内容、uniform 和 draw 输入重放。只有depth bits、func/mask 查询、片段 z、线性化值、draw 顺序与深度截图一起恢复才算修复。
Single fault · first divergence · replay
深度测试、Early-Z 与深度精度:反例与恢复
故障:上一帧把 glDepthMask 设为 false,下一帧直接 clear,深度缓冲实际未被清除
第 1 次沿用同一 context、资源、uniform 与 draw 输入
保持其余输入不变,仅注入“上一帧把 glDepthMask 设为 false,下一帧直接 clear,深度缓冲实际未被清除”
每帧清除可写 depth buffer;透明/天空盒阶段改变状态后显式恢复
depth bits、func/mask 查询、片段 z、线性化值、draw 顺序与深度截图
最小可重放检查
unit: logl-18
owner: framebuffer 的 depth attachment 与 GL depth-test state
state_or_resource: 深度格式、clear value、compare func、write mask、片段深度和 early test 条件
command: 清除 depth,启用测试/写入,绘制不透明物并按需线性化观测值
pass_invariant: 每帧清除可写 depth buffer;透明/天空盒阶段改变状态后显式恢复
single_fault: 上一帧把 glDepthMask 设为 false,下一帧直接 clear,深度缓冲实际未被清除
required_evidence: depth bits、func/mask 查询、片段 z、线性化值、draw 顺序与深度截图复核者先仅依据以上合同写出预期,再运行基线、单故障和清理后重放。若两次基线的资源身份、首个状态变化或 framebuffer 结果不同,必须保留差异,不能用最终截图相似掩盖中间状态错误。