这是2015年 NVDIA 的一篇文章,在此做个整理。更多精彩内容尽在数字孪生平台,技术交流添加VX:digital_twin123
本文分为三个主要部分。在第一部分中,为非线性深度映射解释一些动机;其次,通过展示一些图表,帮助直观地理解非线性深度映射在不同情况下的工作原理;第三部分讨论浮点舍入误差对深度精度的影响。
为什么是 1/z
GPU 硬件深度缓冲区通常不会存储物体位于相机前面的距离的线性表示,这与人们第一次遇到这种情况时的预期相反。相反,深度缓冲区存储与世界空间深度的倒数成比例的值,下面解释一下原因。
在本文中,我们使用 d
表示存储在深度缓冲区中的值(在 [0, 1] 区间),使用 z
表示世界空间深度,即沿相机中心的距离,以世界单位(例如米)表示。一般来说,它们之间的关系是这样的形式
从表面上看,我们可以将 d
视为的 z
的任何函数。那么为什么要做出这样的特殊选择呢?主要有两个原因。
首先,1/z 很适合透视投影的框架。这是最通用的变换类型,可以保证保留直线,这使得硬件光栅化变得很方便,因为三角形的直边在屏幕空间中保持笔直。我们可以利用硬件已经执行的透视划分来生成 1/z 的线性重新映射:
当然,这种方法的真正威力在于投影矩阵可以与其他矩阵相乘,从而允许将许多变换阶段组合在一起。
第二个原因是 1/z 在屏幕空间中是线性的,正如 Emil Persson 所指出的。因此,在光栅化时很容易在三角形上插值 d,并且分层 Z 缓冲区、early Z 剔除和深度缓冲区压缩等操作都更容易完成。
绘制深度图
我们用一些图片来展示:
d
在纵轴表示深度值,区间是 0 - 1,用的是整型深度,所以有16个均匀刻度;横轴 z
表示世界空间深度范围内不同值的落点。我们可以看到 1/z 曲线靠近近平面的值聚集在一起,而靠近远平面的值则相当分散。
也很容易理解为什么近平面对深度精度有如此深远的影响。拉入近平面将使 d
范围猛增至 1/z
曲线的渐近线,从而导致值的分布更加不平衡:
同样,在这种情况下很容易看出为什么将远平面一直推到无穷远并没有那么大的效果。它只是意将 d
范围稍微扩展到 1/z=0
:
那么如果用浮点深度如何呢?下图添加了与具有 3 个指数位和 3 个尾数位的模拟浮点格式相对应的刻度线:
现在 [0, 1] 中有 40 个不同的值,比之前的 16 个值多了很多,但它们中的大多数无用地聚集在近平面上,我们实际上并不需要更高的精度。
现在常用的技巧是反转深度范围,将近平面映射到 d=1
,将远平面映射到 d=0
:
这样看就好多了!现在,浮点的准对数分布在一定程度上消除了 1/z
的非线性,使我们在近平面处的精度与整数深度缓冲区相似,并且在其他地方大大提高了精度。当我们移得更远时,精度只会非常缓慢地降低。
前面所有的图都假设 [0, 1] 作为投影后深度范围,这是 D3D 约定。那么OpenGL呢?
OpenGL 默认情况下假定投影后深度范围为 [-1, 1]。这对于整数格式没有影响,但对于浮点数,所有精度都毫无用处地卡在中间(虽然 d
值稍后被映射到 [0, 1] 以便存储在深度缓冲区中,但没有用,因为到 [-1, 1] 的初始映射已经破坏了范围远半部分的所有精度)。并且根据对称性,reversed-Z 技术在这里起不到任何作用。
舍入误差的影响
即使我们有足够的深度精度来表示场景,也很容易受顶点变换过程的算术误差控制,最终导致精度损失。
之前有人提出了两个主要建议来最小化舍入误差:
- 使用无限远平面。
- 将投影矩阵与其他矩阵分开,并将其应用到顶点着色器中的单独操作,而不是将其组合到视图矩阵中。
这里有套代码可以模拟舍入误差导致的精度问题。它的工作原理是生成一系列随机点,按深度排序,在近平面和远平面之间以线性或对数方式间隔。然后,它通过视图和投影矩阵以及透视除法传递点,始终使用 32 位浮点精度,并可选择将最终结果量化为 24 位整数。最后,它遍历序列并计算两个相邻点(最初具有不同深度)由于映射到相同深度值而变得难以区分的次数,或者实际上交换了顺序的次数。换句话说,它测量不同场景下深度比较错误发生的速率(对应于 Z 冲突等问题)。
以下是near = 0.1、far = 10000、线性间隔深度为 10000 时获得的结果。 在表中,“indist”表示无法区分(两个附近的深度映射到相同的最终深度缓冲区值),“swap”表示两个附近的深度交换了顺序。
从这些数字我们可以看出:
- 在大多数设置中,浮点和整数深度缓冲区之间没有区别。部分原因是 float32 和 int24 在 [0.5, 1] 中具有几乎相同大小的 ulp(因为 float32 具有 23 位尾数),因此实际上在绝大多数深度范围内几乎没有额外的量化误差。
- 在许多情况下,分离视图矩阵和投影矩阵确实会带来一些改进。虽然它并没有降低总体错误率,但它似乎确实将 swap 变成了 indist,这是朝着正确方向迈出的一步。
- 无限远的平面只会导致错误率的微小差异。
不过,上述几点实际上无关紧要,因为这里重要的真正结果是:reversed-Z 映射还是很有用的。
- 具有浮点深度缓冲区的 Reversed-Z 在此测试中给出了零错误率。如果我们继续收紧输入深度值的间距,当然可以让它产生一些错误。尽管如此,带浮点深度缓冲的Reversed-Z 比任何其他选项都要准确得多。
- 具有整数深度缓冲区的 Reversed-Z 与任何其他整数选项一样好。
- Reversed-Z 消除了预合成与单独视图/投影矩阵以及有限与无限远平面之间的区别。换句话说,使用Reversed-Z,我们就可以将投影矩阵与其他矩阵组合,并且可以使用任何远平面,而完全不会影响精度。
我想这里的结论已经很清楚了。在任何透视投影情况下,只需使用带有Reversed-Z 的浮点深度缓冲区即可!如果我们不能使用浮点深度缓冲区,我们仍然应该使用Reversed-Z。当然这并不是解决所有精度问题的灵丹妙药,特别是如果正在构建一个包含极端深度范围的开放世界环境,但这是一个很好的开始。