2026年5月26日

反向 Z 缓冲区

Z 缓冲区(或深度缓冲区)在计算机图形学中用于确定每个像素中物体的可见性。
每次绘制物体时,我们也会将到摄像机的距离存储在 Z 缓冲区中。
下次绘制物体时,我们可以检查 Z 缓冲区中先前存储的值。如果 Z 缓冲区中的值比我们正在绘制的物体更近,我们就知道该物体被遮挡了,因此不绘制该像素。这种技术有时被称为深度测试。

深度测试方法在所有图形引擎中普遍存在,Evergine 也不例外。然而,正如我们将看到的,简单的方法在精度方面并不能给出最佳结果。在此版本中,我们引入了反向 Z 缓冲,它极大地提高了精度,尤其是对于远处的物体。

传统 Z 缓冲区

传统的 Z 缓冲方法将深度值归一化存储于 [0, 1] 区间。其中 0 表示最近的值,1 表示最远的值。

在下图中,我们有一个 3D 相机正对着某个立方体。
绿线代表“近”平面;比近平面更近的像素会被裁剪(丢弃)。
红线代表“远”平面;比远平面更远的像素会被裁剪。

洋红色线代表到我们正在绘制的像素的距离 (d)。
归一化深度缓冲值将使用以下公式计算:

depthBuffer=(far)/(far-near)-(far⋅near)/(far-near)⋅1/d

注意,如果 d 等于 near,结果值为零。
类似地,如果 d 等于 far,结果值为一。

在下图中,你可以看到一个典型的深度缓冲可能的样子。

精度问题

为了展示传统深度缓冲可能出现的精度问题,我们构建了以下演示。

场景中的球体实际上是两个球体:一个在另一个内部。
这些分层球体的排列如下图所示。两层放置得非常接近。

显然,内部的红色球体本应不可见。然而,正如您在我们的演示中所见,随着与摄像机距离的增加,我们开始看到红色伪影。

这种现象称为走样。由于数值表示的精度限制,一旦深度值被归一化,它们会映射到相同的数值,因此我们无法区分哪个更近。

一种选择是增加深度缓冲中用于表示值的位数,但大多数硬件无法超过 32 位,而 32 位又不够用。

反向深度

这个精度问题可以通过一种称为反向深度缓冲的技术来解决。

该技术背后的思想非常简单。不再使用计算归一化深度的常规公式:

depthBuffer=(far)/(far-near)-(far⋅near)/(far-near)⋅1/d

我们交换 near 和 far 平面:

depthBuffer=(near)/(near-far)-(near⋅far)/(near-far)⋅1/d

使用此公式,近平面映射到 1,远平面映射到 0。因此深度缓冲看起来是反转的。

通过这一微小改动,结合浮点深度格式,我们的演示全部变为绿色。

为什么反向 Z 能提高精度?

反向 Z 是一种非常简单的技术:只需交换近平面和远平面。然而,结果却非常好。
此外,该技术似乎极其稳健;选择近平面和远平面不再需要精细调整。
实际上,你可以仅将远平面设置为无穷大,一切都会完美运行。能够处理无限远视距对于需要处理巨大世界的应用来说意义重大。例如,渲染整个地球,就像我们在新的 Cesium 插件中所做的那样。近平面也可以更近,尽管不能为零。

如果你想知道反向 Z 为何有效,在本节中我们将深入探讨数学和技术细节。

UNorm 与浮点表示

投影深度值存储在 [0, 1] 区间内。
0 和 1 之间有无限多个实数。然而,计算机无法精确表示所有实数,就像你无法在纸上写下圆周率的所有位数一样。因此,计算机会使用不同的表示法来近似实数,就像你可以通过写下前 7 位数字来近似圆周率一样。

深度缓冲中有两种用于表示深度值的格式:UNorm 和浮点。

UNorm 代表无符号归一化。使用无符号整数来表示 [0, 1] 区间内的实数。
表示的值在 [0, 1] 区间内均匀分布。

下图描绘了 UNorm 表示的值。如你所见,表示值之间的间距是恒定的。

Float 代表浮点表示。
浮点表示覆盖范围 (-∞, +∞)。
单精度浮点数使用 32 位,分为 3 部分:符号、指数和尾数。

(来源:https://en.wikipedia.org/wiki/Single-precision_floating-point_format#/media/File:Float_example.svg)

我们无法在本文中涵盖浮点表示法的所有细节,因此我将为好奇的读者留下这两个链接:https://floating-point-gui.de,https://en.wikipedia.org/wiki/Single-precision_floating-point_format。
就我们的问题而言,关于浮点数需要了解的重要一点是,表示的值在 [0, 1] 区间内的分布方式,这正是投影深度值映射到的区间。

如上一张图所示,采样点并非均匀分布。更多的精度被分配给了更接近零的值。这对于科学计算应用来说是一个有趣的特性,但我们也将利用这一特点。

现在让我们回到深度投影公式:

depthBuffer=(far)/(far-near)-(far⋅near)/(far-near)⋅1/d

如果我们绘制此公式,将得到这条曲线:

X 轴表示线性深度。Y 轴给出投影到 [0, 1] 范围的深度。
请注意此函数增长的速度有多快。
在 Y 轴上,您可以看到采样是均匀分布的,因为我们使用的是 UNorm 表示。
X 轴上有一系列小刻度。这些对应于投影前 Y 轴上均匀分布的刻度。请注意,大多数刻度都聚集在近平面附近,而随着您到达远平面,刻度的密度变得非常低。

而这还是在近平面和远平面选择非常有利的情况下。看看当 near = 0.1,far = 100 时会发生什么。

这里的问题是,从 20 到 100 的深度值都映射到了 1。因此,在我们将它们投影之后,我们无法判断 45 是否小于 46。

在这里使用浮点数表示法并没有帮助。事实上,它让情况变得更糟,因为表示的线性深度值会更加集中在近平面附近。

到目前为止,我们已经看到有两件事导致了采样的不均匀分布,偏向于更靠近近平面的值:1) 投影公式的曲线 2) 浮点数表示法

Reverse-Z 的想法是翻转投影公式。这将创建相反的采样分布,偏向于靠近远平面的距离。浮点数表示法具有相反的效果,将对此进行补偿,最终我们得到更平衡的采样分布。请看下图中 X 轴刻度的分布。

Reverse Z 如何影响作为 Evergine 用户的您

更新到新版本后,反向深度将自动启用。

对于大多数项目,这不会有任何影响,但以下两种用例可能会出现问题。

使用深度缓冲区的着色器

如果您编写了读取原始深度缓冲区的自定义着色器,则需要相应地更改您的着色器。

我们添加了一些着色器参数,可帮助您将反向深度转换为正向深度。

如果我们使用传统的正向深度缓冲进行渲染,ReverseDepth 为 0;如果我们使用反向深度,则为 1。然而,从现在开始,它将始终为 1。
ReverseDepthFactor 旨在用于通过以下函数转换为正向深度:

// This is a function that converts a reverse-depth value to forward-depth, if needed, without branching
inline float forwardDepth(float depth)
{
    return depth * ReverseDepthFactor + ReverseDepth;
}

自定义渲染层

如果您的项目中有自定义 Render Layers,则必须反转深度测试条件。

在新版本的 Evergine 文档中,您将找到一个迁移指南,其中包含一些旨在自动化此过程的脚本。

作者
Alejandro Juan Pérez
Plain Concepts Research