OpenGL学习笔记(十一)

OpenGL 中级篇(七)

深度测试

将会更加深入地讨论这些储存在深度缓冲(或z缓冲(z - buffer))中的深度值(Depth Value),以及它们是如何确定一个片段是处于其它片段后方的。
深度缓冲就像颜色缓冲(Color Buffer) 一样,在每个片段中储存了信息,并且和颜色缓冲有着一样的宽度和高度。深度缓冲是由窗口系统自动创建的,它会以16、24或32位float的形式储存它的深度值,在大部分的系统中,深度缓冲的精度都是24位的。
当深度测试(Depth Testing)被启用的时候,OpenGL会将一个片段的的深度值与深度缓冲的内容进行对比。OpenGL会执行一个深度测试,如果这个测试通过了的话,深度缓冲将会更新为新的深度值。如果深度测试失败了,片段将会被丢弃。
深度测试在片段着色器运行之后,在屏幕空间中执行。
屏幕空间坐标与通过OpenGLglViewport 所定义的视口密切相关,并且可以直接使用GLSL内建变量 gl_FragCoord 从片段着色器中直接访问。
gl_FragCoord 的x和y分量代表了片段的屏幕空间坐标(其中(0, 0)位于左下角)。gl_FragCoord 中也包含了一个z分量,它包含了片段真正的深度值(z值就是需要与深度缓冲中的值进行对比)。
注意:现在大部分的GPU都提供一个叫做提前深度测试(Early Depth Testing)的硬件特性。提前深度测试允许深度测试在片段着色器之前运行。只要清楚一个片段永远不会是可见的(它在其他物体之后),就能提前丢弃这个片段。片段着色器通常开销都是很大的,所以应该尽可能避免运行它们。当使用提前深度测试时,片段着色器的一个限制是你不能写入片段的深度值。
深度测试默认是禁用的,所以如果要启用深度测试的话,需要用GL_DEPTH_TEST选项来启用它:

glEnable(GL_DEPTH_TEST);

当它启用的时候,如果一个片段通过了深度测试的话,OpenGL会在深度缓冲中储存该片段的z值;如果没有通过深度缓冲,则会丢弃该片段。
如果启用了深度缓冲,还应该在每个渲染迭代之前使用GL_DEPTH_BUFFER_BIT来清除深度缓冲,否则会仍在使用上一次渲染迭代中写入的深度值:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

在某些情况下会需要对所有片段都执行深度测试并丢弃相应的片段,但不希望更新深度缓冲。基本上来说,在使用一个只读的(Read - only)深度缓冲。
OpenGL允许禁用深度缓冲的写入,只需要设置它的深度掩码(Depth Mask)设置为GL_FALSE就可以了:

glDepthMask(GL_FALSE);

注意这只在深度测试被启用的时候才有效果。

1.深度测试函数

OpenGL允许修改深度测试中使用的比较运算符。这允许来控制OpenGL什么时候该通过或丢弃一个片段,什么时候去更新深度缓冲。可以调用 glDepthFunc 函数来设置比较运算符(或者说深度函数(Depth Function)):

glDepthFunc(GL_LESS);

该函数接受在下表中列出的比较运算符:
在这里插入图片描述

glDepthFunc(GL_LESS);

默认情况下使用的深度函数是GL_LESS,它将会丢弃深度值大于等于当前深度缓冲值的所有片段。
在源代码中,将深度函数改为GL_ALWAYS:

glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_ALWAYS);

这将会模拟没有启用深度测试时所得到的结果。深度测试将会永远通过,所以最后绘制的片段将总是会渲染在之前绘制片段的上面,即使之前绘制的片段本就应该渲染在最前面。因为是最后渲染地板的,它会覆盖所有的箱子片段:
在这里插入图片描述

glDepthFunc(GL_LESS);

若将深度函数重新设置为GL_LESS,会将场景还原为原有的样子:
在这里插入图片描述

