Skip to content

到现在为止,我们还没有接触过如何为模型增加透明度效果,大家也许会说,在模型的顶点缓冲信息中,改变颜色的 alpha 值不就是增加透明度了吗?没错,透明度信息是这么传入着色器中的,但是仅仅有透明度信息,并不能实现透过前面透明物体看到后面物体的效果,我们只能改变前面物体的颜色,后面的物体在深度检测阶段会检测失败,相应的片段就会被抛弃,不被渲染。

片元舍弃

片元是像素的前置阶段,片元在成为屏幕像素之前要经历一些检测过程,如果检测失败的话 GPU 会舍弃该片元,从而不被渲染。也就是说片元要么显示,要么不显示,不存在片元混合的现象。比如前后两个物体,处于后面的物体,由于被前面物体遮挡,深度测试就会失败,GPU 便会舍弃该片元不再进行渲染。

深度检测

下面这个例子:

javascript
let face1 = createFace(2, 3, 1, [2550, 0, 125]);
let face2 = createFace(3, 2, 0, [0, 255, 0, 255]);

创建了两个矩形,face1 为世界空间下深度值为 1 的半透明红色矩形,face2 为世界空间下深度值为 0 的不透明绿色矩形,此时 face1 在face2 的前面。当开启深度检测的时候,face1 会遮挡住 face2,大家猜猜看能不能透过红色矩形看到后面的绿色矩形?

下图是显示结果:

事实上,由于深度检测,尽管 face1 的颜色信息包含半透明,但是 GPU 检测到 face2 的片元在 face1 的后面,基于深度检测机制,GPU 舍弃了 face2 的片元,使得我们只能看到红色的face1。

主动舍弃片元

舍弃片元的时机除了深度测试不通过以外,我们还可以用编程的手段来主动舍弃。 这涉及到一个着色器命令discard

下面这段代码演示了舍弃片元操作:

glsl
varying vec4 v_Color;
uniform bool u_Discard;
void main(){
    if(v_Color.r < 0.8){
        if(u_Discard)
        discard;
    }
    gl_FragColor = v_Color;
}

当我们启用 discard 命令时候,片元着色器就会判断当前片元颜色的 r 分量是否小于 0.8 ,小于 0.8 的话,就将该片元舍弃不再渲染。

那么,如何实现真正的透明效果呢?这就是本节要讲的混合Blending技巧,混合的实质就是将前后多个重叠模型的颜色混合成一种新的颜色,该颜色包含前后多个模型的颜色信息。换句话说,这种技术能够实现透过透明物体(也可以是其他有透明度的物体)看到后面的物体效果。

透明度

我们知道,透明度是一个从 0 到 1 之间的浮点数,0 代表全透明,1 代表不透明,介于 0 和 1 之间的数值代表半透明。

全透明的特点是能够让后面物体的颜色完全体现出来,半透明效果体现的颜色则是透明物体和后面物体颜色的混合颜色,比如当透明度为 0.5 时,显示的颜色由自身颜色的一半和后面物体颜色的一半混合而成,不透明则是完全不显示后面物体的颜色成分。

如何实现混合

那么,如何实现颜色的混合呢?

首先我们需要开启混合特效。

javascript
gl.enable(gl.BLEND);

接下来我们需要告诉 GPU 如何混合两种颜色。

下面是 WebGL 混合两种颜色的方式之一:

$ Result = C_{source} * F_{source} + C_{dest} * F_{dest} $

此处我们以相加的方式来混合两种颜色,事实上,WebGL 可以设置运算方式,不一定是相加,也可以是相减、取两者中较大值、取两者中较小值、以及一些逻辑运算等。

思考如下一个场景,有一个绿色物体,在它前面放一个透明度为 0.6 的红色玻璃,那么透过红色玻璃看到绿色物体的颜色是什么呢?我们找出公式中四个参数:

  • $C_{source}$:源颜色,代表将要绘制的RGBA 颜色信息,用向量(Rs, Gs, Bs, As) 来表示,此处即红色玻璃的颜色RGBA(1, 0,0 ,0.6)。

  • $F_{source}$:源因子,代表将要绘制的颜色的透明度,用向量(Sr, Sg, Sb, Sa)来表示,此处代表红色玻璃使用的透明度因子,我们可以采用玻璃自身的透明度,也可以重新设置。

  • $ C_{dest}$:目标颜色的颜色信息,用向量(Rd, Gd, Bd, Ad)来表示,此处代表绿色物体的RGBA颜色。

  • $F_{source}$:目标(绿色物体)颜色的透明度因子,用向量(Dr, Dg, Db, Da)来表示。通常我们用 1 减去源颜色(即玻璃)的透明度因子作为目标颜色的透明度因子,即(1- Sr, 1- Sg, 1- Sb, 1- Sa)。

需要记住的是,这四个参数都是 4 维向量,分别代表 RGBA 对应位置的信息。

因子设置

那么,源颜色和目标颜色是已知的,我们只需要设置好源因子和目标因子就可以了,WebGL 为我们内置了一些因子:

参数
gl.ZERO(0,0,0,0)
gl.ONE(1,1,1,1)
gl.SRC_COLOR(Rs, Gs, Bs, As)
gl.DST_COLOR(Rd, Gd, Bd, Ad)
gl.ONE_MINUS_SRC_COLOR(1- Rs, 1- Gs, 1- Bs, 1- As)
gl.ONE_MINUS_DST_COLOR(1- Rd, 1- Gd, 1- Bd, 1- Ad)
gl.SRC_ALPHA(As, As, As, As)
gl.DST_ALPHA(Ad,Ad, Ad, Ad)
gl.ONE_MINUS_SRC_ALPHA(1-As, 1- As, 1- As, 1- As)
gl.ONE_MINUS_DST_ALPHA(1-Ad, 1- Ad, 1- Ad, 1- Ad)
gl.CONSTANT_COLOR常量颜色的RGBA值
gl.ONE_MINUS_CONSTANT_COLOR1 减去常量颜色的 RGBA值
gl.CONSTANT_ALPHA常量透明度
gl.ONE_MINUS_CONSTANT_ALPHA1减去常量透明度

以上就是内置的因子系数,这些系数都是包含 4 个分量的向量。

请一定要分清源颜色和目标颜色:

  • 源颜色:将要绘制的颜色,通常指将要绘制的拥有透明度物体的颜色。
  • 目标颜色:已经绘制的颜色,通常指被透明度物体盖住的后面物体的颜色。

有了这四个参数,GPU 就会按照如下公式替我们计算像素的最终颜色了:

$ \begin{aligned} Result &= (1, 0, 0, 0.6) * (0.6, 0.6, 0.6, 0.6)\\ \\ &+ (0,1,0, 1) * (0.4,0.4,0.4,0.4) \
\
&=(0.6, 0.4, 0, 0.76) \end{aligned} $

源颜色因子和目标颜色因子也都是 4 维向量,也就是(0.6, 0.6, 0.6, 0.6)和(0.4, 0.4, 0.4,0.4)

上面我们是将源颜色和目标颜色的RGBA 信息乘以一个固定的向量因子,事实上,WebGL 允许我们单独为 RGB 和 Alpha 设置对应的因子,使用 gl.blendFuncSeparate函数可完成该功能。

javascript
gl.blendFuncSeparate(src_rgb_factor,  des_rgb_factor, src_alpha_factor,  des_alpha_factor)

其中:

  • src_rgb_factor: 代表源颜色的 RGB 因子。
  • des_rgb_factor: 代表目标颜色的RGB 因子。
  • src_alpha_factor:代表源颜色的透明度部分的因子。
  • des_alpha_factor:代表目标颜色的透明度部分的因子。

假设我们需要将源颜色的 RGB 因子设置为源颜色的透明度,源颜色的透明度因子采用自身的透明度,目标颜色的 RGB 因子设置为 1,目标颜色的透明度因子设置为 0,那么我们可以这样设置:

javascript
gl.blendFuncSeparate(gl.SRC_ALPHA,  gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ZERO);

混合公式设置

