OpenGL 深度测试(Depth testing)

Reading time ~1 minute

深度值(Z值):

我们知道,OpenGL的glViewport函数给定了屏幕空间坐标的视区。而我们可以通过片段着色器中内置的gl_FragCoord变量对其访问。gl_FragCoord 的 X 和 y 表示该片段的屏幕空间坐标 ((0,0) 在左下角)。gl_FragCoord 还包含一个 z 坐标,它包含了片段的实际深度值。

所谓深度,就是在openGL坐标系中,像素点Z坐标距离摄像机的距离。摄像机可能放在坐标系的任何位置,那么,就不能简单的说Z数值越大或越小,就是越靠近摄像机。

为何需要深度:

在不使用深度测试的时候,如果我们先绘制一个距离较近的物体,再绘制距离较远的物体,则距离远的物体因为后绘制,会把距离近的物体覆盖掉,这样的效果并不是我们所希望的。而有了深度缓冲以后,绘制物体的顺序就不那么重要了,都能按照远近(Z值)正常显示,这很关键。 实际上,只要存在深度缓冲区,无论是否启用深度测试,OpenGL在像素被绘制时都会尝试将深度数据写入到缓冲区内,除非调用了glDepthMask(GL_FALSE)来禁止写入。这些深度数据除了用于常规的测试外,还可以有一些有趣的用途,比如绘制阴影等等。

深度缓存(Z Buffer):

深度值是存储在深度缓存里面的。深度缓存由窗口系统自动创建并将其深度值存储为 16、24或32位浮点数。在大多数系统中深度缓冲区为24位。深度缓存位数越高,则精确度越高。 深度缓存原理就是把一个距离观察平面(近裁剪面)的深度值(或距离)与窗口中的每个像素相关联。

深度测试(Depth testing):

在绘制时,如果屏幕上当前像素要绘制新的候选颜色,只有对应物体比之前的物体更靠近观察者,我们才能绘制它。 初始状态下,深度缓存的值是一个距视点尽可能远的最大值,而所有物体的深度值都要比这个值更靠近视点。每帧重绘场景时都要清除深度缓存数据。

首先,使用glClear(GL_DEPTH_BUFFER_BIT),把所有像素的深度值设置为最大值(一般是远裁剪面)。 然后,在场景中以任意次序绘制所有物体。硬件或者软件所执行的图形计算把每一个绘制表面转换为窗口上一些像素的集合,此时并不考虑是否被其他物体遮挡。 其次,OpenGL会计算这些表面和观察平面的距离。如果启用了深度缓冲区,在绘制每个像素之前,OpenGL会把它的深度值和已经存储在这个像素的深度值进行比较。新像素深度值小于原先像素深度值,则新像素值会取代原先的;反之,新像素值被遮挡,其颜色值和深度将被丢弃。

深度测试默认是关闭的,要启用深度测试的话,我们需要用GL_DEPTH_TEST选项来打开它:glEnable(GL_DEPTH_TEST)。

在某些情况下我们需要进行深度测试并相应地丢弃片段,但我们不希望更新深度缓冲区,即使用一个只读的深度缓冲区。可以将深度掩码设置为GL_FALSE禁用深度缓冲区写入:glDepthMask(GL_FALSE)。注意这只在深度测试被启用的时候有效。

深度测试函数:

OpenGL 允许我们修改深度测试使用的比较运算符(comparison operators)。这样我们可以控制是通过还是丢弃碎片,以及如何更新深度缓冲区。可以通过深度函数来设置:

1
void glDepthFunc(GLenum func)

该函数接受下列几个比较运算符:

glDepthFunc运算符

默认情况下使用GL_LESS,这将丢弃深度值高于或等于当前深度缓冲区的值的片段。

深度值精度

深度缓冲区中的深度值介于0.0和1.0之间,其值与场景中的所有物体Z值进行比较。这些视图空间中的Z值可以是透视投影平截头体的近平面和远平面之间的任意值。因此我们需要一些方法将这些视图空间Z值转换到[0,1]范围内。

