本篇介绍一下深度测试,模板测试和面剔除,并给出示例代码。
OpenGL 入门教程(8) 深度测试,模板测试和面剔除
引言
在开始渲染3D模型时,我们就已经打开了深度测试,现在是时候展开讲了。
本节讲深度测试,模板测试和面剔除技术。
深度测试
深度缓冲(也称zbuffer)是一个与颜色缓冲相似的数据结构,用于存储每个像素的深度信息,帮助确定物体或片段在场景中的前后位置。
深度测试:在片段着色器执行完毕后,OpenGL会进行深度测试,比较当前片段的深度值与深度缓冲中的存储值。
测试通过的标准由glDepthFunc
函数设定的比较运算符决定,默认使用GL_LESS
,意味着只有当新片段更接近观察者(深度值更小)时,才会通过测试并更新深度缓冲,否则该片段将被丢弃。
深度测试函数
深度测试默认是禁用的,如果要启用深度测试需要设置GL_DEPTH_TEST选项;
当它启用的时候,如果一个片段通过了深度测试的话,OpenGL会在深度缓冲中储存该片段的z值;如果没有通过深度缓冲,则会丢弃该片段。
启用深度缓冲时,每一次渲染需要用 GL_DEPTH_BUFFER_BIT 清除深度缓冲,不然深度缓冲仍为上一帧的。
通过glDepthFunc
可以选择不同的深度比较方式,如GL_LESS、GL_EQUAL等,以适应不同的渲染需求,控制哪些片段应通过深度测试。
- GL_ALWAYS 永远通过深度测试
- GL_NEVER 永远不通过深度测试
- GL_LESS 在片段深度值小于缓冲的深度值时通过测试
- GL_EQUAL 在片段深度值等于缓冲区的深度值时通过测试
- GL_LEQUAL 在片段深度值小于等于缓冲区的深度值时通过测试
- GL_GREATER 在片段深度值大于缓冲区的深度值时通过测试
- GL_NOTEQUAL 在片段深度值不等于缓冲区的深度值时通过测试
- GL_GEQUAL 在片段深度值大于等于缓冲区的深度值时通过测试
1 |
|
如果不想更新深度缓冲,(即丢掉所有片段),可以设置深度掩码(Depth Mask)为GL_FALSE来禁用深度缓冲的写入。 设为GL_FALSE时,深度缓冲变为只读状态,虽然仍然执行深度测试,但不会更新缓冲内容。
深度值精度
深度缓冲包含了一个介于0.0和1.0之间的深度值,它将会与观察者视角所看见的场景中所有物体的z值进行比较。
观察空间的z值可能是投影平截头体的近平面(Near)和远平面(Far)之间的任何值。
线性深度缓冲(Linear Depth Buffer):用线性变换把z值变换到[0, 1]范围之间: \(\begin{equation} F_{depth} = \frac{z - near}{far - near} \end{equation}\)
然而实际渲染中,近处的物体比远处的物体更重要。
因此使用一个非线性的深度方程,与 1/z 成正比。
在z值很小的时候提供非常高的精度,而在z值很大的时候提供更少的精度。
\[\begin{equation} F_{depth} = \frac{1/z - 1/near}{1/far - 1/near} \end{equation}\]深度可以在片段着色器获取:
1 |
|
深度冲突
深度冲突(Z-fighting):当几个面紧密平行排列时,深度缓冲没有足够的精度来决定两个形状哪个在前面。 因此这几个形状不断地在切换前后顺序,会产生奇怪的花纹。
深度冲突原则上不能完全解决,只能尽量减小它的影响。常用的办法如下:
-
不要把多个物体摆得太靠近,以至于它们的一些三角形会重叠。通过在两个物体之间设置一个用户无法注意到的偏移值,可以避免这两个物体之间的深度冲突。
-
尽可能将近平面设置远一些。精度在靠近近平面时是非常高的,所以如果我们将近平面远离观察者,我们将会对整个平截头体有着更大的精度。 然而,将近平面设置太远将会导致近处的物体被裁剪掉,所以这通常需要实验和微调来决定最适合你的场景的近平面距离。
-
使用更高精度的深度缓冲。大部分深度缓冲的精度都是24位的,但现在大部分的显卡都支持32位的深度缓冲,这将会极大地提高精度。
模板测试
模板测试(Stencil Test)位于片段着色器之后,深度测试之前。
它利用模板缓冲区(Stencil Buffer)中的数据来决定片段(像素)是否应该被渲染到屏幕上。
模板缓冲是一个附加的缓冲区,与颜色缓冲区、深度缓冲区并列,每个像素在模板缓冲中都有一个单独的模板值。
模板测试用于控制渲染过程中的像素可见性,实现各种特殊效果,如遮罩、轮廓描边、镜像效果等。
模板测试常见的应用如下:
- 遮罩效果:先用特定值填充模板缓冲的特定区域(如一个空心矩形内部),然后配置模板测试仅保留模板值匹配的片段,从而实现只在该区域内绘制物体。
- 轮廓描边:先绘制物体并更新模板缓冲(如增加模板值),然后再次绘制物体但这次根据模板值剔除物体本身(即仅保留边缘),达到描边效果。
- 多重渲染效果:通过在不同渲染阶段设置不同的模板测试条件和操作,可以在场景中叠加或排除特定的视觉效果,比如镜像、阴影等。
模板测试兼具灵活性和更高的性能:
- 灵活性:模板测试提供了非常灵活的方式来控制渲染过程,可以实现复杂的视觉效果。
- 性能优化:通过提前剔除不可见片段,使深度测试舍弃部分片段,提升渲染效率。
模板测试函数
启用GL_STENCIL_TEST来启用模板测试。
和颜色和深度缓冲一样,需要在每次迭代之前清除模板缓冲。
和深度测试中的 glDepthMask
一样,模板缓冲也有 glStencilMask
控制何时允许写入模板缓冲。
glStencilMask
允许我们设置一个位掩码(Bitmask),它会与将要写入缓冲的模板值进行与(AND)运算。
默认情况下设置的位掩码所有位都为1,不影响输出。
使用 glStencilFunc
函数定义模板测试的条件:
1 |
|
- 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。
使用 glStencilOp
函数更新缓冲。
即在模板测试通过、失败或深度测试失败时,模板缓冲中的值应该如何更新(保持、置零、增加、减少、替换、反转等)。
1 |
|
- sfail:模板测试失败时采取的行为。
- dpfail:模板测试通过,但深度测试失败时采取的行为。
- dppass:模板测试和深度测试都通过时采取的行为。
每个选项都可以选用以下的其中一种行为:
- GL_KEEP 保持当前储存的模板值
- GL_ZERO 将模板值设置为0
- GL_REPLACE 将模板值设置为glStencilFunc函数设置的ref值
- GL_INCR 如果模板值小于最大值则将模板值加1
- GL_INCR_WRAP 与GL_INCR一样,但如果模板值超过了最大值则归零
- GL_DECR 如果模板值大于最小值则将模板值减1
- GL_DECR_WRAP 与GL_DECR一样,但如果模板值小于0则将其设置为最大值
- GL_INVERT 按位翻转当前的模板缓冲值
默认情况下glStencilOp
是设置为(GL_KEEP, GL_KEEP, GL_KEEP)的,所以不论任何测试的结果是如何,模板缓冲都会保留它的值。
默认的行为不会更新模板缓冲,所以写入模板缓需要至少对其中一个选项设置不同的值。
面剔除
在渲染中,理论上我们只需要渲染要看到的物体的正面,而不需要看不到的物体背面。
面剔除的关键在于确定一个三角形的“正面”(Front Face)和“背面”(Back Face),可以由环绕顺序(Winding Order)判断。
这通常是基于三角形顶点在空间中的排列顺序(顺时针CW或逆时针CCW)来判断。
OpenGL默认认为逆时针排列的三角形为正面。
启用面剔除,只需要启用OpenGL的GL_CULL_FACE选项;
OpenGL允许改变需要剔除的面的类型。可以调用glCullFace
来定义剔除的面的方向;
同时也可以调用glFrontFace
修改正面的定义。
OpenGL 顺时针顺序代表正向面的示例:
1 |
|
运行渲染程序,只有背向面被渲染。
至此,读者已经明白模板测试,深度测试和面剔除的相关知识了。