Skip to content

Lesson 044|Material、Effect、Technique、Pass、Shader 到底是什么关系? ​

Tags: #Creator2.x #Renderer #Material #Effect #Pass #ShaderDifficulty: ⭐⭐⭐⭐☆


🎯 本课目标 ​

学完本课后,你应该能够:

  1. 从一个 Sprite 反向说清楚 Material、Effect、Technique、Pass 和 Shader 的关系。
  2. 解释为什么修改一份材质,可能让多个对象同时变化。
  3. 解释为什么复制材质能隔离效果,却可能增加 DrawCall。
  4. 判断需求应该修改节点颜色、Material 参数、Pass 状态,还是 Shader 算法。
  5. 用最小场景和 Profiler 验证材质共享与合批变化,而不是凭感觉下结论。

🧩 上节课回顾 ​

上一课讲了 Label 为什么通常比 Sprite 更复杂。无论 Label 还是 Sprite,Renderer 最终都要回答两类问题:

text
画什么?
→ 顶点、UV、颜色、纹理

怎样画?
→ 使用什么 GPU 程序、混合规则、深度规则和模板规则

前一类数据主要来自组件、Assembler 和 RenderData;后一类规则由本课要讲的几个对象共同描述。

如果这些概念没有分清,项目中很容易出现两个相反的问题:

text
为了改变一个对象,误改了所有对象共享的材质;
为了避免共享,给每个对象复制材质,结果破坏合批。

一、先不要背定义:从一次“全员闪白”开始 ​

战斗界面中有 20 个敌人,它们使用同一套角色图片和材质。策划提出:

某个敌人受击时闪白 0.1 秒,其他敌人不变。

开发者取得这个敌人正在使用的材质,把“闪白强度”从 0 改成 1。结果不是一个敌人闪白,而是 20 个敌人一起闪白。

第一反应可能是:

text
是不是 Shader 写错了?
是不是合批以后分不出敌人了?
是不是 SpriteFrame 被修改了?

先不要改代码。这个现象至少说明:多个 Sprite 很可能引用同一个可修改对象。

text
Sprite A ─┐
Sprite B ─┼──→ Material M
Sprite C ─┘

修改 Material M 不是给 A 保存一个私有值,而是在修改三者共同引用的对象:

ts
const shared = { flash: 0 };
const enemyA = { material: shared };
const enemyB = { material: shared };

enemyA.material.flash = 1;
cc.log(enemyB.material.flash); // 1

问题首先是“共享对象被修改”,还不能直接断言是 Shader、合批或引擎 Bug。


二、一个 Sprite 提交给 GPU 时需要什么 ​

一个 Sprite 要出现在屏幕上,只有图片还不够。Renderer 至少要组合三部分信息:

text
几何数据
→ 顶点位置、UV、顶点颜色、索引

资源与参数
→ Texture、颜色、透明度、自定义参数

绘制程序与状态
→ Shader、Blend、Depth、Stencil、Cull

它们的高层关系是:

text
Renderable(Sprite 等组件)
        │
        ├── 提供几何、纹理和组件数据
        └── 引用 Material
                 ↓
       Material 保存具体参数
                 ↓
       Effect 提供可用渲染方案
                 ↓
       Technique 选择一组 Pass
                 ↓
       Pass 确定程序与 Render State
                 ↓
       Vertex / Fragment Shader 在 GPU 执行

这是一张职责图,不表示这些对象每帧都会按顺序重新创建。


三、Shader:GPU 真正执行的程序 ​

Shader 是运行在 GPU 上的程序。当前阶段先认识两个部分:

text
Vertex Shader
→ 处理每个顶点的位置,以及传给后续阶段的数据

Fragment Shader
→ 计算光栅化后每个片段的颜色

受击闪白主要发生在 Fragment Shader。下面是概念性伪代码:

glsl
vec4 baseColor = texture2D(mainTexture, uv);
vec3 finalRgb = mix(baseColor.rgb, vec3(1.0), flashStrength);
outputColor = vec4(finalRgb, baseColor.a);

flashStrength = 0 时保留原颜色,等于 1 时输出白色。

但 Shader 不知道“哪个敌人刚刚受击”。它不会去场景树寻找 Node,也不会主动读取业务组件。Renderer 必须先把业务状态转换成 Shader 能接收的数据,例如顶点颜色或 Material 参数。

