Cocos Creator Shader 入门 ⑶ —— 给染色节点设置透明度

50 阅读7分钟

💡 本系列文章收录于个人专栏 ShaderMyHead:juejin.cn/column/7505…

在上篇文章我们编写了最简单的一个着色器 Effect 文件,来为一个节点染上完全不透明的绿色:

  vec4 frag() {
    // 在片元着色器里固定返回绿色的 RGBA 色值,
    // 因为 A 值为 1.0,故完全不透明
    return vec4(0.0, 1.0, 0.0, 1.0);
  }

如果只想给节点设置 50% 的透明度,我们可能会觉得,把片元着色器入口函数返回的 Alpha 值更改为 0.5 即可:

  vec4 frag() {
    // A 值修改为 0.5
    return vec4(0.0, 1.0, 0.0, 0.5);
  }

但会发现该节点的透明度并没有发生任何变化(染色节点依旧是完全不透明的):

image.png

这是因为 GPU 从性能上考虑,会默认使用 opaque 的不透明方案来进行渲染(即完全不考虑 Alpha 值)。

一、开启 Blending 混合模式

在 Cocos Creator 的 Effect 里,要通知 GPU 使透明度生效,必须显式开启 Blending 混合模式。其开启方式很简单,在 passes 中补充混合状态相关配置即可:

      blendState:       # 混合状态配置
        targets:
        - blend: true                     # 开启混合模式
          blendSrc: src_alpha             # 源颜色(片元着色器输出的颜色)混合因子使用源颜色的 Alpha 值
          blendDst: one_minus_src_alpha   # 目标颜色(帧缓冲中的颜色)混合因子使用 (1 - 源颜色的Alpha值)

其中 blendSrc 指定了混合源(即片元着色器输出的新色值)的 RGB 混合因子,这里我们设置为 src_alpha,表示直接使用片元着色器输出色值的 Alpha 值(即 0.5)作为混合因子。

blendDst 指定了混合目标(缓存中的旧色值,可以理解为前一帧的色值)的 RGB 混合因子,这里我们设置为 one_minus_src_alpha,表示 1 减去片元着色器输出的 Alpha 值之后的计算结果(即 1 - 0.5 = 0.5),来作为混合因子。

根据上述配置,GPU 会把混合源的 RGB 分量都乘以 0.5,把混合目标的 RGB 分量也都乘以 0.5,再把两个计算结果加起来得到最终要渲染的 RGB 值:

// [ 源颜色 × src_alpha ] + [ 目标颜色 × (1 - src_alpha) ]
R = (0.5 * 源颜色R) + (0.5 * 目标颜色R);
G = (0.5 * 源颜色G) + (0.5 * 目标颜色G);
B = (0.5 * 源颜色B) + (0.5 * 目标颜色B);

image.png

此时节点已能按照预期渲染出 50% 透明度的效果:

image.png

💡 从该案例可以获悉,GPU 通过颜色加权混合来模拟透明效果,Alpha 值仅作为混合权重参考。

另外 blendSrcblendDst 的可选值清单如下:

名称含义说明 / 使用场景示例
one常数 1保留源或目标颜色全部值(不变)
zero常数 0完全舍弃颜色(结果为黑或透明)
src_alpha源颜色的 Alpha 值常用于半透明混合:src × alpha + dst × (1 - alpha)
one_minus_src_alpha1 - 源颜色的 alpha 值常用于目标颜色的反向衰减
dst_alpha目标颜色的 Alpha 值适用于反向混合、遮罩等高级透明处理
one_minus_dst_alpha1 - 目标颜色的 alpha 值用于源颜色按目标透明度做加权
src_color源颜色的 RGB 分量可实现基于自身颜色亮度混合,粒子特效中偶尔使用
one_minus_src_color1 - 源颜色的 RGB 分量反色混合,有时用于闪烁或特效
dst_color目标颜色的 RGB 分量粒子、特殊光照叠加中可能用到
one_minus_dst_color1 - 目标颜色的 RGB 分量偏暗调混合用法
src_alpha_saturatemin(src_alpha, 1 - dst_alpha),仅用于 blendSrc可防止过度叠加,适合绘制阴影或遮罩边缘
constant_color固定颜色(需另行设置)较少使用,需在代码中设定 gl.blendColor()
one_minus_constant_color1 - constant_color同上,反色混合
constant_alpha固定 Alpha 值(需另行设置)同样依赖 blendColor(),用于统一透明值控制
one_minus_constant_alpha1 - constant_alphaconstant_alpha 搭配使用

