1. 混合介绍
首先,先简单的谈一下什么是混合。一个常见的例子就是绘制一个透明的对象,我们希望的效果是可以透过这个透明的对象,看见后面的内容。这个时候,我们就要用到混合了。
对于透明和不透明对象的渲染,从缓冲区的角度来说:
- 如果绘制一个不透明的对象,则只需要将采样到的颜色填入到对应的缓冲区即可,如果缓冲区已经有值了,直接覆盖即可
- 如果绘制一个透明对象,则不是简单的使用采样后的颜色覆盖缓冲区已经存在的颜色,而应该通过某种算法,将两种颜色进行混合
也就是说,如果绘制当前对象时,需要依赖颜色缓冲区中已经存在的值,我们就需要使用混合。
1.1 Three.js中,透明效果的混合方式
我们将透明对象的颜色的RGB值记为(因为包含R、G、B三个通道,这里记为一个向量),alpha值记为;因为Three.js会先渲染不透明对象,之后渲染透明对象,因此,在渲染透明对象时,不透明对象的颜色信息已经写入到颜色缓冲区中了,我们把在颜色缓冲区中的颜色,记为,颜色缓冲区中的透明信息,记为。
我们试着解释一下这里的混合公式,因为我们首先看到的是透明对象,但是因为它是透明的,最终的颜色透明对象只“贡献”一部分,这一部分的值是;另一部分颜色是由缓冲区中的颜色贡献的,它的值是。所以,混合后的颜色的公式就是。
那我们上面得到的公式怎么反映到代码中呢?在WebGL中,可以通过一组函数来定义混合的方式。
1.2 定义混合的一组函数
再回头看一下这个公式,,这是在渲染透明对象时的特定的混合方式,我们现在把这个公式拓展为更加通用的形式。对于这个通用的公式,需要指定三个参数(状态),来实现一种特定的混合:
- 和分别是需要混合的连个颜色的系数(因子)
- ,两个颜色分别与上面说的因子相乘,然后使用定义的操作再次进行计算
blendFunc(sfactor, dfactor)
这个函数用来定义和这两个系数的。这里需要注意的一点是blendFunc(sfactor, dfactor)接受的并不是具体的值。还拿透明对象举例,如果颜色的透明度为0.3,这里的调用方式不是blendFunc(0.3, (1-0.3)),而是传递描述性的宏定义值。
例如,透明混合的时候就是src的透明度,它对应的宏定义值是gl.SRC_ALPHA;是,它对应的宏定义值是gl.ONE_MINUS_SRC_ALPHA。所以,透明混合正确的调用是blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)。
关于sfacotr和dfactor的具体取值,可以查看MND文档,对于每个宏定义后有具体的描述。
blendEquation(mode)
这个函数用来定义,参数mode也是描述性的宏定义值。常见的混合模式有三种:
- 加法:
gl.FUNC_ADD。 - 减法:
gl.FUNC_SUBTRACT。 - 反向减法:
gl.FUNC_REVERSE_SUBTRACT。
还有通过扩展,或者新版本WebGL提供的其他混合模式,这里就不过多介绍,同样可以查看文档获取详情。
1.3 纠正一些错误的说法
前面为了说明白混合是什么、怎么混合,我简化了混合的公式。现在,纠正一下这些错误:
- 和不应该只是
RGB三个通道,还应该包括透明度,所以应该是一个四维向量。 - 系数因子和也应该是一个向量。
所以,混合公式应该写成。
这里的*既不是数量积,也不是向量积,是对应分量之间相乘。
对于gl.SRC_ALPHA,来说,它对应的;同理,gl.ONE_MINUS_SRC_ALPHA为。
到这里,我们也可以理清楚之前回避掉的一个问题,那就是透明度也需要混合。一旦透明度参与混合,那我们之前讨论的透明混合的公式也有一些问题。先看一个实际的例子:
这里省略了不必要的代码,场景就是两个平面,一个在前面的蓝色平面,透明度为0.6;一个在后面的红色平面,透明度为0.8。在这两个canvas下面,都有一个“透明提示”元素,用来观察canvas的透明度(即,颜色缓冲区中的alpha通道)。完整示例代码见本文末尾
{
const { scene, front, back } = createScene()
renderTo('#case1', scene) // 使用three.js默认的透明混合方式
}
{
const { scene, front, back } = createScene()
const fm = front.material
fm.blending = THREE.CustomBlending // 自定义混合模式
// 相当于调用blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
fm.blendSrc = THREE.SrcAlphaFactor
fm.blendDst = THREE.OneMinusSrcAlphaFactor
// 相当于调用blendEquation(gl.FUNC_ADD)
fm.blendEquation = HREE.AddEquation
renderTo('#case2', scene)
}
注:three.js在这里其实调用的并不是我们上面介绍的两个函数,但效果是一样的,后面会详细解释。详见本文后续2.1节,填坑:使用blendEquationSeparate和blendFuncSeparate兼容blendEquation和blendFunc
可以看到,我们自定义的混合模式和three.js默认的混合模式渲染的效果不一致,自定义的看起来“更透明”。为了解释清楚这个,我们就需要了解到另一组函数。
1.4 将颜色和透明度分开混合
我们模拟计算我们自定义的混合方式一下:
- 还没有绘制任何平面,场景为空,颜色缓冲区的色值全部为
- 绘制后面的红色平面,红色平面需要与颜色缓冲区混合。混合过程。
- 绘制前面的蓝色平面
- 没有和红色平面重叠的部分:
- 和红色平面重叠的部分
可以看到,三部分颜色分别为:
- 红色平面未重叠部分
- 蓝色平面为重叠部分
- 重叠部分
我们通过创建相同色值的元素比较验证一下:注意,这里要在创建WebGLRenderer时,指定premultipliedAlpha为false,这里涉及到浏览器的颜色混合,这个后面再说,一步一步来。详见本文后续2.2节,填坑:premultipliedAlpha属性
#case2-check .back {
background-color: rgba(calc(0.8 * 255) , 0, 0, 0.64);
}
#case2-check .blend{
background-color: rgba(calc(0.32 * 255) , 0, calc(0.6 * 255), 0.576);
}
#case2-check .front {
background-color: rgba(0 , 0, calc(0.6 * 255), 0.36);
}
可以看到,和CSS设置的颜色是一致的,这说明是按照我们设置的混合方式绘制的。
但是这个混合方式和Three.js中使用的有一些不同,看一下Three.js的源码来了解一下:
// src/renderers/webgl/WebGLState.js
function setBlending( blending, blendEquation, blendSrc, blendDst, blendEquationAlpha, blendSrcAlpha, blendDstAlpha, premultipliedAlpha ) {
//...
switch ( blending ) {
case NormalBlending:
gl.blendFuncSeparate( gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA );
break;
//...
}
}
这里使用了一个类似于blendFunc(sfactor, dfactor)的函数blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha);从函数的名字和参数就可以看出来,这个函数是对颜色和透明应用不同的系数。
Three.js中的gl.blendFuncSeparate( gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA );,混合模式取默认值gl.FUNC_ADD;因此,使用的混合公式是:
接下来,模拟计算一下:
- 还没有绘制任何平面,场景为空,颜色缓冲区的色值全部为
- 绘制后面的红色平面,红色平面需要与颜色缓冲区混合。混合过程。
- 绘制前面的蓝色平面
- 没有和红色平面重叠的部分:
- 和红色平面重叠的部分
可以看到,三部分颜色分别为:
- 红色平面未重叠部分
- 蓝色平面为重叠部分
- 重叠部分
同样使用CSS验证:
#case1-check .back {
background-color: rgba(calc(0.8 * 255) , 0, 0, 0.8);
}
#case1-check .blend{
background-color: rgba(calc(0.32 * 255) , 0, calc(0.6 * 255), 0.92);
}
#case1-check .front {
background-color: rgba(0 , 0, calc(0.6 * 255), 0.6);
}
简单总结一下:
blendFunc()函数对应着一个blendFuncSeparate()函数;同样,blendEquation(mode)函数对应着一个blendEquationSeparate(modeRGB, modeAlpha);就是可以分别设置RGB和Alpha的运算方式,相信读者应该可以理解,这里就不再写公式和举例了。
读到这里,读者应该理清楚了Material中以下的属性:
blendSrc:表示blendFuncSeparate(sRGB, dRGB, sAlpha, dAlpha)中的sRGB,如果blendSrcAlpha为null,这里同时表示sAlphablendDst:表示blendFuncSeparate(sRGB, dRGB, sAlpha, dAlpha)中的dRGB,如果blendDstAlpha为null,这里同时表示dAlphablendEquation:表示blendEquationSeparate(modeRGB, modeAlpha)中的modeRGB,如果blendEquationAlpha为null,这里同时表示modeAlphablendSrcAlpha:表示blendFuncSeparate(sRGB, dRGB, sAlpha, dAlpha)中的sAlphablendDstAlpha:表示blendFuncSeparate(sRGB, dRGB, sAlpha, dAlpha)中的sAlphablendEquationAlpha:表示blendEquationSeparate(modeRGB, modeAlpha)中的modeAlpha
需要注意的是,以上属性想要生效,需要将blending属性设置为THREE.CustomBlending。
2. Three.js混合相关的源码
// src/renderers/webgl/WebGLState.js
// 这个函数的参数我们在上边基本上都已经介绍过了
// 剩余的premultipliedAlpha后面解释
function setBlending(blending, blendEquation, blendSrc, blendDst, blendEquationAlpha, blendSrcAlpha, blendDstAlpha, premultipliedAlpha) {
// 不开启混合
if (blending === NoBlending) {
if (currentBlendingEnabled === true) {
disable(gl.BLEND);
currentBlendingEnabled = false;
}
return;
}
// 隐含条件 blending !== NoBlending 表示需要开启混合
if (currentBlendingEnabled === false) { // 如果当前没有开启混合
enable(gl.BLEND); // 开启混合
currentBlendingEnabled = true; // 设置flag
}
// 不是自定义混合
if (blending !== CustomBlending) {
// blending发生改变
// 或者 premultipliedAlpha发生改变
if (blending !== currentBlending || premultipliedAlpha !== currentPremultipledAlpha) {
// three.js预定义的一些混合方式 都是用 gl.FUNC_ADD混合模式
if (currentBlendEquation !== AddEquation || currentBlendEquationAlpha !== AddEquation) {
gl.blendEquation(gl.FUNC_ADD);
currentBlendEquation = AddEquation;
currentBlendEquationAlpha = AddEquation;
}
// 在premultipliedAlpha不同的时候
// 四种内置的混合方式处理不相同
// 关于这个属性,看完代码后详细介绍
if (premultipliedAlpha) {
switch (blending) {
case NormalBlending:
gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
break;
case AdditiveBlending:
gl.blendFunc(gl.ONE, gl.ONE);
break;
case SubtractiveBlending:
gl.blendFuncSeparate(gl.ZERO, gl.ONE_MINUS_SRC_COLOR, gl.ZERO, gl.ONE);
break;
case MultiplyBlending:
gl.blendFuncSeparate(gl.ZERO, gl.SRC_COLOR, gl.ZERO, gl.SRC_ALPHA);
break;
default:
console.error('THREE.WebGLState: Invalid blending: ', blending);
break;
}
} else {
switch (blending) {
case NormalBlending:
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); // 这个就是我们上边举例使用的那一段源码
break;
case AdditiveBlending:
gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
break;
case SubtractiveBlending:
gl.blendFuncSeparate(gl.ZERO, gl.ONE_MINUS_SRC_COLOR, gl.ZERO, gl.ONE);
break;
case MultiplyBlending:
gl.blendFunc(gl.ZERO, gl.SRC_COLOR);
break;
default:
console.error('THREE.WebGLState: Invalid blending: ', blending);
break;
}
}
currentBlendSrc = null;
currentBlendDst = null;
currentBlendSrcAlpha = null;
currentBlendDstAlpha = null;
currentBlending = blending;
currentPremultipledAlpha = premultipliedAlpha;
}
return;
}
// custom blending
// 之后就是自定义混合相关的代码
// 处理blend...Alpha是null时的情况
blendEquationAlpha = blendEquationAlpha || blendEquation;
blendSrcAlpha = blendSrcAlpha || blendSrc;
blendDstAlpha = blendDstAlpha || blendDst;
if (blendEquation !== currentBlendEquation || blendEquationAlpha !== currentBlendEquationAlpha) {
// 应用对应的属性
gl.blendEquationSeparate(equationToGL[blendEquation], equationToGL[blendEquationAlpha]);
currentBlendEquation = blendEquation;
currentBlendEquationAlpha = blendEquationAlpha;
}
if (blendSrc !== currentBlendSrc || blendDst !== currentBlendDst || blendSrcAlpha !== currentBlendSrcAlpha || blendDstAlpha !== currentBlendDstAlpha) {
// 应用对应的属性
gl.blendFuncSeparate(factorToGL[blendSrc], factorToGL[blendDst], factorToGL[blendSrcAlpha], factorToGL[blendDstAlpha]);
currentBlendSrc = blendSrc;
currentBlendDst = blendDst;
currentBlendSrcAlpha = blendSrcAlpha;
currentBlendDstAlpha = blendDstAlpha;
}
currentBlending = blending;
currentPremultipledAlpha = false;
}
2.1 填坑:使用blendEquationSeparate和blendFuncSeparate兼容blendEquation和blendFunc
抛开three.js的源码不谈,使用blendEquationSeparate和blendFuncSeparate完全可以实现blendEquation和blendFunc的功能。
甚至我猜测它的源码是这样写的:没有看过这里的源码,但功能上,确实就是下面这样
function blendFunc(mode) {
return blendEquationSeparate(mode, mode)
}
function blendEquation(sfactor, dfactor) {
return blendEquationSeparate(sfactor, dfactor, sfactor, dfactor)
}
因此,在Three.js中,不需要区分有没有设置blendSrcAlpha从而调用blendFuncSeparate或者blendFunc。在blendSrcAlpha为null时,给blendSrcAlpha赋值为blendSrc,然后直接调用blendFuncSeparate即可,这样就实现了和调用blendFunc一样的效果。
2.2 填坑:详见本文后续2.2节,填坑:premultipliedAlpha属性
Materila的premultipliedAlpha属性
我们现在最熟悉的例子就是对透明对象的混合,也就是Three.js内置的THREE.NormalBlending。我们来比较一下在premultipliedAlpha不同的情况下,混合的方式有什么区别:
// premultipliedAlpha为true
gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
// premultipliedAlpha为false
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
比较发现,只有第一个参数不同。这个参数表示srcRGB的系数。
premultipliedAlpha为true时,这个系数为1;premultipliedAlpha为false时,就是我们上面讨论的,为srcAlpha。
区别就是,需不需要使用srcAlpha乘srcRGB。
我们再回头看一下premultipliedAlpha这个属性的名字:“预先乘了Alpha”。
premultipliedAlpha为true时,表示已经预先乘了Alpha,就不需要再乘一次了,所以,系数使用1就可以。premultipliedAlpha为false时,表示没有预先乘Alpha,就需要与Alpha相乘。
WebGLRenderer的premultipliedAlpha属性
在这里,这个属性其实是获取canvas的webgl上下文时,传入的一个属性。可以查阅WebGL规范了解这个值。
If the value is true the page compositor will assume the drawing buffer contains colors with premultiplied alpha. If the value is false the page compositor will assume that colors in the drawing buffer are not premultiplied.
如果这个值为true,页面组合器会认为颜色缓冲区的颜色已经是与alpha相乘后的结果了;如果为false,则认为没有乘过。
浏览器的本质上,其实就是个绘制软件,它也需要进行混合。就像绘制3D对象一样,浏览器(或者更具体的组合器Composer)也需要将canvas和canvas元素“下面”的元素进行混合。透明的混合算法,也分为premultipliedAlpha为true和false的两种情况,应用的公式也是一样。
那我们使用CSS设置颜色与我们的例子进行比较时,为什么要设置canvas的premultipliedAlpha为false呢?
因为浏览器(组合器)认为CSSrgba()设置的色值,是没有预乘alpha的。因此我们要与它保持一致,让组合器认为canvas中颜色缓冲区中的色值,是没有预乘alpha的。这样,在组合器混合之后,才能得到相同的展示结果。
最后再强调一下,在一般情况下,如果没有特殊需求,不需要将webgl上下文中的premultipliedAlpha设置为false。因为最终在颜色缓冲区中的颜色,是已经混合完毕的颜色,里面已经包含了透明度的信息了,也就是说,它已经是premultiplied。