Shader 决定算法,但不负责保存每个游戏对象的业务状态。


四、Pass:一次绘制具体怎样执行 ​

只有 Shader 代码还不能完成一次绘制。Renderer 还要知道:

text
是否开启透明混合?
源颜色和目标颜色怎样混合?
是否进行深度测试和深度写入?
是否使用 Stencil?
正反面是否被剔除?

Pass 可以理解为:

完成一次绘制所需的一组 GPU 程序和渲染状态。

text
Pass
├── Vertex Shader
├── Fragment Shader
├── Blend State
├── Depth State
├── Stencil State
└── Cull 等其他状态

同一段 Shader,在不同状态下也可能产生完全不同的结果。例如半透明角色关闭正确的 Blend 后,透明区域可能变成色块。

这也解释了为什么 Blend、Depth 或 Stencil 变化会打断合批:一次兼容提交不能同时使用两套互相冲突的 GPU 状态。


五、Technique:一套可以完成目标的渲染方案 ​

一个效果可能要面对不同平台、质量等级或渲染路径。Effect 可以提供不止一套方案,每套方案由一个 Technique 表达。

Technique 是为某种使用条件准备的一组 Pass。

概念上可以有:

text
Technique A:普通方案
└── Pass 1:绘制角色主体

Technique B:高质量描边方案
├── Pass 1:绘制放大的轮廓
└── Pass 2:绘制角色主体

两者区别是:

text
Technique 回答:选择哪一套渲染方案?
Pass 回答:方案中的某一次绘制怎样执行?

多一个 Pass 不只是配置多几行。它通常意味着再次准备状态、绑定程序和参数、提交几何,并让 GPU 再处理相关顶点和片段。因此多 Pass 效果要同时评估 DrawCall、覆盖像素和目标设备帧时间。


六、Effect:把程序、方案和参数组织起来 ​

如果项目只有零散的 Shader 代码,Renderer 仍不知道:

text
有哪些 Technique?
每个 Technique 有哪些 Pass?
Pass 使用哪些 Shader 和状态?
哪些参数允许 Material 配置?
参数的类型和默认值是什么?

Effect 负责组织这些信息。它描述“这个效果提供什么能力”,而不是某个敌人此刻的闪白值。

text
Shader:GPU 执行的程序逻辑
Effect:组织 Shader、Technique、Pass 和可配置属性的效果定义

同一组 Shader 可以在不同 Pass 状态下使用;一个 Effect 也可能组织多组程序或多个 Pass。

Creator 2.4.x 与 3.x 的 Effect 语法和内部结构不同。本课建立职责关系,不要求背私有字段,也不能跨版本直接照搬配置。


七、Material:给 Effect 填入具体数据 ​

Effect 描述“可以调什么”,Material 保存“这一次具体使用什么”。

假设 Effect 暴露:

text
mainTexture
tintColor
flashStrength

两份 Material 可以使用相同 Effect,却保存不同值:

text
Material Normal
├── Effect:HitFlashEffect
├── mainTexture:enemy.png
└── flashStrength:0

Material Hit
├── Effect:HitFlashEffect
├── mainTexture:enemy.png
└── flashStrength:1

所以 Material 不是 Shader 文件的另一个名字:

text
Effect 规定参数结构和渲染方案;
Material 选择并保存实际参数;
Renderable 引用 Material 参加绘制。

回到开头:

text
20 个敌人
→ 引用同一个 Material
→ Material.flashStrength 被改为 1
→ 20 个敌人读取到同一个 1
→ 全员闪白

第一层根因已经明确:开发者想修改单个对象的状态,却修改了多个对象共享的 Material。


八、复制 Material 为什么能修画面,却可能破坏合批 ​

最直接的修复是给受击敌人一份独立 Material:

text
普通敌人 A ─┐
普通敌人 B ─┼──→ Shared Material,flash = 0
普通敌人 C ─┘

受击敌人 D ────→ Instance Material,flash = 1

这样修改 D 不会影响 A、B、C。但“隔离状态”和“保持合批”是两个不同目标。

Renderer 合并连续对象时,需要绘制条件兼容:

text
几何格式兼容
+ Texture / Sampler 兼容
+ Shader 及变体兼容
+ Material 参数提交方式兼容
+ Blend / Depth / Stencil 兼容
+ 绘制顺序允许连续
= 才有机会进入同一批次

如果渲染顺序是:

text
A 普通 → B 普通 → D 闪白 → C 普通

提交可能变成:

text
提交 A、B 的普通批次
→ 切换到 D 的闪白状态并提交
→ 恢复普通状态并提交 C

C 不能简单回到已经结束的批次。透明对象通常也不能任意重排,否则会改变混合结果。

因此复制材质的完整结论是:

text
好处:单对象参数互不影响;
代价:可能增加材质实例、状态切换和 DrawCall。

是否真的拆批取决于 Creator 版本、参数传递方式、Shader 变体、纹理、节点顺序和平台,必须测量,不能把“可能”写成“一定”。


九、受击闪白应该改哪一层 ​

方案一:组件颜色或顶点颜色 ​

如果视觉需求能由顶点颜色表达,每个对象的颜色可以进入顶点数据,同时继续使用相同 Material 和 Texture,因此仍有机会合批。

但普通颜色相乘未必能准确实现“保留透明度并把非透明区域变白”。先验证视觉,再谈性能。

方案二:独立 Material 参数 ​

表达灵活,每个对象可以有不同闪白强度;但每对象 Uniform 或材质状态可能拆批。适合同时闪白对象较少,且实测成本可接受的场景。

方案三:共享少量状态 ​

如果一批对象总是在同一时间使用相同强度,可以按状态分组共享少量 Material,而不是每个对象复制一份。业务分组和渲染顺序仍会影响批次。

方案四:修改 Effect 或增加 Pass ​

溶解、描边、扭曲或额外采样通常需要改变 Shader/Effect。若增加 Pass,必须把额外绘制和像素成本纳入预算。

选择过程应该是:

text
确认视觉需求
→ 判断数据能否进入顶点
→ 判断是否需要独立参数
→ 检查共享范围和渲染顺序
→ 在目标设备测 DrawCall 与帧时间
→ 再决定实现

十、Creator 2.4.x 最小实验 ​

Material、Effect 选择和组件绑定都通过 Creator 编辑器完成。不要直接编辑 .effect、.mtl、.scene、.prefab 或它们的 .meta。

1. 建立共享基线 ​

在编辑器中:

  1. 在 Canvas 下放置 30 个相同 Sprite,并让它们连续排列。
  2. 所有 Sprite 使用同一个 SpriteFrame 和同一份 Material。
  3. 打开 Creator Profiler,记录稳定时的 DrawCall。

不要追求固定数字。Canvas、Label 和调试界面也可能贡献 DrawCall,我们只比较同一环境的前后变化。

2. 验证共享 ​

通过 Inspector 修改共享 Material 暴露的一个可观察参数,记录:

text
哪些 Sprite 同时变化?
它们是否引用同一份 Material?
恢复参数后是否一起恢复?

如果结果不同,检查组件是否取得了运行时实例、材质 slot 是否一致,以及当前 Effect 是否真正使用该参数。

3. 验证独立状态与顺序 ​

通过编辑器为一个 Sprite 准备并绑定另一份 Material,然后:

  1. 只修改这份材质,确认视觉是否隔离。
  2. 把特殊 Sprite 放到普通对象中间,记录 DrawCall。
  3. 把它移到队列末尾,再记录。
  4. 恢复共享 Material,确认是否回到基线。
实验组特殊材质位置视觉是否隔离DrawCall要验证的事
A无否实测共享基线
B队列中间是实测是否切断前后批次
C队列末尾是实测绘制顺序的影响
D恢复共享否实测是否回到基线

只有做完 D 组恢复实验,才能排除其他场景变化造成的偶然波动。

“材质文件名不同”不等于一定不能合批;“文件名相同”也不等于实际绘制状态一定相同。


十一、故障诊断 ​

修改一个对象,其他对象也变化 ​

text
1. 多个 RenderComponent 是否引用同一 Material?
2. 修改的是共享资源还是独立实例?
3. 参数属于 Material,还是写进每对象顶点数据?
4. 业务真正需要全局变化还是单对象变化?

不要一上来给所有对象复制材质,先确认共享是否原本就符合需求。

换 Material 后 DrawCall 增加 ​

检查批次边界两侧:

text
Effect / Technique / Pass
Shader 宏或变体
Texture / Sampler
Uniform 提交
Blend / Depth / Stencil
特殊对象的绘制顺序

然后用禁用、移动和恢复对象做单变量验证。

参数修改了但画面不变 ​

可能是:

text
Effect 没有暴露或使用该参数;
参数名或类型不匹配;
修改的不是当前材质 slot;
当前 Technique / Pass 没走到预期程序;
运行时实例与编辑器资源身份不同;
Shader 在目标平台编译失败或走了不同变体。

同时记录参数值、实际材质身份、Shader 编译日志和目标平台。

换材质后对象消失 ​

依次检查纹理绑定、顶点输出、Fragment 输出、Blend、Depth、Stencil、Shader 编译和平台支持。对象消失不只可能是颜色为零,也可能是顶点位置或深度、模板测试造成的。


十二、常见误区 ​

Shader 就是 Material ​

Shader 是 GPU 程序;Material 是 Effect 的具体参数配置;Effect 组织 Technique、Pass、程序和属性。

Material 不同就一定不能合批 ​

应比较 Renderer 实际使用的程序、资源、参数、状态、几何格式和顺序,不能只看文件名。

Material 相同就一定能合批 ​

Texture、Mask、顶点格式、Camera、顺序、缓冲容量和特殊组件仍可能形成边界。

修改 Uniform 只是改一个数字,没有成本 ​

CPU 改值很轻,不代表后续没有参数上传、状态切换或拆批。

多一个 Pass 只是多执行一段代码 ​

Pass 通常代表额外绘制步骤,也会增加顶点、片段、采样或带宽成本。

为了合批应该强制所有对象使用同一材质 ​

视觉正确性优先。Blend、Stencil 和透明顺序不能为了减少 DrawCall 被错误合并。


十三、练习 ​

练习一:需求应该进入哪一层 ​

  1. 所有按钮统一切换夜间主题颜色。
  2. 一个角色短暂闪白。
  3. UI 面板改变透明混合规则。
  4. 角色增加需要额外绘制的外轮廓。
  5. 只替换某个 Sprite 的图片。

练习二:分析批次 ​

text
A:普通共享材质
B:普通共享材质
C:独立闪白材质
D:普通共享材质
  1. 为什么 C 可能把普通对象拆成前后两个批次?
  2. 把 C 移到末尾为什么可能减少一次状态切换?
  3. 为什么仍必须用 Profiler 验证?
完成练习后展开参考答案

练习一 ​

  1. 可考虑统一共享 Material 参数,但先确认所有引用者都应变化。
  2. 先判断顶点颜色能否满足;不能满足再考虑独立 Material,并测拆批成本。
  3. Blend 属于 Pass 的 Render State。
  4. 需要改变 Effect/Technique/Pass;额外轮廓 Pass 通常意味着额外绘制。
  5. 修改 SpriteFrame/Texture;纹理变化也可能影响合批。

练习二 ​

  1. C 的程序、参数或状态需要独立提交,普通批次先结束,C 之后恢复普通状态又开启新批次。
  2. A、B、D 有机会先连续提交,之后只切换到 C。
  3. Creator 版本、参数传递、纹理、节点顺序和平台实现都会影响实际结果。

🚀 能力验收 ​

如果真正学会了本课,你应该能独立完成:

  • 构造三个 Sprite 验证 Material 是否共享;
  • 把特殊材质放在队列中间和末尾比较 DrawCall;
  • 判断视觉需求应该进入顶点数据、Material 参数还是新 Pass;
  • 面对“改一个全都变”时先检查对象身份和共享边界;
  • 完整解释下面这条关系,而不是只背五个定义。
text
Renderable
→ Material
→ Effect
→ Technique
→ Pass
→ Vertex Shader / Fragment Shader

✅ 本课总结 ​

text
Renderable 提供要画的几何和资源。
Material 保存一次具体使用的参数。
Effect 描述可用的渲染方案和属性。
Technique 是一套由一个或多个 Pass 组成的方案。
Pass 组合一次绘制所需的 Shader 与 Render State。
Shader 是最终运行在 GPU 上的程序。

工程中还要记住:

text
共享 Material:容易统一和复用,但修改会影响所有引用者。
独立 Material:容易隔离状态,但可能增加批次和管理成本。

不要问“哪种写法永远最好”,而要问:

text
视觉是否正确?
状态应该共享还是独立?
参数能否进入顶点数据?
实际是否打断合批?
目标设备的成本是否可接受?

下一课: Vertex Shader 和 Fragment Shader 分别做什么?为什么一个按顶点工作,一个却可能被全屏像素拖慢?