💡 本系列文章收录于个人专栏 ShaderMyHead,欢迎订阅。
一、着色器片段(Chunk)
Cocos Creator 支持创建着色器片段文件(.chunk
格式)在跨文件中复用。在资源管理器某文件夹下点击右键,选择「创建 → 着色器片段」即可创建一个 Chunk 文件:

我们创建一个 normal-vert.chunk
文件,用来存放经常使用的顶点着色器代码:
c
precision highp float;
#include <cc-global>
#if USE_LOCAL
#include <builtin/uniforms/cc-local>
#endif
in vec3 a_position;
in vec2 a_texCoord;
out vec2 uv;
#if USE_LOCAL
in vec4 a_color;
out vec4 v_color;
#endif
vec4 vert() {
vec4 pos = vec4(a_position, 1);
#if USE_LOCAL
pos = cc_matWorld * pos;
v_color = a_color;
#endif
pos = cc_matViewProj * pos;
uv = a_texCoord;
return pos;
}
之后我们就可以在任意的 Effect 文件中,使用 #include
语句引入这个 Chunk 文件的相对路径来进行复用。
本文将介绍绿幕背景抠除技术(Chromakey)的实现,我们使用一个复用上方顶点着色器代码段的 Effect 文件来进行扩展开发:
c
CCEffect %{
techniques:
- name: chromakey
passes:
- vert: vs:vert
frag: fs:frag
blendState:
targets:
- blend: true
blendSrc: src_alpha
blendDst: one_minus_src_alpha
depthStencilState:
depthTest: false
depthWrite: false
}%
CCProgram vs %{
#include "../../resources/chunk/normal-vert.chunk" // 复用 Chunk
}%
CCProgram fs %{
precision highp float;
#include <sprite-texture>
in vec2 uv;
vec4 frag() {
vec4 color = texture(cc_spriteTexture, uv);
// 在这里扩展片元着色器的开发
return color;
}
}%
💡 更多 Chunk 相关的内容,可查阅官方文档《着色器片段》。
二、绿幕背景抠除
绿幕背景抠除技术在影视领域非常常见,泛指一个动画、影视片段通过后期处理,移除和替换其背景色(常规为比较单一的颜色,且不仅限于绿色)的能力。

💡 绿幕背景的颜色,一般会与前景的人和物的颜色尽可能地分开,方便后期做颜色分割的处理。例如拍摄中有演员穿了绿色的服装,则可以选用蓝色的背景作为"绿幕"。
在 Cocos Creator 中,我们可以通过剔除指定色值的顶点像素,来轻松移除一张图甚至一个动画的背景。
假设我们有一个 Animation Clip 动画,是由多张具有绿色渐变背景的 SpriteFrame 构成:

需要关注的点是,图片中的绿色背景并非单一的绿色,且前景的角色 RGB 通道中自然也包含有 G 通道的色值,我们无法只移除固定色值的像素。
因此「确定需移除像素的RGB分量范围」,是该技术的关键。
上方动画的渐变背景,我们可以通过自行取色的方式,来确定其渐变最深和最浅两色的色值:

可以看到其 G 通道的取值范围是 [86, 229]
,转换为着色器里的取值范围为:
js
[86/255, 229/255] ≈ [0.33, 0.91]
💡 计算机存储的基本单位是字节(1 Byte = 8 bit),而 8 bit 二进制数的取值范围正好是
0~255
,因此常规 RGB 各通道的色彩取值范围被限定为[0, 255]
,来节省颜色数据的存储空间。而在着色器领域,是使用浮点范围
[0.0, 1.0]
来表示颜色通道取值范围,因此本案例需要除以255
来转换为着色器支持的格式。
我们可以在片元着色器中先判断当前像素的 G 分量值是否处于此区间,是的话则使用 GLSL 内置的 discard
指令来抛弃该像素:
c
vec4 frag() {
vec4 color = texture(cc_spriteTexture, uv);
// 判断 G 通道分量值
if (color.g >= 0.33 && color.g <= 0.91) {
discard; // 使用 GLSL 内置的 discard 指令抛弃该像素,并退出当前片元着色器的执行
}
return color;
}
此时你会发现绿色渐变的背景被移除了,但角色也丢失了大部分像素:

这是因为很多较亮的颜色,其 RGB 的通道值都是比较大的,仅判断 G 通道会导致这些像素被命中和被剔除:

因此需要进一步判断 R 和 B 两个通道的取值范围,这两个通道分量取值范围是:
js
// R 通道取值范围
[15/255, 18/255] ≈ [0.06, 0.07]
// B 通道取值范围
[7/255, 14/255] ≈ [0.03, 0.05]
我们进一步扩展片元着色器代码,判断像素的 R 和 B 通道。鉴于往往前景色和绿幕背景色差别较大,所以边界的计算无需非常精确,例如只判断这两个通道分量值是否小于 0.08
,甚至 G 通道只需判断分量值是否大于 0.3
即可:
js
if (color.r < 0.08 && color.b < 0.08 && color.g > 0.3) {
discard;
}
执行效果如下:

细看会发现在个别帧里,依旧会出现一些预料之外的绿线(角色脚下位置)。
我们使用截图工具截取问题帧,对未被抠除的绿色像素进行取色,会发现其 R、B 两通道的值,超过了我们前面对背景渐变取色后设定的取值范围:

这是由于该动画在导出帧图片后,我使用了工具对每帧图片都做了压缩处理(来减小图片文件体积),进而出现部分像素的色彩失真。
针对此问题,我们尝试进一步增大 R 跟 B 俩通道的边界阈值(从 0.08
增大到 0.15
):
js
if (color.r < 0.15 && color.b < 0.15 && color.g > 0.3) {
discard;
}
再执行动画时,杂点均被清除:

💡 本文提供的是一个比较理想化的案例,如果背景色和前景色有很多相近的色值,则需要做不少额外处理(例如判断顶点坐标、使用蒙版,或者借用工具更换背景色等)。
最后在底部添加一个自定义的背景图节点,便最终实现了电影绿幕换底的后期能力:
