在 3D 渲染中如何正确且高效地处理物体的遮挡关系是核心问题之一。本文将深入探讨 OpenGL 中的**传统深度测试Depth Test**以及现代 GPU 硬件优化技术——**Early-Z 拒绝Early Depth Test**的工作原理、管线位置、常见问题与优化方案。一、 深度测试Depth Test基础1. 渲染管线中的位置传统深度测试发生在光栅化Rasterization之后、混合Blending之前。在片段操作阶段其具体顺序如下光栅化 (Rasterization) ↓ [ 早期片段测试 (Early-Z) ] ← (GPU 硬件优化阶段可选) ↓ 片段着色器 (Fragment Shader) ↓ 裁剪测试 (Scissor Test) ↓ 多重采样操作 (Multisample) ↓ 模板测试 (Stencil Test) ← (必须在传统深度测试之前) ↓ 传统深度测试 (Depth Test) ← (常规执行阶段) ↓ 混合 (Blending) ↓ 抖动与逻辑操作 (Dithering Logical Ops) ↓ 写入帧缓冲 (Framebuffer Write)关键机制解析模板测试在深度测试之前若启用了模板测试片段会首先进行模板比较。若模板测试失败片段直接被丢弃不再执行深度测试。传统测试在片段着色器之后因为传统的深度测试Late-Z在片段着色器FS执行完毕后才进行这意味着即便片段最终因被遮挡而被深度测试丢弃GPU 也已经为其运行了片段着色器。这造成了严重的计算资源浪费即 Overdraw 重绘。2. 深度测试工作原理(1) 深度缓冲Depth Buffer深度缓冲是一个与颜色缓冲Color Buffer等大的一维/二维数组。每个像素点对应存储一个深度值常用格式为 24-bit 整数或 32-bit 浮点数。深度值范围通常被归一化在[0.0, 1.0]之间0.0代表近裁剪面Near Plane1.0代表远裁剪面Far Plane。(2) 深度测试比较当光栅化生成的新片段到达深度测试阶段时GPU 会将**当前片段的深度值Z_new与深度缓冲中对应位置已有的深度值Z_buffer**进行比较。比较规则由glDepthFunc()函数设置比较函数说明GL_LESS新深度值 缓冲深度值时通过默认值GL_LEQUAL新深度值 ≤ 缓冲深度值时通过GL_GREATER新深度值 缓冲深度值时通过GL_GEQUAL新深度值 ≥ 缓冲深度值时通过GL_EQUAL新深度值 缓冲深度值时通过GL_NOTEQUAL新深度值 ≠ 缓冲深度值时通过GL_ALWAYS总是通过测试GL_NEVER从不通过测试(3) 决策流程图┌───────────────────────────┐ │ 新片段到达测试阶段 │ └─────────────┬─────────────┘ ▼ ┌───────────────────────────┐ │ 进行模板测试 │ └─────────────┬─────────────┘ ├────────── 失败 ──────────┐ ▼ 通过 │ ┌───────────────────────────┐ │ │ 执行 glDepthFunc 比较 │ │ │ (Z_new vs Z_buffer) │ │ └─────────────┬─────────────┘ │ ├────────── 失败 ──────────┼──► 丢弃片段 ▼ 通过 │ (依据规则更新模板缓冲) ┌───────────────────────────┐ │ │ 1. 写入/更新颜色缓冲区 │ │ │ 2. 更新深度缓冲区(若允许)│ │ └───────────────────────────┘ ▼3. 启用与配置在 OpenGL 中深度测试的生命周期控制如下// 1. 开启深度测试glEnable(GL_DEPTH_TEST);// 2. 设置比较函数默认是 GL_LESSglDepthFunc(GL_LESS);// 3. 控制深度写入权限glDepthMask(GL_TRUE);// 允许写入深度缓冲区不透明物体glDepthMask(GL_FALSE);// 禁止写入深度缓冲区只进行只读比较半透明物体// 4. 设置深度范围映射默认 0.0 到 1.0glDepthRange(0.0f,1.0f);4. 深度缓冲区的格式与精度(1) 常见深度格式GL_DEPTH_COMPONENT1616位非线性正规化整数精度较低。GL_DEPTH_COMPONENT2424位非线性正规化整数最常用的高性价比格式。GL_DEPTH_COMPONENT32F32位浮点数高精度常配合反向ZReversed-Z使用。(2) 深度值的非线性映射在透视投影下视空间中的深度Z v i e w Z_{view}Zview​并不是线性映射到屏幕空间的Z w i n d o w Z_{window}Zwindow​。映射公式如下F d e p t h 1 / z − 1 / n e a r 1 / f a r − 1 / n e a r F_{depth} \frac{1/z - 1/near}{1/far - 1/near}Fdepth​1/far−1/near1/z−1/near​这种非线性映射使得靠近近裁剪面Near Plane的区域精度极高而靠近远裁剪面Far Plane的区域精度极低。这也是导致深度冲突Z-Fighting的主要原因。5. 深度冲突Z-Fighting当两个表面距离非常近且位于深度缓冲区精度较差的区域如远处时GPU 无法区分两个片段的深度值大小导致渲染画面中两个表面的图案相互闪烁穿插。解决方案详解拉远近裁剪面Near Plane原理根据透视投影的深度公式F d e p t h f f − n ⋅ ( 1 − n z ) F_{depth} \frac{f}{f - n} \cdot (1 - \frac{n}{z})Fdepth​f−nf​⋅(1−zn​)深度值的变化率即导数与n z 2 \frac{n}{z^2}z2n​成正比。示例分析若设置近裁剪面n 0.1 n 0.1n0.1远裁剪面f 1000 f 1000f1000。当物体从真实深度z 0.1 z 0.1z0.1移动到z 1.0 z 1.0z1.0时深度值F d e p t h F_{depth}Fdepth​会从0.0暴增到约0.9。这意味着前 1 米的空间消耗了整个深度缓冲90 % 90\%90%的精度而剩下的999 999999米空间只能共享可怜的10 % 10\%10%精度。若将近裁剪面拉远到n 1.0 n 1.0n1.0则同样的1.0 ∼ 10.0 1.0 \sim 10.01.0∼10.0米区间消耗90 % 90\%90%精度。原先在1.0 ∼ 1000 1.0 \sim 10001.0∼1000米区间的深度分辨率因此被放大了数倍大大降低了中远景发生 Z-Fighting 的概率。最佳实践尽量避免将 Near Plane 设为极小值如0.001通常设为0.1或1.0为宜。提高深度缓冲区精度使用 24 位GL_DEPTH_COMPONENT24甚至 32 位浮点型GL_DEPTH_COMPONENT32F深度缓冲区从而增加可表达的深度数值范围和密度。Polygon Offset多边形偏移原理在光栅化阶段通过为片段的深度值手动附加一个与多边形斜率相关的偏移量Offset强制拉开共面或极度贴合的多边形深度差。代码示例glEnable(GL_POLYGON_OFFSET_FILL);// 参数1: factor (缩放因子针对多边形的最大深度斜率)// 参数2: units (单位因子针对深度缓冲的最小可分辨值)glPolygonOffset(1.0f,1.0f);使用反向ZReversed-Z技术配合浮点深度缓冲极力推荐原理解析传统的精度错配非线性深度投影公式在近裁剪面即F d e p t h ≈ 0.0 F_{depth} \approx 0.0Fdepth​≈0.0附近拥有极高几何精度而浮点数IEEE 754 标准的存储特性也是越靠近 0.0 精度越高靠近 1.0 精度越低。两者的最高精度点都堆叠在近裁剪面处造成了浪费而远裁剪面处F d e p t h ≈ 1.0 F_{depth} \approx 1.0Fdepth​≈1.0则两项精度都最差。反向对消反向 Z 将近裁剪面映射为1.0远裁剪面映射为0.0。近裁剪面z 较小几何精度高但由于映射为1.0浮点存储精度低。远裁剪面z 较大几何精度极低但由于映射为0.0浮点存储拥有极其庞大的可用精度空间。效果这种“对向抵消”使得在整个视锥体空间中可用的深度精度分布几乎达到了完美的线性均匀状态。配合 32-bit Float 深度缓冲甚至能将远裁剪面设为无穷大Infinity而不会在任何距离发生 Z-Fighting。实现步骤清空深度值设为0.0glClearDepth(0.0f);将深度比较函数修改为大于通过glDepthFunc(GL_GREATER);投影矩阵修改在生成透视投影矩阵时反转n nn和f ff的映射输出。6. 实战渲染流程与常见问题(1) 经典渲染循环框架// 初始化阶段glEnable(GL_DEPTH_TEST);glDepthFunc(GL_LESS);// 渲染主循环while(!glfwWindowShouldClose(window)){// 必须清空颜色缓冲与深度缓冲glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);// 1. 先绘制所有不透明物体 (开启深度测试、开启深度写入)glDepthMask(GL_TRUE);drawOpaqueObjects();// 2. 后绘制所有半透明物体 (开启深度测试、关闭深度写入)// 注意半透明物体需要从远到近进行排序渲染glEnable(GL_BLEND);glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);glDepthMask(GL_FALSE);// 只做深度测试不写入深度避免遮挡后面的透明像素drawTransparentObjects();glDisable(GL_BLEND);}(2) 常见排错表现象可能原因解决方案画面重叠后绘制的物体总是覆盖先绘制的物体未启用深度测试或者每帧未清空深度缓冲。检查是否调用glEnable(GL_DEPTH_TEST)每帧使用glClear(GL_DEPTH_BUFFER_BIT)。透明物体相互遮挡关系混乱或者直接把背景抠空渲染透明物体时未关闭深度写入导致其透明区域写入了较浅的深度。渲染透明物体前调用glDepthMask(GL_FALSE)且确保排序正确。远处的山体或建筑表面出现严重的黑色锯齿闪烁发生了 Z-Fighting。拉远近裁剪面或使用glPolygonOffset。二、 Early-Z 优化详解1. 基本概念Early-Z是现代 GPU 的一种硬件层面优化技术。Late-Z传统管线光栅化 → 片段着色器 → 深度/模板测试。Early-Z优化管线光栅化 →Early-Z / Stencil 测试→ 片段着色器。通过在片段着色器FS执行之前先一步进行深度和模板测试凡是未通过测试的片段会被直接丢弃从而完全跳过该片段的片段着色器计算。这对于复杂着色多光源、重度光照计算的场景能带来成倍的性能提升。2. 硬件实现原理Hi-Z为了在硬件层面更快速地过滤大面积被遮挡的区域AMD 和 NVIDIA 引入了Hi-ZHierarchical Z层次化 Z-Cache机制。(1) Z-Cache 金字塔结构GPU 会在片上On-Chip维护一个低分辨率的深度金字塔。与纹理的 Mipmap 类似Mip 0最精细对应完整的逐像素深度缓冲存于显存中。Mip 1将 Mip 0 按2 × 2 2\times22×2区域合并记录该区域内的最大深度值Z m a x Z_{max}Zmax​假设使用GL_LESS。Mip 2更粗糙将 Mip 1 再按2 × 2 2\times22×2合并记录该区域的最大深度值。【 Hi-Z 层次化深度金字塔 】 [ Mip 2: 极粗糙 ] [ Mip 1: 中等 ] [ Mip 0: 逐像素 ] ┌─────────────────┐ ┌────────┬────────┐ ┌──┬──┬──┬──┐ │ │ │ │ │ ├──┼──┼──┼──┤ │ 1/16 尺寸 │ ──► ├────────┼────────┤ ──► ├──┼──┼──┼──┤ │ (4×4最大深度) │ │ │ │ ├──┼──┼──┼──┤ └─────────────────┘ └────────┴────────┘ └──┴──┴──┴──┘(2) 快速剔除Z-Cull / Hi-Z Test当一个三角形光栅化时GPU 首先将其包围盒与较粗糙层级如 Mip 2的Z m a x Z_{max}Zmax​进行比较。如果该三角形在 Mip 2 对应区域的最小深度值仍大于Z m a x Z_{max}Zmax​则说明整个三角形在屏幕上该区域被完全遮挡。GPU 会直接整块丢弃Z-Cull无需访问耗时的逐像素显存深度缓冲。如果无法整块剔除再逐级向下检查直至在 Mip 0 级别进行精确 of Early-Z 测试。3. 管线工作流程下图展示了现代 GPU 在处理片段时的完整分支路径。光栅化生成片段 │ ▼ ┌───────────────────────┐ │ Hi-Z / Z-Cull 测试 │ └───────────┬───────────┘ ├────────── 失败 ──► 丢弃片段 (不执行 FS) ▼ 通过 ┌───────────────────────┐ │ Early-Z / Stencil │ └───────────┬───────────┘ ├────────── 失败 ──► 丢弃片段 (不执行 FS) ▼ 通过 ┌───────────────────────┐ │ 片段着色器 (FS 执行) │ └───────────┬───────────┘ │ ┌─────────────┴─────────────┐ │ 是否修改过深度/包含 discard? │ └─────────────┬─────────────┘ ├─ 是 (FS修改了深度) │ │ │ ▼ │ ┌───────────────────────┐ │ │ Late-Z 深度测试 │ ← 必须在此阶段重新测试 │ └──────────┬────────────┘ │ ├────── 失败 ──► 丢弃片段 │ ▼ 通过 ▼ ▼ ┌──────────────────┐ │ 混合与写入帧缓冲 │ └──────────────────┘4. Early-Z 失效的场景尽管 Early-Z 性能优异但在编写片段着色器或配置管线时以下操作会使得 GPU 无法在 FS 之前预知或确定深度从而迫使 GPU禁用 Early-Z退化为 Late-Z片段着色器中修改了深度值写入gl_FragDepth。此时GPU 无法在 FS 执行前得知最终深度只能在 FS 执行完后进行 Late-Z。使用了丢弃操作discard即使没有修改深度但如果 FS 中有discardGPU 就不能提前进行深度写入。因为如果提前写了深度而 FS 随后执行了discard深度缓冲区就会被污染。注部分现代 GPU 可以在有discard时执行 Early-Z 测试Cull但会禁用早期深度写入待 FS 执行完确定不丢弃后才写入这仍会损失一部分性能。启用了 Alpha-to-Coverage或手工进行的 Alpha 测试它们会在 FS 运行中动态改变片段的 Coverage覆盖率导致深度无法提前确定。混合状态或深度比较函数频繁切换频繁更改glDepthFunc会导致 Hi-Z 缓存数据失效导致硬件保守地关闭优化。5. 显式开启与优化方案方案 1Pre-Z Pass深度预通道应用层控制这是大中型 3D 引擎最常用的方案分两个 Pass 渲染// Pass 1: 仅填充深度缓冲 (Pre-Z Pass) glColorMask(GL_FALSE,GL_FALSE,GL_FALSE,GL_FALSE);// 禁用颜色写入glDepthMask(GL_TRUE);// 开启深度写入glEnable(GL_DEPTH_TEST);glDepthFunc(GL_LESS);// 渲染所有不透明物体此时 FS 可以是极简的空着色器避免计算开销drawAllOpaqueObjectsWithSimpleShader();// Pass 2: 正常光照着色渲染 (Main Pass) glColorMask(GL_TRUE,GL_TRUE,GL_TRUE,GL_TRUE);// 恢复颜色写入glDepthMask(GL_FALSE);// 关闭深度写入 (因为深度已在 Pass 1 确定)glDepthFunc(GL_EQUAL);// 仅渲染深度完全相等的片段// 使用复杂的 Shader 渲染物体所有被遮挡的片段会在此瞬间被 Early-Z 剔除drawAllOpaqueObjectsWithComplexShader();方案 2GLSL 显式声明强制 Early-ZShader 层控制在GLSL 4.2或使用ARB_shader_image_load_store扩展中你可以使用布局修饰符显式地告诉 GPU 强制进行 Early-Z 测试#version 420 core // 强制开启早期片段测试 layout(early_fragment_tests) in; out vec4 FragColor; in vec2 TexCoords; uniform sampler2D alphaTex; void main() { // 即使我们使用了 discard 操作GPU 依然会在 FS 执行之前进行深度/模板测试 // 如果测试失败FS 直接不执行。如果测试通过FS 执行并在通过后写入深度。 if (texture(alphaTex, TexCoords).r 0.1) { discard; } FragColor vec4(1.0, 0.0, 0.0, 1.0); }方案 3保守深度修改Conservative Depth如果你的片段着色器必须修改gl_FragDepth但你知道修改的趋势如深度只会变大或变小可以使用保守深度布局修饰符GLSL 4.2允许 GPU 在特定比较下保留 Early-Z 优化#version 420 core // 声明我们写入的深度只会比原插值深度大即往远剪裁面方向推 layout (depth_greater) out float gl_FragDepth; void main() { float customDepth calculateDepth(); // 计算出比插值深度更大的值 gl_FragDepth customDepth; // 依然能够享受部分 Early-Z / Hi-Z 优化 }6. Early-Z vs Late-Z 对比表特性Early-Z (早期深度测试)Late-Z (传统深度测试)执行时机片段着色器FS执行之前。片段着色器FS执行之后。硬件要求现代 GPU 硬件支持默认自动尝试开启。任何硬件皆支持。深度缓存写入可以在 FS 执行前写入若含discard则需延迟写入。仅在 FS 执行完毕并通过测试后写入。主要性能优势避免了被遮挡片段的 FS 计算大幅降低重绘Overdraw开销。无性能优势为 FS 修改深度提供保底正确性。触发失效的条件FS 写入gl_FragDepth、启用 Alpha 测试等。无限制。相关官方规范参考OpenGL Wiki - Early Fragment TestLearnOpenGL - Depth testing