WebGL学习(十三)α混合

206 阅读7分钟

1. 介绍

听起来这个名词很厉害,其实本质就是实现物体的半透明效果

image.png

2. 代码实现

直接给颜色设置透明度出来是这样

// 颜色参数
// 每个顶点颜色设置成了透明度0.5
var colors = new Float32Array([
  0.4, 0.4, 1.0, 0.5,   0.4, 0.4, 1.0, 0.5,   0.4, 0.4, 1.0, 0.5,   0.4, 0.4, 1.0, 0.5,  // v0-v1-v2-v3 前(blue)
  0.4, 1.0, 0.4, 0.5,   0.4, 1.0, 0.4, 0.5,   0.4, 1.0, 0.4, 0.5,   0.4, 1.0, 0.4, 0.5,  // v0-v3-v4-v5 右(green)
  1.0, 0.4, 0.4, 0.5,   1.0, 0.4, 0.4, 0.5,   1.0, 0.4, 0.4, 0.5,   1.0, 0.4, 0.4, 0.5,  // v0-v5-v6-v1 上(red)
  1.0, 1.0, 0.4, 0.5,   1.0, 1.0, 0.4, 0.5,   1.0, 1.0, 0.4, 0.5,   1.0, 1.0, 0.4, 0.5,  // v1-v6-v7-v2 左
  1.0, 1.0, 1.0, 0.5,   1.0, 1.0, 1.0, 0.5,   1.0, 1.0, 1.0, 0.5,   1.0, 1.0, 1.0, 0.5,  // v7-v4-v3-v2 下
  0.4, 1.0, 1.0, 0.5,   0.4, 1.0, 1.0, 0.5,   0.4, 1.0, 1.0, 0.5,   0.4, 1.0, 1.0, 0.5,  // v4-v7-v6-v5 后
]);

image.png 确实设置成功,颜色也变淡了,但是按理来说前面应该会透出其他面的颜色,就像玻璃窗户一样。

2.1 gl.BLENDgl.blendFunc

实现透明效果只需要增加两行代码


// 开启混合
gl.enable(gl.BLEND) ;
// 设置混合函数
gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA) ;

blendFunc就是设置混合方式,混合的公式遵循:

<混合后的颜色>=源颜色src_factor+目标颜色dst_factor<混合后的颜色> = 源颜色*src\_factor + 目标颜色*dst\_factor

这里举个例子: image.png 下表就是支持的factor配置:

ConstantFactorDescription
gl.ZERO0,0,0,0所有颜色乘 0.
gl.ONE1,1,1,1所有颜色乘 1.
gl.SRC_COLORRS, GS, BS, AS将所有颜色乘上源颜色。
gl.ONE_MINUS_SRC_COLOR1-RS, 1-GS, 1-BS, 1-AS每个源颜色所有颜色乘 1 .
gl.DST_COLORRD, GD, BD, AD将所有颜色与目标颜色相乘。
gl.ONE_MINUS_DST_COLOR1-RD, 1-GD, 1-BD, 1-AD将所有颜色乘以 1 减去每个目标颜色。
gl.SRC_ALPHAAS, AS, AS, AS将所有颜色乘以源 alpha 值。
gl.ONE_MINUS_SRC_ALPHA1-AS, 1-AS, 1-AS, 1-AS将所有颜色乘以 1 减去源 alpha 值。
gl.DST_ALPHAAD, AD, AD, AD将所有颜色与目标 alpha 值相乘。
gl.ONE_MINUS_DST_ALPHA1-AD, 1-AD, 1-AD, 1-AD将所有颜色乘以 1 减去目标 alpha 值。
gl.CONSTANT_COLORRC, GC, BC, AC将所有颜色乘以一个常数颜色。
gl.ONE_MINUS_CONSTANT_COLOR1-RC, 1-GC, 1-BC, 1-AC所有颜色乘以 1 减去一个常数颜色。
gl.CONSTANT_ALPHAAC, AC, AC, AC将所有颜色乘以一个常数。
gl.ONE_MINUS_CONSTANT_ALPHA1-AC, 1-AC, 1-AC, 1-AC所有颜色乘以 1 减去一个常数。
gl.SRC_ALPHA_SATURATEmin(AS, 1 - AD), min(AS, 1 - AD), min(AS, 1 - AD), 1将 RGB 颜色乘以源 alpha 值或 1 减去目标 alpha 值中的较小值。alpha 值乘以 1.

这里面的factor就是上面公式中src_factor/dst_factor的配置,有四个分量,分别表示RGBA

