OpenGL 深度测试及Early-Z详解

OpenGL 深度测试详解

一、深度测试在渲染管线中的位置

深度测试(Depth Test)发生在光栅化阶段(Rasterization)之后,在模板测试(Stencil Test)之后混合(Blending)之前。具体位置如下:

应用阶段 → 几何处理 → 光栅化 → 片段着色器 → 深度测试 → 模板测试 → 混合 → 帧缓冲

关键点:深度测试是在片段着色器执行之后进行的,这意味着:

  1. 即使片段着色器计算了颜色,深度测试失败的话该片段也会被丢弃
  2. 深度测试失败不会触发 discard 操作(那是片段着色器的工作)

二、深度测试的工作原理

1. 深度缓冲(Depth Buffer)
  • 深度缓冲是一个与颜色缓冲同等大小的缓冲区
  • 每个像素存储一个深度值(通常为 16-bit、24-bit 或 32-bit 浮点数)
  • 深度值范围通常是 [0, 1],表示从近裁剪平面到远裁剪平面
2. 深度测试比较

当一个新的片段到达深度测试阶段时,OpenGL 会比较:

新片段深度值 vs 深度缓冲中存储的深度值

比较函数由 glDepthFunc() 设置:

函数描述
GL_LESS新片段 < 存储深度 ✓(默认
GL_LEQUAL新片段 ≤ 存储深度
GL_GREATER新片段 > 存储深度
GL_GEQUAL新片段 ≥ 存储深度
GL_EQUAL新片段 == 存储深度
GL_NOTEQUAL新片段 ≠ 存储深度
GL_ALWAYS总是通过测试
GL_NEVER从不通过测试
3. 决策流程
┌─────────────────────────────┐
│     新片段到达深度测试        │
└─────────────┬───────────────┘
              ▼
┌─────────────────────────────┐
│   glDepthFunc(比较函数)       │
│   新深度 vs 存储深度          │
└─────────────┬───────────────┘
              ▼
        ┌─────┴─────┐
        │  通过?    │
        └─────┬─────┘
         是/  \否
        ↙      ↘
   更新颜色缓冲    丢弃片段
   + 更新深度缓冲

三、深度测试的启用和配置

// 1. 启用深度测试
glEnable(GL_DEPTH_TEST);

// 2. 设置比较函数(默认 GL_LESS)
glDepthFunc(GL_LESS);

// 3. 设置深度写入范围
glDepthRange(0.0f, 1.0f);  // 通常默认

// 4. 控制是否写入深度缓冲
glDepthMask(GL_TRUE);   // 允许写入(默认)
glDepthMask(GL_FALSE); // 禁止写入(用于只读深度,如镜面效果)

四、深度测试类型

1. 普通深度测试
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
2. 先深度后颜色(Depth Before Color)
  • 先进行深度测试,失败的片段直接丢弃
  • 节省片段着色器执行和颜色缓冲写入
  • 大部分情况下性能最优
3. 顺序无关透明度(OIT / Order-Independent Transparency)

需要使用特殊技术处理透明物体:

  • Weighted Blended OIT
  • Depth Peeling
  • Linked List

五、深度缓冲的格式

格式位数精度
GL_DEPTH_COMPONENT1616-bit低(可能产生ZB-fighting)
GL_DEPTH_COMPONENT2424-bit中等
GL_DEPTH_COMPONENT32F32-bit float高(需要深度浮点扩展)

推荐:24-bit 或 32-bit 以避免深度冲突(Z-fighting)


六、深度冲突(Z-Fighting)

当两个平面深度值非常接近时会出现深度冲突:

原因:
- 深度缓冲精度不足
- 两个平面共面或非常接近

解决方案:
1. 减少近裁剪平面距离
2. 增加深度缓冲精度
3. 使用 Polygon Offset
// 使用 Polygon Offset 避免深度冲突
glEnable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(scale, bias);

七、深度测试实战代码

// 初始化深度缓冲
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);

// 每帧清除
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// 渲染场景
for (auto& object : objects) {
    // 物体可能需要不同的深度处理
    if (object.usesAlpha) {
        glDepthMask(GL_FALSE);  // 禁止写入深度
        // 透明物体渲染...
        glDepthMask(GL_TRUE);   // 恢复写入
    } else {
        // 不透明物体正常渲染
    }
}

八、深度缓冲的读取

// 读取单个像素深度
glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth);

// 读取整个深度缓冲
glReadPixels(0, 0, width, height, GL_DEPTH_COMPONENT, GL_FLOAT, depthData);

九、常见问题

问题原因解决方案
物体消失深度缓冲未清除每帧调用 glClear(GL_DEPTH_BUFFER_BIT)
透明物体显示错误透明物体写入深度对透明物体使用 glDepthMask(GL_FALSE)
Z-Fighting深度精度不足增加精度或使用 Polygon Offset
深度测试不工作未启用深度测试调用 glEnable(GL_DEPTH_TEST)

十、性能优化建议

  1. 先渲染不透明物体(开启深度测试和写入)
  2. 后渲染透明物体(关闭深度写入)
  3. 早期Z拒绝(Early-Z):在片段着色器之前进行深度测试
  4. 层次细节(LOD):远处物体使用更简单的着色器

