从0开始的OpenGL学习(十八)-深度测试

本文主要解决两个问题:

1、深度测试的原理是什么?
2、如何解决深度冲突(z-fighting)问题?

引言

在之前的章节中,我们就已经用到了深度测试的功能,大概知道深度测试是用来调整物体遮挡关系,使后面的东西不会“挡”住前面的东西。但当时我们只是启用了深度测试就解决了问题,而没有去深入了解为什么启用它之后就能解决遮挡问题。本文中,我们会深入理解深度测试的原理,将深度值以一种能观察的方式显现出来,这样我们就能直观的观察到深度值了。另外,伴随着深度测试会出现一种名叫深度冲突(z-fighting)的问题,我们也会讨论几种解决深度冲突的方法,应用到程序之中,可以降低甚至完全避免深度冲突。

深度缓冲(depth buffer)

和颜色缓冲类似,深度缓冲中也保存了每一个片元的信息。窗口系统会自动创建深度缓存,使用16位、24位或者32位浮点数来保存信息。大多数情况下,深度缓存的精度是24位的。

启用深度测试之后,OpenGL会对当前片元的深度值和缓存中的值进行比较,如果比较成功,则将当前的深度值保存到缓存中,如果比较失败,则会放弃这个片元。

深度测试是在屏幕坐标空间中进行的,在片元着色器和模板测试(下一章讨论)运行之后。屏幕坐标受到glViewport函数的直接影响,并且我们可以通过GLSL内置变量gl_FragCoord来访问z值。

现金的大部分GPU支持一种叫早期深度测试(early depth testing)的功能。这种测试允许深度测试在片元着色器运行之前就做测试。这里我们只做简单的介绍,有兴趣的童鞋可以自行研究。

我们已经知道,深度测试在OpenGL中是默认不开启的,如果要开启深度测试,我们需要调用这样一行代码:

glEnable(GL_DEPTH_TEST);

这还没完,每一次的渲染都会写入z值到深度缓冲中,如果我们不希望上一次的运行结果对这一次的渲染有影响(通常都不会希望如此),我们需要在每次渲染前都清除深度缓存。清除的方式和清除颜色缓存一样:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  

顺带一提,OpenGL允许我们只进行深度测试而不将结果写入到深度缓存中,方法就是调用glDepthMash(GL_FALSE);。不过要注意的是,这个函数只在启用了深度测试后才有效。

深度测试函数

OpenGL允许我们修改深度测试的比较操作,这对我们来说实在是太方便了。默认情况下,深度值小的会通过测试,我们可以通过下面这个函数来改变这种情况:

glDepthFunc(GL_LESS); 

下面这种表里列出glDepthFunc函数接收的参数以及相应的结果:

函数 效果
GL_ALWAYS 深度测试永远成功
GL_NEVER 深度测试永远失败
GL_LESS 片元的深度值小于保存的深度值时成功
GL_EQUAL 片元的深度值等于保存的深度值时成功
GL_LEQUAL 片元的深度值小于等于保存的深度值时成功
GL_GREATER 片元的深度值大于保存的深度值时成功
GL_NOTEQUAL 片元的深度值不等于保存的深度值时成功
GL_GEQUAL 片元的深度值大于等于保存的深度值时成功

我们来看看改变测试操作之后的效果上有啥不同。下载这里的源码将其放到工程当中。

有了代码之后,我们先把测试函数设置成GL_ALWAYS:

glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_ALWAYS); 

得到的效果是:


从0开始的OpenGL学习(十八)-深度测试_第1张图片
显示效果

物体看上去透明了,被盒子遮挡的地面都能看到。把测试函数设置回GL_LESS后得到的效果是:


从0开始的OpenGL学习(十八)-深度测试_第2张图片
显示效果

很明显,效果确实如我们所预料的。

深度值精度

深度缓存中保存的深度值的范围是0.0到1.0,它是根据物体距离观察者的远近来确定深度值的。场景中物体的z值可能是近裁剪面和远裁剪面之间的任意值,我们要通过某种方法将这个z值转换成深度值。其中一种非常直观的算法是计算z到近裁剪面的距离占视景体z范围的比例,这种转换被称为线性转换,其公式如下:


线性深度缓存计算公式

这里的near和far是近裁剪面和远裁剪面的z坐标。通过计算,物体的z值就被转换成一个[0,1]之间的数值。深度和z坐标之间的关系成线性:


从0开始的OpenGL学习(十八)-深度测试_第3张图片
z值和深度值关系

但是,这种看起来非常不错的计算方法再实际的工程里几乎从未使用过,因为其忽略了一个重要的因素:我们对离观察者非常远的地方要那么高的深度值的精度吗?打个比方,我可以看到距离我0.1单位和100单位的所有物体,但是距离我50单位到100单位之间的物体我根本就没有必要看的那么仔细,反而是离我近的我一眼就能看出它们的遮挡是否正确。所以,在实际的应用中,我们通常采用下面这个公式:
从0开始的OpenGL学习(十八)-深度测试_第4张图片
实际应用中的深度值计算公式

