需要预先学习:


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) 可见性判断

二者有两个重要的关系:

  1. 并列结构:帧缓冲与深度缓冲是两块并列的存储,尺寸与屏幕分辨率相同、逐像素一一对应——第 (x,y)(x, y) 个像素对应帧缓冲里的一个颜色、深度缓冲里的一个深度。它们不是包含关系,而是两条平行数据。
  2. 同时生成:在光栅化与片元处理过程中,每处理一个片元,两个缓冲被同步更新——深度测试通过时,既把片元颜色写入帧缓冲,也把它的深度写入深度缓冲。

注:严格来说,OpenGL 中 framebuffer(帧缓冲)是颜色、深度、模板等多个缓冲的集合,其中颜色部分叫 color buffer。本篇笔记沿用图形学课程的惯例,用"帧缓冲区"指代颜色缓冲,与深度缓冲并列讨论。具体的OpenGL中的 FBO 见 帧缓冲


1.2 画家算法

在深度缓冲之前,最直观的隐藏面消除(hidden surface removal,决定谁遮挡谁)方案是画家算法(painter’s algorithm):像画家画画一样,从远到近依次绘制三角形,后画的覆盖先画的,近处的自然盖住远处的。

画家算法有两个问题:

  1. 需要排序:绘制前必须把所有三角形按深度排序,场景复杂时开销很大,而且三角形的深度排序本身并不简单;
  2. 循环遮挡无法排序:三个三角形 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 附近精度更高的特性。