原文地址:https://zhuanlan.zhihu.com/p/75517534
引言
深度缓冲是GPU在绘制时用于决定当前绘制像素是否被遮挡的判断依据。
在早些时候,深度缓冲中的值的分布遵循近小远大。在DirectX的游戏中,近平面处的像素会有0.0的深度值,远平面的像素会有1.0的深度值。如果使用的是OpenGL,则为近平面-1.0,远平面1.0。
而如今的游戏中,往往会使用近处1.0,远处0.0的深度分布。比如Unity引擎中,某些Graphics API上会默认开启这样的改动。[1]
我在面试时曾经被问到,为什么要做这样的改动,当时没有很好的答上来;这两天闲在家里,重新回顾了一下,自己也写了点代码做了验证,把一些想法分享一下。
评估方法
我们将近平面0.0,远平面1.0的方法称为原始方法,另一方法称为反向z方法。
要比较两种方法的优劣,我们需要给出一个评估的方法。
一个深度缓冲,受到其像素的大小(8bit、16bit、24bit)限制,能表示的深度数量是有限的。一般认为,如果可表示的不同的深度在View Space下对应的平面尽量均匀分布,使得不管近处或是远处的物体都不会有z-fighting现象为佳。
通过得到深度,即NDC空间下的z值( ���� ),与View Space下的z值 (����� )的对应关系,分析有效的深度值对应至View Space下的z值的分布是否足够平均,即可得出优劣。
原始方法
本节我们对原始方法,两个z值的对应关系进行推导。
首先我们看一下深度值具体是怎么得到的。
在Vertex Shader中,我们会将顶点通过投影矩阵变换至Clip Space下的齐次坐标。之后GPU将齐次坐标的各个分量除以w分量,得到NDC空间坐标。其中NDC空间坐标的z值将被GPU用于深度测试、写入深度缓冲。
设置远平面为100.0单位,近平面为1.0单位,得到如下结果:
近平面1.0,远平面100.0,8位整形深度缓冲
右图则是更直观地统计View Space下各区间的点数的柱形图,其中横轴为View Space下的距离,每一个柱形反映了View Space的一段距离区间下的点的数量,上图中可以看到在0~10的距离中,聚集了超过200个点。
更糟糕的是,如果我们把近平面拉到0.1,那图形会变成:
近平面0.1,远平面100.0,8位整形深度缓冲
有人说了,这是8位整形的,那现在的硬件怎么可能有整形的深度缓冲,都是浮点数了。
那我们再看看浮点数的表现。当然为了看得出东西,我们不可能把2^32个浮点数全塞到图里面,因此我们同样[0,1]范围上取256个符合浮点数精度分布的点,以同样方式展现出来。
近平面1.0,远平面100.0,8位浮点数深度缓冲
近平面0.1,远平面100.0,8位浮点数深度缓冲
可以看到结果比使用整形做缓冲时更差了。[0,1]的浮点数,越接近0精度越高,导致原本就欠缺精度的接近1.0的部分,更加的欠缺精度了。
反向Z方法
首先要说明一下反向z具体是怎么实现,然后我们再用跟上一节相同的方式看看效果。
反向z的实现很简单,只需要:
- 修改投影矩阵,使近平面处的像素映射至NDC下1.0坐标,远平面处像素映射至NDC下的0.0坐标。
- ZTest改为Greater
反向Z评估
我们先使用8位整形缓冲的256个整数看看效果:
近平面1.0,远平面100.0,8位整形深度缓冲-反向z
因为现在z值倒过来了,所以左图的图像左右翻转了一下,但是整体分布似乎没太大变化。然后再看看256个浮点数的效果:
近平面1.0,远平面100.0,8位浮点数深度缓冲-反向z
使用浮点数加上反向z时,突然好起来了。在View Space的各个距离上都分布了有效的深度值。
分析一下,从8位整形缓冲的图像中可以看到,大部分的有效值是聚集在近平面附近的;但是当使用了浮点数缓冲后,因为浮点数本身越靠近0.0精度更高,1.0精度低,而1.0现在对应了近平面,两个因素互相弥补,最终促成了深度缓冲的大和谐。
同时,反向z受拉近近平面、拉远远平面的影响也比较小。比如设置近平面为0.1,远平面为10000,效果如下:
近平面0.1,远平面10000.0,8位浮点数深度缓冲(反向z)
尽管0~1000范围内的堆积变多,但是各个距离处依然有一定数量的有效值,比原始方法不知道高到哪里去了。
总结
传统的近平面映射至0,远平面至1的方法,因为 ���� 和 ����� 并非线性映射,再加上浮点数的精度在靠近0的范围聚集两点原因,导致高精度范围完全堆积在了靠近近平面的位置。
而使用反向z的方法,利用了上述两个原因互相弥补,进而使得各处的精度都得到保证。
OpenGL使用的是[-1.0, 1.0]的剪裁范围,无法使用反向z;但是可以通过4.5版本的API修改剪裁范围为[0.0, 1.0],从而同样可以使用反向z。[2]
Nvidia的一篇文章里有一些更加原理性的描述,里面还有提出反向z方法的文献引用等等。[3]
代码
本文用Python做的图,并没有写OpenGL或者DX的代码。我们PPT人是这样的。
生成浮点数样本的代码借了https://stackoverflow.com/questions/7006510/density-of-floating-point-number-magnitude-of-the-number,里面的一段计算两个数之间有效的浮点数个数的代码。
import numpy as np
import math
import matplotlib.pyplot as plt
def proj_to_view(zproj, far = 100, near = 1.0):
return -(far*near)/((far-near)*zproj-far)
def proj_to_view_reversed(zproj, far = 100, near = 1.0):
return (far*near) / ((far-near)*zproj+near)
def generate_float_samples(count=256):
def num_floats(begin, end):
#https://stackoverflow.com/questions/7006510/density-of-floating-point-number-magnitude-of-the-number
# pow(2, 23) * (log(end, 2) - log(start, 2)) == pow(2, 23) * log(end/start, 2)
if end <= begin:
return 0
return 8388608 * math.log(float(end)/float(begin), 2)
result = []
def fill_results(begin, end, count, total_float_count, results): #Binary search to fill the results.
mid = (begin + end) / 2.0
low_float_count = int(num_floats(begin, mid))
up_float_count = total_float_count - low_float_count
low_percent = low_float_count / total_float_count
low_remain = int(count * low_percent)
up_remain = count - low_remain
if low_remain == 0 or up_remain == 0:#Can't be divided furthur.
for i in range(count):
results.append(begin + (i / count) * (end-begin))
return
#recursive fill.
if low_remain > 0:
fill_results(begin, mid, low_remain, low_float_count, results)
if up_remain > 0:
fill_results(mid, end, up_remain, up_float_count, results)
fill_results(0.00001, 1.0, count, num_floats(0.00001, 1.0), result)
return result
int_proj_points = [i / 256.0 for i in range(256)]
float_proj_points = generate_float_samples(256)
def depth_plot(sample_points, title, proj_method = 'normal', near = 1.0, far = 100.0):
plt.figure(figsize=(10, 5))
func = proj_to_view if proj_method == 'normal' else proj_to_view_reversed
view_space = [func(x, far, near) for x in sample_points]
plt.subplot(1, 2, 1)
plt.title(title)
plt.ylim(-1.0, far)
plt.plot(sample_points, view_space, 'ro', markersize=1)
plt.ylabel('View Space Depth')
plt.xlabel('NDC Space Z')
#bar
plt.subplot(1, 2, 2)
plt.hist(view_space)
plt.xlabel('View Space Depth')
plt.ylabel('Point(s) Count')
plt.title('Distribution in View Space')
plt.show()
depth_plot(int256_proj_points, 'Int8 depth buffer')
depth_plot(float_proj_points, 'Float depth buffer')
depth_plot(int256_proj_points, 'Int depth buffer(reversed)', 'reversed')
depth_plot(float_proj_points, 'Float depth buffer(reversed)', 'reversed')
参考
- ^Unity Technologies. Platform-specific rendering differences. Unity - Manual: Writing shaders for different graphics APIs
- ^nlguillemot. Reversed-Z in OpenGL. Reversed-Z in OpenGL | nlguillemot
- ^Nathan Reed. Depth Precision Visualized. Depth Precision Visualized | NVIDIA Developer