不用太在意这个公式实际表示的意义,它并没有具体的意义。这个公式给我们的最大的好处就是在离近裁剪面较近的距离上提供了很广的深度值的范围,而在距离远的地方提供的范围很小:


从0开始的OpenGL学习(十八)-深度测试_第5张图片
z值和深度值的关系

可以看到,z值在1到4区间内,深度值的跨度从0到0.75,绝大部分的精度都分配给了这么一个小区间,后面的那么多距离就只占了25%的精度。

显示深度值

正如之前所说,物体的深度值存储在gl_FragCoord的z分量中,我们可以通过一些小聪明来观察深度值:将深度值作为一个颜色的渲染出来!

要做到这点非常简单,只需要在片元着色器中小小地做点改动即可:

FragColor = vec4(vec3(gl_FragCoord.z), 1.0);
从0开始的OpenGL学习(十八)-深度测试_第6张图片
运行效果

通过示意图就可以清楚地看到远处的呈现出非常明显的白色。跟上面z值和深度值的关系图比较,距离稍远的深度值会更接近1,所以呈现出白色。前后移动一下摄像机,你会看到只需要稍微往后移动一点点物体就变得非常白,这也和图上非线性的关系表现一致。

来搞定事情!我们可以把这种非线性的深度值转换成线性的深度值。要做到这一点,首先我们要将深度值转换成齐次设备坐标(NDC),范围是[-1,1]。然后根据上面的公式二来计算出z值,将其应用到公式一种计算出线性深度值,推理过程就不详细解释了,直接给出代码:

float near = 0.1;
float far = 100.0;

float LinearizeDepth(float depth) {
    float z = depth * 2.0 - 1.0;    //转换成NDC
    return (2.0 * near * far) / (far + near - z * (far - near));
}

void main()
{    
   float depth = LinearizeDepth(gl_FragCoord.z) / far;
   FragColor = vec4(vec3(depth), 1.0);
}
从0开始的OpenGL学习(十八)-深度测试_第7张图片
运行效果

运行之后,可以看到远处的地面稍微变白了一点。当你离这个平面越来越远的时候,你就能看到一个渐渐变亮的场景,这就是线性深度值的变化过程。

附上源码以供参考。

Z冲突(z-fighting)

讲到这里,相信你已经对深度测试的原理有了很好的理解。在文章的最后我们来讨论一个伴随着深度测试而存在的一个问题,称作z冲突(深度冲突)。这个问题产生的原因是我们想用[0,1]区间来表示所有的深度关系,计算机中表示浮点数的精度有限,不可能非常精确的[0,1000]甚至是[0,1000000]这么大范围的z值都算成一个唯一的深度值。当有两个非常靠近的平面时,我们无法确定这两个平面谁在谁前面,就可能会出现非常奇怪的问题,如下图所示:


从0开始的OpenGL学习(十八)-深度测试_第8张图片
z冲突的效果

把摄像机移动到盒子内部,朝下面看,就会出现这种诡异的图案。

出现问题解决问题是我们一向的宗旨。既然我们都了解了深度测试的原理,我们至少可以提出三种不同的解决方法:

  • 第一种:既然是两个平面靠的太近才会出现问题,那我把它们稍稍拉远一点就不会有事了。当然,如果要在场景中一个个去把物体拉远点,是很费事的一件事。
  • 第二种:既然是深度值的精度不够,我们给一个精度更高的空间好了。24位的不够就用32位的,现在市面上的显卡有很多都支持。
  • 第三种:也是从深度值精度不够想出来的,从上面的图上可以看出,在z坐标较小的范围内深度值的范围很大,那么将近裁剪面设置的远一点就能给出一个很大的深度值范围了。当然,近裁剪面也不能太远,否则会把眼前的东西都裁剪掉,一个合适的近裁剪面的位置就要考虑到深度测试和物体剔除两个方面的因素。

当然,解决z冲突的方法不只这3种,还有很多复杂的方法,不过也不能完全规避掉z冲突。z冲突是一个普遍的问题,组合使用上面这三种方法,我们可以规避大多数的z冲突问题。

总结

本文中,我们深入的理解了深度测试的原理,也知道了深度值是如何计算的,并亲眼看到了深度值的效果。不仅如此,我们还通过非线性的深度值计算出线性深度值来观察效果。最后,我们还讨论了z冲突的问题,并想出了几种解决z冲突的方法。

下一篇
目录
上一篇

参考资料

www.learnopengl.com(非常好的网站,建议学习)

你可能感兴趣的:(从0开始的OpenGL学习(十八)-深度测试)