外观
Lesson 050|哪些状态变化会打断合批?
Tags: #Creator2.x #Stage04 #Batching #RenderState #MaterialDifficulty: ⭐⭐⭐⭐☆
真实问题:加了一个高亮按钮,后面几十个节点都多了批次
列表中第 20 个按钮需要发光,开发者给它绑定独立 Material。Profiler 不只是增加一个 DrawCall,而是在高亮前后各形成新批次。
text
普通批次(1~19)
→ 高亮按钮
→ 普通批次(21~N)状态边界像一道隔断。即使隔断后恢复原状态,也不能与隔断前已经提交的数据跨过去合并。
🎯 本课目标
本课从“能不能合批”进一步进入“为什么批次被打断”:
- Texture 切换。
- Material / Shader 切换。
- Blend、Depth、Stencil 变化。
- Mask、Camera、排序和特殊组件。
二、状态边界的概念
Renderer 可以把连续对象放入同一批次的前提是:
text
前一个对象的绘制状态
和后一个对象的绘制状态
可以不切换地继续使用一旦状态需要改变,就可能出现:
text
结束当前批次
设置新状态
开始新的 DrawCall三、常见打断因素
1. 纹理不同
不同 Texture 通常需要重新绑定。
原因不是 SpriteFrame 名字不同,而是当前 Texture binding 只能指向确定资源。同一 Atlas 中不同 Frame 只改变 UV,仍可能共享绑定;两张独立 Texture 即使像素完全一样,身份仍不同。
2. Shader 或 Material 不同
GPU 程序和参数布局不同,通常不能直接合并。
Shader 变体不同还可能来自宏开关。材质界面看起来相似,不代表最后选中的程序、Pass 和参数布局相同。
3. Blend 状态不同
透明、加法和不透明混合规则不同。
一次 DrawCall 只能使用一套固定 Blend 配置。普通 Alpha 与加法发光对象通常需要分开提交。
4. Mask / Stencil
模板状态形成新的绘制边界。
进入 Mask 前要写入或切换模板条件,子内容在新的 Stencil 上下文中绘制,离开时又要恢复。即使 Mask 内外 Sprite 使用同一纹理,也不能简单跨越模板边界。
5. Render Layer / Camera
不同 Camera 或渲染目标需要分开处理。
不同 Camera 可能有不同 View/Projection、Viewport、Clear 和 RenderTarget。两个对象最终写向不同目标时,本质上不是同一次绘制任务。
6. 特殊组件
Graphics、Spine、Particle 或自定义渲染器可能使用不同顶点格式和流程。
不同顶点格式意味着当前 Shader 对缓冲中每段数据的解释不同。普通 Sprite 的位置/UV/颜色布局不能直接和需要骨骼权重或其他属性的网格混用。
四、排序和正确性优先
Renderer 不能为了少一个 DrawCall 就任意重排透明对象:
text
批次优化
↓ 不能破坏
遮挡、透明混合、UI 层级和视觉结果因此合批优化永远要同时验证画面正确性。
五、案例:一个特殊高亮按钮
text
普通按钮:默认 UI Material
高亮按钮:闪白 Shader高亮按钮可能把连续的普通按钮批次拆开。可以考虑:
- 只在高亮期间使用特殊材质。
- 用顶点颜色或共享 Shader 参数实现效果。
- 将特效单独放在独立层。
选择时要比较实现复杂度和实际收益。
六、案例:Mask 打断列表
text
List Item A
Mask
List Item BMask 可能需要写模板、测试模板或额外 Pass,因此列表对象不能像普通 Sprite 一样连续提交。大量 Mask 嵌套时尤其要测量。
七、调试打断的顺序
看到 DrawCall 突然增加,可以按下面顺序检查:
text
1. 是否换了 Texture / SpriteFrame?
2. 是否换了 Material / Shader?
3. 是否启用了 Blend、Mask、Stencil?
4. 是否跨了 Camera 或 Render Layer?
5. 是否有特殊组件插入?
6. 是否只是对象数量或顶点数上升?不要一上来就重做全部图集。
八、常见误区
误区一:每次换颜色都会打断合批
如果颜色作为兼容的顶点属性传递,可能不需要切换材质。
误区二:只要用了自定义 Shader 就完全不能合批
相同 Shader 和兼容参数仍可能批处理,具体看 Renderer 规则。
误区三:DrawCall 增加一定是纹理问题
状态、排序、Mask 和特殊组件都可能导致拆批。
九、深入推导:状态变化必须分层排查
资源层
- Texture/SpriteFrame 底层 Texture;
- Sampler;
- Material;
- Shader 宏/变体。
Render State 层
- Blend;
- Depth;
- Stencil/Mask;
- Cull;
- Color Write。
几何层
- 不兼容顶点格式;
- 特殊 Assembler;
- 缓冲容量边界。
上下文层
- Camera;
- RenderTarget;
- 渲染队列;
- 必须保持的透明顺序。
把所有原因都叫“材质不同”会让诊断失真。
材质资源身份和实际绘制状态也不是同一件事:
text
两份 Material 资源
→ 可能最终选择兼容的 Pass、纹理和状态
同一份 Material
→ 也可能因为运行时参数、宏变体或 Mask 上下文不同而分开所以文件名、资源名和 Inspector 外观只能提供线索,不能替代对批次边界两侧实际状态的比较。
十、Creator 2.4.x 实验:移动同一个批次隔断
场景
text
Container
├── 30 个 NormalSprite
└── 1 个 SpecialSpriteNormal 使用同 Atlas/材质;Special 使用独立 Texture 或 Material。
BatchBreakProbe.ts
ts
const { ccclass, property } = cc._decorator;
@ccclass
export default class BatchBreakProbe extends cc.Component {
@property(cc.Node)
special: cc.Node = null;
@property(cc.Node)
container: cc.Node = null;
moveSpecial(index: number) {
const clamped = Math.max(
0,
Math.min(index, this.container.childrenCount - 1)
);
this.special.setSiblingIndex(clamped);
cc.log('[batch-break] index=', clamped);
}
}实验步骤
- Special 放在所有 Normal 之前,记录批次。
- 放在中间,记录批次。
- 放在最后,记录批次。
- 恢复相同 Material/Texture,确认隔断是否消失。
- 把 Special 放进 Mask 下,观察 Stencil 上下文变化。
预期:状态相同对象保持连续时更有机会合批;一个特殊对象位于中间会切开前后。
十一、为什么恢复原状态仍需要新提交
GPU 命令是有顺序的:
text
提交普通 A
→ 切高亮状态并提交 B
→ 恢复普通状态并提交 CC 不能倒流回已经提交的 A 缓冲中,除非 Renderer 能安全重排 B 与 C;透明 UI 常不允许。
把它想成按顺序录制命令:
text
命令 1:用普通状态画 A
命令 2:用高亮状态画 B
命令 3:用普通状态画 C当命令 2 被确定时,命令 1 的普通批次已经结束。要让 A、C 合并,就必须把命令顺序改成 A、C、B;只有在遮挡和透明混合完全允许时才能这么做。
十二、真实项目故障:ScrollView 每个 Item 自带 Mask
结构
每个头像 Item 都用 Mask 做圆形裁剪。100 个 Item 产生大量 Stencil 状态进入/退出,普通背景和图标被反复切开。
替代方案评估
- 头像源资源直接提供圆形 Alpha;
- 使用统一 shader/纹理方案;
- 只为可见 Item 启用 Mask;
- 虚拟列表减少同时存在数量;
- 多个 Item 是否可共享上层裁剪边界;
- 比较透明像素 Overdraw 与 Stencil/DrawCall 成本。
没有一种方案跨设备永远最优,需要测量视觉和性能。
十三、用二分法定位批次边界
页面复杂时:
text
1. 记录目标区域 DrawCall
2. 禁用后一半节点
3. 看异常边界是否仍存在
4. 继续缩小到少数组件
5. 对比边界前后 Texture/Material/State
6. 恢复节点并做单变量验证这比盯着整棵 Hierarchy 猜更快。
十四、四种看似有效的错误优化
把所有节点强制换同一材质
可能丢失正确 Blend、Mask 或视觉效果。
为合批重排透明对象
可能改变遮挡和颜色。
禁用所有 Mask
只能证明 Mask 参与成本,不能作为产品修复。
只看 DrawCall 数量
替代方案可能增加 Overdraw、纹理内存或 shader 成本。
十五、练习、推导答案与版本边界
具体合批边界以 Creator 2.4.x 当前 Renderer、Assembler 和平台为准。私有调试字段只可在固定源码版本研究,不进入业务。
- 中间一个特殊对象为何可能增加两个批次? 它结束前一个普通批次,绘制自身;之后恢复普通状态还要开启新批次。
- Mask 为什么常打断? 需要进入/退出 Stencil 状态和额外绘制,改变当前渲染上下文。
- 同材质实例名相同能证明兼容吗? 不能,要比较实际 Effect/Pass、参数、纹理和 Render State。
- 最可靠的定位方法是什么? 最小化场景或二分节点,逐项恢复变量并记录批次。
能力验收
你应该能解释“普通—特殊—普通”为何形成三个顺序区间,能用二分节点法定位复杂页面中的隔断,并能区分资源层、Render State、几何层和渲染上下文四类原因。
本课总结
text
批次被打断的本质是绘制状态不再兼容。
纹理只是其中一个因素。
正确性、排序和遮挡优先于单纯减少 DrawCall。十一、下一课预告
text
Lesson051|Dynamic Atlas 如何降低 DrawCall?