CocosEffect与Shader入门
从零开始学会CocosCreator 3.8.x的Shader编写。每个概念都有"为什么需要它"、"没有它会怎样"、生活比喻和完整代码示例。
一、Shader是什么?
"画画的配方"比喻
Shader就是GPU画画的"配方":
- 顶点着色器 = "定位配方":告诉GPU每个顶点画在哪里
- 片元着色器 = "上色配方":告诉GPU每个像素涂什么颜色
就像做菜有配方一样,不同的Shader配方能画出不同的效果:
- 默认配方:正常显示图片
- 灰度配方:把图片变灰
- 溶解配方:让图片逐渐消失
- 发光配方:给图片加光晕
顶点着色器 vs 片元着色器
| 特性 | 顶点着色器 | 片元着色器 |
|---|---|---|
| 比喻 | "定位置" | "上颜色" |
| 执行次数 | = 顶点数(4次/精灵) | = 像素数(可达百万次) |
| 核心任务 | MVP变换,计算屏幕位置 | 纹理采样,计算像素颜色 |
| 输入 | 顶点属性(位置、UV、颜色) | 插值后的顶点属性 |
| 输出 | gl_Position(屏幕坐标) | gl_FragColor(像素颜色) |
| 性能影响 | 小 | 大(百万倍放大) |
关键认知:为什么片元着色器性能影响大?
一个100×100的Sprite:
顶点着色器:4次(4个顶点)
片元着色器:10,000次(1万个像素)
比例 = 1 : 2500
全屏1080P画面:
顶点着色器:几百次
片元着色器:2,073,600次(207万像素!)
比例 = 1 : 10000+
→ 片元着色器的每一行代码都被放大了万倍以上!
→ 能放在顶点着色器的计算,绝不要放片元着色器
二、手把手:创建你的第一个Effect文件
这是学Shader的第一道门槛!跟着做,5分钟就能创建一个能运行的Effect文件。
步骤1:在CocosCreator中创建Effect文件
- 打开CocosCreator 3.8.x编辑器
- 在资源管理器(左侧面板)中,右键点击你想存放Effect的文件夹
- 选择 右键 → 创建 → Effect(或"新建 → Effect文件")
- 命名为
my-first-effect,回车确认 - 双击打开,你会看到Cocos自动生成的模板代码
步骤2:将Effect关联到Sprite
- 在场景中创建一个Sprite节点(右键层级管理器 → 创建 → 2D对象 → Sprite)
- 选中Sprite节点,在属性检查器(右侧面板)中找到Sprite组件
- 找到 CustomMaterial 属性(在Sprite组件下方)
- 把刚创建的
my-first-effect拖拽到 CustomMaterial 属性上 - 运行游戏,如果Sprite正常显示,说明Effect文件创建成功!
步骤3:验证Effect是否生效
修改Effect文件中的片元着色器,在main函数最后加一行:
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 强制输出红色
保存后回到编辑器,如果Sprite变成全红色,恭喜你——Effect文件已经生效了!
⚠️ 如果Sprite没有变红,检查:①CustomMaterial是否正确拖拽 ②Effect文件是否保存 ③控制台是否有编译错误
步骤4:恢复并开始自定义
验证成功后,把强制红色的那行删掉,恢复为正常的纹理采样代码。接下来我们学习Effect文件的完整结构。
三、CocosCreator中的Effect文件结构
Effect文件 = "配方手册"
一个Effect文件就像一本"配方手册",可以包含多套配方(technique),每套配方可以有多个步骤(pass)。
完整结构解析
custom-sprite.effect
│
├── CCEffect 块 ─── "配方目录"(定义属性和参数)
│ ├── techniques[] ─── "配方系列"
│ │ └── passes[] ─── "配方步骤"
│ │ ├── vert: 引用顶点着色器
│ │ ├── frag: 引用片元着色器
│ │ ├── depthStencilState ─── 深度/模板测试配置
│ │ ├── blendState ─── 混合模式配置
│ │ ├── rasterizerState ─── 光栅化配置
│ │ └── properties ─── 暴露给外部的参数
│ │ ├── alphaThreshold: { value: 0.5 }
│ │ └── u_gray: { value: 0.0 }
│
├── CCProgram sprite-vs ─── "定位配方"(顶点着色器代码)
│ ├── attribute ─── 输入的顶点属性
│ ├── uniform ─── 外部传入的统一变量
│ ├── varying ─── 传递给片元着色器的插值变量
│ └── void main() ─── 主函数
│
└── CCProgram sprite-fs ─── "上色配方"(片元着色器代码)
├── varying ─── 从顶点着色器接收的插值变量
├── uniform ─── 外部传入的统一变量
└── void main() ─── 主函数
一个完整的自定义Effect示例
// ==========================================
// CCEffect 块:定义配方参数和渲染状态
// ==========================================
CCEffect %{
techniques:
- name: opaque
passes:
- vert: custom-sprite-vs:vert // 引用下面的顶点着色器
frag: custom-sprite-fs:frag // 引用下面的片元着色器
depthStencilState: &depthStencilState
depthTest: false // 2D游戏通常关闭深度测试
depthWrite: false // 2D游戏通常关闭深度写入
blendState: &blendState
targets:
- blend: true // 开启混合
blendSrc: src_alpha // 源因子
blendDst: one_minus_src_alpha // 目标因子
rasterizerState:
cullMode: none // 不做背面剔除
properties: // 暴露给TypeScript的参数
alphaThreshold: { value: 0.5 }
u_gray: // 灰度开关
value: 0.0
editor: { tooltip: "灰度开关 0=正常 1=灰度" }
u_dissolve: // 溶解程度
value: 0.0
editor: { tooltip: "溶解程度 0=完整 1=完全溶解", range: [0, 1] }
}%
// ==========================================
// CCProgram 顶点着色器:定位配方
// ==========================================
CCProgram custom-sprite-vs %{
precision highp float;
// 输入属性(从VBO读取)
attribute vec3 a_position; // 顶点位置
attribute vec2 a_uv; // UV坐标
attribute vec4 a_color; // 顶点颜色
// Uniform变量(从CPU传入)
uniform mat4 u_mvp; // MVP矩阵
// 输出给片元着色器(会自动插值)
varying vec2 v_uv;
varying vec4 v_color;
void main () {
v_uv = a_uv; // 传递UV
v_color = a_color; // 传递颜色
gl_Position = u_mvp * vec4(a_position, 1.0); // MVP变换
}
}%
// ==========================================
// CCProgram 片元着色器:上色配方
// ==========================================
CCProgram custom-sprite-fs %{
precision highp float;
// 从顶点着色器接收(已插值)
varying vec2 v_uv;
varying vec4 v_color;
// Uniform变量
uniform sampler2D u_texture; // 纹理
uniform float u_gray; // 灰度开关
uniform float u_dissolve; // 溶解程度
void main () {
// 1. 采样纹理
vec4 color = v_color * texture2D(u_texture, v_uv);
// 2. 灰度效果
if (u_gray > 0.5) {
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
color.rgb = vec3(gray);
}
// 3. 溶解效果
if (u_dissolve > 0.0) {
float noise = fract(sin(dot(v_uv, vec2(12.9898, 78.233))) * 43758.5453);
if (noise < u_dissolve) {
discard; // 丢弃片元(透明)
}
// 边缘发光
float edge = smoothstep(u_dissolve - 0.05, u_dissolve, noise);
color.rgb += vec3(1.0, 0.3, 0.0) * (1.0 - edge) * 2.0;
}
gl_FragColor = color;
}
}%
关键概念详解
Uniform变量 —— "外部参数"
// 在TypeScript中设置Uniform
const mat = sprite.getMaterial(0);
mat.setProperty('u_gray', 1.0); // 开启灰度
mat.setProperty('u_dissolve', 0.5); // 溶解50%
Uniform就像函数的参数——Shader定义了"公式",Uniform是代入公式的"具体数值"。
varying变量 —— "顶点到片元的桥梁"
顶点着色器输出 varying → GPU自动插值 → 片元着色器接收 varying
例如:三角形3个顶点颜色分别是红、绿、蓝
→ GPU自动插值 → 三角形内部颜色平滑过渡
sampler2D —— "纹理采样器"
uniform sampler2D u_texture; // 声明纹理变量
vec4 color = texture2D(u_texture, v_uv); // 采样:从纹理的v_uv位置取颜色
四、CocosCreator 3.8.x内置Shader解析
sprite内置shader源码解读
CocosCreator 3.8.x的内置sprite shader做了以下事情:
// 顶点着色器(简化版)
void main() {
v_uv = a_uv; // 传递UV
v_color = a_color; // 传递颜色
gl_Position = u_mvp * vec4(a_position, 1.0); // MVP变换
}
// 片元着色器(简化版)
void main() {
vec4 color = v_color * CCSampleWithAlphaSeparated(u_texture, v_uv);
// CCSampleWithAlphaSeparated = 预乘Alpha采样
// 避免半透明边缘出现黑边
#if USE_ALPHA_TEST
if (color.a < alphaThreshold) discard; // Alpha测试
#endif
gl_FragColor = color;
}
内置Shader的关键特性:
- 预乘Alpha:
CCSampleWithAlphaSeparated处理了预乘Alpha,避免黑边 - Alpha Test:可通过
USE_ALPHA_TEST宏开关 - SRGB处理:自动处理颜色空间转换
ui-sprite-effect解读
ui-sprite-effect是Cocos内置的带特效Sprite Shader:
// 支持的效果:
// 1. 灰度 (Grayscale)
// 2. 颜色叠加 (Tint)
// 3. 闪烁 (Flash)
五、常见Shader编译错误及解决方法
初学者写Shader必然会遇到编译错误,不要慌!以下是最高频的错误和解决方案。
| 错误信息 | 原因 | 解决方法 |
|---|---|---|
ERROR: 0:XX: 'xxx' : undeclared identifier |
变量未声明 | 检查拼写,确认变量已用uniform/varying/attribute声明 |
ERROR: 0:XX: 'xxx' : syntax error |
语法错误 | 检查是否漏了分号;、大括号{}是否匹配 |
ERROR: 0:XX: 'varying' : declaration must precede statement |
varying声明位置不对 | GLSL要求所有声明在函数之前,不能放在函数中间 |
ERROR: 0:XX: 'precision' : illegal use of reserved word |
precision声明位置不对 | precision必须在CCProgram块的最开头 |
WARNING: 0:XX: 'xxx' : varying not read by fragment shader |
顶点着色器输出了varying但片元着色器没接收 | 检查顶点和片元着色器的varying声明是否一致 |
ERROR: Cannot find program xxx |
Effect中引用的CCProgram名称不存在 | 检查vert: xxx-vs:vert中的名称是否和CCProgram名称匹配 |
ERROR: xxx: property not found |
TypeScript设置了不存在的Uniform | 检查CCEffect的properties中是否声明了该参数 |
| 黑屏/全透明 | Shader编译通过但逻辑错误 | 用调试技巧:强制输出红色gl_FragColor = vec4(1,0,0,1)排查 |
Cocos内置Shader函数速查
| 函数名 | 用途 | 示例 |
|---|---|---|
CCSampleWithAlphaSeparated(sampler, uv) |
预乘Alpha纹理采样,避免黑边 | vec4 c = CCSampleWithAlphaSeparated(u_texture, v_uv) |
CCDecode(sampler, uv) |
解码纹理(ETC/ASTC压缩格式) | 配合CCSampleWithAlphaSeparated使用 |
CCRandom(v) |
伪随机数生成 | float r = CCRandom(vec2(12.98, 78.23)) |
unpackAlphaFromShared(sampler, uv) |
从共享Alpha通道提取Alpha | 用于Alpha分离纹理 |
💡 这些内置函数定义在Cocos引擎的chunk文件中。你可以在引擎目录
cocos/renderer/core/下找到它们。
六、自定义Shader实战
实战1:自定义灰度Shader
// gray-sprite.effect
CCEffect %{
techniques:
- passes:
- vert: sprite-vs:vert
frag: gray-fs:frag
properties:
u_grayAmount:
value: 0.0
editor: { range: [0, 1] }
}%
CCProgram sprite-vs %{
precision highp float;
attribute vec3 a_position;
attribute vec2 a_uv;
attribute vec4 a_color;
uniform mat4 u_mvp;
varying vec2 v_uv;
varying vec4 v_color;
void main() {
v_uv = a_uv;
v_color = a_color;
gl_Position = u_mvp * vec4(a_position, 1.0);
}
}%
CCProgram gray-fs %{
precision highp float;
varying vec2 v_uv;
varying vec4 v_color;
uniform sampler2D u_texture;
uniform float u_grayAmount;
void main() {
vec4 color = v_color * texture2D(u_texture, v_uv);
// 灰度混合:normal和gray之间插值
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
color.rgb = mix(color.rgb, vec3(gray), u_grayAmount);
gl_FragColor = color;
}
}%
// 使用灰度Shader
@ccclass('GrayEffect')
export class GrayEffect extends Component {
@property(Sprite)
sprite: Sprite = null;
@property(Material)
grayMaterial: Material = null;
setGray(isGray: boolean) {
if (isGray) {
this.sprite.material = this.grayMaterial;
} else {
this.sprite.material = this.sprite.sharedMaterials[0]; // 恢复默认材质
}
}
// 渐变灰度
tweenGray(duration: number) {
const mat = this.grayMaterial;
tween(mat)
.to(duration, { u_grayAmount: 1.0 })
.start();
}
}
实战2:自定义溶解Shader
// dissolve-sprite.effect
CCEffect %{
techniques:
- passes:
- vert: sprite-vs:vert
frag: dissolve-fs:frag
properties:
u_dissolveAmount:
value: 0.0
editor: { range: [0, 1] }
u_edgeWidth:
value: 0.05
editor: { range: [0, 0.2] }
u_edgeColor:
value: { r: 1.0, g: 0.3, b: 0.0, a: 1.0 }
}%
CCProgram sprite-vs %{
precision highp float;
attribute vec3 a_position;
attribute vec2 a_uv;
attribute vec4 a_color;
uniform mat4 u_mvp;
varying vec2 v_uv;
varying vec4 v_color;
void main() {
v_uv = a_uv;
v_color = a_color;
gl_Position = u_mvp * vec4(a_position, 1.0);
}
}%
CCProgram dissolve-fs %{
precision highp float;
varying vec2 v_uv;
varying vec4 v_color;
uniform sampler2D u_texture;
uniform float u_dissolveAmount;
uniform float u_edgeWidth;
uniform vec4 u_edgeColor;
// 简单的噪声函数(不需要额外纹理)
float random(vec2 st) {
return fract(sin(dot(st, vec2(12.9898, 78.233))) * 43758.5453);
}
void main() {
vec4 color = v_color * texture2D(u_texture, v_uv);
float noise = random(v_uv);
// 溶解:噪声值小于溶解量的片元被丢弃
if (noise < u_dissolveAmount) {
discard;
}
// 边缘发光效果
float edgeStart = u_dissolveAmount;
float edgeEnd = u_dissolveAmount + u_edgeWidth;
if (noise < edgeEnd) {
float edgeFactor = 1.0 - (noise - edgeStart) / u_edgeWidth;
color.rgb = mix(color.rgb, u_edgeColor.rgb, edgeFactor);
}
gl_FragColor = color;
}
}%
// 使用溶解Shader
@ccclass('DissolveEffect')
export class DissolveEffect extends Component {
@property(Sprite)
sprite: Sprite = null;
@property(Material)
dissolveMaterial: Material = null;
playDissolve(duration: number = 1.0) {
this.sprite.material = this.dissolveMaterial;
const mat = this.dissolveMaterial;
mat.setProperty('u_dissolveAmount', 0.0);
tween({ value: 0.0 })
.to(duration, { value: 1.0 }, {
onUpdate: (target) => {
mat.setProperty('u_dissolveAmount', target.value);
}
})
.start();
}
}
实战3:通过Shader合并不同纹理的渲染
核心思路:把多张小图合并到一张大纹理,通过Uniform传递UV偏移,让不同"图片"的Sprite用同一个纹理 → 可以合批!
// atlas-selector.effect - 通过UV偏移选择图集中的子图
CCEffect %{
techniques:
- passes:
- vert: atlas-vs:vert
frag: atlas-fs:frag
properties:
u_uvOffset:
value: { x: 0.0, y: 0.0 }
u_uvScale:
value: { x: 0.25, y: 0.25 }
}%
CCProgram atlas-vs %{
precision highp float;
attribute vec3 a_position;
attribute vec2 a_uv;
attribute vec4 a_color;
uniform mat4 u_mvp;
varying vec2 v_uv;
varying vec4 v_color;
void main() {
v_uv = a_uv;
v_color = a_color;
gl_Position = u_mvp * vec4(a_position, 1.0);
}
}%
CCProgram atlas-fs %{
precision highp float;
varying vec2 v_uv;
varying vec4 v_color;
uniform sampler2D u_texture;
uniform vec2 u_uvOffset;
uniform vec2 u_uvScale;
void main() {
// UV变换:从大图集中选择子区域
vec2 uv = v_uv * u_uvScale + u_uvOffset;
vec4 color = v_color * texture2D(u_texture, uv);
gl_FragColor = color;
}
}%
// 使用图集选择Shader
@ccclass('AtlasSelector')
export class AtlasSelector extends Component {
@property(Sprite)
sprite: Sprite = null;
// 4×4图集,每个子图占1/4
private readonly COLS = 4;
private readonly ROWS = 4;
selectSubImage(col: number, row: number) {
const mat = this.sprite.getMaterial(0);
const scaleX = 1.0 / this.COLS;
const scaleY = 1.0 / this.ROWS;
const offsetX = col * scaleX;
const offsetY = row * scaleY;
mat.setProperty('u_uvScale', { x: scaleX, y: scaleY });
mat.setProperty('u_uvOffset', { x: offsetX, y: offsetY });
}
}
为什么这能减少DrawCall?
原来:10个Sprite用10张不同小图 → 10个纹理 → 10个DrawCall
现在:10个Sprite用同一张大图 + 不同UV偏移 → 1个纹理 → 1个DrawCall!
所有Sprite使用同一个材质(同一个Effect + 同一张纹理)
→ 满足合批条件 → 自动合批 → 1个DrawCall搞定!
七、如何通过修改Shader减少DrawCall
原理回顾
DrawCall切换的原因:
纹理不同 → 新DrawCall
材质不同 → 新DrawCall
Shader不同 → 新DrawCall
混合模式不同 → 新DrawCall
Shader优化的核心:把"需要切换材质/纹理"的差异,变成"Uniform参数"的差异。
原来:正常Sprite用材质A,灰度Sprite用材质B → 2个DrawCall
现在:所有Sprite用材质A(包含灰度Uniform)→ 1个DrawCall
正常的:u_gray = 0.0
灰度的:u_gray = 1.0
注意:修改Uniform是否会打断合批?
关键问题:如果两个Sprite用同一个Material,但设置了不同的Uniform值,会打断合批吗?
答案:会!因为Material是共享的,修改Uniform会影响所有使用该Material的Sprite。
解决方案:使用Material实例化!
// ❌ 错误:直接修改共享材质,会影响所有Sprite
const mat = sprite.sharedMaterials[0];
mat.setProperty('u_gray', 1.0); // 所有用这个材质的Sprite都变灰!
// ✅ 正确:使用材质实例
const matInstance = sprite.getMaterial(0); // 获取实例
matInstance.setProperty('u_gray', 1.0); // 只影响当前Sprite
但是! 实例化材质后,每个Sprite的材质引用不同 → 不满足合批条件 → 打断合批!
终极解决方案:利用顶点颜色传递参数!
这是最关键的技巧:用顶点颜色传递Shader参数,所有Sprite共享同一个材质 → 不打断合批!
顶点颜色传参:完整端到端示例
核心思路:顶点颜色是每个Sprite独有的,不需要实例化材质。在Shader中读取顶点颜色的某个通道,根据通道值决定渲染效果。
第1步:编写支持顶点颜色传参的Effect
// gray-by-alpha.effect
CCEffect %{
techniques:
- passes:
- vert: sprite-vs:vert
frag: gray-fs:frag
properties:
alphaThreshold: { value: 0.5 }
}%
CCProgram sprite-vs %{
precision highp float;
attribute vec3 a_position;
attribute vec2 a_uv;
attribute vec4 a_color;
uniform mat4 u_mvp;
varying vec2 v_uv;
varying vec4 v_color;
void main() {
v_uv = a_uv;
v_color = a_color;
gl_Position = u_mvp * vec4(a_position, 1.0);
}
}%
CCProgram gray-fs %{
precision highp float;
varying vec2 v_uv;
varying vec4 v_color;
uniform sampler2D u_texture;
void main() {
vec4 color = v_color * texture2D(u_texture, v_uv);
// alpha通道 < 0.98 表示灰度(对应TypeScript中alpha=200/255≈0.78)
if (v_color.a < 0.98) {
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
color.rgb = vec3(gray);
}
// 恢复alpha为1.0(因为我们用alpha传参,不是真的半透明)
color.a = 1.0;
gl_FragColor = color;
}
}%
第2步:在TypeScript中设置顶点颜色
import { _decorator, Component, Sprite, Color } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('GrayByAlpha')
export class GrayByAlpha extends Component {
@property(Sprite)
sprite: Sprite = null;
setGray(isGray: boolean) {
const color = this.sprite.color;
this.sprite.color = new Color(
color.r,
color.g,
color.b,
isGray ? 200 : 255 // alpha=200表示灰度,alpha=255表示正常
);
}
}
第3步:验证合批成功
- 创建3个Sprite,都使用同一个Effect材质
- 第1个Sprite设置
alpha=200(灰度) - 第2个Sprite设置
alpha=255(正常) - 第3个Sprite设置
alpha=200(灰度) - 开启DisplayStats查看DrawCall
如果DrawCall = 1 → 合批成功!✅
如果DrawCall = 3 → 合批失败,检查材质是否共享
为什么这能合批?
3个Sprite使用同一个材质(同一个Effect + 同一张纹理)
→ 满足合批条件(hash相同、material相同、depthStencilState相同)
→ 自动合批 → 1个DrawCall!
区别只是顶点颜色不同(alpha=200 vs alpha=255)
→ 顶点颜色是VBO数据的一部分,不影响合批判断
→ 就像两个三角形颜色不同也能合批一样
注意事项:
- 这种方式"借用"了alpha通道,意味着Sprite不能同时是半透明的
- 如果需要同时支持半透明和灰度,可以用RGB通道的某个位来传参(如R通道的最低位)
- 顶点颜色传参的"带宽"有限,只能传递简单的开关或少量枚举值
八、Shader优化技巧
1. 使用lowp精度
// ❌ 不好:所有变量用highp(32位浮点)
precision highp float;
varying vec2 v_uv;
// ✅ 好:颜色和UV用lowp(8位浮点),节省GPU计算
precision lowp float; // 默认精度
varying lowp vec2 v_uv; // UV坐标不需要高精度
varying lowp vec4 v_color; // 颜色不需要高精度
uniform lowp sampler2D u_texture;
2. 减少纹理采样
// ❌ 不好:多次采样
vec4 c1 = texture2D(u_texture, v_uv);
vec4 c2 = texture2D(u_texture, v_uv + vec2(0.01, 0.0));
vec4 c3 = texture2D(u_texture, v_uv + vec2(-0.01, 0.0));
vec4 blur = (c1 + c2 + c3) / 3.0;
// ✅ 好:用数学近似代替多次采样
vec4 c1 = texture2D(u_texture, v_uv);
// 如果只需要简单的模糊效果,可以在顶点着色器中偏移UV
3. 避免分支
// ❌ 不好:if分支(GPU并行执行,分支导致部分核心空转)
if (u_gray > 0.5) {
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
color.rgb = vec3(gray);
}
// ✅ 好:用数学函数代替分支
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
color.rgb = mix(color.rgb, vec3(gray), step(0.5, u_gray));
// step(0.5, u_gray) = u_gray >= 0.5 ? 1.0 : 0.0
// mix(a, b, t) = a * (1-t) + b * t
4. 把计算从片元着色器移到顶点着色器
// ❌ 不好:在片元着色器中计算(执行百万次)
void main() {
float wave = sin(v_uv.x * 10.0 + u_time) * 0.01;
vec2 uv = vec2(v_uv.x + wave, v_uv.y);
gl_FragColor = texture2D(u_texture, uv);
}
// ✅ 好:在顶点着色器中计算(只执行4次)
void main() {
v_uv = a_uv;
float wave = sin(a_uv.x * 10.0 + u_time) * 0.01;
vec2 uv = vec2(a_uv.x + wave, a_uv.y);
v_uv = uv; // 传递偏移后的UV
gl_Position = u_mvp * vec4(a_position, 1.0);
}
九、Shader调试技巧
Shader没有断点,没有console.log,调试只能靠"看颜色":
// 调试技巧1:把变量映射到颜色
void main() {
float value = someCalculation();
// 把value映射到0-1范围,作为红色通道输出
gl_FragColor = vec4(value, 0.0, 0.0, 1.0);
}
// 调试技巧2:用颜色区分分支
void main() {
if (someCondition) {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色 = 条件成立
} else {
gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0); // 蓝色 = 条件不成立
}
}
// 调试技巧3:可视化UV坐标
void main() {
gl_FragColor = vec4(v_uv.x, v_uv.y, 0.0, 1.0);
// 左下角黑色(0,0),右下角红色(1,0),左上角绿色(0,1),右上角黄色(1,1)
}
十、深度测试、模板测试与Alpha混合(Cocos Effect配置速查)
💡 深度测试、模板测试、Alpha混合的完整原理和详细讲解,请参考 01-GPU渲染管线入门。本节只补充 Cocos Effect中的配置方法。
深度测试配置
CCEffect %{
techniques:
- passes:
- vert: sprite-vs:vert
frag: sprite-fs:frag
depthStencilState:
depthTest: false // 2D游戏通常关闭
depthWrite: false // 2D游戏通常关闭
}%
| 配置 | 2D游戏推荐 | 3D不透明物体 | 3D透明物体 |
|---|---|---|---|
depthTest |
false |
true |
true |
depthWrite |
false |
true |
false |
模板测试配置(Mask实现原理)
depthStencilState:
stencilTestFront:
stencilTest: true
stencilFunc: equal // 比较函数
stencilRef: 1 // 参考值
stencilReadMask: 0xFF
stencilPassOp: replace
stencilFailOp: keep
stencilZFailOp: keep
⚠️ 每个Mask = +2 DrawCall,嵌套Mask = +4 DrawCall。尽量减少Mask使用。
Alpha混合配置
CCEffect %{
techniques:
- passes:
- vert: sprite-vs:vert
frag: sprite-fs:frag
blendState:
targets:
- blend: true
blendSrc: src_alpha
blendDst: one_minus_src_alpha
}%
| 混合模式 | srcFactor | dstFactor | 适用场景 |
|---|---|---|---|
| 正常透明 | SRC_ALPHA | ONE_MINUS_SRC_ALPHA | 大部分半透明UI |
| 叠加 | SRC_ALPHA | ONE | 光效、火焰 |
| 正片叠底 | DST_COLOR | ZERO | 阴影 |
踩坑速查
- 透明物体必须关闭
depthWrite,否则遮挡后面的透明物体 - 透明物体必须从后往前渲染,否则混合结果不对
- 不要在片元着色器中写
gl_FragDepth,会禁用Early-Z - 每个Mask = +2 DrawCall,嵌套Mask = +4 DrawCall
- 如果只需要矩形裁剪,用Scissor Test(零开销),不要用Mask
十一、动手练习
练习1:创建一个闪白Shader
参考灰度Shader,创建一个闪白Shader:当u_flashAmount = 1.0时,Sprite全白;u_flashAmount = 0.0时,正常显示。
点击查看提示
在片元着色器中,用mix函数混合原色和白色:
vec4 white = vec4(1.0, 1.0, 1.0, color.a);
color = mix(color, white, u_flashAmount);
练习2:用顶点颜色控制闪白
把练习1的闪白Shader改为用顶点颜色alpha通道控制,实现不打断合批的闪白效果。
练习3:调试UV坐标
在片元着色器中输出UV坐标作为颜色:gl_FragColor = vec4(v_uv.x, v_uv.y, 0.0, 1.0),观察颜色分布,理解UV坐标的含义。