2.深度值精度

深度缓冲包含了一个介于0.0和1.0之间的深度值,它将会与观察者视角所看见的场景中所有物体的z值进行比较。观察空间的z值可能是投影平截头体的近平面(Near)和远平面(Far)之间的任何值。需要一种方式来将这些观察空间的z值变换到[0, 1]范围之间,其中的一种方式就是将它们线性变换到[0, 1]范围之间。下面这个(线性)方程将z值变换到了0.0到1.0之间的深度值:
在这里插入图片描述
这里的near和far值是之前提供给投影矩阵设置可视平截头体的(见坐标系统那节课)那个 near 和 far 值。
这个方程需要平截头体中的一个z值,并将它变换到了[0, 1]的范围中。z值和对应的深度值之间的关系可以在下图中看到:
在这里插入图片描述
注意所有的方程都会将非常近的物体的深度值设置为接近0.0的值,而当物体非常接近远平面的时候,它的深度值会非常接近1.0
由于非线性函数是和 1 / z 成正比,例如1.0 和 2.0 之间的 z 值,将变为 1.0 到 0.5之间, 这样在 z 非常小的时候给了很高的精度。50.0 和 100.0 之间的 z 值将只占 2 % 的浮点数的精度,这正是想要的。这类方程,也需要近和远距离考虑,下面给出:
在这里插入图片描述
注意:深度缓冲中 0.5 的值并不代表着物体的z值是位于平截头体的中间了,这个顶点的z值实际上非常接近近平面!
可以在下图中看到z值和最终的深度缓冲值之间的非线性关系:
在这里插入图片描述
可以看到,深度值很大一部分是由很小的z值所决定的,这给了近处的物体很大的深度精度。这个(从观察者的视角)变换z值的方程是嵌入在投影矩阵中的,所以当想将一个顶点坐标从观察空间至裁剪空间的时候这个非线性方程就需要被应用。

3.深度缓冲区的可视化

片段着色器中,内建 gl_FragCoord 向量的z值包含了那个特定片段的深度值。如果将这个深度值输出为颜色,可以显示场景中所有片段的深度值。可以根据片段的深度值返回一个颜色向量来完成这一工作:

void main() {
FragColor = vec4(vec3(gl_FragCoord.z), 1.0);
}

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

float z = depth * 2.0 - 1.0;

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

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

这个方程是用投影矩阵推导得出的,它使用了方程2来非线性化深度值,返回一个near与far之间的深度值。
将屏幕空间中非线性的深度值变换至线性深度值的完整片段着色器如下:
在这里插入图片描述
由于线性化的深度值处于near与far之间,它的大部分值都会大于1.0并显示为完全的白色。通过在main函数中将线性深度值除以far,近似地将线性深度值转化到[0, 1]的范围之间。这样子就能逐渐看到一个片段越接近投影平截头体的远平面,它就会变得越亮,更适用于展示目的。
运行程序,尝试在场景中移动,看看深度值是怎样以线性变化的。
在这里插入图片描述

4.深度冲突

一个很常见的视觉错误会在两个平面或者三角形非常紧密地平行排列在一起时会发生,深度缓冲没有足够的精度来决定两个形状哪个在前面。结果就是这两个形状不断地在切换前后顺序,这会导致很奇怪的花纹。这个现象叫做深度冲突(Z - fighting),因为它看起来像是这两个形状在争夺(Fight)谁该处于前面。
在一直使用的场景中,有几个地方的深度冲突还是非常明显的。箱子被放置在地板的同一高度上,这也就意味着箱子的底面和地板是共面的(Coplanar)。这两个面的深度值都是一样的,所以深度测试没有办法决定应该显示哪一个。
如果移动摄像机到容器的里面,那么这个影响清晰可见,箱子的底部不断地在箱子底面与地板之间切换,形成一个锯齿的花纹:
在这里插入图片描述
深度冲突是深度缓冲的一个常见问题,当物体在远处时效果会更明显(因为深度缓冲在z值比较大的时候有着更小的精度)。深度冲突不能够被完全避免,但一般会有一些技巧有助于在你的场景中减轻或者完全避免深度冲突。
第一个也是最重要的技巧是不要把多个物体摆得太靠近,以至于它们的一些三角形会重叠。通过在两个物体之间设置一个用户无法注意到的偏移值,可以完全避免这两个物体之间的深度冲突。在箱子和地板的例子中,可以将箱子沿着正y轴稍微移动一点。箱子位置的这点微小改变将不太可能被注意到,但它能够完全减少深度冲突的发生。
然而,这需要对每个物体都手动调整,并且需要进行彻底的测试来保证场景中没有物体会产生深度冲突。
第二个技巧是尽可能将近平面设置远一些。在前面提到了精度在靠近近平面时是非常高的,所以如果将近平面远离观察者,将会对整个平截头体有着更大的精度。然而,将近平面设置太远将会导致近处的物体被裁剪掉,所以这通常需要实验和微调来决定最适合你的场景的近平面距离。
另外一个很好的技巧是牺牲一些性能,使用更高精度的深度缓冲。大部分深度缓冲的精度都是24位的,但现在大部分的显卡都支持32位的深度缓冲,这将会极大地提高精度。所以,牺牲掉一些性能,就能获得更高精度的深度测试,减少深度冲突。
上面讨论的三个技术是最普遍也是很容易实现的抗深度冲突技术了。还有一些更复杂的技术,但它们依然不能完全消除深度冲突。深度冲突是一个常见的问题,但如果组合使用了上面列举出来的技术,可能不会再需要处理深度冲突了。

总结

了解深度缓冲的定义与概念;
掌握深度缓冲的实现方法;
了解深度冲突以及解决方案;

模板测试

当片段着色器处理完一个片段之后,模板测试(Stencil Test) 会开始执行,和深度测试一样,它也可能会丢弃片段。接下来,被保留的片段会进入深度测试,它可能会丢弃更多的片段。模板测试是根据又一个缓冲来进行的,它叫做模板缓冲(Stencil Buffer),可以在渲染的时候更新它来获得一些很有意思的效果。
一个模板缓冲中,每个模板值(Stencil Value) 通常是8位的。所以每个像素 / 片段一共能有256种不同的模板值。可以将这些模板值设置为想要的值,然后当某一个片段有某一个模板值的时候,就可以选择丢弃或是保留这个片段了。
下面是一个模板缓冲的简单例子:
在这里插入图片描述
模板缓冲首先会被清除为0,之后在模板缓冲中使用1填充了一个空心矩形。场景中的片段将会只在片段的模板值为1的时候会被渲染(其它的都被丢弃了)。
模板缓冲操作允许在渲染片段时将模板缓冲设定为一个特定的值。通过在渲染时修改模板缓冲的内容,写入了模板缓冲。在同一个(或者接下来的)渲染迭代中,可以读取这些值,来决定丢弃还是保留某个片段。使用模板缓冲的时候我们可以尽情发挥,但大体的步骤如下:

  • 开启模板缓冲写入。
  • 渲染物体,更新模板缓冲的内容。
  • 禁用模板缓冲写入。
  • 渲染(其它)物体,这次根据模板缓冲的内容丢弃特定的片段。

所以,通过使用模板缓冲,可以根据场景中已绘制的其它物体的片段,来决定是否丢弃特定的片段。
可以启用GL_STENCIL_TEST来启用模板测试。在这一行代码之后,所有的渲染调用都会以某种方式影响着模板缓冲。

glEnable(GL_STENCIL_TEST);

注意,和颜色和深度缓冲一样,也需要在每次迭代之前清除模板缓冲。

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

