LearnOpenGL学习笔记——深度测试

什么是深度测试

我们在绘制物体的时候,物体之间是有遮挡关系的。比如下图,箱子遮挡住地面。遮挡关系是由深度确定的。从视觉上来讲,可以简单的理解为从摄像机(眼睛)看过去,距离摄像机(眼睛距离越近),深度值越小,越不会被遮挡。如下图:
在这里插入图片描述
OpenGL中,默认是禁用深度测试的,也就是说,OpengGL在绘制物体时候,并不关心哪个物体遮挡哪个物体。后绘制的物体总是遮挡前面的。如果不做深度测试,那我们上图的地面和箱子就变成了下图:
在这里插入图片描述

现在大部分的GPU都提供一个叫做提前深度测试(Early Depth Testing)的硬件特性。提前深度测试允许深度测试在片段着色器之前运行。只要我们清楚一个片段永远不会是可见的(它在其他物体之后),我们就能提前丢弃这个片段。 片段着色器通常开销都是很大的,所以我们应该尽可能避免运行它们。当使用提前深度测试时,片段着色器的一个限制是你不能写入片段的深度值。如果一个片段着色器对它的深度值进行了写入,提前深度测试是不可能的。OpenGL不能提前知道深度值。

深度测试函数

//深度测试默认是禁用的,所以如果要启用深度测试的话,我们需要用GL_DEPTH_TEST选项来启用它:
glEnable(GL_DEPTH_TEST);
//当它启用的时候,如果一个片段通过了深度测试的话,OpenGL会在深度缓冲中储存该片段的z值;如果没有通过深度缓冲,则会丢弃该片段
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//你启用了深度缓冲,你还应该在每个渲染迭代之前使用GL_DEPTH_BUFFER_BIT来清除深度缓冲,否则你会仍在使用上一次渲染迭代中的写入的深度值
//可以想象,在某些情况下你会需要对所有片段都执行深度测试并丢弃相应的片段,但不希望更新深度缓冲。基本上来说,你在使用一个只读的(Read-only)深度缓冲。OpenGL允许我们禁用深度缓冲的写入,只需要设置它的深度掩码(Depth Mask)设置为GL_FALSE就可以了:
glDepthMask(GL_FALSE);

OpenGL允许我们修改深度测试中使用的比较运算符。

glDepthFunc(GL_LESS);

函数 描述
GL_ALWAYS 永远通过深度测试
GL_NEVER 永远不通过深度测试
GL_LESS 在片段深度值小于缓冲的深度值时通过测试
GL_EQUAL 在片段深度值等于缓冲区的深度值时通过测试
GL_LEQUAL 在片段深度值小于等于缓冲区的深度值时通过测试
GL_GREATER 在片段深度值大于缓冲区的深度值时通过测试
GL_NOTEQUAL 在片段深度值不等于缓冲区的深度值时通过测试
GL_GEQUAL 在片段深度值大于等于缓冲区的深度值时通过测试

深度值精度

深度缓冲包含了一个介于0.0和1.0之间的深度值,它将会与观察者视角所看见的场景中所有物体的z进行比较。观察空间的z值可能是投影平截头体的近平面和元平面之间的任何值。我们需要一种方式将这些观察空间的z值变换到[0, 1]范围之间,其中的一种方式就是将它们连线变换到[0, 1]范围之间。下面这个(线性)方程将z值变换到了0.0和1.0之间的深度值:
在这里插入图片描述
在这里插入图片描述
然而,在实践中是几乎永远不会使用这样的线性深度缓冲(Linear Depth Buffer)的。要想有正确的投影性质,需要使用一个非线性的深度方程,它是与 1/z 成正比的。它做的就是在z值很小的时候提供非常高的精度,而在z值很远的时候提供更少的精度。花时间想想这个:我们真的需要对1000单位远的深度值和只有1单位远的充满细节的物体使用相同的精度吗?线性方程并不会考虑这一点。
由于非线性方程与 1/z 成正比,在1.0和2.0之间的z值将会变换至1.0到0.5之间的深度值,这就是一个float提供给我们的一半精度了,这在z值很小的情况下提供了非常大的精度。在50.0和100.0之间的z值将会只占2%的float精度,这正是我们所需要的。这样的一个考虑了远近距离的方程是这样的:
在这里插入图片描述
在这里插入图片描述
深度值很大一部分是由很小的z值所决定的,这给了近处的物体很大的深度精度。这个(从观察者的视角)变换z值的方程是嵌入在投影矩阵中的,所以当我们想将一个顶点坐标从观察空间至裁剪空间的时候这个非线性方程就被应用了。
推介阅读:http://www.songho.ca/opengl/gl_projectionmatrix.html

