前言
现实中物体是3D的,当一个物体遮挡住另一个物体时,后面的物体我们说看不见的。同样,在绘制3D场景时,应该达到同样的效果,这样看起来才更加真实。当然,前面的物体应该是不透明时,才会完全遮挡,如果前面物体有一定透明度,就涉及到了“混合”问题。本文我们只讨论物体不透明的情况。
既然物体间有遮挡的问题,那么该如何处理呢?也就是显示出前面的物体,消除后面的物体,准确讲是消除隐藏面——Hidden surface elimination,即3D物体的三角面的处理问题,近处的三角面片会遮住远处的三角面片。
最简单的做法是「画家算法」(Painter’s algorithm)。该算法较为简单,即按顺序画物体,先画远处的物体,再画近处的物体,这样近处的物体就会遮挡住远处的物体,符合实际,像下面这样(来自Z buffer 和 W buffer 簡介)。
但是有时会出现很难判断的情况,像下面的这样,三个三角形之间有重叠,那么该如何判断呢?
深度缓冲——Z-Buffer
Opengl中我们常用的做法是使用 Z-Buffer,深度缓冲区。深度缓冲和颜色缓冲类似,颜色缓冲区存储的是片段的颜色信息,而深度缓冲区存储的是深度信息。深度缓冲有系统窗口创建并将其深度值存储为16、24或32位的浮点数。一般使用的是24位深度缓冲区。
由于Opengl 是状态机,默认情况下深度测试是关闭的,可以通过GL_DEPTH_TEST 选项来打开:
glEnable(GL_DEPTH_TEST);
开启深度测试后,片段的深度信息z值将存储在深度缓冲区中,测试时,将当前的片段的深度值与缓冲区的 z 值进行比较,大于缓冲区的 z 值时,将丢弃该片段;否则保留该片段并更新深度缓冲区的深度值。启用深度测试时还应该使用GL_DEPTH_BUFFER_BIT清除深度缓冲区,否则将保留上一次进行深度测试时写入的深度信值。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
深度测试函数
Opengl允许我们修改深度测试的规则,所以提供了一个函数供我们调用,通过修改参数来改变比较的规则。(深度测试函数(depth function)):
glDepthFunc(GL_LESS);
该函数是默认的函数,意思是在片段深度值小于深度缓冲区使测试通过,下列列出了几个比较运算符:
运算符 | 说明 |
---|---|
GL_ALWAYS | 总是通过测试 |
GL_NEVER | 总是不通过测试 |
GL_LESS | 片段值 < 缓冲区深度通过测试 |
GL_EQUAL | 片段值 = 缓冲区深度通过测试 |
GL_LEQUAL | 片段值 <= 缓冲区深度通过测试 |
GL_GREATER | 片段值 > 缓冲区深度通过测试 |
GL_NOTEQUAL | 片段值 != 缓冲区深度通过测试 |
GL_GEQUAL | 片段值 >= 缓冲区深度通过测试 |
测试实例
代码中我们将深度函数设为GL_ALWAYS:
glDepthFunc(GL_ALWAYS);
使用该函数相当于默认情况下的没有开启深度测试的效果相同。场景中先绘制两个 Cube,后绘制 Plane,则效果如下,Plane遮住了Cube的一部分。
设置为 GL_LESS 或开启深度测试,效果如下:
glDepthFunc(GL_LESS);
注意:测试时如果使用glEnable(GL_CULL_FACE),可能出现问题,如下图,原因是面剔除,本文不讨论该问题,简单说就是绘制三角形时是有顺序的,理解为逆时针绘制的面保留,否则剔除,所以有的面没有绘制出来是顶点的绘制顺序导致的,所以可以先去掉该函数。
深度值精度
这部分讨论一下深度值精度,里面会有一些数学的东西,不感兴趣的话可以大概了解或简单记下结论,当真正用到时再去细究。
深度缓冲区的z值在 0.0 和 1.0 之间,可以观看到的场景均在近平面和远平面之间,所以z值也在 far 和 near 之间,下面的公式可以将深度值转换到[0.0,1.0]:
这样就得到线性的深度值。
实际中,远处的物体不用很好分辨出远近,而近处的物体则需要很好分辨远近,即远处和近处得深度值应该有不同的精度,所以采用了 1/z 来计算深度信息,则距离近平面较近的拥有很高的精度,距离越远精度值越低,下面是公式:
下面曲线是z值和深度缓冲中的值的非线性关系,近平面在1.0处,远平面在50.0处,可以看出在达到10.0位置时就已经达到了90%的深度信息,也就是近处深度精度高,远处深度精度低。
深度可视化
深度值可以通过gl_FragCoord向量的z值获取到,将该值作为颜色输出,看看深度值的变化
void main()
{
color = vec4(vec3(gl_FragCoord.z), 1.0f);
}
开始运行可能都是白色画面,输出的颜色为1.0,远处的颜色精度低,看起来都是白色的,此时通过将相机移动近一点观察,当逐渐靠近物体时,颜色越来越暗,z值减小,应该知道深度的非线性特点,近处时,稍微移动一点,颜色就会明显由暗变亮或由亮变暗。
深度线性可视化
我们将深度值从范围 [0,1] 中转换为NDC范围内 [-1,1] 然后通过逆变换来得到一个线性的深度值,具体推导过程可以看看投影矩阵
#version 330 core
out vec4 color;
in vec2 TexCoords;
uniform sampler2D text;
float linearDepth(float depth)
{
float near = 0.1f;
float far = 100.0f;
float z = depth *2.0f -1.0f;
return (2.0f * near) / (far + near - z *(far - near));
}
void main()
{
float depth = linearDepth(gl_FragCoord.z);
color = vec4(vec3(depth),1.0f);
}
我们距离物体越来越近,颜色也越来越暗,变化的速度和之前的非线性可以感受到区别。
深度冲突
深度冲突一般出现在深度信息难以比较出来的时候,当距离越远时越容易出现,因为距离较远时精度较低,这时候无法区分开哪些三角形在前,哪些在后,就会导致两个物体的面相互交替出现,玩过CS应该见过这种情形。
移动相机位置到Cube的里面,发现Cube的底面和Plane平面交替出现,这种现象就是深度冲突。
解决深度冲突的几种方法:
1. 让物体之间不要距离过小,因为距离过小就可能导致深度值无法比较出大小。一个小技巧就是即使要求两个物体有相同在同一位置,也可以将其中一个物体的位置稍微一动一点,移动的距离人眼无法分辨但是对于深度测试却很有效。
2. 尽可能把近平面设置得远一些,越靠近近平面,精度越高,所以使近平面远离观察者,可以在椎体内很有效的提高精度,下面的是计算深度值的公式,通过增大n(近平面),使得精度提高。然而把近平面移动的太远会导致近处的物体被裁剪掉。所以需要不断调整测试近平面的值,找到合适的近平面值。
- 使用更高位数的深度缓冲区,通常使用的深度缓冲区是24位的,有一些硬件使用32位的缓冲区,可以使精确度得到提高。
参考资料
1.Learn Opengl
2.Z buffer 和 W buffer 簡介
3.The Industry’s Foundation for High Performance Graphics