Skip to content

Lesson 049|两个 Sprite 在什么条件下可以合批? ​

Tags: #Creator2.x #Stage04 #Batching #Sprite #DrawCallDifficulty: ⭐⭐⭐⭐☆


真实问题:两百个相同图标,为什么不是一个 DrawCall ​

成就墙有 200 个图标,都来自同一张 Atlas,也都使用默认 Sprite 材质。开发者预期一次合批,实际却有几十个 DrawCall。

检查层级后发现,每个图标之间都插入了一个 Label 和一个使用独立材质的进度圈:

text
Icon1 → Label1 → Ring1
→ Icon2 → Label2 → Ring2

同 Atlas 只是兼容条件之一。可合批对象还要在渲染顺序中连续,并共享当前提交所需状态。

🎯 本课目标 ​

本课回答:

两个看起来相同的 Sprite,为什么有时能合并绘制,有时必须分开提交?


二、合批的基本思想 ​

text
多个兼容对象
    ↓
合并顶点和索引数据
    ↓
共享一组材质、纹理和状态
    ↓
一次或少量 DrawCall 提交

合批的核心是“兼容”,不是“对象数量少”。


三、常见兼容条件 ​

两个 Sprite 更容易合批的条件包括:

  • 使用兼容的材质和 Shader。
  • 使用同一张 Texture 或同一图集。
  • Blend、Depth、Stencil 等状态兼容。
  • 渲染层和排序关系允许合并。
  • 顶点格式和组件类型兼容。
  • 没有 Mask 或特殊 Pass 插入边界。

具体规则取决于 Creator 2.x 的 Renderer 和批处理实现。


四、为什么同图集有利 ​

text
Sprite A → Atlas Texture
Sprite B → Atlas Texture

两者可以使用不同 SpriteFrame,但底层纹理绑定相同。Renderer 不需要在两者之间切换 Texture,合批机会更高。

但同图集不是绝对保证,材质和渲染状态仍然需要兼容。


五、合批后的数据 ​

合批可能把多个对象的几何拼接:

text
A 顶点 + A 索引
    ↓
B 顶点 + 偏移后的 B 索引
    ↓
统一提交

每个对象的 UV、颜色和 Transform 仍需保留,只是共享一次状态和提交机会。

假设 A、B 都是四顶点 Sprite。A 使用索引 0,1,2,0,2,3。把 B 追加到同一顶点缓冲后,B 的顶点从索引 4 开始,因此索引要偏移:

text
A 顶点:0,1,2,3
A 索引:0,1,2,0,2,3

B 顶点:4,5,6,7
B 索引:4,5,6,4,6,7

最终一次 DrawCall 可以绘制 8 个顶点、12 个索引。合批不是把两个 Node 合成一个 Node,而是把兼容的底层几何放进同一提交范围。

每个 Sprite 仍需要自己的位置、UV 和颜色。它们可以写进各自顶点数据;但一次 DrawCall 共享的 Texture 和 Render State 不能对 A、B 分别取两个互相冲突的值。


六、合批的代价 ​

合批不是没有成本:

  • CPU 需要收集和拼接数据。
  • 动态对象变化时可能重新构建批次。
  • 单个对象更新可能影响一整批缓存。
  • 批次过大可能增加顶点上传压力。

因此目标是减少无意义的状态切换,而不是盲目把所有对象塞进一个批次。

动态合批还要考虑更新方式:

text
100 个静止 Sprite
→ 几何可能长期复用

100 个每帧移动或改变 Filled 范围的 Sprite
→ 顶点或变换数据持续更新
→ 即使最终 DrawCall 少,CPU 写缓冲仍可能很高

“已经合批”只说明提交被合并,不说明生成和维护这批数据没有成本。


七、案例:同图集但不同材质 ​

text
Sprite A:Atlas + Material A
Sprite B:Atlas + Material B

如果 Material A 和 B 使用不同 Shader 或 Render State,仍可能拆成多个 DrawCall。排查合批时要从纹理、材质和状态三层看。


八、案例:列表 Item 的合批规划 ​

列表 Item 可以统一:

  • 图集。
  • 默认材质。
  • 顶点格式。
  • 颜色和透明度处理。

而特殊状态如选中高亮、Mask、特效可以单独分层,避免所有 Item 因一个特殊对象都失去合批。


九、常见误区 ​

误区一:同一张图就一定合批 ​

材质、Shader、Render State 和排序仍可能不同。

误区二:合批越大越好 ​

动态更新、顶点上传和批次维护也有成本。

误区三:调整节点顺序一定能合批 ​

顺序只能影响排序条件,不能消除材质和纹理不兼容。

误区四:每个 Sprite 一个 DrawCall ​

合批和 Renderer 类型会改变实际提交数量。



十、深入推导:用“批次指纹”理解兼容 ​

可以把 Renderer 当前批次抽象成一组指纹:

text
Pass / Shader variant
Material state
Texture / Sampler
Blend / Depth / Stencil
Vertex format
Mask context
Render target / Camera

下一个组件的指纹兼容,且顺序允许连续,就可把顶点/索引追加到当前批次;否则先提交当前批次,再开始新批次。

这不是 Creator 2.4.x 内部实际 hash 字段定义,而是排查模型。

还要加上一个经常被忽略的条件:当前对象必须能连续追加。