Early-Z 拒绝详解

一、Early-Z 的基本概念

Early-Z 是 GPU 架构中的一种硬件优化技术,在片段着色器执行之前进行深度测试,从而提前丢弃被遮挡的片段,节省后续计算资源。

传统管线:
几何处理 → 光栅化 → 片段着色器 → 深度测试 → 混合

Early-Z 管线:
几何处理 → 光栅化 → Early-Z → 片段着色器 → 深度测试 → 混合
                     ↑↑↑↑↑↑↑↑↑↑↑
                    提前进行深度测试

二、Early-Z 的硬件实现原理

1. 传统 Z-Check 流程的问题

在没有 Early-Z 的情况下,即使一个片段最终会被深度测试丢弃,片段着色器仍然会完整执行,造成浪费:

场景:一个被完全遮挡的像素

传统流程:
1. 光栅化生成片段
2. 执行片段着色器(计算光照、贴图等)← 浪费!结果会被丢弃
3. 深度测试失败
4. 片段被丢弃

Early-Z 流程:
1. 光栅化生成片段
2. Early-Z 测试 → 失败
3. 直接丢弃片段 ← 不执行片段着色器
4. 节省了大量计算
2. Early-Z 的实现机制

现代 GPU 使用**层次化 Z-Cache(Hierarchical Z-Cache)**实现 Early-Z:

┌─────────────────────────────────────────────────────────────┐
│                    屏幕空间                                  │
│  ┌──────────┬──────────┬──────────┬──────────┐            │
│  │ 256×256  │ 256×256  │ 256×256  │ 256×256  │ ← Mip 0    │
│  │          │          │          │          │            │
│  ├──────────┼──────────┴──────────┼──────────┤            │
│  │ 512×512  │      512×512        │ 512×512  │ ← Mip 1    │
│  │          │                    │          │            │
│  ├──────────┴────────────────────┴──────────┤            │
│  │              1024×1024                   │ ← Mip 2    │
│  │                                        │            │
│  └────────────────────────────────────────┘            │
└─────────────────────────────────────────────────────────────┘

原理:
- 每个层级存储该区域的"最大深度值"
- 测试时从最粗糙层级开始,逐层向下
- 如果某区域深度测试必定失败,直接跳过整个区域
3. Z-Cache 查找过程
1. 接收光栅化后的片段及其深度值 Z_new

2. 从最高层级(最粗糙)开始查询:
   ┌────────────────────────────────────┐
   │ 检查 Z_new vs Z_max(该区域最大深度) │
   └────────────┬───────────────────────┘
                │
        ┌───────┴───────┐
        ↓               ↓
   Z_new > Z_max    Z_new ≤ Z_max
        ↓               ↓
    丢弃片段       继续检查下一层级
                │
                ↓
   ... 直到最小层级或确定通过测试

三、Early-Z 的详细工作流程

阶段 1:深度缓冲重建(Pre-Z Pass)
┌─────────────────────────────────────────────────────────────┐
│                    第一遍渲染                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 1. 关闭颜色写入 (glColorMask(GL_FALSE, ...))        │   │
│  │ 2. 只渲染不透明物体                                  │   │
│  │ 3. 只写入深度缓冲                                    │   │
│  │ 4. 渲染顺序:从前到后(可选优化)                      │   │
│  └─────────────────────────────────────────────────────┘   │
│                              ↓                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              深度缓冲填充完成                          │   │
│  │  GPU 根据深度缓冲构建层次化 Z-Cache                   │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
阶段 2:正式渲染(Main Pass)
┌─────────────────────────────────────────────────────────────┐
│                    第二遍渲染                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 1. 开启颜色写入                                      │   │
│  │ 2. 开启深度测试                                      │   │
│  │ 3. 渲染所有物体(包括透明物体)                       │   │
│  └─────────────────────────────────────────────────────┘   │
│                              ↓                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                  Early-Z 阶段                        │   │
│  │  ┌─────────────────────────────────────────────┐    │   │
│  │  │ 查询层次化 Z-Cache                           │    │   │
│  │  │ 如果片段深度 > 该区域最大深度 → 丢弃           │    │   │
│  │  │ 否则 → 继续执行片段着色器                      │    │   │
│  │  └─────────────────────────────────────────────┘    │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

四、Early-Z 失败的常见原因

1. 片段着色器修改深度
// ❌ 会导致 Early-Z 失效
void main() {
    gl_FragDepth = some_computed_depth;  // 手动写入深度
    // Early-Z 在此之前执行,无法预知最终深度
}

// 编译器可能禁用 Early-Z
2. 丢弃片段(Discard)
// ❌ 可能导致 Early-Z 失效
void main() {
    if (texture2D(tex, uv).a < 0.5) {
        discard;  // 动态丢弃,GPU 可能保守地禁用 Early-Z
    }
}
3. Alpha 测试(已废弃)
// ❌ Alpha 测试会禁用 Early-Z
glEnable(GL_ALPHA_TEST);  // 传统方式
glAlphaFunc(GL_GREATER, 0.5f);
4. 颜色写入被禁用
// 在某些驱动上,禁用颜色写入可能导致 Early-Z 失效
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);

