正背面剔除 & 深度测试 & 颜色混合

463 阅读8分钟

一. 正背面剔除

在平时绘制立体图形(比如一个救生圈)的时候,如果当我们旋转所绘制的图形会出现如下情况:

异常场

黑色的部分其实是这个救生圈背面的图像,按道理来说这个背面我们其实是不应该看到的。在绘制 3D 场景的时候,我们需要决定哪些部分对于观察者是可见的,哪些是不可见的。对于不可见的部分应该尽早丢弃,不去渲染,减少一些不必要的绘制也能提高 OpenGL 的绘图性能。我们将这个动作称之为: 隐藏面消除(Hidden surface elimination)

在 OpenGL 里面我们使用**正背面剔除(Face Culling)**来处理这个情况。

那么在 OpenGL 绘图过程中如何确定那个是正面,哪个是反面? 答案是通过分析顶点数据的顺序,在 OpenGL 中我们把顶点逆时针连接方式称之为正面,顺时针顶点链接方式叫做反面。

1. 分析立方体中的正背面

立方体

  • 分析
    • 左侧三角形顶点的顺序为 1->2->3 ;右侧三角形的顶点顺序为:1->2->3
    • 当观察者在右侧时,则右边的三角形方向为逆时针,为正面,而左侧的三角形方向为顺时针,是反面。
    • 当观察者在左侧时,则左边的三角形方向为逆时针,为正面,而右侧的三角形方向为顺时针,是反面。
  • 总结
    • 正面和背面是由三角形的顶点连接顺序观察者方向共同决定的,随着观察者的角度方向发生改变,正背面也会发生改变。
2. 正背面剔除 API
  • 开启表面剔除(默认背面剔除): void glEnable(GL_CULL_FACE)
  • 关闭表面剔除(默认背面剔除): void glDisable(GL_CULL_FACE)
  • 用户选择剔除哪个面(正面/背面): void glCullFace(GLenum mode)
    • mode 参数为:GL_FRONT, GL_BACK, GL_FRONT_AND_BACK, 默认值为 GL_BACK
  • 用户指定环绕顺序哪个为正面 void glFrontFace(GLenum mode)
    • mode 参数为:GL_CW, GL_CCW, 默认值为逆时针 GL_CCW
// 开启/关闭正背面剔除的示例代码
if (iCull) {
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);
} else {
    glDisable(GL_CULL_FACE);
}

二. 深度测试

1. 了解深度
  • 什么是深度? 深度其实就是该像素点在 3D 世界中距离摄像机的距离 Z 值。
  • 什么是深度缓冲区? 深度缓冲区就是一块内存区域,专门存储着每个像素点(绘制在屏幕上的)深度值,Z 值越大表明离摄像机越远,反正则表明离摄像机越近。
  • 为什么需要深度缓冲区? 在不使用深度测试的时候,如果我们先绘制一个距离比较近的物体,再绘制距离较远的物体,则距离远的位图因为后绘制,就会把距离近的物体覆盖掉。有了深度缓冲区后,绘制物体的顺序就不那么重要了。实际上,只要存在深度缓冲区,OpenGL 都会把像素的深度值写入缓冲区中。除非调用 glDepthMask(GL_FALSE)来禁止写入。
2. 解决方案:Z-buffer 方法(深度缓冲区 Depth-buffer)

深度测试:深度缓冲区(DepthBuffer)和颜色缓冲区(ColorBuffer)是对应的,颜色缓冲区存储像素的颜色信息,而深度缓冲区存储像素的深度信息。在决定是否绘制一个物体表面时,首先要将表面的像素的深度值与当前深度缓冲区中的值进行比较。如果大于深度缓冲区的值则丢弃这部分,否则利用这个像素对应的深度值和颜色值分别更新深度缓冲区和颜色缓冲区,这个过程称之为“深度测试”。