text
Sprite A:指纹 X
Label:指纹 Y
Sprite B:指纹 X

遍历到 Label 时,X 批次必须先结束;之后 Sprite B 即使重新回到 X,也只能开启一个新的 X 批次。透明顺序不允许 Renderer 把 B 随意搬到 Label 前面。

可以把 Renderer 遍历过程想成一个顺序状态机:

text
当前批次为空
→ 读取 Sprite A 的批次指纹,建立批次
→ 读取 Sprite B:兼容,追加几何
→ 读取 Sprite C:不兼容,先提交 A+B
→ 用 C 建立新批次
→ 读取 Sprite D:若兼容 C,则继续追加

合批不是先把全场景相同图片找出来再任意拼接,而是在允许的渲染顺序里连续积累兼容对象。这就是“相邻”为什么和“同纹理”一样重要。

十一、Creator 2.4.x 实验:逐项破坏兼容条件 ​

准备 ​

创建 100 个 Sprite,使用同一 Atlas 中 Frame、默认材质、相同颜色与连续兄弟顺序。

BatchingProbe.ts ​

ts
const { ccclass, property } = cc._decorator;

@ccclass
export default class BatchingProbe extends cc.Component {
    @property(cc.Node)
    container: cc.Node = null;

    @property([cc.SpriteFrame])
    frames: cc.SpriteFrame[] = [];

    useFrame(index: number) {
        for (const child of this.container.children) {
            child.getComponent(cc.Sprite).spriteFrame =
                this.frames[index];
        }
    }

    alternateFrames() {
        for (let i = 0; i < this.container.childrenCount; i++) {
            this.container.children[i]
                .getComponent(cc.Sprite)
                .spriteFrame = this.frames[i % this.frames.length];
        }
    }
}

先输出 Frame 底层 Texture 是否相同:

ts
cc.log(
    frames[0].getTexture() === frames[1].getTexture()
);

对照步骤 ​

  1. 同 Frame、同材质、连续顺序,记录基线。
  2. 换成同 Atlas 的另一 Frame,观察是否仍兼容。
  3. 换成独立 Texture,观察批次。
  4. 给中间一个 Sprite 使用独立 Material,观察前后批次。
  5. 在中间插入 Label,观察顺序切分。
  6. 把同类节点重新组织为可交换的连续区,再验证视觉与批次。

每步记录 DrawCall,禁止同时改变两项。

十二、状态相同为什么仍可能因缓冲容量拆批 ​

合批后的数据仍要放入 CPU/GPU 缓冲:

text
Sprite 1 顶点/索引
+ Sprite 2 顶点/索引
+ ...
→ 当前批次缓冲
→ 一次绘制范围

达到实现的顶点/索引/缓冲上限时,即使状态相同也可能拆批。具体限制与引擎实现、索引类型和平台有关。

因此“状态完全相同就永远一个 DrawCall”也不是绝对结论。

十三、真实项目案例:列表按 Item 还是渲染层组织 ​

业务结构通常按组件复用:

text
Item
├── Background
├── Icon
├── NameLabel
└── Badge

Renderer 顺序变为每个 Item 内部交错。优化不能只把所有 Background 移到一个与 Item 无关的全局节点,否则数据绑定和遮挡可能复杂化。

可选策略:

  • 只重排不影响遮挡的装饰层;
  • 合并固定背景与边框资源;
  • 让 Icon/Background 共用 Atlas;
  • 降低特殊 Badge 材质数量;
  • 只对大列表采用专门渲染结构;
  • 以维护成本和真实收益共同决策。

十四、合批故障诊断 ​

同 Atlas 仍拆批 ​

text
确认 getTexture() 真相同
→ 比较 Material/Pass
→ 比较 Blend/Stencil/Mask
→ 检查中间组件和顺序
→ 检查 Sprite Type/顶点格式
→ 检查 Camera/RenderTarget
→ 检查批次容量限制

合批后画面顺序错 ​

说明重排破坏透明渲染语义。恢复正确顺序,再找不会改变视觉的局部连续区。

十五、练习、推导答案与版本边界 ​

Creator 2.4.x 的具体批处理规则随 WebGL/Native、Assembler 与版本变化。课程用公开组件和 Profiler验证,不依赖私有 batch key。

  1. 两个 Sprite 同 Texture 是否一定合批? 不一定,还需材质、Pass、状态、顶点格式、Mask、Camera 和连续顺序兼容。
  2. 相邻为何重要? 中间若必须绘制另一状态,当前批次需先提交;透明顺序又不允许随意跨越。
  3. 合批是否总有利? 合批也有缓冲构建、内存和维护成本;应以 CPU 提交瓶颈和实测收益判断。
  4. 为什么用 getTexture() 验证? 文件名或 Frame 名不能证明底层 Texture 身份,Atlas 子 Frame 可能共享,独立图可能不共享。

能力验收 ​

你应该能为相邻两个 Sprite 列出完整的批次指纹,逐项破坏 Texture、Material、状态、顺序和 Mask 条件,并通过恢复变量证明真正的批次边界。

本课总结 ​

text
合批的核心是共享兼容的绘制状态。
同图集有利于共享纹理,但不是绝对保证。
材质、Shader、状态和排序都可能决定是否拆批。

十二、下一课预告 ​

text
Lesson050|哪些状态变化会打断合批?