一、深度测试
在 OpenGL基础13:第一个正方体 这一章中,就开始用深度测试了
深度缓冲就像颜色缓冲(Color Buffer)(储存所有的片段颜色:视觉输出)一样,在每个片段中储存了信息,并且通常和颜色缓冲有着一样的宽度和高度,深度缓冲是由窗口系统自动创建的,它会以16、24或32位float的形式储存它的深度值,在大部分的系统中,深度缓冲的精度都是24位的
当深度测试(Depth Testing)被启用的时候,OpenGL会将一个片段的的深度值与深度缓冲的内容进行对比。OpenGL会执行一个深度测试,如果这个测试通过了的话,深度缓冲将会更新为新的深度值。如果深度测试失败了,片段将会被丢弃
除了前面的开启深度测试glEnable,还有一些深度测试相关的函数:
- glEnable(GL_DEPTH_TEST):开启深度测试
- glDepthMask(GL_FALSE):设置只读深度缓冲,开启后深度测试仍会丢弃不通过的片段,但不再更新深度缓冲
- glDepthFunc(GL_LESS):设置深度测试规则,相当于重载比较运算符
有以下几种规则(除了第三个,其它极少会用到):
- GL_ALWAYS:永远通过深度测试
- GL_NEVER:永远不通过深度测试
- GL_LESS:在片段深度值小于缓冲的深度值时通过测试(默认设置)
- GL_EQUAL:在片段深度值等于缓冲区的深度值时通过测试
- GL_LEQUAL:在片段深度值小于等于缓冲区的深度值时通过测试
- GL_GREATER:在片段深度值大于缓冲区的深度值时通过测试
- GL_NOTEQUAL:在片段深度值不等于缓冲区的深度值时通过测试
- GL_GEQUAL:在片段深度值大于等于缓冲区的深度值时通过测试
二、深度值精度
深度缓冲是在片段着色器运行、以及模板测试(Stencil Testing)之后、在屏幕空间中运行的,在最上面的前置章节《OpenGL基础11:空间》中,讲了NDC归一化标准坐标系,如果有点忘了的话可以看下面这张图:
这张图来源于:https://www.cnblogs.com/leixinyue/p/11166135.html,里面也有投影矩阵的计算方法
深度值精度可以从片段着色器的 gl_FragCoord 变量中获得
- GLSL内建变量 gl_FragCoord:片段着色器的只读输入变量vec4,其中x和y为窗口坐标(别忘了openGL默认以左下为原点),其中小数部分恒为(0.5, 0.5),这是因为原点并非(0, 0)而是(0.5, 0.5)的原因,整数部分就是数第几个像素点了,若viewport范围 为(0, 0, 2560, 1440)时,x, y 的取值范围就为(0.5, 0.5, 2559.5, 1439.5);z坐标为当前片元的深度信息,由顶点着色器处理过后系统插值得到,第4个分量为 1/w
这里需要考虑的,正是 gl_FragCoord.z,它就是实际深度值,在深度缓冲区中参与比较的值
考虑到 gl_FragCoord.z 的计算过程:
- 世界坐标系坐标乘以观察矩阵变换到观察空间:;
- 观察坐标系内的坐标通过乘上投影矩阵变换到裁剪空间:
- 裁剪坐标系内的坐标通过透视除法(w归一化)到NDC:;
- 设备规范化坐标系到窗口坐标系:
其中的 win.z 正是 gl_FragCoord.z, 和 为平头截体的远近平面(也就是能看到的最近点和最远点)
NDC坐标系中,坐标范围为[-1, 1],而在窗口中,z的范围就是[0, 1]
对应上图,根据 的运算,可以得到最后的完整公式为:
可以得出:gl_FragCoord.z 和物体离玩家的距离并非呈线性关系,在近裁剪面(near plane)附近它具有很高的精度,而在远裁剪面(far plane)附近具有非常小的精度,其中 gl_FragCoord.z 为 1 对应在平头截体的远平面,gl_FragCoord.z 为 0 对应在平头截体的近平面,但 gl_FragCoord.z 的值若为 0.5,那么它就在离平头截体的近平面一个很近的位置并非正中间
可以将片段着色器中的颜色改为 gl_FragCoord.z 来测试
color = vec4(vec3(gl_FragCoord.z), 1.0f);
图片中颜色从黑色到白色的渐变非常的明显,只要超过很小的一段距离,人眼就已经无法区分深度了(它们都几乎接近于1.0),这也非常的科学:想想我们真的需要让1000单位远的物体和只有1单位远的物体的深度值有相同的精度吗?
因为矩阵是可以进行逆变换的,所以我们有了 gl_FragCoord 后,可以在片段着色器中进行各种骚操作(这个后面有机会再讲吧),包括但不限于把深度值改回线性
一个能够将屏幕空间的非线性深度值转变为线性深度值的完整的片段着色器计算方法如下:
#version 330 core
out vec4 color;
float LinearizeDepth(float depth)
{
float near = 0.1;
float far = 100.0;
float z = depth * 2.0 - 1.0; //回到NDC空间
return (2.0 * near) / (far + near - z * (far - near)); //逆变换
}
void main()
{
float depth = LinearizeDepth(gl_FragCoord.z);
color = vec4(vec3(depth), 1.0f);
}
运行之后,就会发现物体一片黑暗,这是因为它们都离你非常近,当物体在一个极远的位置时(平头截体的远平面、你能看到的最远处),它就会是白色的
三、深度冲突
两个平面或三角形如此紧密相互平行深度缓冲区不具有足够的精度以至于无法得到哪一个靠前。结果是,这两个形状不断似乎切换顺序导致怪异出问题。这被称为深度冲突(Z-fighting)
深度冲突是个非常常见的问题,并且无法完全避免,当物体越远的时候可能越容易出现,在某些游戏中你也可以观察到这种现象,尽管看上去也不会太觉得算穿帮
如上图,如果红色箭头处箱子的底和地面是在同一个高度,又可以通过一定手段看到箱子的底面,那么就会出现深度冲突
有几种常见的理论解决方法:
- 两个贴近的物体,可以将它们稍微分开一点,只要肉眼看不到即可(最常见的解决方法)
- 设置完美的近裁剪面,因为上面说过,离近裁剪面近的地方精度会高,但这样可能造成离camera很近的物体被裁剪掉,往往需要大量经验才能找到适合的距离
- 接上,尽量缩短远近平面之间的距离
- 使用更高精度的depth buffer,当然这会降低性能