同时,和深度测试的glDepthMask函数一样,模板缓冲也有一个类似的函数。glStencilMask允许设置一个位掩码(Bitmask),它会与将要写入缓冲的模板值进行与(AND) 运算。默认情况下设置的位掩码所有位都为1,不影响输出,但如果将它设置为0x00,写入缓冲的所有模板值最后都会变成0.这与深度测试中的glDepthMask(GL_FALSE) 是等价的。

glStencilMask(0xFF); // 每一位写入模板缓冲时都保持原样
glStencilMask(0x00); // 每一位在写入模板缓冲时都会变成0(禁用写入)

大部分情况下都只会使用0x00或者0xFF作为模板掩码(Stencil Mask),但是知道有选项可以设置自定义的位掩码总是好的。
和深度测试一样,对模板缓冲应该通过还是失败,以及它应该如何影响模板缓冲,也是有一定控制的。一共有两个函数能够用来配置模板测试:glStencilFunc和glStencilOp。
void glStencilFunc(GLenum func, GLint ref, GLuint mask) 有三个参数:

  • func:设置模板测试函数(Stencil Test Function)。这个测试函数将会应用到已储存的模板值上和glStencilFunc函数的ref值上。可用的选项有:GL_NEVER、GL_LESS、GL_LEQUAL、GL_GREATER、GL_GEQUAL、GL_EQUAL、GL_NOTEQUAL和GL_ALWAYS。它们的语义和深度缓冲的函数类似。
  • ref:设置了模板测试的参考值(Reference Value)。模板缓冲的内容将会与这个值进行比较。
  • mask:设置一个掩码,它将会与参考值和储存的模板值在测试比较它们之前进行与(AND)运算。初始情况下所有位都为1。

在一开始的那个简单的模板例子中,函数被设置为:

glStencilFunc(GL_EQUAL, 1, 0xFF)

这会告诉OpenGL,只要一个片段的模板值等于(GL_EQUAL)参考值1,片段将会通过测试并被绘制,否则会被丢弃。
在这里插入图片描述
但是glStencilFunc仅描述了OpenGL应该对模板缓冲内容做什么,而不是应该如何更新缓冲。这就需要glStencilOp这个函数了。
glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)一共包含三个选项,能够设定每个选项应该采取的行为:

  • sfail:模板测试失败时采取的行为。
  • dpfail:模板测试通过,但深度测试失败时采取的行为。
  • dppass:模板测试和深度测试都通过时采取的行为。

每个选项都可以选用以下的其中一种行为:
在这里插入图片描述
默认情况下glStencilOp是设置为(GL_KEEP, GL_KEEP, GL_KEEP)的,所以不论任何测试的结果是如何,模板缓冲都会保留它的值。默认的行为不会更新模板缓冲,所以如果想写入模板缓冲的话,需要至少对其中一个选项设置不同的值。
所以,通过使用glStencilFunc和glStencilOp,可以精确地指定更新模板缓冲的时机与行为了,也可以指定什么时候该让模板缓冲通过,即什么时候片段需要被丢弃。

物体轮廓

仅仅看了前面的部分还是不太可能能够完全理解模板测试的工作原理,所以将会展示一个使用模板测试就可以完成的有用特性,它叫做物体轮廓(Object Outlining)
在这里插入图片描述
物体轮廓所能做的事情正如它名字所描述的那样。将会为每个(或者一个)物体在它的周围创建一个很小的有色边框。当你想要在策略游戏中选中一个单位进行操作的,想要告诉玩家选中的是哪个单位的时候,这个效果就非常有用了。
为物体创建轮廓的步骤如下:
1、在绘制(需要添加轮廓的)物体之前,将模板函数设置为GL_ALWAYS,每当物体的片段被渲染时,将模板缓冲更新为1。
2、渲染物体。
3、禁用模板写入以及深度测试。
4、将每个物体缩放一点点。
5、使用一个不同的片段着色器,输出一个单独的(边框)颜色。
6、再次绘制物体,但只在它们片段的模板值不等于1时才绘制。
7、再次启用模板写入和深度测试。
这个过程将每个物体的片段的模板缓冲设置为1,当想要绘制边框的时候,主要绘制放大版本的物体中模板测试通过的部分,也就是物体的边框的位置。主要使用模板缓冲丢弃了放大版本中属于原物体片段的部分。
所以首先来创建一个很简单的片段着色器,它会输出一个边框颜色。简单地给它设置一个颜色值,将这个着色器命名为shaderSingleColor

