Skip to content

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 B

Mask 可能需要写模板、测试模板或额外 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 个 SpecialSprite

Normal 使用同 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);
    }
}

实验步骤 ​

  1. Special 放在所有 Normal 之前,记录批次。
  2. 放在中间,记录批次。
  3. 放在最后,记录批次。
  4. 恢复相同 Material/Texture,确认隔断是否消失。
  5. 把 Special 放进 Mask 下,观察 Stencil 上下文变化。

预期:状态相同对象保持连续时更有机会合批;一个特殊对象位于中间会切开前后。

十一、为什么恢复原状态仍需要新提交 ​

GPU 命令是有顺序的:

text
提交普通 A
→ 切高亮状态并提交 B
→ 恢复普通状态并提交 C

C 不能倒流回已经提交的 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 和平台为准。私有调试字段只可在固定源码版本研究,不进入业务。

  1. 中间一个特殊对象为何可能增加两个批次? 它结束前一个普通批次,绘制自身;之后恢复普通状态还要开启新批次。
  2. Mask 为什么常打断? 需要进入/退出 Stencil 状态和额外绘制,改变当前渲染上下文。
  3. 同材质实例名相同能证明兼容吗? 不能,要比较实际 Effect/Pass、参数、纹理和 Render State。
  4. 最可靠的定位方法是什么? 最小化场景或二分节点,逐项恢复变量并记录批次。

能力验收 ​

你应该能解释“普通—特殊—普通”为何形成三个顺序区间,能用二分节点法定位复杂页面中的隔断,并能区分资源层、Render State、几何层和渲染上下文四类原因。

本课总结 ​

text
批次被打断的本质是绘制状态不再兼容。
纹理只是其中一个因素。
正确性、排序和遮挡优先于单纯减少 DrawCall。

十一、下一课预告 ​

text
Lesson051|Dynamic Atlas 如何降低 DrawCall?