如何降低 DrawCall ?

前言

在游戏开发中,DrawCall作为一个非常重要的性能指标,直接影响游戏的整体性能表现。

还记得游戏渲染时是按顺序渲染的吗,所以"相邻"很关键! 当渲染一张贴图的时候,动态合图系统会自动检测这张贴图是否已经被合并到了图集(图片集合)中,如果没有,并且此贴图又符合动态合图的条件,就会将此贴图合并到图集中。

同时CHAR模式的局限也很明显,一般用于场景中出现大量数字文本,类似于经验值增加、血量减少之类的特效的情况。


# 一. DrawCall

DrawCall 中文译为“绘制调用”或“绘图指令”。

DrawCall 是一种行为(指令),即 CPU 调用图形 API,命令 GPU 进行图形绘制。

OpenGL/WebGL的API层面去看,可以狭义地和“对glDrawElements()glDrawArrays()的调用”划等号。

实际上 CocosCreator 引擎的 Drawcalls 计数,就是对 glDrawElements() 调用的计数。

故而,我们常说的 “降低DrawCalls”,其实就是降低 glDrawElements() 的执行次数。


# 二. 为何减少 DrawCall

其实我们真正需要减少的并不是 DrawCall 这个行为本身,而是减少每个 DrawCall 前置的一些消耗性能和时间的行为。

# 1. 举个栗子

问:尝试在两个硬盘之间传输文件,「传输 1 个 1MB 的文件和传输 1024 个 1KB 的文件」,同样是传输了共 1MB 的文件,「哪个更快?」

答:「传输 1 个 1MB 的文件要比传输 1024 个 1KB 的文件要快得多得多」。因为在每一个文件传输前,CPU 都需要做许多额外的工作来保证文件能够正确地被传输,而这些额外工 作造成了大量额外的性能和时间开销,导致传输速度下降。

# 2. 回到渲染

图形渲染管线的大致流程如下:

上图只是对渲染管线的部分概括,实际的图形渲染管线比较复杂,详细内容请移步至图形渲染流程

结果

从图中可以看到在渲染管线中,在每一次 DrawCall 前,CPU 都需要做一系列准备工作,才能让 GPU 正确渲染出图像。

而 CPU 的每一次内存显存读写、数据处理和渲染状态切换都会带来一定的性能和时间消耗。

# 3. 是谁的锅?

一般来说 GPU 渲染图像的速度其实是非常快的,绘制 100 个三角形和绘制 1000 个三角形所消耗的时间没差多少。

但是 CPU 的内存显存读写、数据处理和渲染状态切换相对于 GPU 渲染来说是 非常非常慢 的。

实际的瓶颈在于 CPU 这边,大量的 DrawCall 会让 CPU 忙到焦头烂额晕头转向不可开交,而 GPU 大部分时间都在摸鱼,是导致游戏性能下降的主要原因。

所以 DrawCall 这玩意越少越好~


# 三. 如何减少 DrawCall

在游戏运行时引擎是按照节点层级顺序从上往下由浅到深进行渲染的,理论上每渲染一张图像(文本最终也是图像)都需要一次 DrawCall。

既然如此,只要我们想办法将尽可能多的图像在一次 DrawCall 中渲染出来(也就是“渲染合批”),就可以尽量少去调用 CPU,从而减少 DrawCall。

TIP

简单点,就是减少让 CPU 工作的次数,但是每次都多给点活,不就可以省去一些“CPU 准备工具然后工作”和“工作结束叫 GPU 加工”的步骤了嘛,代价就是每次工作的时间会变长~

明白了这个原理之后,下面让我们看看在实际游戏开发中应该如何操作吧。


# 四. 使用静态合图

静态合图就是在开发时 「将一系列碎图整合成一张大图」

图集对于 DrawCall 优化来说非常重要,但是并不是说我们把所有图片统统打成图集就万事大吉了,这里面也有它的门道,胡乱打图集的话说不定还会变成负优化。

最重要的是「尽量将处于同一界面(UI)下的相邻且渲染状态相同的碎图打包成图集」,才能达到减少 DrawCall 的目的。


还记得游戏渲染时是按顺序渲染的吗? 所以 “相邻” 很关键 。

改变渲染状态会打断渲染合批,例如改变纹理状态(预乘、循环模式和过滤模式)或改变 Material(材质)、Blend(混合模式)等等,所以使用自定义 Shader 也会打断合批。


# 五. 使用位图字体

位图字体(BMFont)

在场景中使用系统字体或 TTF 字体的 Label 会打断渲染合批,特别是 Label 和 Sprite 层叠交错的情况,每一个 Label 都会打断合批增加一个 DrawCall,文本多的场景下轻轻松松 100+。

对于游戏中的文本,特别是数字、字母和符号,都建议「使用 BMFont 来代替 TTF 或系统字体」,并且「将 BMFont 与 UI 碎图打包到同一图集中」(或「开启动态合图」),可以免除大部分文本导致的 DrawCall。


# 六. 设置文本缓存模式

