实践项目:https://github.com/JackieLong/OpenGL/tree/main/project_depth_test
深度缓冲和颜色缓冲(Color Buffer)一样,每个片段都存储了深度值,宽度和高度也一般相同。由窗口系统自动创建,精度有16bit、24bit、32bit的float形式,一般都是24bit。
发生在逐片段操作中的模板测试(Stencil Test)之后。
点击查看【processon】

工作原理

伪代码如下:

  1. frag_depth // 新生成的片元的Z值
  2. frag_index // 在深度缓冲中,对应片元的索引
  3. depthBuffer // 深度缓冲,存储了每个像素(片段)的z值
  4. if ( 开启了深度测试 )
  5. {
  6. if(depthFunc(frag_depth, frag_index, depthBuffer)) // 深度测试函数:执行深度测试
  7. {
  8. depthBuffer[frag_index] = frag_depth; // 深度测试通过,更新到深度缓冲中
  9. }
  10. else
  11. {
  12. discard; // 测试没有通过,丢弃该片段
  13. }
  14. }

内建变量

如何获取片段的深度值:

  1. #version 330 core
  2. void main()
  3. {
  4. vec3 screenPos = gl_FragCoord; // 内建变量,片段的屏幕空间坐标,左下角为坐标原点
  5. float depth = screenPos.z; // 片段的深度值,用于与深度缓冲的深度值比较。
  6. }

开启深度测试

  1. glEnable(GL_DEPTH_TEST); // 开启深度测试,默认是关闭的。
  2. glDepthMask(GL_FALSE); // 对所有片段都执行深度测试并丢弃相应的片段,但不希望更新深度缓冲。
  3. // 必须要开启深度测试才有效
  4. while(!glfwWindowShouldClose(window))
  5. {
  6. glClear(GL_DEPTH_BUFFER_BIT); // 在每帧开始前清除深度缓冲,否则会使用上一次的深度值。
  7. ......;
  8. }

深度测试函数

可以自定义深度测试逻辑。

  1. glDepthFunc(FUNC); // 默认是GL_LESS
  2. // FUNC的可能取值如下:(depthFrag为片段深度值,depthBuffer为深度缓冲区中的深度值)
  3. // GL_ALWAYS 永远通过深度测试,后绘制的一律覆盖之前绘制的。
  4. // GL_NEVER 永远不通过深度测试,一律保留最先绘制的。
  5. // GL_LESS depthFrag < depthBuffer,通过测试,即丢弃 >= depthBuffer的片段,默认值
  6. // GL_EQUAL depthFrag == depthBuffer,通过测试
  7. // GL_LEQUAL depthFrag <= depthBuffer,通过测试
  8. // GL_GREATER depthFrag > depthBuffer,通过测试
  9. // GL_NOTEQUAL depthFrag != depthBuffer,通过测试
  10. // GL_GEQUAL depthFrag >= depthBuffer,通过测试

Z值与深度值

这两者是完全不同的东西,OpenGL_深度测试(Depth Test) - 图1值是OpenGL_深度测试(Depth Test) - 图2坐标值,可以为任意的浮点数值,深度值则必须满足以下特性:

  1. depthBuffer; // 深度缓冲区中的深度值
  2. 0.0 <= depthBuffer <= 1.0; // 深度值的取值范围
  3. depthBuffer = 0.0; // 表示片段在平截头体的近平面上(Near Plane),即就在我们眼前。
  4. depthBuffer = 1.0; // 表示片段在平截头体的远平面上(Far Plane)
  5. // depthBuffer越小,越靠近我们,即“在越前面”

如何得到深度值?通过投影矩阵变换,将顶点坐标的OpenGL_深度测试(Depth Test) - 图3值转换成深度值。

  1. #version 330 core
  2. uniform vec3 pos; // 局部空间坐标
  3. uniform mat4 modelMatrix; // 模型变换矩阵:局部坐标 -> 世界坐标
  4. uniform mat4 viewMatrix; // 视图变换矩阵:世界坐标 -> 摄像机坐标
  5. uniform mat4 projectionMatrix; // 投影变换矩阵:摄像机坐标 -> 裁剪坐标
  6. void main()
  7. {
  8. // 裁剪空间坐标
  9. vec3 clipPos = vec3(projectionMatrix * viewMatrix * modelMatrix * vec4(pos, 1.0));
  10. // 裁剪坐标,特征如下:
  11. float w = clipPos.w; // w分量
  12. -w <= clipPos.x <= w;
  13. -w <= clipPos.y <= w;
  14. -w <= clipPos.z <= w;
  15. float depthFrag = clipPos.z / w; // 透视除法:得到了片段的深度值。
  16. ......
  17. }

深度值精度

具体是如何做到有如上深度值特征的呢?很自然地想到线性方程:
OpenGL_深度测试(Depth Test) - 图4
按这种方式实现的叫做线性深度缓冲(Linear Depth Buffer)。实际当中,很少采用,原因很简单,我们应该对越近的顶点采用越高的精度,较远的顶点采用较低的精度,具体啥意思?

  1. z_near = 0.0f; // 近平面z坐标值
  2. z_far = 100.0f; // 远平面z坐标值
  3. z_node; // 平截头体内部任意顶点的z坐标值。
  4. depth_node; // 该顶点的深度值
  5. // 深度值的范围是[0.0, 1.0]
  6. // 当采用线性深度缓冲时:
  7. // 当z_near < z_node < 50.0f时,depth_node分布在[0.0,0.5]区间。
  8. // 当50.0f < z_node < z_far时,depth_node分布在[0.5,1.0]区间。
  9. // 这两者的深度值分布区间相同,也就是深度值精度相同。
  10. // 按照实际,1000单位远的深度值和只有1单位远的充满细节的物体使用相同的精度?这大大浪费了分配给远处顶点的精度。
  11. // 我们应该多分配一点给近的顶点,越近的,分配的越多。

显然我们需要一个非线性方程,z值越小,增速越快,才能做到分配给近处顶点更多的精度:

OpenGL_深度测试(Depth Test) - 图5

OpenGL_深度测试(Depth Test) - 图6,函数值分布曲线如下:
OpenGL_深度测试(Depth Test) - 图7
OpenGL_深度测试(Depth Test) - 图8OpenGL_深度测试(Depth Test) - 图9
OpenGL_深度测试(Depth Test) - 图10

深度冲突(Z-fighting)

两个三角形平行排列在一起,由于深度缓冲精度限制,无法判断哪个三角形在前面,结果导致这两个三角形不断的切换前后顺序,出现很奇怪的花纹,这就是深度冲突。
防止出现深度冲突的方法:

  • 不用把两个物体摆太近,防止三角形重叠
    • 即使是真的靠在一起的物体,也可以人为偏离一点点,避免三角形重叠,在视觉上几乎无法发现。
  • 把近平面设置远一些
    • 靠近近平面,精度高一些。
  • 使用更高精度缓冲
    • 一般是24bit的float,可以改成32bit的float,这样会牺牲部分性能。