3. 深度值计算
  • 深度值一般由 16 位,24 位或者 32 位值表示,通常使用 24 位。位数越高深度值越精确。深度值一般在[0,1]之间,值越小表示越靠近观察者,值越大表示远离观察者。
  • 深度缓冲主要是通过计算深度值来比较大小,在深度缓冲区中包含的深度值介于 0.0 和 1.0 之间,从观察者看到其内容与场景中的所有有对象的 Z 值进行了比较。这些视图控件中的 Z 值可以在投影平头截体的近平面和远平面之间的任何值。我们因此需要一些方法来转换这些视图空间 Z 值到 [0,1] 的范围内,下面的线性方程式把 Z 值转换为 0.0 到 1.0 之间的值。
4. 使用深度测试
  • 开启深度测试
    • glEnable(GL_DEPTH_TEST)
  • 关闭深度测试
    • glDisable(GL_DEPTH_TEST)
  • 在绘制场景前,清除颜色缓冲区,深度缓冲区
    • glClearColor(0.3, 0.3, 0.3, 1.0)
    • glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
  • 清除深度缓冲区默认值为 1.0,表示最大的深度值, 深度值范围在[0,1]之间,值越小表示越靠近观察者,值越大表示远离观察者。
5. 指定深度测试判断式

指定深度测试判断式使用 glDepthFunc(GLenum mode)。mode 值对应的说明见下表:

Mode 说明
GL_ALWAYS 总是通过测试
GL_NEVER 总是不通过测试
GL_LESS 当前深度值 < 存储深度值时通过
GL_EQUAL 当前深度值 = 存储深度值时通过
GL_LEQUAL 当前深度值 <= 存储深度值时通过
GL_GREATER 当前深度值 > 存储深度值时通过
GL_NOTEQUAL 当前深度值 != 存储深度值时通过
GL_GEQUAL 当前深度值 >= 存储深度值时通过
6. 开启/关闭 深度缓冲区写入

glDepthMask(GLBool value) value: GL_TRUE 表示开启深度缓冲区写入,GL_FALSE 表示关闭深度缓冲区写入!

7. ZFighting 闪烁问题的原因
  • 为什么会出现 ZFighting 闪烁问题?

    因为开启深度测试后,OpenGL 就不会再去绘制模型被遮挡的部分。这样实现的显然更加真实,但是由于深度缓冲区精度的限制对于深度差非常小的情况下(例如在同一平面进行 2 次绘制),OpenGL 就可能出现不能正确判断两者的深度值,导致深度测试的结果不可预测,显示出来的图像交错闪烁!简单的理解为两个物体的在同一平面内的深度值一样,OpenGL 不知道应该绘制哪个物体。

  • ZFighting 闪烁解决方案

    • 启用 Polygon Offset 方式解决 让两个深度值之间产生间隔,如果 2 个图形之间有间隔,也就意味着深度值不一样,这样就不会产生干涉。可以理解为在执行深度测试前将立方体的深度值做一些细小的增加,于是就能将重叠的 2 个图形深度值有所区分。

      调用 glEnable(GL_POLYGON_OFFSET_FILL) 函数。参数值有三种: GL_POLYGON_OFFSET_POINT 对应的光栅化模式:GL_POINT GL_POLYGON_OFFSET_LINE 对应的光栅化模式:GL_LINE GL_POLYGON_OFFSET_FILL 对应的光栅化模式:GL_FILL

    • 指定偏移量

      • 通过 glPolygonOffset(GLfloat factor, GLfloat units) 函数来指定
      • 指定以后每个 fragment 的深度值都会增加如下所示的偏移量:offset = (m * factor) + (r * units), 其中 m 表示多边形的深度的斜率的最大值,可以理解为一个多边形越是与近裁剪面平行,m 值就越接近于 0。r 表示能产生于窗口坐标系的深度值中可分辨的差异最小值,r 是由具体的 OpenGL 平台所指定的一个常量。
      • 一个大于 0 的 offset 会把模型推向更远的位置。相应的一个小于 0 的 offset 则会把模型拉近。
      • 一般而言,只需要将 -1.01.0 这样简单的赋值给 glPolygonOffset 就可以满足基本需要了。
    • 关闭 Polygon Offset

      调用 glDisable(GL_POLYGON_OFFSET_FILL) 函数来关闭。

三. 颜色混合

我们在 OpenGL 渲染时会把颜色值存在颜色缓冲区,每个片段的深度值放在深度缓冲区。当深度缓冲区被关闭时,新的颜色将简单地覆盖原来颜色缓冲区存在的颜色值,当深度缓冲区再次打开时,只有当新的颜色片段比原来的值更接近临近的裁剪平面才会替换原来的颜色片段。 使用 glEnable(GL_BLEND) 开启颜色混合,使用 glDisable(GL_BLEND) 关闭颜色混合。 在这里有两个概念需要明确一下:目标颜色和源颜色

  • 目标颜色:已经存储在颜色缓冲区的颜色值
  • 源颜色:作为当前渲染命令结果进入颜色缓存区的颜色值

当混合功能开启时,源颜色和目标颜色的组合方式是由混合方程式计算的,在默认情况下混合方程式的计算共计为 Cf = (Cs * S) + (Cd * D)

  • Cf: 最终计算出来的颜色
  • Cs: 源颜色
  • Cd: 目标颜色
  • S: 源混合因子
  • D: 目标混合因子

在设置混合因子的时候需要使用 glBlendFunc(GLenum S, GLenum D) 函数, 关于混合因子的具体说明见下表:

混合因子参数 RGB 混合因子 Alpha 混合因子
GL_ZERO (0,0,0) 0
GL_ONE (1,1,1) 1
GL_SRC_COLOR (Rs,Gs,Bs) As
GL_ONE_MINUS_SRC_COLOR (1,1,1)-(Rs,Gs,Bs) 1-As
GL_DST_COLOR (Rd,Gd,Bd) Ad
GL_ONE_MINUS_DST_COLOR (1,1,1)-(Rd,Gd,Bd) 1-Ad
GL_SRC_ALPHA (As,As,As) As
GL_ONE_MINUS_SRC_ALPHA (1,1,1)-(As,As,As) 1-As
GL_DST_ALPHA (Ad,Ad,Ad) Ad
GL_ONE_MINUS_DST_ALPHA (1,1,1)-(Ad,Ad,Ad) 1-Ad
GL_CONSTANT_COLOR (Rc,Gc,Bc) Ac
GL_ONE_MINUS_CONSTANT_COLOR (1,1,1)-(Rc,Gc,Bc) 1-Ac
GL_CONSTANT_APLHA (Ac,Ac,Ac) Ac
GL_ONE_MINUS_CONSTANT_APLHA (1,1,1)-(Ac,Ac,Ac) 1-Ac
GL_SRC_APLHA_SATURATE (f,f,f)*f=min(As,1-Ad) 1

表中 R, G, B, A 分别代表红,绿,蓝,透明度。 表中下标 S、D 分别代表源、目标。 表中 C 表示常量颜色(默认为黑色)

举例说明: 我们使用 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) 来进行颜色混合,当前颜色缓存区已经有了一种颜色(1.0,0.0,0.0,0.0),这个表示目标颜色 Cd,如果在这个上面使用一种 alpha 为 0.6 的蓝色(0.0,0.0,1.0,0.6)进行混合,那么

  • Cd(目标颜色) = (1.0,0.0,0.0,0.0)
  • Cs(源颜色) = (0.0,0.0,1.0,0.6)
  • S(源 alpha 值) = 0.6
  • D(1-源 alpha 值) = 1-0.6 = 0.4

使用方程式 Cf = (Cs * S) + (Cd * D) 就等价于(Blue * 0.6)+ (Red * 0.4)