Cocos Creator 2.0.9 版本在 Label 组件上增加了 「Cache Mode」选项,来解决系统字体和 TTF 字体带来的性能问题。

Cache Mode 官方文档:https://docs.cocos.com/creator/manual/zh/components/label.html#文本缓存类型(cache-mode)

# 1. NONE(默认)

每一个 Label 都会生成为一张单独的位图,且不会参与动态合图,所以每一个 Label 都会打断渲染合批。

# 2. BITMAP

当 Label 组件开启 BITMAP 模式后,文本同样会生成为一张位图,但是「只要符合动态合图要求就可以参与动态合图,和周围的精灵合并 DrawCall」

「一定要注意 BITMAP 模式只适用于不频繁更改的文本,否则内存爆炸了后果自负!」

# 3. CHAR

当 Label 组件开启 CHAR 模式后,引擎会将该 Label 中出现的所有字符缓存到一张全局共享的位图中,相当于是生成了一个 BMFont。

「适用于文本频繁更改的情况,对性能和内存最友好。」

注意

「该模式只能用于字体样式和字号固定,并且不会频繁出现巨量未使用过的字符的 Label。因为共享位图的最大尺寸为 2048*2048,占满了之后就没办法再渲染新的字符,需要切换场景才会清除共享位图。」

开启了 CHAR 模式的 Label 无法参与动态合图,但是可以和相邻的同样是 CHAR 模式的 Label 合并 DrawCall(相当于是一张未打包进图集的 BMFont)。

# 4. 总结

结论已经很明显了,对于大量频繁更改的文本,使用 CHAR 模式带来的性能提升是非常明显的。

同时 CHAR 模式的局限也很明显,一般用于场景中出现大量数字文本,类似于经验值增加、血量减少之类的特效的情况。

使用了 BITMAP模式,如果精灵打包成了图集 DrawCall 会上升,因为图集默认不参与动态合图。所以当前这种情况(少精灵多文本)不打图集反而是比较好的选择。


# 七. UI 层级管理

UI 布局 也是影响 DrawCall 的比较主要点, 好的 UI 布局可以直接减少大部分 DrawCall。

除了以上的优化方案,我们还可以在游戏场景中下功夫,将性能优化做到极致。

我们可以通过「优化节点层级,分离图像节点和文本节点,文本使用 BMFont 或 Cache Mode 选项,尽量出现避免文本打断渲染合批的情况」

特别是对于战斗场景中大量的文本提示(伤害值、血量值和法力值等等)或合成游戏中大量的经验值文本,因为这些文本基本都是数字,使用这种方式即使再多文本也只需要 1 个 DrawCall 就可以全部渲染出来。

# 1. 外部跟随

用例: 要在一个移动的 moveingSprite 上添加一个Spine子节点,Spine的贴图无法参与合批,通常这会导致合批被打断

更好的办法: Spine子节点添加到整个舞台的最高层级,这样Spine就不是 moveingSprite 的子节点,不会打断底层元素的合批。 而在Spine节点的upadate()方法中,根据 moveingSprite 的位置变化自身位置。

# 2. 节点二次添加

用例: prefab UI 文件中,若干个 spriteButton 上都包含了一些文本框 cc.Label 子节点,这些 cc.Label 会打断 spriteButton 和其他元素的合批。

处理方法: 重排UI层级。先遍历所有UI节点,将不能参与合批的子节点找出来,记录它们的全局坐标。重设这些子节点的 parent 或者 removeSelf 后再 addChild 它们,将它们添加到UI的最上层,并根据全局坐标重设位置。这样能参与合批的元素drawCalls将为1。

# 3. 粒子和Spine

粒子和Spine动画,往往会使用不属于全局图集的纹理。要注意将他们的层级和其他参与合批的内容分离。

# 4. cc.Graphics

cc.Graphics 直接调用GL的图形绘制API。 他也是无法参与全局合批的元素。 不仅如此,因为引擎的问题(直到2.3.3依然存在),即便是相同颜色的cc.Graphics节点,cocos都无法做到合批(理论上可以做到)。所以在引擎解决整个问题之前,项目要尽量避免 cc.Graphics 的使用。或者对引擎底层做修改,让 cc.Graphics 之间能够合批。


# 八. 总结

  1. 改变渲染状态会打断渲染合批,例如改变纹理状态(预乘、循环模式和过滤模式)或改变 Material(材质)、Blend(混合模式)等等,所以使用自定义 Shader 也会打断合批。

  2. 图集默认不参与动态合图,手动开启自动图集资源的 Packable 选项后如果最终图集符合动态合图要求也可以参与动态合图。

  3. 纹理开启 Packable 选项参与动态合图后无法使用自定义 Shader,因为动态合图会修改原始贴图的 UV 坐标。

  4. 使用 Cache Mode 的 BITMAP 模式需要注意内存情况,CHAR 模式需要注意文本内容是否多且不重复。

最后还需要注意

在 Cocos Creator 2.0.7 之前的版本中,改变节点的颜色或透明度、Sprite 组件使用九宫格(Sliced)都会打断渲染合批。