Cocos Creator Shader 入门 (2) —— 给节点染色

💡 本系列文章收录于个人专栏 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 包含了两个必填的配置参数 vertfrag,它们分别告诉 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,它在 CCEffectpasses 中告诉了 Cocos Creator 名为 dye-demo 的染色技术所对应的顶点着色器代码段在 CCProgram vs 包裹处、顶点着色器入口函数名为 vert、片元着色器代码段在 CCProgram fs 包裹处、片元着色器入口函数名为 frag

passes 还包含 propertiespipelineStates 等可选配置参数,初学者可先不了解(后续文章用到时会做相关介绍),如有兴趣可到官方文档「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 标量类型

表示单个值的基础类型:

  1. 浮点数float

    • 用于存储单精度浮点数(如坐标、颜色分量)。
    • 示例:float alpha = 0.5;
  2. 整数int

    • 用于整数计算(如循环计数、索引)。
    • 示例:int iterations = 10;
  3. 布尔值bool

    • 存储 truefalse,用于条件判断。
    • 示例: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 系列的浮点型多维向量,还有整数和布尔向量:

  1. 整数向量ivec2, ivec3, ivec4

    • 示例:ivec2 pixelCoord = ivec2(100, 200);
  2. 布尔向量bvec2, bvec3, bvec4

    • 示例:bvec3 check = bvec3(true, false, true);

3.2.4 矩阵类型(Matrix Types)

用于表示 2x2、3x3、4x4 的对角矩阵,常用于坐标变换(如模型视图投影矩阵):

  1. 浮点矩阵

    • 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);
  2. 矩阵运算

    • 支持矩阵乘法、转置等操作。

    • 示例(使用 mat4 把一个 vec4 放大 2 倍):

      c 复制代码
      mat4 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 关键字

inout 是用于在着色器阶段之间传递数据的关键字,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坐标」的相关概念。为了鉴于避免篇幅过长,这块概念迁移到《附录 ------ 第五节~第七节》,请读者自行查阅。

相关推荐
LcGero11 小时前
TypeScript 快速上手:泛型与工具类型
typescript·cocos creator·游戏开发
LcGero1 天前
Cocos Creator 3.x 高维护性打字机对话系统设计与实现
cocos creator·打字机
LcGero2 天前
Cocos Creator 三端接入穿山甲 SDK
sdk·cocos creator·穿山甲
LcGero3 天前
Cocos Creator平台适配层框架设计
cocos creator·平台·框架设计
LcGero3 天前
Cocos Creator 业务与原生通信详解
android·ios·cocos creator·游戏开发·jsb
LcGero5 天前
TypeScript 快速上手:前言
typescript·cocos creator·游戏开发
Setsuna_F_Seiei5 天前
CocosCreator 游戏开发 - 多维度状态机架构设计与实现
前端·cocos creator·游戏开发
CodeCaptain3 个月前
cocoscreator 2.4.x 场景运行时的JS生命周期浅析
cocos creator·开发经验
CodeCaptain3 个月前
CocosCreator 3.8.x [.gitignore]文件内容,仅供参考
经验分享·cocos creator
VaJoy4 个月前
Cocos Creator Shader 入门 (21) —— 高斯模糊的高性能实现
前端·cocos creator