💡 本系列文章收录于个人专栏 ShaderMyHead:juejin.cn/column/7505...
在上篇文章我们编写了最简单的一个着色器 Effect 文件,来为一个节点染上完全不透明的绿色:
c
vec4 frag() {
// 在片元着色器里固定返回绿色的 RGBA 色值,
// 因为 A 值为 1.0,故完全不透明
return vec4(0.0, 1.0, 0.0, 1.0);
}
如果只想给节点设置 50% 的透明度,我们可能会觉得,把片元着色器入口函数返回的 Alpha 值更改为 0.5
即可:
c
vec4 frag() {
// A 值修改为 0.5
return vec4(0.0, 1.0, 0.0, 0.5);
}
但会发现该节点的透明度并没有发生任何变化(染色节点依旧是完全不透明的):

这是因为 GPU 从性能上考虑,会默认使用 opaque 的不透明方案来进行渲染(即完全不考虑 Alpha 值)。
一、开启 Blending 混合模式
在 Cocos Creator 的 Effect 里,要通知 GPU 使透明度生效,必须显式开启 Blending 混合模式 。其开启方式很简单,在 passes
中补充混合状态相关配置即可:
yaml
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 值:
js
// [ 源颜色 × 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);

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

💡 从该案例可以获悉,GPU 通过颜色加权混合来模拟透明效果,Alpha 值仅作为混合权重参考。
另外 blendSrc
和 blendDst
的可选值清单如下:
名称 | 含义 | 说明 / 使用场景示例 |
---|---|---|
one |
常数 1 |
保留源或目标颜色全部值(不变) |
zero |
常数 0 |
完全舍弃颜色(结果为黑或透明) |
src_alpha |
源颜色的 Alpha 值 | 常用于半透明混合:src × alpha + dst × (1 - alpha) |
one_minus_src_alpha |
1 - 源颜色的 alpha 值 |
常用于目标颜色的反向衰减 |
dst_alpha |
目标颜色的 Alpha 值 | 适用于反向混合、遮罩等高级透明处理 |
one_minus_dst_alpha |
1 - 目标颜色的 alpha 值 |
用于源颜色按目标透明度做加权 |
src_color |
源颜色的 RGB 分量 | 可实现基于自身颜色亮度混合,粒子特效中偶尔使用 |
one_minus_src_color |
1 - 源颜色的 RGB 分量 |
反色混合,有时用于闪烁或特效 |
dst_color |
目标颜色的 RGB 分量 | 粒子、特殊光照叠加中可能用到 |
one_minus_dst_color |
1 - 目标颜色的 RGB 分量 |
偏暗调混合用法 |
src_alpha_saturate |
min(src_alpha, 1 - dst_alpha) ,仅用于 blendSrc |
可防止过度叠加,适合绘制阴影或遮罩边缘 |
constant_color |
固定颜色(需另行设置) | 较少使用,需在代码中设定 gl.blendColor() |
one_minus_constant_color |
1 - constant_color |
同上,反色混合 |
constant_alpha |
固定 Alpha 值(需另行设置) | 同样依赖 blendColor() ,用于统一透明值控制 |
one_minus_constant_alpha |
1 - constant_alpha |
与 constant_alpha 搭配使用 |
为了验证 blendSrc
和 blendDst
仅用作源颜色和目标颜色的混合(而非直接修改节点的透明度),我们尝试把混合配置修改为:
yaml
blendState: # 添加混合状态
targets:
- blend: true
blendSrc: zero # 源颜色(片元着色器输出的颜色)混合因子为 0
blendDst: src_alpha # 目标颜色(缓存中的旧颜色)混合因子使用源颜色的 Alpha 值
执行结果如下:

此时染色节点混合后的 RGB 为:
js
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%,表现为树叶图腾节点被染色节点覆盖的区域,其颜色强度减半。
二、关闭深度缓冲区
当我们克隆上述的半透明染色节点,且将它们叠加到一起时,会发现交叠部分的绿色透明度没有发生叠加:

这是在 GPU 的深度检测流程中出现的问题 ------ 每次光栅化之后(片元着色器之前),GPU 都会通过深度缓存中获取信息,来判断当前像素是否被其它已渲染的像素挡住了?如果是的话会直接丢弃这个像素,进而节省大量的片元着色器执行时间。
上图两个节点交叠的部分,正是被 GPU 视为遮挡的部分,导致该区域更晚处理的像素被丢弃。
对于完全不透明的物体而言,GPU 的这块操作是无感且高效的,但对于带透明度的物体就会导致视觉上的错误。
为了处理此问题,对于携带透明度的物体需要手动加上 depthStencilState
配置,关闭像素在光栅化阶段把深度信息写入缓存的能力,绕过 GPU 后续在该物体区域的深度检测:
yaml
- 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
会获得更佳性能。
修改后的效果如下:

可以看到两个染色节点中间交叠的部分显示出了更深的绿色,可以正常呈现出节点的层次感了。
💡 透明混合不是线性相加,而是按比例加权(由
blendSrc
和blendDst
决定),因此两个 50% 透明度的绿色节点交叠部分不会是 100% 不透明。
三、2D 游戏关闭深度测试
GPU 的深度检测(Depth Test
)是基于 Z 轴数值的检测。
常规纯 2D 游戏的前后关系主要由层级决定(SiblingIndex
),而不使用 Z 值(或默认所有节点都在一个深度平面,Z 值都是 0
)。
因此对于纯 2D 的游戏而言,Effect 中通过配置 depthTest: false
禁用深度检测功能可以避免恼人的层级渲染问题,也可以减少少量的 GPU 计算功耗:
yaml
# 纯2D游戏使用
depthStencilState:
depthTest: false # 【新增】关闭深度测试
depthWrite: false # 关闭深度缓存写入
💡 关闭深度测试后,不写
depthWrite: false
虽然也不会有渲染上的视觉问题,但 GPU 仍然会将深度值(NDC 的 Z 分量)写入到深度缓冲区。显示地关闭深度缓存写入,可以额外提升些许性能。
四、遗留问题
最后其实还存在一个问题 ------ 被染色的节点如果带有纹理(Sprite Frame),染色后我们只会看到单一的绿色,其原本的纹理图案完全丢失了:

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