其中RS表示源颜色的R分量,以此类推。RD表示目标颜色的R分量,RC表示R是常量。

其实可以看出,这个函数不仅仅是实现透明,而是如何混合颜色

比如

glBlendFunc(Gl_SRC_ALHPA,GI_ONE)

相当于:假设源颜色是(1.0, 0.0, 0.0, 1.0),目标颜色是(0.0, 0.0, 0.0, 1.0)

源颜色:RGB1.00.00.0110101目标颜色:RGB0.00.00.00101011.00.00.0+0001.00.00.0\frac{ 源颜色: \begin{matrix} R & G & B\\ 1.0 & 0.0&0.0 \\ *\\ 1*1& 0*1&0*1 \end{matrix} \qquad目标颜色: \begin{matrix} R & G & B \\ 0.0 & 0.0&0.0\\ *\\ 0*1& 0*1&0*1 \end{matrix}\\ } { \frac{ \begin{matrix} 1.0 & 0.0&0.0 \\ +\\ 0& 0&0 \end{matrix} } { \begin{matrix} 1.0 & 0.0&0.0 \\ \end{matrix} } }

其实代表的意思就是增强了目标颜色,可以用在高亮显示,爆照光照反射。

我们实际操作一下,看一下效果,这里我画一个透明的立方体:

// 每个面的颜色的α=0.5
// 颜色参数
var colors = new Float32Array([
  0.4, 0.4, 1.0, 0.5,   0.4, 0.4, 1.0, 0.5,   0.4, 0.4, 1.0, 0.5,   0.4, 0.4, 1.0, 0.5,  // v0-v1-v2-v3 前(blue)
  0.4, 1.0, 0.4, 1.0,   0.4, 1.0, 0.4, 1.0,   0.4, 1.0, 0.4, 1.0,   0.4, 1.0, 0.4, 1.0,  // v0-v3-v4-v5 右(green)
  1.0, 0.4, 0.4, 1.0,   1.0, 0.4, 0.4, 1.0,   1.0, 0.4, 0.4, 1.0,   1.0, 0.4, 0.4, 1.0,  // v0-v5-v6-v1 上(red)
  1.0, 1.0, 0.4, 1.0,   1.0, 1.0, 0.4, 1.0,   1.0, 1.0, 0.4, 1.0,   1.0, 1.0, 0.4, 1.0,  // v1-v6-v7-v2 左
  1.0, 1.0, 1.0, 1.0,   1.0, 1.0, 1.0, 1.0,   1.0, 1.0, 1.0, 1.0,   1.0, 1.0, 1.0, 1.0,  // v7-v4-v3-v2 下
  0.4, 1.0, 1.0, 1.0,   0.4, 1.0, 1.0, 1.0,   0.4, 1.0, 1.0, 1.0,   0.4, 1.0, 1.0, 1.0,  // v4-v7-v6-v5 后
]);
// ....
// 开启混合
gl.enable (gl.BLEND) ;
// 设置混合函数
gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA) ;
// ...

msedge_NxSi9qDtjL.gif 你会发现总有几个面不是透明的

下面我们来解释为什么会这样。

2.2 什么是源颜色、目标颜色

解释之前我们先确定两个概念:

  • 源颜色:当前片元着色器输出的颜色

  • 目标颜色:颜色缓冲区中上一个绘制的颜色

opengl实际上不知道你想要混合的是哪两种颜色,它只是机械的将当前片元着色器输出的颜色和上一次绘制的颜色进行混合。

2.2. 与深度检测的冲突

绘制3D图形通常都需要设置深度检测gl.enable(gl.DEPTH_TEST),因为看不见的、超出裁剪空间的顶点应该被切除,不渲染。深度检测只关心深度信息,颜色是否透明对于它来说是一样的。

所以上面那个立方体,在初始绘制的时候,由于深度检测,后面和下面都被丢弃了。

也就导致没有可以混合的颜色,看起来就是本来的颜色(不透明的样子)。

2.3. 解决方案1.0

既然知道是与深度检测冲突了,那我们就关闭深度检测:

// ...
// gl.enable(gl.DEPTH_TEST)
// 开启混合
gl.enable (gl.BLEND) ;
// 设置混合函数
// ...
gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA) ;

msedge_kz76d3Ygjr.gif 看起来很酷,一个透明的立方体。

但是这个方法有个致命缺陷,如果场景有多个物体,而你关闭了深度检测,也就会破坏前后关系,渲染出来的图像将会一片混乱。

2.3. 解决方案2.0

