OpenGL 深度测试详解
一、深度测试在渲染管线中的位置
深度测试(Depth Test)发生在光栅化阶段(Rasterization)之后,在模板测试(Stencil Test)之后、混合(Blending)之前。具体位置如下:
应用阶段 → 几何处理 → 光栅化 → 片段着色器 → 深度测试 → 模板测试 → 混合 → 帧缓冲
关键点:深度测试是在片段着色器执行之后进行的,这意味着:
- 即使片段着色器计算了颜色,深度测试失败的话该片段也会被丢弃
- 深度测试失败不会触发
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_COMPONENT16 | 16-bit | 低(可能产生ZB-fighting) |
GL_DEPTH_COMPONENT24 | 24-bit | 中等 |
GL_DEPTH_COMPONENT32F | 32-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) |
十、性能优化建议
- 先渲染不透明物体(开启深度测试和写入)
- 后渲染透明物体(关闭深度写入)
- 早期Z拒绝(Early-Z):在片段着色器之前进行深度测试
- 层次细节(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-Z | Late-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 吗? | 不能,透明物体需要先渲染不透明物体建立深度缓冲 |
相关文章:深度测试

2256

被折叠的 条评论
为什么被折叠?



