深度缓冲
需要预先学习:
1. 深度缓冲(Depth Buffer 或 Z-Buffer)
光栅化之后,屏幕上每个像素可能对应多个片元——多个三角形在投影后会互相重叠,同一个像素可能被多个三角形覆盖。最终该显示哪个片元,需要判断谁离摄像机最近。深度缓冲(depth buffer)就是为此设计的存储结构,也是深度测试(depth test)的基础。
1.1 帧缓冲区与深度缓冲
先分清两个容易混淆的缓冲。
帧缓冲区(frame buffer,这里特指颜色缓冲 color buffer)保存每个像素最终显示的颜色;深度缓冲(depth buffer,也称 Z-Buffer,因为存的是 z 坐标)保存每个像素当前最近的深度值。
| 缓冲 | 存储内容 | 每个像素存什么 | 用途 |
|---|---|---|---|
| 帧缓冲区(frame buffer) | 颜色 | 一个颜色值(RGB) | 最终显示 |
| 深度缓冲(depth buffer / z-buffer) | 深度 | 一个深度值(z) | 可见性判断 |
二者有两个重要的关系:
- 并列结构:帧缓冲与深度缓冲是两块并列的存储,尺寸与屏幕分辨率相同、逐像素一一对应——第 个像素对应帧缓冲里的一个颜色、深度缓冲里的一个深度。它们不是包含关系,而是两条平行数据。
- 同时生成:在光栅化与片元处理过程中,每处理一个片元,两个缓冲被同步更新——深度测试通过时,既把片元颜色写入帧缓冲,也把它的深度写入深度缓冲。
注:严格来说,OpenGL 中 framebuffer(帧缓冲)是颜色、深度、模板等多个缓冲的集合,其中颜色部分叫 color buffer。本篇笔记沿用图形学课程的惯例,用"帧缓冲区"指代颜色缓冲,与深度缓冲并列讨论。具体的OpenGL中的 FBO 见 帧缓冲
1.2 画家算法
在深度缓冲之前,最直观的隐藏面消除(hidden surface removal,决定谁遮挡谁)方案是画家算法(painter’s algorithm):像画家画画一样,从远到近依次绘制三角形,后画的覆盖先画的,近处的自然盖住远处的。
画家算法有两个问题:
- 需要排序:绘制前必须把所有三角形按深度排序,场景复杂时开销很大,而且三角形的深度排序本身并不简单;
- 循环遮挡无法排序:三个三角形 A 挡 B、B 挡 C、C 挡 A 时,不存在"从远到近"的合法顺序,算法直接失效。
1.3 深度缓冲算法
深度缓冲算法(Z-Buffer algorithm)换了一个思路:不排序三角形,而是逐像素比较深度。每个像素记录"当前最近的深度",新片元只有比它更近(深度更小)时才替换:
// 伪代码:深度缓冲算法
for (每个三角形)
for (三角形覆盖的每个像素 (x, y))
if (fragment.depth < depth_buffer[x][y])
depth_buffer[x][y] = fragment.depth // 更新深度缓冲
frame_buffer[x][y] = fragment.color // 更新帧缓冲(颜色缓冲)
算法的主要性质:
- 不需要对三角形排序,总开销与三角形数量成线性关系;
- 与处理顺序无关:只要深度比较正确,无论三角形按什么顺序光栅化,每个像素最终都保留最近的片元;
- 深度相等的片元需要约定处理方式(比如不更新),否则相邻三角形共享边时可能出现闪烁。
1.4 深度精度
透视投影在挤压(squish)时把 z 坐标非线性压缩:离摄像机越近精度越高、越远精度越低(见 视图(摄像机)与投影 的注)。这带来一个实际可见的问题——z-fighting(深度冲突):两个距离很近的表面(比如地面上叠放的一张纸),在远处时深度值几乎相同,深度测试结果不稳定,画面交替闪烁。
缓解办法:
- 近平面与远平面的比值不要过大(near/far 相差太多会加剧非线性压缩);
- 使用更高精度的深度缓冲格式(如 24 位而非 16 位);
- 反转 z(reverse-z):把近处映射到大深度值,利用浮点数在 0 附近精度更高的特性。


