新 UI 渲染批次合并指南
在官方文档的进阶主题中有一个 UI 渲染批次合并指南,批次合并一直是游戏开发中重要的优化手段,如果你未阅读过官方的指南,可以先阅读一遍。
当 多纹理渲染、新动态图集 等特性出现后,在进行批次合并时可以变得更加简单且自动化。
什么是多纹理渲染?
在以前的认识里,我们知道相邻的节点使用不同的纹理(Texture)会导致不能合并批次。
其根本原因是纹理是使用 uniform 变量传给着色器的,而需要合并批次的话不允许每次渲染都拥有不同的 uniform 变量值。
增强包实现的是先设置多个 uniform 变量,比如将 8 张纹理写入到 "texture1" "texture2" "texture3"... 的 8 个 uniform 变量中,然后在着色器里再判断应该在渲染时使用哪个 uniform 变量。
这样的话如果所有渲染都只用这 8 张纹理,就都能合并为 1 个批次。
这要求设备支持采样多个纹理,而在现代绝大多数设备中都至少支持采样 8 张纹理,所以这不是问题。
当然除了这种方法,还有另外几种进行多纹理合批的方法,例如 "Texture Array" 和 "Bindless",但都有实用性与兼容性的问题。
提示
启用动态合图
在之前的开发中我们通常会关闭动态图集,更倾向于靠静态图集或者自动图集达到降低 Draw Call 的目的。
不使用动态图集最重要的原因是其不能复用图集的废弃区域,随着游戏的运行动态图集会完全用完。
引擎只提供了在切换场景(Scene)后重置所有图集的机制来解决这个问题,但对于大部分项目来说,这种治标不治本的机制基本等于没有解决。
现在,增强包几乎重构了整个动态合图系统,你可以考虑重新启用动态合图了。
提示
开启动态图集常见的反对意见是:
在部分小游戏平台里,启用动态图集会有保留 Image 对象导致内存占用大的问题。
建议:
- 实际测试是否启用动态图集的内存占用差距。
- 有没有一种可能,只是说可能,内存占用大更多是因为你的项目根本没做任何资源释放呢?
充分利用动态合图
一般情况下只需要保持动态图集的默认设置即可,如果出现动态图集很多纹理不会打入或者图集很快用完的情况,可以参考以下建议调整。
放宽能参与合图的纹理尺寸限制
动态图集会自动进行多纹理合批,你可以放心地使用多达 7 张图集而不用担心交叉渲染导致的打断批次!
你可以根据项目的具体情况来放宽能参与合图的纹理尺寸限制。
cc.dynamicAtlasManager.maxFrameSize = 1024; // 推荐 512、1024 甚至 2048
tip
增强包会自动将图集的最大数量调整至(设备能同时采样纹理数 - Char 缓存模式自动合批图集数),这个值默认为 7
。
无需管理动态图集,只需要释放资源
动态合图会在纹理被释放的同时释放其在动态图集使用的空间。
如果动态图集的使用量一直在增长,请检查是否做了资源释放,因为你不需要关心动态图集的使用情况,只需要做好应有的资源释放就能保持动态图集的长期有效。
更加细致地优化图集的使用效率
除了通过调整纹理的 packable
属性可以控制纹理是否会参与动态合图之外,增强包提供了控制组件是否默认参与动态合图,控制单个组件是否参与动态合图的新特性。
可前往 动态合图 的文档了解详情。
你可以考虑将纹理尺寸限制放宽到 2048
,这听起来貌似有点离谱,但只要规划得当确实可行,比如:
- 禁止优化程度有限但尺寸巨大的纹理参与动态合图
- 分模块存放资源,禁止冷门(如活动界面)的纹理参与动态合图或尽早地释放掉
- 在资源太多的项目中,可考虑控制界面的渲染组件是否参与动态合图
完成上面几点这可能需要一些工作量,但能将动态图集用在刀刃上,发挥更大的作用。
Label 不再是合批噩梦
在之前的开发中我们可能会使用字体图集、调整节点顺序、甚至修改渲染流程来解决 Label 的性能问题。
也是因为引擎提供的 Bitmap 和 Char 两种缓存模式在稍大一点的项目上显得力所不及:
Bitmap 缓存模式:字体纹理会打入动态图集,但动态图集却无法复用,随着游戏的进行,图集用完则直接失去作用。
Char 缓存模式的缺点:还是无法复用,并且只有一张图集,图集用完则直接无法渲染,应该没人能接受游戏可能跑着跑着字就全部消失了的情况。
但现在你可以使用这两种缓存模式了,增强包重构了 Char 缓存模式,除了解决了不能复用的问题之外,还支持了多纹理渲染,所以既能与动态图集合批,还有最多 8 张字符图集可以使用。
脱胎换骨的 Char 缓存模式
如果你不知道该选择什么缓存模式,那就遇事不决,先选 Char 缓存模式。
虽然 Char 模式也有一些缺点,但由于它既能与动态图集一起合批,还是是按字符进行复用的,所以相比 Bitmap 模式它有着更高的性能优势。
不用担心字符图集会被用完,内部会用引用计数自动释放废弃字符所占用的空间。
但 Char 缓存模式不适合下面的场景:
- 无法显示带有像 emoji 的字素簇的字符串,这种字符串现在不能被完美地分割成单个字符,所以 Char 缓存模式也就不能正常显示了。
- 接上条,像聊天消息、输入框这类不可控的内容文本都不建议用 Char 缓存模式。
- Char 缓存模式不支持一些字体样式,可以在官方文档中了解详情。
- 巨大的字体大小(比如几百的)可能会瞬间占满整张字符图集,字符图集虽然有 8 张但也不能这么霍霍。
兜底的 Bitmap 缓存模式
即使不能选择 Char 缓存模式,Bitmap 缓存模式也能成为批次的最后一道防线。
在解决了动态图集的复用问题后,Bitmap 缓存模式的纹理也会使用引用计数自动释放,并且不会有 Char 缓存模式无法显示字素簇的问题。
但当然,Bitmap 缓存模式也不是万能的,如果遇到了下面这种情况,就需要考虑使用调整节点顺序这样的老办法来解决了:
- 巨大的字体大小也会瞬间占满整张动态图集,动态图集也不能这么霍霍。
- 在大量的 Label 需频繁改变文本的情况下,请使用性能分析工具检查动态图集的性能消耗,避免合批的弊大于利。
注意
无论使用哪种缓存模式,在做缩放动画时不要对 fontSize
属性进行缓动,这会导致每帧都需要重新生成文字纹理,造成巨大的性能负担,可以使用节点的 scale
来代替。
注意事项
- Char 缓存模式所使用的字符图集与动态图集不是一个东西
有多种因素导致没有让 Char 缓存模式直接使用动态图集来实现,这个原因在 Char 缓存模式的原理文档中有详细解释。
- Char 缓存模式依然不能在图集用完的情况下正常渲染
原因有以下几点:
- 我们认为 8 张数量已经够多了,8 张都用完的情况大部分是没有合理搭配使用两种缓存模式
- 8 张是多纹理渲染的上限,这意味着如果超过 8 张,1 个 Label 有 100 个字,就可能有 100 个 Draw Call
总结
以上就是新合批指南的全部内容了,稍微总结一下渲染批次合并的几个要点:
- 启用动态合图,只需要合理地释放资源即可保持动态合图的一直有效
- Label 优先使用 Char 缓存模式,不适合则使用 Bitmap 缓存模式
- 不要优先考虑修改节点顺序这种需要维护成本的优化方式
如果你对批次合并还有着更高的需求,可以阅读 进阶合批指南。