LearnOpenGL学习(高级OpenGL -- 深度测试,模板测试,)
完整代码见:zaizai77/Cherno-OpenGL: OpenGL 小白学习之路
深度测试
深度缓冲用来防止被阻挡的面渲染到其他面的前面,深度缓冲就像颜色缓冲,在每个片段中储存了信息,
当深度测试(Depth Testing)被启用的时候,OpenGL会将一个片段的深度值与深度缓冲的内容进行对比。OpenGL会执行一个深度测试,如果这个测试通过了的话,深度缓冲将会更新为新的深度值。如果深度测试失败了,片段将会被丢弃。
深度缓冲运行在片段着色器之后(以及模板测试之后)
gl_TexCoord 是GLSL内建变量,它是一个vec3,x和y分量代表了片段的屏幕坐标(左下角为原点),z分量为片段的深度值。
提前深度测试(Early Depth Testing, Early-Z)允许深度测试在Fragment着色器之前运行。只要判断该片段在其他物体之后,便会将他提前剔除。
使用Early-Z的条件是,Fragment Shader里不能有写入深度值的操作。
深度测试一般是禁用的,需要开启
gl_Enable(GL_DEPTH_TEST)
在渲染迭代之前清除深度缓冲。否则会使用上一次渲染迭代中的写入的深度值
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
禁用深度缓冲的写入,但是可以进行深度测试:
glDepthMask(GL_FALSE);
深度测试函数
OpenGL允许我们修改深度测试中使用的比较运算符。这允许我们来控制OpenGL什么时候该通过或丢弃一个片段
glDepthFunc(GL_LESS); //默认情况
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_ALWAYS);
想象一个地板上放着一个方块,如果我们先绘制方块之后绘制地板,那么地板就会覆盖方块
重新设置为 GL_LESS 之后
深度值精度
深度缓冲包含了一个介于0.0和1.0之间的深度值,它将会与观察者视角所看见的场景中所有物体的z值进行比较。观察空间的z值可能是投影平截头体的近平面(Near)和远平面(Far)之间的任何值。我们需要一种方式来将这些观察空间的z值变换到[0, 1]范围之间
这种线性深度缓冲实践中不会使用,一般会使用下面这种,更符合现实
深度值很大一部分是由很小的z值所决定的,这给了近处的物体很大的深度精度
非线性深度值转换方程被嵌入到了投影矩阵中,在观察空间->裁剪空间的转换过程中被应用。
这意味着,我们使用gl_FragCoord.z得到的值就是非线性深度值。
可以将非线性的深度值转换为线性的,它使用投影矩阵推到得到的,其中 z 是 NDC 坐标下的 z 值
float z = depth * 2.0 - 1.0;
float linearDepth = (2.0 * near * far) / (far + near - z * (far - near));
深度冲突
两个平面或者三角形非常紧密的排列在一起时,由于深度缓冲没有足够的精度来决定两个形状哪个在前面,就会发生 深度冲突,看起来像两个形状在争夺谁在前面
深度冲突是深度缓冲的一个常见问题,当物体在远处时效果会更明显(因为深度缓冲在z值比较大的时候有着更小的精度)。深度冲突不能够被完全避免
防止深度冲突
- 最重要的技巧是永远不要把多个物体摆得太靠近,以至于它们的一些三角形会重叠。
- 尽可能将近平面设置远一些。在前面我们提到了精度在靠近近平面时是非常高的,所以如果我们将近平面远离观察者,我们将会对整个平截头体有着更大的精度。
- 牺牲一些性能,使用更高精度的深度缓冲。
模板测试
当片段着色器处理完一个片段之后,模板测试(Stencil Test)会开始执行,和深度测试一样,它也可能会丢弃片段
模板测试使用模板缓冲,模板缓冲类似于一个遮罩。一个模板缓冲中,(通常)每个模板值(Stencil Value)是8位的。所以每个像素/片段一共能有256种不同的模板值。我们可以将这些模板值设置为我们想要的值,然后当某一个片段有某一个模板值的时候,我们就可以选择丢弃或是保留这个片段了。
模板缓冲操作允许我们在渲染片段时将模板缓冲设定为一个特定的值。通过在渲染时修改模板缓冲的内容,我们写入了模板缓冲。在同一个(或者接下来的)渲染迭代中,我们可以读取这些值,来决定丢弃还是保留某个片段。
- 启用模板缓冲的写入。
- 渲染物体,更新模板缓冲的内容。
- 禁用模板缓冲的写入。
- 渲染(其它)物体,这次根据模板缓冲的内容丢弃特定的片段。
所以,通过使用模板缓冲,我们可以根据场景中已绘制的其它物体的片段,来决定是否丢弃特定的片段。
glEnable(GL_STENCIL_TEST); // 开始模板测试
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); //清除模板缓存
glStencilMask(0xFF); // 每一位写入模板缓冲时都保持原样
glStencilMask(0x00); // 每一位在写入模板缓冲时都会变成0(禁用写入)
glStencilMask 允许我们设置一个位掩码,它会将要写入缓冲的模板值进行与运算,默认情况下位掩码的所有位是1,不影响写入
模板函数
glStencilFunc(GLenum func,Glint ref,Gluint mask) // 描述了OpenGL应该对模板缓冲内容做什么
- 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(Glenum sfail,Glenum dpfail,GLenum dppass) //我们应该如何更新缓冲
- sfail: 模板测试失败时采取的行为。
- dpfail: 模板测试通过,但深度测试失败时采取的行为。
- dppass: 模板测试和深度测试都通过时采取的行为。
每个选项都可以选用以下的其中一种行为:
默认情况下 glStencilOp 是设置为 (GL_KEEP,GL_KEEP,GL_KEEP) 的,所以不论任何测试的结果如何,模板缓冲都会保留它的值,默认的行为不会更新模板缓冲。
描边
glEnable(GL_DEPTH_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); glStencilMask(0x00); // 记得保证我们在绘制地板的时候不会更新模板缓冲
normalShader.use();
DrawFloor() glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);
DrawTwoContainers();glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00);
glDisable(GL_DEPTH_TEST);
shaderSingleColor.use();
DrawTwoScaledUpContainers();
glStencilMask(0xFF);
glEnable(GL_DEPTH_TEST);
为物体创建轮廓的步骤如下:
- 在绘制(需要添加轮廓的)物体之前,将模板函数设置为GL_ALWAYS,每当物体的片段被渲染时,将模板缓冲更新为1。
- 渲染物体。
- 禁用模板写入以及深度测试。
- 将每个物体缩放一点点。
- 使用一个不同的片段着色器,输出一个单独的(边框)颜色。
- 再次绘制物体,但只在它们片段的模板值不等于1时才绘制。
- 再次启用模板写入和深度测试。
混合
OpenGL中,混合(Blending)通常是实现物体透明度(Transparency)的一种技术。
透明的物体可以是完全透明的(让所有的颜色穿过),或者是半透明的(它让颜色通过,同时也会显示自身的颜色)。
一些材质会有一个内嵌的alpha通道,对每个纹素(Texel)都包含了一个alpha值。这个alpha值精确地告诉我们纹理各个部分的透明度。比如说,下面这个窗户纹理中的玻璃部分的alpha值为0.25(它在一般情况下是完全的红色,但由于它有75%的透明度,能让很大一部分的网站背景颜色穿过,让它看起来不那么红了),角落的alpha值是0.0。
丢弃片段
有些图片并不需要半透明,只需要根据纹理颜色值,显示一部分,或者不显示一部分,没有中间情况。比如这个草图片,只需要绘制不透明的就可以
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); //告诉我们的电脑,我们现在使用Alpha通道
void main() {
// FragColor = vec4(vec3(texture(texture1, TexCoords)), 1.0);
FragColor = texture(texture1, TexCoords);
} //片段着色器得到四个颜色分量,而不是RGB
#version 330 core
out vec4 FragColor;in vec2 TexCoords;uniform sampler2D texture1;void main()
{ vec4 texColor = texture(texture1, TexCoords);if(texColor.a < 0.1)discard;FragColor = texColor;
}
discard 命令,一旦被调用,它就会保证片段不会被进一步处理,所以就不会进入颜色缓冲。
混合
glEnable(GL_BLEND); //开启混合,绘制半透明图片
片段着色器运行完成后,并且所有的测试都通过之后,这个混合方程(Blend Equation)才会应用到片段颜色输出与当前颜色缓冲中的值(当前片段之前储存的之前片段的颜色)上。
glBlendFunc(GLenum sfactor, GLenum dfactor)函数接受两个参数,来设置源和目标因子。
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
//使用glBlendFuncSeparate为RGB和alpha通道分别设置不同的选项:
glBlendEquation(GLenum mode)允许我们设置运算符,它提供了三个选项:
渲染半透明纹理
首先,在初始化时我们启用混合,并设定相应的混合函数:
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
由于启用了混合,我们就不需要丢弃片段了,所以我们把片段着色器还原:
#version 330 core
out vec4 FragColor;in vec2 TexCoords;uniform sampler2D texture1;void main()
{ FragColor = texture(texture1, TexCoords);
}
现在(每当OpenGL渲染了一个片段时)它都会将当前片段的颜色和当前颜色缓冲中的片段颜色根据alpha值来进行混合。由于窗户纹理的玻璃部分是半透明的,我们应该能通窗户中看到背后的场景了。
前面的窗户遮挡了后面的窗户,为什么?
发生这一现象的原因是,深度测试和混合一起使用的话会产生一些麻烦。当写入深度缓冲时,深度缓冲不会检查片段是否是透明的,所以透明的部分会和其它值一样写入到深度缓冲中。结果就是窗户的整个四边形不论透明度都会进行深度测试。即使透明的部分应该显示背后的窗户,深度测试仍然丢弃了它们。
所以我们不能随意地决定如何渲染窗户,让深度缓冲解决所有的问题了。这也是混合变得有些麻烦的部分。要想保证窗户中能够显示它们背后的窗户,我们需要首先绘制背后的这部分窗户。这也就是说在绘制的时候,我们必须先手动将窗户按照最远到最近来排序,再按照顺序渲染。
不要打乱顺序
参考:深度测试 - LearnOpenGL CN
混合 - LearnOpenGL CN