方法之一就是线性转换:

这里far和near是投影矩阵设置可见视图截锥的远近值。方程取frustum内的Z值,并将其转换到[0,1]范围内。下图给出 z 值和其相应的深度值的关系:

然而,实践中是几乎从不使用这样的线性深度缓冲区。正确的投影特性的非线性深度方程是和1/z成正比的 。这样基本上做的是,在Z很近时精度很高,而Z很远的时候精度低。我们并没必要让1000单位远的物体和1单位远的物体有相同的精度,而线性方程没有考虑这一点。 由于非线性函数是和 1/z 成正比,例如1.0 和 2.0 之间的 z 值,将变为 1.0 到 0.5之间, 这样在z非常小的时候给了我们很高的精度。这类方程,也需要视图截锥的远近值,下面给出:

要记住的重要一点是深度缓冲区的值在屏幕空间中不是线性的(它们在视图空间投影矩阵应用之前是线性的)。值为 0.5 在深度缓冲区并不意味着该对象的 z 值是投影平头截体的中间。你可以看到 Z值和深度缓冲区的值在下列图中的非线性关系:

正如你所看到,一个附近的物体的小的Z值因此给了我们很高的深度精度。变换Z值的方程式被嵌入在投影矩阵,所以当我们变换顶点坐标从视图到裁剪然后到屏幕空间,非线性方程将被应用。如果你好奇投影矩阵究竟做了什么,建议阅读这篇文章

深度缓冲区的可视化

我们知道在片段着色器的内置gl_FragCoord向量的z值包含那个片段的深度值。如果我们要把深度值作为颜色输出,那么我们可以在场景中显示的所有片段的深度值。我们可以返回基于片段的深度值的颜色向量: color = vec4(vec3(gl_FragCoord.z), 1.0f);

如果再次运行程序你可能会发现一切都是白的,看起来像我们的深度值都是最大值1.0。这是因为屏幕空间的深度值是非线性的,几乎都接近1.0,如果你小心靠近物体,会发现颜色越来越暗,即z值越来越小。

深度冲突:

有一种常见的视觉现象可能发生,就是当两个平面或三角形对齐的过于紧凑,深度缓冲区没有足够的精度来区分哪个靠前哪个靠后。结果是,这两个形状看起来像不断切换顺序导致奇怪的样子。这被称为深度冲突,因为它看上去像形状争夺顶靠前的位置。

深度冲突是深度缓冲区的常见问题,当对象的距离越远一般越强(因为深度缓冲区在z值非常大的时候没有很高的精度)。

防止深度冲突

首先也是最重要的技巧是让物体之间不要离得太近,以至于他们的三角形重叠。通过在物体之间制造一点用户无法察觉到的偏移,可以完全解决深度冲突。然而这需要人工的干预每个物体,并进行彻底地测试,以确保这个场景的物体之间没有深度冲突。

另一个技巧是尽可能把近平面设置得远一些。前面我们讨论过越靠近近平面的位置精度越高。所以我们移动近平面远离观察者,我们可以在椎体内很有效的提高精度。然而把近平面移动的太远会导致近处的物体被裁剪掉。所以应不断调整测试近平面的值,为你的场景找出最好的近平面的距离。

还有是放弃一些性能来得到更高的深度值的精度。大多数的深度缓冲区都是24位。但现在显卡支持32位深度值,这让深度缓冲区的精度提高了一大节。所以牺牲一些性能你会得到更精确的深度测试,减少深度冲突。


参考 LearnOpenGL-CN 深度测试

Scriptable Objects 及 游戏架构

Scriptable Objects 相关介绍,及基于其的游戏架构技术 Continue reading

AssetBundle 最佳实践

Published on January 29, 2019

AssetBundle 基础总结

Published on January 27, 2019