【零基础学WebGL】合成与混合

541 阅读4分钟

浏览器合成

从一段简单的HTML代码开始。

如下,在背景色是黑色的body上,绘制一个透明度是0.5的蓝色div。

<style>
body {
  background: rgb(0, 0, 0);
}
.app {
  width: 300px;
  height: 300px;
  background: rgba(0, 0, 255, 0.5);
}
</style>

<body>
<div class="app"></div>
</body>

通过拾色器,可以发现div和body相交的区域的色值是 rgb(0,0,128).

浏览器图层合成结果遵循如下公式:

color_{final} = color_{source} * alpha_{source} + color_{destination} * (1 - alpha_{source})color_{final} = color_{source} * alpha_{source} + color_{destination} * (1 - alpha_{source})

也就是,源图层的rgba分量分别 乘以 源图层的alpha分量,然后加上,目标图层的rgba分量 乘以 1减去源图层的alpha分量 之后的结果。

本示例中,div是原图层,body是目标图层,因此合成图层的rgba计算过程如下:

rgba = (0, 0, 255, 0.5) * 0.5 + (0, 0, 0, 1) * (1 - 0.5)
     = (0, 0, 128 ,0.75)

WebGL与浏览器合成

WebGL绘制的内容,最终都需要绘制到屏幕上。WebGL内容可以看做源图层,浏览器视窗可以看做目标图层,浏览器是如何处理两者的合成了?

实践出真知。如下,使用webgl绘制一个填充色是rgba(0, 0, 255, 0.5)的矩形,html body的背景色仍保持rgb(0,0,0)。

const vertextSource = `
  attribute vec2 a_position;
  void main(void) {
    gl_Position = vec4(a_position, 0, 1.0);
  }
`;

const fragmentSource = `
  precision mediump float;

  uniform vec4 v_color;

  void main() {
    gl_FragColor = v_color;
  }`;


// 核心js代码
// 设置顶点坐标属性
const vertexData = [-1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0];
setAttribute(gl, program, vertexData, "a_position");

const v_filter = gl.getUniformLocation(program, 'v_color');
gl.uniform4f(v_color, 0, 0, 1, 0.5);

合成色值是rgb(0, 0, 255),到此,表面上看 WebGL内容与浏览器内容的合成,并没有遵循浏览器合成的规则。

const gl = canvas.getContext("webgl", {
  premultipliedAlpha: true
});

浏览器默认视为,WebGL内容的rgb值已经与alpah值相乘了。因此,进行图层合成时,公式就变成如下:

color_{final} = color_{source} + color_{destination} * (1 - alpha_{source})color_{final} = color_{source} + color_{destination} * (1 - alpha_{source})

按照如上的公式,重新计算下合成结果,与取色结果是吻合一致的:

rgba =(0,0,255,0.5) + (0,0,0,1)*0.5
     =(0,0,255,1)

通过设置premultipliedAlpha = false,使得合成结果和CSS合成保持一致。

WebGL混合

如上介绍了WebGL和浏览器之间的合成过程,那么WebGL内部的图层是如何合成的了?

接下来,我们绘制同一个位置,绘制两个同样大小的矩形,第一个矩形的填充色是rgba(0, 0, 255, 0.5),第二个矩形的填充色是rgba(0, 255, 0, 0.5)。

  // 设置顶点坐标属性
  const vertexData = [-1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0];
  setAttribute(gl, program, vertexData, "a_position");

  const v_color = gl.getUniformLocation(program, 'v_color');
  gl.uniform4f(v_color, 0, 0, 1, 0.5);

  // 绘制第一个矩形:填充色是蓝色
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

  // 绘制第二个矩形,填充色是绿色
  gl.uniform4f(v_color, 0, 1, 0, 0.5);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

结果显示,第二次绘制的内容会完全替换第一次的绘制内容,然后再与浏览器背景色进行混合。

默认每一次绘制,WebGL都会清除当前位置的颜色缓冲区。这里,我们在同一个位置绘制两个矩形,结果就是第二次绘制的颜色会完全代替第一次绘制的颜色。

我们可以通过混合两种颜色,达到两种透明物体叠加的真实效果。WebGL允许我们开启混合模式,同时指定混合函数。

gl.enable(gl.BLEND); // 使用混合函数之前,必须激活混合能力

在混合过程中,即将写入缓冲区的颜色称为 source color,已经存在于缓冲区的颜色称为 destination color。颜色混合的数学公式如下:

color_{final} = combined(color_{source} * factor_{source} \ , \  color_{destination} * factor_{destination})color_{final} = combined(color_{source} * factor_{source} \ , \ color_{destination} * factor_{destination})

想得到混合颜色,需要指定源色的乘数因子、目标色的乘数因子,以及结合方程。

首先,通过gl.blendFunc指定源色和目标色的乘数因子,

void gl.blendFunc(sfactor, dfactor);

可选的乘数因子,如下表。乘数因子可以是一个常量,也可以是颜色的某几个分量。

乘数因子(来源于MDN)

比如,gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) 指定了源色的乘数因子是源色的透明分量,目标色的乘数因子是1 减去 源色的透明分量。

  • 源色与乘数因子的乘积: (0, 1, 0, 0.5) * (0.5, 0.5, 0.5, 0.5);
  • 目标色与乘数因子的乘积: (0, 0, 1, 0.5) * (1 - 0.5, 1- 0.5, 1 - 0.5, 1 - 0.5);

接下来,需要通过 gl.blendEquation 指定源色乘积和目标色乘积的结合方式:

void gl.blendEquation(mode);

mode可以是如下其一:

  • gl.FUNC_ADD,源色乘积 与 目标色乘积相加。该值是默认值;
  • gl.FUNC_SUBTRACT,源色乘积 与 目标色相减;
  • gl.FUNC_REVERSE_SUBTRACT,目标色乘积与源色乘积相减;

在熟悉以上的理论知识之后,我们开启混合功能,并按照浏览器的合成公式,指定WebGL的混合函数:

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

蓝色矩形先和浏览器黑色背景混合,进入颜色缓冲区,然后绿色矩形再与颜色缓冲区的颜色混合,最终呈现如下效果。

参考

Alpha Blending and WebGL

\