更新于 

α混合

α分量

代表 R G B A 中的 A
通过控制A分量来实现半透明效果的方式,
被称为 α混合(alpha blending)

实现α混合

教堂半透明的彩色玻璃是宗教美学的典型代表
教堂半透明的彩色玻璃是宗教美学的典型代表
如何开启α混合
  1. 开启混合功能
    1
    gl.enable(gl.BLEND);
  2. 指定混合函数
    1
    gl.blendFunc(gl.SRC.ALPHA,gl.ONE_MINUS_SRC_ALPHA);
LookAtBlendedTriangles Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute vec4 a_Color;
uniform mat4 u_ViewMatrix;
uniform mat4 u_ProjMatrix;
varying vec4 v_Color;
void main(){
gl_Position = u_ProjMatrix * u_ViewMatrix * a_Position;
v_Color = a_Color;
}
`;

let FSHADER_SOURCE = `
#ifdef GL_ES
precision mediump float;
#endif
varying vec4 v_Color;
void main(){
gl_FragColor = v_Color;
}
`;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function main() { 
let canvas = document.getElementById('webgl');
let gl = getWebGLContext(canvas);
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('failed to init shaders');
return;
}

gl.clearColor(0, 0, 0, 1);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

let u_ProjMatrix = gl.getUniformLocation(gl.program, 'u_ProjMatrix');
let u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');
let viewMatrix = new Matrix4();
let projMatrix = new Matrix4();
projMatrix.setOrtho(
-1, 1, -1, 1, 0, 2
);
gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix.elements);

let n = initVertexBuffers(gl);
draw(gl, n, u_ViewMatrix, viewMatrix);
window.onkeydown = function (ev) {
keydown(gl, ev, n, u_ViewMatrix, viewMatrix);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function initVertexBuffers(gl) {
let verticesColors = new Float32Array([
0.0, 0.5, -0.4, 0.4, 1.0, 0.4, 0.4, // The back green one
-0.5, -0.5, -0.4, 0.4, 1.0, 0.4, 0.4,
0.5, -0.5, -0.4, 1.0, 0.4, 0.4, 0.4,

0.5, 0.4, -0.2, 1.0, 0.4, 0.4, 0.4, // The middle yerrow one
-0.5, 0.4, -0.2, 1.0, 1.0, 0.4, 0.4,
0.0, -0.6, -0.2, 1.0, 1.0, 0.4, 0.4,

0.0, 0.5, 0.0, 0.4, 0.4, 1.0, 0.4, // The front blue one
-0.5, -0.5, 0.0, 0.4, 0.4, 1.0, 0.4,
0.5, -0.5, 0.0, 1.0, 0.4, 0.4, 0.4,
]);
let n = 9;
let FSIZE = verticesColors.BYTES_PER_ELEMENT;
let a_Position = gl.getAttribLocation(gl.program, 'a_Position');
let a_Color = gl.getAttribLocation(gl.program, 'a_Color');

let vertexColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE*7, 0);
gl.enableVertexAttribArray(a_Position);

gl.vertexAttribPointer(a_Color, 4, gl.FLOAT, false, FSIZE*7, FSIZE*3);
gl.enableVertexAttribArray(a_Color);

return n;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let g_EyeX = 0.2, g_EyeY = 0.25, g_EyeZ = 0.25;
function keydown(gl, ev, n,u_ViewMatrix, viewMatrix) {
switch (ev.keyCode) {
case 37:
g_EyeX -= 0.01;
break;
case 39:
g_EyeX += 0.01;
break;
default:
break;
}
draw(gl, n, u_ViewMatrix, viewMatrix);
}

function draw(gl, n, u_ViewMatrix, viewMatrix) {

viewMatrix.setLookAt(
g_EyeX, g_EyeY, g_EyeZ,
0, 0, 0,
0, 1, 0
);
gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements);

gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, n);
}

混合函数

gl.blendFunc()

在LookAtBlendedTriangles中有两行涉及α混合的关键代码:

1
2
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);

α混合涉及两个颜色:

  • 源颜色(source color)
    • 待混合进去的颜色,也可以说是要叠加的对象的颜色
    • 加奶咖啡 里的 牛奶(加进去的原料)
  • 目标颜色(destination color)
    • 待被混合进去的颜色,也可以说是画板上已有的颜色
    • 加奶咖啡 里的 咖啡(被加的基底)
源颜色就像拿铁里调和风味的牛奶,目标颜色则是作为基底的咖啡
源颜色就像拿铁里调和风味的牛奶,目标颜色则是作为基底的咖啡
加法混合(additive blending)

加法混合会使被混合的区域更加明亮,
所以常常用来实现 爆炸 的光照效果。

blendFunc的参数
  • 源颜色分量:(Rs,Gs,Bs,As)
  • 目标颜色分量:(Rd,Gd,Bd,Ad)
镂空效果:gl.blendFunc(gl.ZERO,gl.ZERO)
  • src_factor = gl.ZERO 表示忽视源颜色
  • dst_factor = gl.ZERO 表示忽视目标颜色
  • 同时忽视源颜色和目标颜色,就表示镂空处理
gl.blendFunc(gl.ZERO,gl.ZERO)涉及到的计算
gl.blendFunc(gl.ZERO,gl.ZERO)涉及到的计算
镂空效果
镂空效果
只取源颜色gl.blendFunc(gl.ONE,gl.ZERO)
  • src_factor = gl.ONE 表示完全保留源颜色
  • dst_factor = gl.ZERO 表示忽视目标颜色
  • 完全忽视底色,直接将源颜色绘制在无视canvas背景画布的dom容器之中
gl.blendFunc(gl.ONE,gl.ZERO)涉及到的计算
gl.blendFunc(gl.ONE,gl.ZERO)涉及到的计算
由于物体是半透明的,直接和body的颜色混合在了一起,无视了canvas背景色
由于物体是半透明的,直接和body的颜色混合在了一起,无视了canvas背景色
只取目标颜色gl.blendFunc(gl.ZERO,gl.ONE)
  • src_factor = gl.ONE 表示忽视源颜色
  • dst_factor = gl.ZERO 表示完全保留目标颜色
  • 完全忽视源颜色,保留目标颜色就表示保存画布,什么都不绘制
  • 好像在用透明墨水画画一样
gl.blendFunc(gl.ZERO,gl.ONE)涉及到的计算
gl.blendFunc(gl.ZERO,gl.ONE)涉及到的计算
物体的确是画了,但只是颜色是透明的(笑)
物体的确是画了,但只是颜色是透明的(笑)

半透明的三维物体

彩虹水晶
彩虹水晶
在ColoredCube基础之上

按照之前介绍的实现 α混合 的方法,我们为颜色缓冲区填入的数据添加上透明度:

1
2
3
4
5
6
7
8
9
let colors = new Float32Array([
0.4, 0.4, 1.0, 0.4, 0.4, 0.4, 1.0,0.4, 0.4, 0.4, 1.0,0.4, 0.4, 0.4, 1.0,0.4, //front
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, 1.0,0.4, //back
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, 1.0, 0.4,0.4, //left
0.4, 1.0, 0.4, 0.4, 0.4, 1.0, 0.4,0.4, 0.4, 1.0, 0.4,0.4, 0.4, 1.0, 0.4,0.4, //right
1.0, 0.4, 0.4, 0.4, 1.0, 0.4, 0.4,0.4, 1.0, 0.4, 0.4,0.4, 1.0, 0.4, 0.4,0.4, //up
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,0.4, //down
]);
initArrayBuffer(gl, 'a_Color', colors, 4, gl.FLOAT);

再加上开启α混合的配置代码:

1
2
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

应该就可以实现了,但是事实上的运行效果却和期望得到的不同:

实际上并没有实现透明
实际上并没有实现透明
为什么没有实现透明呢?

答: 因为开启了 DEPTH_TEST(隐藏面消除)

1
gl.enable(gl.DEPTH_TEST);

开启了隐藏面消除会使得 被隐藏的片元根本不被绘制 , 自然更不会有混合过程。
所以只需要 关闭gl.DEPTH_TEST 即可。

开启DEPTH_TEST

隐藏面没有被绘制,因此不存在混合过程
隐藏面没有被绘制,因此不存在混合过程

关闭DEPTH_TEST

将隐藏面纳入混合过程之中
将隐藏面纳入混合过程之中

透明与不透明物体共存

如何同时实现隐藏面消除和半透明效果?

BlendedCube直接把DEPTH_TEST关闭了,这种方法太简单粗暴,一点都不优雅。

可以通过使用下面这种机制同时实现 隐藏面消除半透明效果
提前剧透: 将透明物体和非透明物体分开绘制

1.开启gl.DEPTH_TEST

1
gl.enable(gl.DEPTH_TEST);

2.绘制出所有不透明的物体

1
gl.drawElements(非透明物体)

3.禁止深度缓冲区的写入操作

深度缓冲区是用于进行 隐藏面消除 的,
这一步需要禁止接下来深度缓冲区的 写入操作
使之变得 只读

1
gl.depthMask(false);

4.绘制所有半透明物体

注意它们应当按照 深度进行排序

1
gl.drawElements(半透明物体)

5.释放深度缓冲区

重新使深度缓冲区 可读写

1
gl.depthMask(true);
BlendedWithDepthMask
BlendedWithDepthMask运行效果
BlendedWithDepthMask运行效果
> 主要绘制逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function draw(gl, u_MvpMatrix, mvpMatrix, projMatrix){
// 1. 清除缓冲区
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

// 2. 绘制不透明物体
let n_Solid = initVertexBuffer_Solid(gl);
gl.drawElements(gl.TRIANGLES,n_Solid,gl.UNSIGNED_BYTE,0);

// 3.禁止深度缓冲区的写入操作
gl.depthMask(false);

// 4.绘制透明物体
let n_Blend = initVertexBuffer_Blend(gl);
gl.drawElements(gl.TRIANGLES,n_Blend,gl.UNSIGNED_BYTE,0);

// 5.解锁深度缓冲区写入操作
gl.depthMask(true);