反向Z(Reversed-Z)的深度缓冲原理

原文地址: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')

参考

  1. ^Unity Technologies. Platform-specific rendering differences. Unity - Manual: Writing shaders for different graphics APIs
  2. ^nlguillemot. Reversed-Z in OpenGL. Reversed-Z in OpenGL | nlguillemot
  3. ^Nathan Reed. Depth Precision Visualized. Depth Precision Visualized | NVIDIA Developer
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值