💡 本系列文章收录于个人专栏 ShaderMyHead:juejin.cn/column/7505...
一、在 Cocos Creator 中编写 Shader
在 Cocos Creator 中,着色器是以 Effect 文件(扩展名为 .effect
)的形式存在的,且需要在节点组件(例如 Sprite 组件)所使用的材质中,选用要使用的 Effect 文件:

在 Cocos Creator 的资源管理器中点击右键,选择「创建 - 材质 / 表面着色器」可以快速新建一个材质或 Effect 文件:

将新建好的 Effect 文件内容更改为如下代码(我们会在后文解释这段代码):
c
// 【示例代码 1.1】
CCEffect %{
techniques:
- name: dye-demo
passes:
- vert: vs:vert
frag: fs:frag
}%
CCProgram vs %{
precision highp float;
#include <cc-global>
in vec3 a_position;
vec4 vert() {
vec4 pos = vec4(a_position, 1);
return cc_matViewProj * pos;
}
}%
CCProgram fs %{
precision highp float;
vec4 frag() {
return vec4(0.0, 1.0, 0.0, 1.0);
}
}%
此时若有节点使用了此特效的材质,该节点会被染为绿色:

二、Effect 语法规范
Cocos Creator 中的着色器(Cocos Shader ,文件扩展名为 .effect
),是一种基于 YAML 和 GLSL 的单源码嵌入式领域特定语言(single-source embedded domain-specific language)。
其中 YAML 部分声明流程控制清单,GLSL 部分声明实际的 Shader 片段,这两部分内容相互补充,共同构成了一个完整的渲染流程描述。
💡 Cocos 官方提供了指引文档,读者可自行进行查阅。本系列文章则会由浅及深来逐步学习 Effect 文件的语法。
以上一节的示例代码 1.1 为例,一个简单的着色器文件可分为如下三部分:

2.1 CCEffect 规范
CCEffect
包裹了一段 YAML 格式的配置清单,用于声明当前 Effect 文件所包含的每个渲染技术(Technique)的名称(Name) 和 渲染过程(Pass)。
⑴ 渲染技术名称 name
下方代码段告诉了 Cocos Creator ------ 当前 Effect 文件包含两个技术,一个名为 tech1
,另一个名为 tech2
:
yaml
CCEffect %{
techniques:
- name: tech1 // 自定义渲染技术名称
passes:
- vert: t1-vs:vert
frag: t1-fs:frag
properties:
tilingOffset: { value: [1, 1, 0, 0] }
mainColor: { value: [1, 1, 1, 1], linear: true, editor: { type: color } }
...
- name: tech2 // 另一个自定义的渲染技术名称
passes:
- vert: t2-vs
frag: t2-fs
blendColor: { value: [1, 1, 1, 1] }
...
}%
在应用了该 Effect 文件的材质的属性检查器界面,可以指定该材质所要使用的渲染技术:

⑵ 渲染过程 passes
passes
包含了两个必填的配置参数 vert
和 frag
,它们分别告诉 Cocos Creator 当前的渲染技术所对应的顶点着色器和片元着色器的 CCProgram
代码段在哪里,以及代码段内的着色器入口函数名是什么。
这两个配置参数的填写值格式为: CCProgram 声明的代码段名称 : 入口函数名称
, 冒号及后半段的 入口函数名称
可以不填写,不填写时着色器入口函数名称将默认为 main
。
示例代码:
yaml
CCEffect %{
techniques:
- name: tech1
passes:
- vert: t1-vs:entry # 顶点着色器对应 CCProgram 声明的 't1-vs' 代码段,入口函数是 'entry'
frag: t1-fs:entry # 片元着色器对应 CCProgram 声明的 't1-fs' 代码段,入口函数是 'entry'
...
- name: tech2
passes:
- vert: t2-vs # 顶点着色器对应 CCProgram 声明的 't2-vs' 代码段,入口函数默认为 'main'
frag: t2-fs # 片元着色器对应 CCProgram 声明的 't2-fs' 代码段,入口函数默认为 'main'
}%
回看第一节的示例代码 1.1,它在 CCEffect
的 passes
中告诉了 Cocos Creator 名为 dye-demo
的染色技术所对应的顶点着色器代码段在 CCProgram vs
包裹处、顶点着色器入口函数名为 vert
、片元着色器代码段在 CCProgram fs
包裹处、片元着色器入口函数名为 frag
:

passes
还包含 properties
、pipelineStates
等可选配置参数,初学者可先不了解(后续文章用到时会做相关介绍),如有兴趣可到官方文档「Pass 可选配置参数」自行查阅。
2.2 CCProgram 规范
从前文可知 CCProgram
会声明一个着色器代码段的名称,并通过 %{ }
将着色器 GLSL 代码包裹起来。
关于 CCProgram
所包裹的代码段需要留意如下几点:
- 第一行必须声明当前着色器的精度值,常规固定写作
precision highp float;
即可; - 顶点着色器的入口函数必须返回一个
vec4
类型的数据(对应裁剪空间坐标); - 片元着色器的入口函数必须返回一个
vec4
类型的数据(对应 RGBA);
我们将在下一节学习 GLSL 语法。
三、GLSL 语法简介
3.1 主入口
GPU 的渲染管线要求着色器必须有一个名为 main
的函数作为执行起点,在着色器原生 GLSL 代码中可固定写为:
c
void main() {
// 着色器代码
}
在 Cocos Creator 的 Effect 文件中,所书写的 GLSL 代码并非与最终直接发送给显卡驱动程序的代码,故允许用户自定义各着色器的入口函数名称。
需留意的是原生的 GLSL 入口函数必须为 void
返回值,而 Effect 文件中的着色器入口函数必须返回 vec4
类型数据。
Cocos Creator 引擎在编译和打包项目时会预处理 Effect 文件,生成一个标准的 GLSL main
函数并调用你的自定义入口函数,并将自定义入口函数返回的 vec4
类型数据作为标准输出(例如在顶点着色器 main
函数中赋值给原生内置变量 gl_Position
)。
3.2 类型
计算机语言中的数据都会有"类型"来告诉系统这个数据应当是什么形式的、如何分配内存,GLSL 里自然也存在数据类型的概念。
与 typescript 不同,GLSL 在声明变量时,会把类型名称放在变量名之前:
c
// typescript 声明一个布尔值变量
const a: boolean;
// GLSL 声明一个布尔值变量
bool a;
主要的 GLSL 数据类型如下:
3.2.1 标量类型
表示单个值的基础类型:
-
浮点数 :
float
- 用于存储单精度浮点数(如坐标、颜色分量)。
- 示例:
float alpha = 0.5;
-
整数 :
int
- 用于整数计算(如循环计数、索引)。
- 示例:
int iterations = 10;
-
布尔值 :
bool
- 存储
true
或false
,用于条件判断。 - 示例:
bool isEnabled = true;
- 存储
💡 在 GLSL 中不存在原生的字符(
char
)或字符串(string
)等其它标量类型,因为 GLSL 专为 GPU 实时图形计算设计,主要处理数值数据(如坐标、颜色、矩阵、纹理采样),而非文本逻辑。
3.2.2 多维浮点向量类型
用于存储包含多个浮点数(float
)的向量,在图形编程中非常关键(尤其在处理坐标、颜色、变换和插值时)。 多维向量主要有:
vec2
:二维向量,包含 2 个浮点分量(x
,y
),常用于存放纹理坐标信息。vec3
:三维向量,包含 3 个浮点分量(x
,y
,z
),常用于存放位置、法线信息。vec4
:四维向量,包含 4 个浮点分量(x
,y
,z
,w
或者r
,g
,b
,a
),常用于存放齐次坐标(xyzw)、颜色(rgba)信息。
💡 齐次坐标的概念请查阅《附录 ------ 五、齐次坐标》。
可以通过分量名来访问多维向量里的分量值(或组合),示例:
c
vec2 v = vec2(2.0, 5.5); // 表示将 x=2.0, y=5.5 赋值给 a 变量
/** 使用分量名来获取对应的分量值 **/
float x = v.x; // 2.0
float y = v.y; // 5.5
vec4 v4 = vec4(1.0, 2.0, 3.0, 4.0);
/** 使用分量名的组合,来获取对应的分量值组合 **/
vac2 xy = v4.xy; // (1.0, 2.0)
vec4 reversed = v4.abgr; // (4.0, 3.0, 2.0, 1.0)
多维向量还支持扩展和计算,操作起来非常灵活:
c
/** 从低维向量扩展 **/
vec2 xy = vec2(0.5, 0.5);
vec4 v2 = vec4(xy, 0.0, 1.0); // (0.5, 0.5, 0.0, 1.0)
/** 多维向量运算 **/
vec4 a = vec4(1.0, 2.0, 3.0, 4.0);
vec4 b = vec4(0.5, 0.5, 0.5, 0.5);
vec4 c = a + b; // 结果为 (1.5, 2.5, 3.5, 4.5)
vec4 d = a * 2.0; // 结果为 (2.0, 4.0, 6.0, 8.0)
💡 扩展 ------ 为什么需要
vec4
?
- 齐次坐标的数学需求 :
三维坐标通过添加齐次坐标系里的w
分量来支持平移变换和透视投影。- GPU 硬件优化 :
四维向量与 GPU 的 SIMD(单指令多数据)架构对齐,运算效率高。- 数据对齐 :
OpenGL / WebGL 的缓冲区数据通常按 4 字节对齐,vec4
可避免填充浪费。
3.2.3 整数和布尔的多维向量类型
除了上述 vec
系列的浮点型多维向量,还有整数和布尔向量:
-
整数向量 :
ivec2
,ivec3
,ivec4
- 示例:
ivec2 pixelCoord = ivec2(100, 200);
- 示例:
-
布尔向量 :
bvec2
,bvec3
,bvec4
- 示例:
bvec3 check = bvec3(true, false, true);
- 示例:
3.2.4 矩阵类型(Matrix Types)
用于表示 2x2、3x3、4x4 的对角矩阵,常用于坐标变换(如模型视图投影矩阵):
-
浮点矩阵:
-
mat2
(2x2)、mat3
(3x3)、mat4
(4x4)。 -
示例:
c// 初始化矩阵 // [1.0, 0.0, 0.0, 0.0] // [0.0, 1.0, 0.0, 0.0] // [0.0, 0.0, 1.0, 0.0] // [0.0, 0.0, 0.0, 1.0] mat4 modelMatrix = mat4(1.0);
-
-
矩阵运算:
-
支持矩阵乘法、转置等操作。
-
示例(使用
mat4
把一个vec4
放大 2 倍):cmat4 modelMatrix = mat4(2.0); vec4 position = modelMatrix * vec4((1.0, 2.0, 3.0, 4.0); // vec4(2.0, 4.0, 6.0, 8.0) // 相当于 // position.x = 2.0*1.0 + 0.0*2.0 + 0.0*3.0 + 0.0*4.0 = 2.0 // position.y = 0.0*1.0 + 2.0*2.0 + 0.0*3.0 + 0.0*4.0 = 4.0 // position.z = 0.0*1.0 + 0.0*2.0 + 2.0*3.0 + 0.0*4.0 = 6.0 // position.w = 0.0*1.0 + 0.0*2.0 + 0.0*3.0 + 2.0*4.0 = 8.0
-
3.3 in 和 out 关键字
in
和 out
是用于在着色器阶段之间传递数据的关键字,in
表示从上一阶段输入,out
表示输出到下一阶段。
需留意的是,前一阶段的 out
变量与后一阶段的 in
变量名称和类型必须匹配。
示例:
c
// 顶点着色器
CCProgram vs %{
precision highp float;
#include <cc-global>
out vec3 color_green; // 声明一个 vec3 类型变量,它将输出到下一阶段(片元着色器)
vec4 vert() {
color_green = vec3(0.0, 1.0, 0.0); // 赋值
// 略...
}
}%
// 片元着色器
CCProgram fs %{
precision highp float;
in vec3 color_green; // 从上一阶段获取 vec3 类型的变量 color_green
vec4 frag() {
return vec4(color_green, 1); // 直接使用 color_green
}
}%
3.4 Cocos Creator 的内置着色器变量
为了提升开发效率和跨平台的兼容性,Cocos Creator 对 Shader 进行了深度封装、隐藏了底层的 GLSL 细节,开发者无需关心着色器数据缓存创建、数据传递、着色器编译等环节的实现。
Cocos Creator 提供了不少实用的内置变量,完整的变量清单请查阅《附录 ------ 一、Cocos Creator 内置着色器变量》,此处只列举常用的几个:
-
a_position
:顶点空间位置(x, y, z
),vec3
类型。 -
a_texCoord
:主纹理 UV 坐标,定义了顶点在 2D 纹理图像上的对应位置,vec2
类型。 -
a_color
:顶点颜色 RGBA 信息,vec4
类型。 -
cc_matViewProj
: 全局变量,表示视图投影矩阵,常用于转换顶点空间坐标到裁剪空间坐标。
💡 GLSL 本身也有不少内置的原生变量(见《附录 ------ 三、GLSL 内置变量和常量》),但鉴于 Cocos Creator 的高度封装,不推荐开发者使用 GLSL 的内置原生变量,因为此举可能破坏引擎的抽象层,导致 WebGL、Vulkan、Metal 等后端渲染接口的兼容性问题。
注意事项
-
使用 Cocos Creator 内置的全局变量(
cc_
开头的变量)前,需要先通过#include <cc-global>
引入声明了该全局变量的内置代码段(部分全局变量需引入的是#include <cc-local>
)。 -
a_
开头的内置变量仅能在顶点着色器中使用,且需要使用in
关键字引入。 -
片元着色器无法直接访问原始顶点数据,因此在顶点着色器中,若有内置变量的顶点数据需要传递给片元着色器,需要额外定义一个
out
变量去传递。
参考代码:
c
CCProgram vs %{
precision highp float;
#include <cc-global>
in vec3 a_position; // 使用 in 引入内置变量 a_position
in vec2 a_texCoord; // 使用 in 引入内置变量 a_texCoord
// 下一阶段(片元着色器)需要用到当前顶点的 UV 坐标(对应 a_texCoord),
// 故使用 out 定义一个输出变量来赋值和传递。
out vec2 uv0;
vec4 vert() {
vec4 pos = vec4(a_position, 1);
pos = cc_matViewProj * pos;
uv0 = a_texCoord; // 将当前顶点的 UV 坐标赋值给输出变量
return pos;
}
}%
CCProgram fs %{
precision highp float;
in vec2 uv0; // 从上一阶段(顶点着色器)获取 uv0 变量
#pragma builtin(local)
layout() uniform sampler2D cc_spriteTexture;
vec4 frag() {
vec3 green = vec3(0.0, 1.0, 0.0);
vec4 o = texture(cc_spriteTexture, uv0);
o.rgb = mix(green, o.rgb, 0.5);
return o;
}
}%
💡 扩展 ------ 为何片元着色器无法直接读取顶点数据?
顶点着色器和片元着色器并非一一对应的,例如一个三角形只有3个顶点,但可能会覆盖为成千上百个像素(片元),可以简单理解为这个三角形只会执行3次顶点着色器,但会在光栅化后,并行地执行成千上百次片元着色器,每次片元着色器里使用到的顶点数据都是一个平滑计算后的插值,而非直接从顶点着色器里获取的固定值。
四、节点染色代码分析
在了解了 Effect 和 GLSL 的基础语法后,我们再次回顾第一节「给节点染上绿色」的代码段:
c
CCEffect %{
techniques:
- name: dye-demo
passes:
- vert: vs:vert
frag: fs:frag
}%
// 顶点着色器代码段
CCProgram vs %{
precision highp float;
#include <cc-global> // 为了使用 cc_matViewProj 全局变量,需引入 cc-global
in vec3 a_position;
vec4 vert() {
vec4 pos = vec4(a_position, 1);
return cc_matViewProj * pos; // 通过 MVP 乘以空间顶点,得到裁剪空间 (clip space) 坐标并返回
}
}%
// 片元着色器代码段
CCProgram fs %{
precision highp float;
vec4 frag() {
return vec4(0.0, 1.0, 0.0, 1.0); // 固定返回绿色的 RGBA 色值
}
}%
在前文我们有提到,Effect 中的着色器入口函数都必须返回一个 vec4
类型的数据,其中片元着色器里需返回一个 RGBA 信息,告诉 GPU 应该把对应的像素点渲染为什么颜色。
那么从上面第 27 行的代码可以知道,每次片元着色器执行时,都告诉 GPU 应该渲染绿色的像素:
c
return vec4(0.0, 1.0, 0.0, 1.0); // 固定返回绿色的 RGBA 色值
这便是为何应用了该 Effect 材质的节点会被染成绿色。
而顶点着色器的入口函数必须返回一个裁剪空间坐标,这块功能在上方代码段的第 17、18 行中被实现:
c
vec4 pos = vec4(a_position, 1); // 扩展齐次坐标的 w 分量,便于后续计算
return cc_matViewProj * pos; // 通过 MVP 矩阵乘以空间顶点,得到裁剪空间 (clip space) 坐标
其中使用了 MVP(Model-View-Projection 视图投影)矩阵变换来获得裁剪空间,这是图形学、各游戏引擎实现的统一规范。
这里还需要着重了解裁剪空间 (clip space) 的概念 ------ 它是图形管线中空间转换的一个重要知识点,也涉及到了「空间坐标」、「齐次坐标」、「NDC坐标」的相关概念。为了鉴于避免篇幅过长,这块概念迁移到《附录 ------ 第五节~第七节》,请读者自行查阅。