为了验证 blendSrcblendDst 仅用作源颜色和目标颜色的混合(而非直接修改节点的透明度),我们尝试把混合配置修改为:

      blendState: # 添加混合状态
        targets:
        - blend: true
          blendSrc: zero             # 源颜色(片元着色器输出的颜色)混合因子为 0
          blendDst: src_alpha        # 目标颜色(缓存中的旧颜色)混合因子使用源颜色的 Alpha 值

执行结果如下:

image.png

此时染色节点混合后的 RGB 为:

R = (0 * 源颜色R) + (src_alpha * 目标颜色R) = 0.5 * 目标颜色R;
G = (0 * 源颜色G) + (src_alpha * 目标颜色G) = 0.5 * 目标颜色G;
B = (0 * 源颜色B) + (src_alpha * 目标颜色B) = 0.5 * 目标颜色B;

相当于把目标颜色(底部的树叶图腾)的 RGB 分量都降低了 50%,表现为树叶图腾节点被染色节点覆盖的区域,其颜色强度减半。

二、关闭深度缓冲区

当我们克隆上述的半透明染色节点,且将它们叠加到一起时,会发现交叠部分的绿色透明度没有发生叠加:

image.png

这是在 GPU 的深度检测流程中出现的问题 —— 每次光栅化之后(片元着色器之前),GPU 都会通过深度缓存中获取信息,来判断当前像素是否被其它已渲染的像素挡住了?如果是的话会直接丢弃这个像素,进而节省大量的片元着色器执行时间。

上图两个节点交叠的部分,正是被 GPU 视为遮挡的部分,导致该区域更晚处理的像素被丢弃。

对于完全不透明的物体而言,GPU 的这块操作是无感且高效的,但对于带透明度的物体就会导致视觉上的错误。

为了处理此问题,对于携带透明度的物体需要手动加上 depthStencilState 配置,关闭像素在光栅化阶段把深度信息写入缓存的能力,绕过 GPU 后续在该物体区域的深度检测:

    - vert: vs:vert
      frag: fs:frag
      blendState: 
        targets:
        - blend: true
          blendSrc: src_alpha        
          blendDst: one_minus_src_alpha   
      depthStencilState:      # 新增 depthStencilState 配置
        depthWrite: false     # 关闭深度缓存写入

💡 3D 游戏中,物体若是不透明的,保持 depthWrite: true 会获得更佳性能。

修改后的效果如下:

image.png

可以看到两个染色节点中间交叠的部分显示出了更深的绿色,可以正常呈现出节点的层次感了。

💡 透明混合不是线性相加,而是按比例加权(由 blendSrcblendDst 决定),因此两个 50% 透明度的绿色节点交叠部分不会是 100% 不透明。

三、2D 游戏关闭深度测试

GPU 的深度检测(Depth Test)是基于 Z 轴数值的检测。

常规纯 2D 游戏的前后关系主要由层级决定(SiblingIndex),而不使用 Z 值(或默认所有节点都在一个深度平面,Z 值都是 0)。

因此对于纯 2D 的游戏而言,Effect 中通过配置 depthTest: false 禁用深度检测功能可以避免恼人的层级渲染问题,也可以减少少量的 GPU 计算功耗:

      # 纯2D游戏使用
      depthStencilState:
          depthTest: false        # 【新增】关闭深度测试
          depthWrite: false       # 关闭深度缓存写入

💡 关闭深度测试后,不写 depthWrite: false 虽然也不会有渲染上的视觉问题,但 GPU 仍然会将深度值(NDC 的 Z 分量)写入到深度缓冲区。显示地关闭深度缓存写入,可以额外提升些许性能。

四、遗留问题

最后其实还存在一个问题 —— 被染色的节点如果带有纹理(Sprite Frame),染色后我们只会看到单一的绿色,其原本的纹理图案完全丢失了:

Jun-13-2025 18-44-51.gif

我们将在下篇文章通过纹理采样来处理此问题。