void main()
{
FragColor = vec4(0.04, 0.28, 0.26, 1.0);
}

只想给那两个箱子加上边框,所以让地板不参与这个过程。希望首先绘制地板,再绘制两个箱子(并写入模板缓冲),之后绘制放大的箱子(并丢弃覆盖了之前绘制的箱子片段的那些片段)。

首先启用模板测试,并设置测试通过或失败时的行为:

glEnable(GL_DEPTH_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);

如果其中的一个测试失败了,什么都不做,仅仅保留当前储存在模板缓冲中的值。如果模板测试和深度测试都通过了,那么希望将储存的模板值设置为参考值,参考值能够通过glStencilFunc来设置,之后会设置为1。
将模板缓冲清除为0,对箱子中所有绘制的片段,将模板值更新为1:

glStencilFunc(GL_ALWAYS, 1, 0xFF); // 所有的片段都应该更新模板缓冲
glStencilMask(0xFF); // 启用模板缓冲写入
normalShader.use();
DrawTwoContainers();

通过使用GL_ALWAYS模板测试函数,保证了箱子的每个片段都会将模板缓冲的模板值更新为1。因为片段永远会通过模板测试,在绘制片段的地方,模板缓冲会被更新为参考值。
现在模板缓冲在箱子被绘制的地方都更新为1了,将要绘制放大的箱子,但这次要禁用模板缓冲的写入:

glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00); // 禁止模板缓冲的写入
glDisable(GL_DEPTH_TEST);
shaderSingleColor.use();
DrawTwoScaledUpContainers();

将模板函数设置为GL_NOTEQUAL,它会保证只绘制箱子上模板值不为1的部分,即只绘制箱子在之前绘制的箱子之外的部分。注意也禁用了深度测试,让放大的箱子,即边框,不会被地板所覆盖。记得要在完成之后重新启用深度缓冲
场景中物体轮廓的完整步骤会看起来像这样:
只要理解模板缓冲背后的大体思路,这个代码片段就不那么难理解了。如果还不能理解的话,尝试再次仔细阅读之前的部分,并尝试通过上面使用的范例,完全理解每个函数的功能
在这里插入图片描述
这个轮廓算法的结果在深度测试小节的那个场景中,看起来像这样:
在这里插入图片描述
注意:可以看到这两个箱子的边框重合了,这通常都是我们想要的结果(想想策略游戏中,希望选择10个单位,合并边框通常是我们想需要的结果)。如果想让每个物体都有一个完整的边框,需要对每个物体都清空模板缓冲,并有创意地利用深度缓冲。
可以将物体轮廓算法运用在需要显示选中物体的游戏中,这样的算法能够在一个模型类中轻松实现。可以在模型类中设置一个boolean标记,来设置需不需要绘制边框。如果需要富有创造力的话,也可以使用后期处理滤镜(Filter),像是高斯模糊(Gaussian Blur),让边框看起来更自然。
除了物体轮廓之外,模板测试还有很多用途,比如在一个后视镜中绘制纹理,让它能够绘制到镜子形状中,或者使用一个叫做阴影体积(Shadow Volume) 的模板缓冲技术渲染实时阴影。模板缓冲为已经很丰富的OpenGL工具箱又提供了一个很好的工具。

猜你喜欢

转载自blog.csdn.net/weixin_42050609/article/details/125212924
今日推荐