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坐标」的相关概念。为了鉴于避免篇幅过长,这块概念迁移到《附录 ------ 第五节~第七节》,请读者自行查阅。

相关推荐
VaJoy3 天前
Cocos Creator Shader —— 附录
cocos creator
成长ing121384 天前
多层背景视差滚动Parallax Scrolling
cocos creator
似水流年wxk20 天前
cocos creator使用jenkins打包微信小游戏,自动上传资源到cdn,windows版运行jenkins
运维·jenkins·cocos creator
成长ing121382 个月前
点击音效系统
前端·cocos creator
blakeyi2 个月前
vscode保存自动刷新cocos creator编辑器
ide·vscode·cocos creator·热更新
烧仙草奶茶2 个月前
【cocos creator 3.x】3Dui创建,模型遮挡ui效果
ui·3d·cocos creator·cocos3d
糖墨夕3 个月前
【1】Coco2d creator资源管理注意事项 - meta 文件
前端·cocos creator·cocos2d-x
Setsuna_F_Seiei3 个月前
前端切图仔的一次不务正业游戏开发之旅
前端·游戏·cocos creator
jason_yang3 个月前
转眼间,已是十几年前的游戏代码了
cocos creator·游戏开发·cocos2d-x