外观
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()
);对照步骤
- 同 Frame、同材质、连续顺序,记录基线。
- 换成同 Atlas 的另一 Frame,观察是否仍兼容。
- 换成独立 Texture,观察批次。
- 给中间一个 Sprite 使用独立 Material,观察前后批次。
- 在中间插入 Label,观察顺序切分。
- 把同类节点重新组织为可交换的连续区,再验证视觉与批次。
每步记录 DrawCall,禁止同时改变两项。
十二、状态相同为什么仍可能因缓冲容量拆批
合批后的数据仍要放入 CPU/GPU 缓冲:
text
Sprite 1 顶点/索引
+ Sprite 2 顶点/索引
+ ...
→ 当前批次缓冲
→ 一次绘制范围达到实现的顶点/索引/缓冲上限时,即使状态相同也可能拆批。具体限制与引擎实现、索引类型和平台有关。
因此“状态完全相同就永远一个 DrawCall”也不是绝对结论。
十三、真实项目案例:列表按 Item 还是渲染层组织
业务结构通常按组件复用:
text
Item
├── Background
├── Icon
├── NameLabel
└── BadgeRenderer 顺序变为每个 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。
- 两个 Sprite 同 Texture 是否一定合批? 不一定,还需材质、Pass、状态、顶点格式、Mask、Camera 和连续顺序兼容。
- 相邻为何重要? 中间若必须绘制另一状态,当前批次需先提交;透明顺序又不允许随意跨越。
- 合批是否总有利? 合批也有缓冲构建、内存和维护成本;应以 CPU 提交瓶颈和实测收益判断。
- 为什么用
getTexture()验证? 文件名或 Frame 名不能证明底层 Texture 身份,Atlas 子 Frame 可能共享,独立图可能不共享。
能力验收
你应该能为相邻两个 Sprite 列出完整的批次指纹,逐项破坏 Texture、Material、状态、顺序和 Mask 条件,并通过恢复变量证明真正的批次边界。
本课总结
text
合批的核心是共享兼容的绘制状态。
同图集有利于共享纹理,但不是绝对保证。
材质、Shader、状态和排序都可能决定是否拆批。十二、下一课预告
text
Lesson050|哪些状态变化会打断合批?