五、正确使用 Early-Z 的方法

方案 1:Pre-Z Pass(推荐)
// ===== 第一遍:填充深度缓冲 =====
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);  // 禁用颜色写入
glDepthMask(GL_TRUE);  // 启用深度写入
glEnable(GL_DEPTH_TEST);

// 按距离排序(可选,从近到远或从远到近)
std::sort(opaque_objects.begin(), opaque_objects.end(), 
    [](const Object& a, const Object& b) {
        return a.distance < b.distance;
    });

for (const auto& obj : opaque_objects) {
    obj.render();
}

// ===== 第二遍:渲染所有物体 =====
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);  // 启用颜色写入
glDepthFunc(GL_LESS);  // 保持深度测试
glDepthMask(GL_FALSE); // 透明物体不需要写入深度

// 先渲染不透明物体
for (const auto& obj : opaque_objects) {
    obj.render();
}

// 后渲染透明物体
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
for (const auto& obj : transparent_objects) {
    obj.render();
}
方案 2:手动 Early-Z(软件实现)
// 手动在顶点着色器中输出深度
#version 330 core
layout (location = 0) in vec3 aPos;

uniform mat4 MVP;

out float vertexDepth;

void main() {
    vec4 clipPos = MVP * vec4(aPos, 1.0);
    gl_Position = clipPos;
    
    // 计算深度(用于 CPU 端粗略剔除)
    vertexDepth = clipPos.z / clipPos.w;
}

// 在应用层进行粗略深度剔除
void manualEarlyZ(const Object& obj) {
    float maxDepth = calculateMaxDepth(obj);
    
    if (maxDepth > currentMinDepth) {
        // 物体可能被遮挡,跳过或延后渲染
    } else {
        obj.render();
    }
}
方案 3:保守深度(Conservative Depth)
// #version 450
#extension GL_EXT_conservative_depth : enable

layout (depth_greater) out float gl_FragDepth;

void main() {
    // 告诉 GPU:这个着色器只会增加深度值
    // GPU 可以继续使用 Early-Z 优化
    
    float computedDepth = calculateDepth();
    gl_FragDepth = computedDepth;
}

六、Early-Z vs Late-Z

特性Early-ZLate-Z
执行时机片段着色器之前片段着色器之后
目的提前丢弃被遮挡片段处理着色器可能改变深度的情况
性能高(节省着色器计算)低(着色器必须完整执行)
条件着色器不能修改深度无限制
GPU 支持现代 GPU 硬件支持必然支持
渲染管线完整图示:

顶点着色器
    ↓
图元装配
    ↓
裁剪
    ↓
光栅化
    ↓
┌─────────────────────────────────────┐
│           Early-Z 测试              │ ← 可选,提前丢弃
└─────────────────────────────────────┘
    ↓
片段着色器
    ↓
┌─────────────────────────────────────┐
│           Late-Z 测试               │ ← 必定执行,最终深度测试
└─────────────────────────────────────┘
    ↓
混合
    ↓
写入帧缓冲

七、Modern GPU 的 Early-Z 优化技术

1. Z-Optimizations(NVIDIA)
- Z-Cull:保守深度测试,提前拒绝批次
- Early-Z-Stencil:在Stencil测试前执行
- Z-Min/Max:支持双向深度测试
2. Hi-Z(AMD)
- 使用 Mip-Map 级别的 Z-Buffer
- 快速的层次化查询
- 支持保守深度(Conservative Depth)
3. Tile-Based 架构(移动 GPU)
┌────────────────────────────────────┐
│           Frame Buffer             │
│  ┌────┬────┬────┬────┐            │
│  │Tile│Tile│Tile│Tile│ ← 16×16~32×32像素
│  ├────┼────┼────┼────┤            │
│  │Tile│Tile│Tile│Tile│            │
│  └────┴────┴────┴────┘            │
└────────────────────────────────────┘

每个 Tile 独立进行 Early-Z 优化

八、性能测试建议

// 检测 Early-Z 是否启用
// 在某些 GPU 上可以通过性能计数器获取

// 开启 Early-Z 的标志(通常默认开启):
glEnable(GL_DEPTH_TEST);
glDepthMask(GL_TRUE);
glDepthFunc(GL_LESS);

// 检测方法:
// 1. 使用 GPU PerfStudio / RenderDoc 分析
// 2. 对比 Pre-Z Pass 前后的 draw calls 性能
// 3. 观察 fill rate 变化

九、常见问题解答

问题答案
Early-Z 总是开启吗?现代 GPU 默认开启,但某些操作会禁用它
如何确认 Early-Z 是否生效?使用 RenderDoc 等工具观察 draw call 顺序和 early rejection 率
Pre-Z Pass 总是值得做吗?场景复杂(多物体、多光源)时值得,简单场景可能反而增加开销
透明物体能利用 Early-Z 吗?不能,透明物体需要先渲染不透明物体建立深度缓冲

相关文章:深度测试

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值