深度缓冲的可视化

// 修改fragment着色器,返回z值,也就是深度值
void main()
{
    FragColor = vec4(vec3(gl_FragCoord.z), 1.0);
}

结果,几乎全是白色,说明z值全部接近1.看起来就想我们所有的深度值都是最大的1.0。所以为什么没有靠近0.0(即变暗)的深度值呢?
在这里插入图片描述
屏幕空间中的深度值是非线性的,即它在z值很小的时候有很高的精度,而z值很大的时候有较低的精度。片段的深度值会随着距离迅速增加,所以几乎所有的顶点的深度值都是接近于1.0的。如果我们小心地靠近物体,你可能会最终注意到颜色会渐渐变暗,显示它们的z值在逐渐变小:
在这里插入图片描述
这很清楚地展示了深度值的非线性性质。近处的物体比起远处的物体对深度值有着更大的影响。只需要移动几厘米就能让颜色从暗完全变白。
然而,我们也可以让片段非线性的深度值变换为线性的。要实现这个,我们需要仅仅反转深度值的投影变换。这也就意味着我们需要首先将深度值从[0, 1]范围重新变换到[-1, 1]范围的标准化设备坐标(裁剪空间)。接下来我们需要像投影矩阵那样反转这个非线性方程(方程2),并将这个反转的方程应用到最终的深度值上。最终的结果就是一个线性的深度值了。听起来是可行的,对吧?
首先我们将深度值变换为NDC,不是非常困难:

float z = depth * 2.0 - 1.0;

接下来使用获取到的z值,应用逆变换来获取线性的深度值:

float linearDepth = (2.0 * near * far) / (far + near - z * (far - near));

完整的着色器代码:

#version 330 core
out vec4 FragColor;
float near = 0.1; 
float far  = 100.0; 
float LinearizeDepth(float depth) 
{
    float z = depth * 2.0 - 1.0; // back to NDC 
    return (2.0 * near * far) / (far + near - z * (far - near));    
}
void main()
{             
    float depth = LinearizeDepth(gl_FragCoord.z) / far; // 为了演示除以far
    FragColor = vec4(vec3(depth), 1.0);
}

由于线性化的深度值处于near与far之间,它的大部分值都会大于1.0并显示为完全的白色。通过在main函数中将线性深度值除以far,我们近似地将线性深度值转化到[0, 1]的范围之间。这样子我们就能逐渐看到一个片段越接近投影平截头体的远平面,它就会变得越亮,更适用于展示目的。

如果我们现在运行程序,我们就能看见深度值随着距离增大是线性的了。尝试在场景中移动,看看深度值是怎样以线性变化的。

在这里插入图片描述
颜色大部分都是黑色,因为深度值的范围是0.1的近平面到100的远平面,它离我们还是非常远的。结果就是,我们相对靠近近平面,所以会得到更低的(更暗的)深度值。

深度冲突

一个很常见的视觉错误会在两个平面或者三角形非常紧密地平行排列在一起时会发生,深度缓冲没有足够的精度来决定两个形状哪个在前面。结果就是这两个形状不断地在切换前后顺序,这会导致很奇怪的花纹。这个现象叫做深度冲突(Z-fighting),因为它看起来像是这两个形状在争夺(Fight)谁该处于顶端。

防止深度冲突

  • 永远不要把多个物体摆得太靠近,以至于它们的一些三角形会重叠
  • 尽可能将近平面设置远一些
  • 使用更高精度的深度缓冲(大部分深度缓冲的精度都是24位的,但现在大部分的显卡都支持32位的深度缓冲,这将会极大地提高精度)

参考:
https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/01%20Depth%20testing/#_1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值