关闭深度检测不可取,我们换个思路,既然被挡住的顶点不会被绘制,那么我先画被挡住的顶点,再画透明的顶点不就行了。

// Create a cube
//    v6----- v5
//   /|      /|
//  v1------v0|
//  | |     | |
//  | |v7---|-|v4
//  |/      |/
//  v2------v3
// 每个面都单独定义了一组顶点
const vertex = new Float32Array([
  1.0, 1.0, 1.0,1.0,  -1.0, 1.0, 1.0,1.0,  -1.0,-1.0, 1.0,1.0,   1.0,-1.0, 1.0,1.0,    // v0-v1-v2-v3 front
  1.0, 1.0, 1.0,1.0,   1.0,-1.0, 1.0,1.0,   1.0,-1.0,-1.0,1.0,   1.0, 1.0,-1.0,1.0,    // v0-v3-v4-v5 right
  1.0, 1.0, 1.0,1.0,   1.0, 1.0,-1.0,1.0,  -1.0, 1.0,-1.0,1.0,  -1.0, 1.0, 1.0,1.0,    // v0-v5-v6-v1 up
  -1.0, 1.0, 1.0,1.0,  -1.0, 1.0,-1.0,1.0,  -1.0,-1.0,-1.0,1.0,  -1.0,-1.0, 1.0,1.0,    // v1-v6-v7-v2 left
  -1.0,-1.0,-1.0,1.0,   1.0,-1.0,-1.0,1.0,   1.0,-1.0, 1.0,1.0,  -1.0,-1.0, 1.0,1.0,    // v7-v4-v3-v2 down
  1.0,-1.0,-1.0,1.0,  -1.0,-1.0,-1.0,1.0,  -1.0, 1.0,-1.0,1.0,   1.0, 1.0,-1.0,1.0,    // v4-v7-v6-v5 back
])
var indices = new Uint8Array([
  4, 5, 6,      4, 6, 7,   // 右
  8, 9, 10,     8, 10, 11,   // 上
  12, 13, 14,   12, 14, 15,   // 左
  16, 17, 18,   16, 18, 19,   // 下
  20, 21, 22,   20, 22, 23,    // 后
  
    0, 1, 2,      0, 2, 3,   // 最后绘制 前
]);
// ...
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_BYTE, 0);
//....

之前前面没有实现透明,原因就是后面被深度检测切除了,现在我先画后面,最后画前面。

image.png 看着好像对了,前面透明了,但是当我转移视角就不一样了:

msedge_1NpkNg5GLK.gif 上面和右面又被深度检测了。

我们还可以继续修改渲染顺序,但是对于这个立方体,无论你怎么调整,肯定有几个面是会被遮挡的,这样始终会触发深度检测。

2.4 解决方案终极版

思路:

  1. 绘制不透明物体
  2. 关闭深度检测
  3. 绘制透明物体(从远到近)
  4. 开启深度检测

关闭深度检测使用一个函数gl.depthMask(false)。它只有一个参数,true就是开启false就是关闭,切换的是深度缓冲区的写操作。

这里我懒得画多个物体了,我用一个立方体来做演示。现在我要绘制一个只有前面和上面是透明的立方体。

按照上面的步骤我应该这样:

  1. 绘制右、左、下、后
  2. 关闭深度检测
  3. 绘制上、前(顺序没关系,因为上和前没有遮挡关系)
  4. 打开深度检测

为了能清楚的看见处理步骤,我将渲染拆分了一下:

// 颜色
var colors = new Float32Array([
  0.4, 0.4, 1.0, 0.5,   0.4, 0.4, 1.0, 0.5,   0.4, 0.4, 1.0, 0.5,   0.4, 0.4, 1.0, 0.5,  // v0-v1-v2-v3 前(blue)
  0.4, 1.0, 0.4, 1.0,   0.4, 1.0, 0.4, 1.0,   0.4, 1.0, 0.4, 1.0,   0.4, 1.0, 0.4, 1.0,  // v0-v3-v4-v5 右(green)
  1.0, 0.4, 0.4, 0.5,   1.0, 0.4, 0.4, 0.5,   1.0, 0.4, 0.4, 0.5,   1.0, 0.4, 0.4, 0.5,  // v0-v5-v6-v1 上(red)
  1.0, 1.0, 0.4, 1.0,   1.0, 1.0, 0.4, 1.0,   1.0, 1.0, 0.4, 1.0,   1.0, 1.0, 0.4, 1.0,  // v1-v6-v7-v2 左
  1.0, 1.0, 1.0, 1.0,   1.0, 1.0, 1.0, 1.0,   1.0, 1.0, 1.0, 1.0,   1.0, 1.0, 1.0, 1.0,  // v7-v4-v3-v2 下
  0.4, 1.0, 1.0, 1.0,   0.4, 1.0, 1.0, 1.0,   0.4, 1.0, 1.0, 1.0,   0.4, 1.0, 1.0, 1.0,  // v4-v7-v6-v5 后
]);
// 原本是一次性渲染
gl.drawElements(gl.TRIANGLES,indices.length, gl.UNSIGNED_BYTE, 0);
// 我把渲染拆开
  /**
   *  ['右', 6],
  ['左', 18],
  ['下', 24],
  ['后', 30],
  ['上', 12],
  ['前', 0],
  */
  // 一个面两个三角形,6个顶点
  const faceOffsetIndex = [6, 18, 24, 30 , 12, 0]
  // 画6个面
  // 先画不透明的面
  gl.drawElements(gl.TRIANGLES,6, gl.UNSIGNED_BYTE, faceOffsetIndex[0]);
  gl.drawElements(gl.TRIANGLES,6, gl.UNSIGNED_BYTE, faceOffsetIndex[1]);
  gl.drawElements(gl.TRIANGLES,6, gl.UNSIGNED_BYTE, faceOffsetIndex[2]);
  gl.drawElements(gl.TRIANGLES,6, gl.UNSIGNED_BYTE, faceOffsetIndex[3]);
  // 关闭深度检测
  gl.depthMask(false)
  // 再画透明的面
  gl.drawElements(gl.TRIANGLES,6, gl.UNSIGNED_BYTE, faceOffsetIndex[4]);
  gl.drawElements(gl.TRIANGLES,6, gl.UNSIGNED_BYTE, faceOffsetIndex[5]);
  // 开启深度检测
  gl.depthMask(true)

msedge_jWx3dMp107.gif

要全透明也很简单:

// ....
var colors = new Float32Array([
  0.4, 0.4, 1.0, 0.5,   0.4, 0.4, 1.0, 0.5,   0.4, 0.4, 1.0, 0.5,   0.4, 0.4, 1.0, 0.5,  // v0-v1-v2-v3 前(blue)
  0.4, 1.0, 0.4, 0.5,   0.4, 1.0, 0.4, 0.5,   0.4, 1.0, 0.4, 0.5,   0.4, 1.0, 0.4, 0.5,  // v0-v3-v4-v5 右(green)
  1.0, 0.4, 0.4, 0.5,   1.0, 0.4, 0.4, 0.5,   1.0, 0.4, 0.4, 0.5,   1.0, 0.4, 0.4, 0.5,  // v0-v5-v6-v1 上(red)
  1.0, 1.0, 0.4, 0.5,   1.0, 1.0, 0.4, 0.5,   1.0, 1.0, 0.4, 0.5,   1.0, 1.0, 0.4, 0.5,  // v1-v6-v7-v2 左
  1.0, 1.0, 1.0, 0.5,   1.0, 1.0, 1.0, 0.5,   1.0, 1.0, 1.0, 0.5,   1.0, 1.0, 1.0, 0.5,  // v7-v4-v3-v2 下
  0.4, 1.0, 1.0, 0.5,   0.4, 1.0, 1.0, 0.5,   0.4, 1.0, 1.0, 0.5,   0.4, 1.0, 1.0, 0.5,  // v4-v7-v6-v5 后
]);
// ....
const draw = () => {
  gl.clearColor(0, 0, 0, 1)
  gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT)
  // ....
  gl.depthMask(false)
  gl.drawElements(gl.TRIANGLES,indices.length, gl.UNSIGNED_BYTE, 0);
  gl.depthMask(true)
  // ...
}

因为所有面都是透明的,所以绘制之前直接关闭深度检测。

msedge_NnciMnhiEn.gif

3. 注意

绘制完成记得把深度检测打开,因为深度检测的原理是在深度缓冲区中比较这次和上次片元z值。一般情况下是在片元着色器处理之后,如果不打开深度检测,那么在绘制完成顶点之后,片元着色器无法写入深度缓冲区。

深度缓冲范围是[0,1]值越大离观察者越远,如果先绘制A后绘制BBA重叠的地方会进行z值比较,如果Bz值较小,就说明B应该在A前面,所以使用B的颜色缓冲区值覆盖掉A的。

忘记开启的效果就是这样:

msedge_hcxsKxMa6X.gif 代码中我只设置了前和上面是透明的,但是其他面也不正常了。颜色产生了混乱。