LearnOpenGL学习笔记(八) - 测试与混合
本文最后更新于 2024年8月5日 上午
重新开始。
深度测试
深度缓冲(Depth Buffer,or Z-Buffer)用于放置被阻挡的面被渲染到其他面的前面。
在每个Fragment中都存储有Depth Buffer信息,它由程序自动创建,一般情况下是24位的float。
当深度测试(Depth Test)被启用时,OpenGL会把Fragment的深度值和深度缓冲内容进行对比,这个过程被称为深度测试。测试通过时,深度缓冲就会更新为这个片段的深度值,否则这个片段会被剔除。
深度缓冲运行在模板测试后,作用于屏幕空间。
gl_FragCoord
是GLSL内建变量,它是一个vec3,x和y分量代表了片段的屏幕坐标(左下角为原点),z分量为片段的深度值。提前深度测试(Early Depth Testing, Early-Z)允许深度测试在Fragment着色器之前运行。只要判断该片段在其他物体之后,便会将他提前剔除。
使用Early-Z的条件是,Fragment Shader里不能有写入深度值的操作。
使用glEnable(GL_DEPTH_TEST)
开启深度测试。
开启深度测试后,在每个渲染迭代开始之前还应当使用glClear(GL_DEPTH_BUFFER_BIT)
清除深度缓冲。
使用glDepthMask(GL_FALSE)
禁用深度缓冲写入,深度缓冲将不会更新,作为只读属性。
深度测试函数
glDepthFunc
函数用于控制OpenGL什么时候通过、丢弃片段,以及什么时候更新深度缓冲。它接收一个比较符。
函数 | 描述 |
---|---|
GL_ALWAYS | 永远通过深度测试 |
GL_NEVER | 永远不通过深度测试 |
GL_LESS | 在片段深度值小于缓冲的深度值时通过测试 |
GL_EQUAL | 在片段深度值等于缓冲区的深度值时通过测试 |
GL_LEQUAL | 在片段深度值小于等于缓冲区的深度值时通过测试 |
GL_GREATER | 在片段深度值大于缓冲区的深度值时通过测试 |
GL_NOTEQUAL | 在片段深度值不等于缓冲区的深度值时通过测试 |
GL_GEQUAL | 在片段深度值大于等于缓冲区的深度值时通过测试 |
默认比较符为GL_LESS
我该如何理解深度缓冲值的更新?
以
GL_LESS
为例:当视口中还未渲染任何东西时,深度缓冲值是无穷大。当我们渲染了两个方块时,这两个方块的z值必然比无穷大要小,它们通过了深度测试,两个方块占据的片段位置的深度缓冲被更新为新的深度值。
当渲染位于两个方块底下的地板时,对于没被方块遮挡的部分的片段深度值,它们的深度缓冲值依然是无穷大,所以它们能通过深度测试,正常显示。而被方块遮挡的部分,很明显深度值要大于方块的深度值,所以未通过测试,被剔除。
深度值精度
只要一个float表示的是深度,那么它的范围必定是[0.0, 1.0]。深度缓冲存储的float的范围都是如此。
但我们知道,片段的z值可不是这样。为了把z值转换为深度值,我们使用方程:$$\begin{equation} F_{depth} = \frac{z - near}{far - near} \end{equation}$$
其中,near
和far
是平截头体的近平面z值和远平面z值。
上面的公式被称为线性深度缓冲(Linear Depth Buffer)。这种方法实际上不是很好,因为对于透视投影的观察者来说,极远处物体的z轴变化是很难观察到的,而近处物体z轴很微小的变化都会很明显。为了体现这点,我们引入非线性深度缓冲方程:\begin{equation} F_{depth} = \frac{1/z - 1/near}{1/far - 1/near} \end{equation}
对于这个方程,z值和最终深度的变化如下图:
可以看到,深度值的很大一部分都是由很小的z值决定的。
非线性深度值转换方程被嵌入到了投影矩阵中,在观察空间->裁剪空间的转换过程中被应用。
这意味着,我们使用gl_FragCoord.z得到的值就是非线性深度值。
上述方程用于把非线性深度值转换为线性。它是使用投影矩阵推导得出的。其中Zndc
是NDC坐标下的z值,由原深度值*2-1变换得到。在shader中可以这么写:
1 |
|
深度冲突
深度冲突(Z-fight)指两个片段的深度值非常接近,以至于深度缓冲没有足够的精度来决定该显示哪个片段的情况。深度冲突发生时,可以看到锯齿状的贴图闪烁。
一般我们采用下列方法防止深度冲突:
- 不要把两个物体摆的太近。
- 提高近平面的值,从而让整个平截头体的深度缓冲精度提高。代价是近处物体可能会被剔除。
可以这么理解:让非线性转换中,z-深度曲线曲率最大的部分向后移动,从而让z值稍大的部分也能以高精度进行深度测试。
- 使用高精度深度缓冲。
模板测试
模板测试(Stencil Test)紧接着Fragment Shader处理完一个片段后执行。
模板缓冲类似于一个遮罩。当片元的模板缓冲值为1时,通过测试,否则剔除。
与深度缓冲类似,模板缓冲通过glEnable(GL_STENCIL_TEST)
开启,每次渲染循环通过glClear(GL_STENCIL_BUFFER_BIT)
清除上帧缓存,通过glStencilMask
设置位掩码。
glStencilFunc(GLenum func, GLint ref, GLuint mask)
用于告诉程序如何进行模板测试。
func
:设置模板缓冲函数。可用的选项有:GL_NEVER、GL_LESS、GL_LEQUAL、GL_GREATER、GL_GEQUAL、GL_EQUAL、GL_NOTEQUAL和GL_ALWAYS。
ref
:参考值,之后的模板缓冲将与此值比较。
mask
:掩码,一般都是0xFF。以
glStencilFunc(GL_EQUAL, 1, 0xFF)
为例,这个语句代表:只要模板值等于1,就通过模板测试。
glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)
用于告诉程序如何更新模板缓冲值。
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 | 按位翻转当前的模板缓冲值 |
描边
1 |
|
我该如何理解这段代码?
我们使用模板测试来实现描边的效果。
这里用的方法是:在原物体的位置,复制一个物体,将其略微放大一些。这个物体应用的Shader应当是不受光照影响的纯色Shader。
描边就是要让原物体好好地显示出来,而原物体所占据片元以外的地方,允许大物体显示。
首先,我们定义模板测试失败和成功后的结果:glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); 当模板测试通过时,替换片元模板值为ref值。否则保留原本的模板值。
我们需要先绘制不需要描边的物体。需要注意,绘制这些物体时,应当禁用模板缓冲(可以直接glDisable,也可以glStencilMask(0x00))。
当绘制到原物体时,开启模板缓冲,让原物体占据片元区域的模板值变为1。
glStencilFunc(GL_ALWAYS,1,0XFF)
执行后,对于新渲染的片段,模板测试始终通过。
glStencilMask(0xFF)
开启模板值写入。然后绘制原物体。
原物体绘制完毕后,片元的模板值更新完毕。改变模板测试规则:
glStencilFunc(GL_NOTEQUAL,1,0XFF)
,使得片段所处位置的模板值只有不为1时,才通过。然后绘制大物体。因为原物体片元区域的模板值都是1,所以大物体的模板测试不会通过,这些片元不会被渲染。这就达到了描边的效果。
为什么要禁用深度测试呢?因为描边区域通常不可被障碍遮挡。如果有这个需求,也可以不禁用。
混合
png图片是四通道的,第四通道的值代表透明度(alpha)。
通过在Fragment Shader中对采样的alpha值进行判断并剔除(discard),可以实现“透明的地方不渲染”的效果:
1 |
|
采用这种方法实现透明显示时,需要把纹理环绕方式设置为
GL_CLAMP_TO_EDGE
,否则当实际渲染物体的大小超过纹理大小时,底部uv会重复到顶部,导致物体的重复渲染。采用discard方案的缺点是,无法实现半透明物体的渲染。同时,使用discard以后Early-Z将失效。
为实现半透明物体的渲染,我们引入Blend技术。
glEnable(GL_BLEND)
Blend借助alpha值实现”物体本身“和”后方物体“颜色的混合。让我们举一个具体例子:
我们把绿色半透明Quad放在红色不透明Quad前面。绿色Quad的alpha值是0.6,那么当二者叠加时,叠加区域的最终颜色中,绿色Quad对颜色的贡献值就是60%,红色则是(1-60%)=40%。最终颜色就是:
\begin{equation}\bar{C}_{result} = \begin{pmatrix} \color{red}{0.0} \\ \color{green}{1.0} \\ \color{blue}{0.0} \\ \color{purple}{0.6} \end{pmatrix} * \color{green}{0.6} + \begin{pmatrix} \color{red}{1.0} \\ \color{green}{0.0} \\ \color{blue}{0.0} \\ \color{purple}{1.0} \end{pmatrix} * \color{red}{(1 - 0.6)} \end{equation}其中,0.6被称为源因子值,(1-0.6)被称为目标因子值
glBlendFunc(GLenum sfactor, GLenum dfactor)
用于设置源因子和目标因子值。
选项 | 值 |
---|---|
GL_ZERO |
因子等于0 |
GL_ONE |
因子等于1 |
GL_SRC_COLOR |
因子等于源颜色向量C¯source |
GL_ONE_MINUS_SRC_COLOR |
因子等于1−C¯source1 |
GL_DST_COLOR |
因子等于目标颜色向量C¯destination |
GL_ONE_MINUS_DST_COLOR |
因子等于1−C¯destination |
GL_SRC_ALPHA |
因子等于C¯source的alpha分量 |
GL_ONE_MINUS_SRC_ALPHA |
因子等于1−C¯source的alpha分量 |
GL_DST_ALPHA |
因子等于C¯destination的alpha分量 |
GL_ONE_MINUS_DST_ALPHA |
因子等于1− C¯destination的alpha分量 |
GL_CONSTANT_COLOR |
因子等于常数颜色向量C¯constant |
GL_ONE_MINUS_CONSTANT_COLOR |
因子等于1−C¯constant1 |
GL_CONSTANT_ALPHA |
因子等于C¯constant的alpha分量 |
GL_ONE_MINUS_CONSTANT_ALPHA |
因子等于1− C¯constant的alpha分量 |
默认混合方式为glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
。C_constant使用glBlendColor
函数设置。
glBlendFuncSeparate
可以分别对RGBA通道使用不同的混合方式。
glBlendEquation(GLEnum mode)
可以改变混合的计算方式:
- GL_FUNC_ADD:默认选项,将两个分量相加:C¯result=Src+Dst
- GL_FUNC_SUBTRACT:将两个分量相减: C¯result=Src−Dst
- GL_FUNC_REVERSE_SUBTRACT:将两个分量相减,但顺序相反:C¯result=Dst−SrcC¯
混合与深度测试结合时,会出现问题。若一个物体深度值大于半透明物体,但在半透明物体后面渲染,深度测试不会管物体是不是半透明的,而是一刀切地把这个物体的片元全部丢弃了。
为了解决这一问题,我们必须把深度值大的物体放在渲染顺序的前面。
一般渲染顺序如下:
- 先绘制所有不透明的物体。(因为不透明物体无需混合,无所谓渲染顺序)
- 对所有透明的物体排序。
- 按顺序绘制所有透明的物体。
可以采用STL map自动排序的方式,管理所有透明物体:
1 |
|
但这种方法也只是简单地以物体的中心值作为位置顺序。当物体形状很复杂时,这种方法就不太好了,需要手动微调。一种较高级的解决这类问题的技术叫做次序无关透明度(Order Independent Transparency, OIT)。
面剔除
每个封闭形状的面都有正反之分。在OpenGL中,通过三角形片段三个顶点的绘制顺序判断该三角形所在面的正反。默认情况下,逆时针顶点所定义的三角形为正向三角形。
背向观察者的面通常不会被渲染。如果能够取消这些面的渲染,程序速度将会提高约50%。
glEnable(GL_CULL_FACE)
用于开启面剔除。
glCullFace(GLEnum mode)
用于指定剔除的面。
- GL_FRONT:剔除正面
- GL_BACK:剔除背面
- GL_FRONT_AND_BACK:正反面都剔除
glFrontFace(GLEnum mode)
用于指定正向三角形的定义。
- GL_CCW:逆时针三角形为正
- GL_CW:顺时针三角形为正。
使用面缓冲时,必须确保顶点数据的定义是”逆时针为正“的顺序。否则会出现渲染错误。