第四章 高级OpenGL
4.1 深度测试
4.1.1 深度缓冲
1、深度缓冲用来防止被阻挡的面渲染到其它面的前面,由窗口系统自动创建,在每个片段中储存了它的深度值。当启用深度测试时,OpenGL会将一个片段的深度值与深度缓冲的深度值进行对比。如果通过了深度测试,则在深度缓冲中储存该片段的z值;如果没有通过,则会丢弃该片段。
2、深度缓冲创建于片段着色器运行之后,在屏幕空间中运行,与glViewport
所定义的视口密切相关。屏幕空间坐标可使用GLSL内建变量gl_FragCoord
从片段着色器中直接访问。gl_FragCoord
的x和y分量代表了片段的屏幕空间坐标(原点位于左下角),z分量为片段深度值。
3、现在的GPU都有提前深度测试,允许深度测试在片段着色器之前运行,这就要求片段着色器不能写入片段的深度值,否则无法进行提前深度测试。
glEnable(GL_DEPTH_TEST);//启用深度测试
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);//每次渲染前清除深度缓冲
4、某些情况下需要对所有片段都执行深度测试并丢弃相应的片段,但不希望更新深度缓冲。因此需要一个只读的深度缓冲,将它的深度掩码设置为GL_FALSE
就可以了。
glDepthMask(GL_FALSE);
4.1.2 深度测试函数
1、OpenGL允许修改深度测试中使用的比较运算符,控制什么时候该丢弃一个片段,什么时候去更新深度缓冲,调用glDepthFunc
函数来设置比较运算符:
glDepthFunc(GL_LESS);
2、比较运算符
GL_ALWAYS
永远通过深度测试
GL_NEVER
永远不通过深度测试
GL_LESS
在片段深度值小于缓冲的深度值时通过测试
GL_EQUAL
在片段深度值等于缓冲区的深度值时通过测试
GL_LEQUAL
在片段深度值小于等于缓冲区的深度值时通过测试
GL_GREATER
在片段深度值大于缓冲区的深度值时通过测试
GL_NOTEQUAL
在片段深度值不等于缓冲区的深度值时通过测试
GL_GEQUAL
在片段深度值大于等于缓冲区的深度值时通过测试
3、使用GL_ALWAYS
模拟没有启用深度测试时所得到的结果,最后绘制的片段将会总是会渲染在之前绘制片段的上面。
4.1.3 深度值精度
1、深度缓冲的深度值介于[0, 1]范围,而观察空间的z值可能是投影平截头体的近平面和远平面之间的任何值,需要一种方式将观察空间的z值变换到[0, 1]范围之间,其中的一种方式就是线性变换。
2、在线性方程中,1000单位远的物体和1单位远的充满细节的物体使用了相同的精度,这显然是不合理的。因此,使用与 1/z 成正比的非线性深度方程,在1.0和2.0之间的z值将会变换至1.0到0.5之间的深度值,占50%的float精度。在50.0和100.0之间的z值将会只占2%的float精度。
3、所以深度缓冲中的值在屏幕空间中不是线性的,深度缓冲为0.5的顶点的z值实际上非常接近近平面。这个变换方程是嵌入在投影矩阵中的,因此在透视矩阵应用之前在观察空间中是线性的。投影矩阵
4.1.4 深度缓冲可视化
1、将片段着色器内建向量gl_FragCoord
的z值输出为颜色:
void main()
{
FragColor = vec4(vec3(gl_FragCoord.z), 1.0);
}
2、能注意到几乎所有东西都是白色的,看起来所有的深度缓冲都是1.0。因为屏幕空间的深度缓冲是非线性的,片段的颜色值变化也是非线性的,会随着距离增大而迅速增加。所以几乎所有的顶点的深度值都是接近于1.0的。如果我们小心地靠近物体,颜色会渐渐变暗,显示它们的z值在逐渐变小:
4.1.5 深度冲突
1、当两个平面或者三角形非常紧密地平行排列在一起时,深度缓冲没有足够的精度来决定两个形状哪个在前面,结果就是这两个形状不断地在切换前后顺序,这个现象叫做深度冲突。
2、防止深度冲突
- 第一个是不要把多个物体摆得太靠近,通过在两个物体之间设置一个用户无法注意到的偏移值,你可以完全避免这两个物体之间的深度冲突。
- 第二个技巧是尽可能将近平面设置远一些,将近平面远离观察者,整个平截头体有着更大的精度。然而,将近平面设置太远将会导致近处的物体被裁剪掉。
- 第三个是使用更高精度的深度缓冲。大部分深度缓冲的精度都是24位的,但现在大部分的显卡都支持32位的深度缓冲,这将会极大地提高精度。
4.2 模板测试
4.2.1 模板缓冲
1、当片段着色器处理完一个片段之后,先执行模板测试,丢弃某些片段后,被保留的片段进入深度测试。
2、模板测试根据模板缓冲进行,在模板缓冲中,每个模板值是8位的,所以每个片段有256种的模板值。将模板值设置为特定值,然后当某个片段有某个模板值的时候,就可以选择丢弃或是保留这个片段了。
3、例如,在模板缓冲中使用1填充了一个空心矩形,场景中的片段将会只在片段的模板值为1的时候会被渲染(其它的都被丢弃了)。
4、使用模板缓冲的大体步骤如下:
- 启用模板缓冲的写入;
- 渲染物体,更新模板缓冲的内容,将模板缓冲设为特定值;
- 禁用模板缓冲的写入;
- 在接下来渲染其它物体时,根据模板缓冲的内容丢弃特定的片段。
//启用模板测试,清除模板缓冲
glEnable(GL_STENCIL_TEST);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
5、利用glStencilMask
函数来启用/禁用模板写入,该函数设置了一个位掩码,与将要写入缓冲的模板值进行AND运算。当位掩码所有位都为1,即开启模板缓冲写入;当位掩码所有位为0x00,写入缓冲的所有模板值最后都会变成0,即禁用模板缓冲写入。
glStencilMask(0xFF); // 每一位写入模板缓冲时都保持原样
glStencilMask(0x00); // 每一位在写入模板缓冲时都会变成0(禁用写入)
4.2.2 模板函数
1、glStencilFunc
计算模板测试应该通过还是失败,glStencilOp
说明如何更新模板缓冲。
2、glStencilFunc(GLenum func, GLint ref, GLuint mask)
一共包含三个参数:
- func:设置模板测试函数,应用到模板缓冲的模板值和
ref
值上。可用的选项有:GL_NEVER
、GL_LESS
、GL_LEQUAL
、GL_GREATER
、GL_GEQUAL
、GL_EQUAL
、GL_NOTEQUAL
和GL_ALWAYS
。 ref
:设置模板测试的参考值,模板缓冲的模板值将会与这个值进行比较。mask
:设置一个掩码,它将会与参考值和储存的模板值在测试之前进行与(AND)运算。初始情况下所有位都为1。
//只要一个片段的模板值等于参考值1,片段将会通过测试并被绘制,否则会被丢弃。
glStencilFunc(GL_EQUAL, 1, 0xFF)
3、glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)
一共包含三个选项:
sfail
:模板测试失败时采取的行为。dpfail
:模板测试通过,但深度测试失败时采取的行为。dppass
:模板测试和深度测试都通过时采取的行为。
默认情况下是设置为(GL_KEEP, GL_KEEP, GL_KEEP)
,不论任何测试的结果如何,模板缓冲都会保留它的值。如果你想写入模板缓冲的话,至少对其中一个选项设置不同的值。
4.2.3 物体轮廓
1、使用模板测试可以完成物体轮廓的绘制,轮廓的步骤如下:
- 在绘制物体之前,将模板函数设置为
GL_ALWAYS
,每当物体的片段被渲染时,将模板缓冲更新为1; - 渲染物体;
- 禁用模板写入以及深度测试;
- 将每个物体放大一点点;
- 使用一个不同的片段着色器,输出一个单独的(边框)颜色;
- 再次绘制放大版本的物体,但只在它们片段的模板值不等于1时才绘制,也就是物体的边框的位置。使用模板缓冲丢弃了放大版本中属于原物体片段的部分。
- 再次启用模板写入和深度测试。
2、片段着色器,输出一个边框颜色
void main()
{
FragColor = vec4(0.04, 0.28, 0.26, 1.0);
}
3、启用模板测试,并设置测试通过或失败时的行为。如果其中的一个测试失败了,仅仅保留当前储存在模板缓冲中的值。如果都通过了,将储存的模板值设置为参考值。
glEnable(GL_STENCIL_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
4、将模板缓冲清除为0,绘制两个箱子,对箱子中所有绘制的片段,将模板值更新为1。通过GL_ALWAYS
模板测试函数,箱子的每个片段都会将模板缓冲的模板值更新为1。因为片段永远会通过模板测试,在绘制片段的地方,模板缓冲会被更新为参考值。
glStencilFunc(GL_ALWAYS, 1, 0xFF); // 所有的片段都应该更新模板缓冲
glStencilMask(0xFF); // 启用模板缓冲写入
normalShader.use();
DrawTwoContainers();
5、绘制放大的箱子,禁用模板缓冲的写入,将模板函数设置为GL_NOTEQUAL
,保证只绘制箱子上模板值不为1的部分,即只绘制箱子在之前绘制的箱子之外的部分。注意:也禁用了深度测试,让放大的箱子,即边框,不会被地板所覆盖。完成之后重新启用深度缓冲。
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00); // 禁止模板缓冲的写入
glDisable(GL_DEPTH_TEST);
shaderSingleColor.use();
DrawTwoScaledUpContainers