上面我们以源颜色和混合颜色的加法公式来计算,WebGL 还提供了减法、逻辑运算等方式来计算最终颜色。使用gl.blendEquation来指定运算规则,规则有以下几种:

  • gl.FUNC_ADD:相加。
    • 源颜色 * 源因子 + 目标颜色 * 目标因子
  • gl.FUNC_SUBTRACT:相减。
    • 源颜色 * 源因子 - 目标颜色 * 目标因子
  • gl.FUNC_REVERSE_SUBTRACT:反减。
    • 目标颜色 - 源颜色。

混合实战

以上就是混合的理论,比较简单,主要是针对颜色和透明度的设置,接下来,我们就要进入实战阶段了。

混合前的注意事项

在写代码之前,我们再回顾一下深度测试的概念。

深度测试是这样的,GPU 绘制一个像素点的时候,会先检测当前位置是否存在片元,如果存在片元,则比较即将绘制的片元与已经存在的片元之间的 Z 值深度,距离屏幕远的片元会被舍弃,保留距离近的片元,同时将深度缓存值更新为最近保留的片元深度,如此循环测试,最终保留离屏幕最近的片元,完成遮挡效果。

在混合之前,我们需要考虑这个问题。因为场景中透明物体和不透明物体的顺序是不确定的,那如何绘制才能保证正确的混合和正确的遮挡呢?

记住下面这两个准则:

  • 透明物体在不透明物体前面时,混合颜色。
  • 透明物体在不透明物体后面时,不混合颜色,采用深度检测遮挡。

对应的策略是:

  • 将透明物体和不透明物体区分开。
  • 首先开启深度更新功能gl.depthMask(true),关闭混合功能gl.disable(gl.BLEND)。绘制所有不透明物体,绘制完后,深度缓存里保留的是离屏幕最近的物体的深度信息。
  • 对透明物体由远及近进行排序。
  • 开启混合功能,关闭深度更新功能,按照顺序对透明物体绘制。

绘制两个物体

准备两个物体,第一个物体是一个红色玻璃,透明度为 0.5,第二个物体是一个不透明的绿色箱子,红色玻璃在前,绿色箱子在后面。

javascript
//开启混合功能
gl.enable(gl.BLENDING);
let face1 = createFace(2, 3, 1, [2550, 0, 125]);
let face2 = createFace(3, 2, 0, [0, 255, 0, 255]);

源透明因子采用源目标的透明度,目标透明因子用 1减去源透明因子。

javascript
gl.blendFunc(gl.SRC_ALPHA,  gl.ONE_MINUS_SRC_ALPHA);

混合相加

设置成混合相加,这也是默认的混合方式:

javascript
gl.blendEquation(gl.FUNC_ADD);

效果如下:

混合相减

javascript
gl.blendEquation(gl.FUNC_SUBTRACT);

效果如下:

可以看到,中间相交部分的颜色变成了黑色,大家用上面的公式算一下就会理解。

$ \begin{aligned} C_{result} &= (1 * 0.5 - 0 * 0.5, 0 * 0.5 - 1 * 0.5, 0 * 0.5 - 0 * 0.5, 0.5 * 0.5 - 0.5 * 1 ) \
& = (0.5, -0.5, 0, -0.25) \
& = (0.5, 0, 0, 0) \end{aligned} $

透明度是 0 ,所以显示黑色。

混合反减

javascript
gl.blendEquation(gl.FUNC_SUBTRACT);

红色相间部分为半透明绿色,验证步骤如下:

$ \begin{aligned} C_{result} &= ( 0 * 0.5 - 1 * 0.5 , 1 * 0.5 - 0 * 0.5, 0 * 0.5 - 0 * 0.5, 0.5 * 1 - 0.5 * 0.5 )\
& = (-0.5, 0.5, 0, 0.25) \
& = (0, 0.5, 0, 0.25) \end{aligned} $

回顾

本节,我们讲述了混合的使用技巧,学习了透明颜色的计算方式,并进一步理解了深度测试与深度保持的意义。

下一节,我们学习 WebGL 的另一个重要技术:帧缓冲