Blake 分享 | 2D/3D 都能用上的 DrawCall 优化技巧!

本文为Cocos社区征稿文章,作者 Blake 老师基于自己多年来从事游戏行业的实践经历,同大家分享他在 Cocos Creator 3.x 中进行 DrawCall 性能优化的经验与心得。


游戏渲染是性能开销的大头,掌握渲染优化相关技巧对性能调优而言至关重要。渲染优化又可以从很多方面来入手,其中,减少 DrawCall 是一个非常重要的手段。本文将介绍为什么要减少 DrawCall,以及在 Cocos Creator 3.x 中应该如何减少 DrawCall

目录

1. 为什么要减少 DrawCall

2. 常用合批技术的原理及优缺点

3. v3.x 如何优化 3D 物体 DrawCall

4. v3.x 如何优化 2D UI 物体 DrawCall

一、为什么要减少 DrawCall 

游戏引擎绘制一个画面,首先需要遍历场景中的物体,然后把这些物体提交给 GPU 来进行绘制。假设游戏场景中有100个需要绘制的物体,当游戏引擎把这100个物体一个一个提交给 GPU 绘制时,引擎底层会进行以下操作:

  1. CPU 传递物体渲染所需要的数据给 GPU(如网格模型、材质参数、纹理对象、世界矩阵等)。

  2. CPU 把数据准备好以后,向 GPU 下达绘制命令(Draw cmd)。

  3. GPU 开机渲染,将 CPU 提交过来的数据渲染到我们的显示目标上。

这里首先和大家介绍几个概念:

  • Drawcall:CPU 提交数据给 GPU,然后向 GPU 下达渲染命令的这个过程即为 DrawCall,又叫同一个批次渲染。

  • DrawCall 数目(批次数):指的是游戏引擎绘制一个游戏场景中的所有物体需要向 GPU 提交的渲染命令(DrawCall)次数。若有100个物体逐个提交给 GPU 绘制,那么就要提交100次渲染命令,DrawCall 数目即为100。也可以理解为:一个游戏场景中的物体被分成几批次来进行渲染。

  • 合批(减少 DrawCall 数目):把几个物体合在一起提交给 GPU 渲染绘制叫做合批。比如100个物体,把前50个物体、后50个物体分别合在一起提交给 GPU 渲染,那么这100个物体完成渲染只需提交2批次,也就是2次 DrawCall。合批可以减少 DrawCall 数目,我们通常说的减少 DrawCall 指的就是合批。

96d94ca7617d3a699d36476ff00f7492.png

在 Cocos Creator 中,我们可以通过调试参数看到当前的 DrawCall 数目。

DrawCall 导致的性能开销主要在 CPU 端的命令组装,DrawCall 越多,需要组装的命令就越多,消耗的 CPU 资源越多。特别对于低端机和 iOS 这类不能 JIT 的设备,DrawCall 对性能影响很大。

接着来分析一下为什么合批(减少 DrawCall)可以提升渲染性能:

  1. CPU 把能合批的物体一起提交给 GPU,避免重复的数据提交,如同一个模型的多个物体。CPU 给 GPU 提交数据开销比较大,大于 CPU 对内存的数据拷贝。

  2. 100个物体逐个绘制就要下达100次 Draw Cmd,而把100个物体合成一批提交给 GPU,就只需下达一次 Draw Cmd,也就是 GPU 只要开机一次就能完成渲染。

  3. GPU 每次开机绘制都有一个吞吐量,如果我们每次能尽可能多提交一些三角形的面数,GPU 一次渲染能吃更多的面。这就像工厂的流水线,只要开机,产100个商品和产1个商品的代价一样,那么在安排订单的时候,尽量每次开机凑够100个。所以合批提交渲染,能提升 GPU 的吞吐量,提高效率。

经过上述分析不难得出,尽可能把物体合批到一起渲染(减少 DrawCall 数目)对渲染优化有着非常重要的作用。

二、常用合批技术

先来看一个概念——以下称「能够合批」。我们把使用同一个材质+使用能够合批的渲染组件(如 MeshRenderer)的物体称作「能够合批」。合批的物体首先要满足「能够合批」的条件,否者任何技术手段都无法合批。

合批常用的技术手段有静态合批、动态合批、GPU Instancing 合批。静态合批和动态合批要求:模型可以不同,但材质必须相同的渲染对象;而 GPU Instancing 合批则要求模型和材质都必须相同的渲染对象。

静态合批

静态合批是将能够合批物体的网格按照它的位置预先重新合并生成一个大的新网格,然后进行绘制。由于这些物体满足合批条件,都是同一个材质球,因此渲染这些物体只要把这个合并后的新网格对象一次提交给 GPU,就能实现这些物体的合批处理,降低 DrawCall。

但静态合批也有它的缺点:

  1. 静态合批需要预先计算合并网格,增加了运行初始化时的时间;

  2. 静态合批一但预先计算好合并后的网格,这些物体就不能再「移动」了。因此静态合批并不适合用在经常移动的物体的合批上,移动的物体可以使用动态合批。

  3. 静态物体网格合并后可能会增大内存开销。

可能有的小伙伴对上述第3点有所疑问:假设有100个物体的 Mesh 数据,合并后还是100个物体的 Mesh 数据,为什么合并后还可能会增大内存开销呢?

这里首先有「可能」两个字,也就是说某些情况下会增加,某些情况下不会增加。试想一下,如果100个物体完全不一样,那么合并后的 Mesh 顶点的内存开销和合并前都是一样的;但如果100个物体的 Mesh 完全一样,那么合并后就会有100个同一个 Mesh 不同位置的顶点数据了,这种合并就会增大内存开销。所以在实际游戏开发中,我们在做森林(大量相同树木)等场景时,就不使用静态合批。

动态合批

动态合批是指在每次渲染之前,CPU 将能够合批的物体的每个顶点的世界坐标计算出来(模型顶点坐标*世界变化矩阵)后,提交给 GPU,然后世界矩阵用单位矩阵,来达到合批的效果。动态合批适合用于移动的物体,且不会产生额外的内存开销。

但也由于每次渲染之前 CPU 都要重新计算坐标,所以动态合批会增加 CPU 的负担。在实际的使用中,要把 CPU 的额外开销和合批带来的提升做一个权衡。所以动态合批不是万能的,不适合太多顶点数目的物体的渲染绘制。

GPU Instancing 合批

对于游戏场景中同一个物体的 N 个实例可以采用 GPU Instancing 合批技术,它的原理是提交一次物体的模型,然后将实例的位置、旋转、缩放等信息提交给 GPU,再由 GPU 绘制 N 个实例出来。

从技术原理来看 GPU Instancing 是一个非常好的合批手段,几乎不会带来额外的开销。不过 GPU Instancing 需要 Shader 的支持,且有些早期的显卡并不支持 GPU Instancing 的特性。

三、优化 3D 物体 DrawCall

了解了几种合批技术的原理和优缺点后,就可以考虑采用对应的合批技术来进行合批。但在这之前还有两个关键点摆在我们面前:

  1. 分析 DrawCall 消耗在哪些地方;

  2. 为尽可能多的物体创造「能够合批」的条件。

如何摸清具体的物体绘制消耗了多少 DrawCall?一般我们会通过场景的组织来判断分析估算,同时通过显示/隐藏物体查看 DrawCall 数目的变化来确认我们的分析。如下面这个例子,隐藏人物后 DrawCall 由7变为4,说明该人物角色绘制花费了3个 DrawCall。

8fc8d080afebb78a32ce28370ee3486c.png

显示人物角色

fef094c71d42cd68ecc2acb4313e25bd.png

隐藏人物角色

让更多的物体有合并的可能,我采取的方式一般是将多个物体的材质球尽可能合并。材质球主要包含 Shader+纹理,我们应尽可能让更多物体使用同一个 Shader,再将不同物体的纹理合并到一起,让这些物体使用同一个材质球。此外还需要改变渲染组件类型,让更多的物体满足合批所支持的渲染组件,如将 SkinnedMeshRenderer 组件变成 MeshRenderer 组件。

摸清楚了 DrawCall 分布,并尽可能创造合批条件后,接下来就看看如何在 Cocos Creator 3.x 中实现这几种合批技术。

静态合批

把需要静态合批的物体放到一个节点下,然后在初始化接口里面调用静态合批 API 接口,预先计算好新合批后的物体,这样就可以做到合批了。

cc1047fe2cd69c0cc92e0929ca58c829.png

以下面的「1锥体+1cube」为例,首先在初始化中预先计算好节点下面所有物体的新的 Mesh;可以看到,没有开启静态合批之前需要2个 DrawCall,开启后只要1个 DrawCall,节约了1个 DrawCall。

d6ae3929d0cd92d2409736ede3f80cee.png

开启前

67efe20d5136e850e0bff6a17d6e694d.png

开启后

动态合批

动态合批只需要在材质球上勾选 USE BATCHING 即可:

b156f61a89e064e4406c35d995743e4f.png

GPU Instancing 合批

GPU Instancing 合批也是一样,在材质球上勾选 USE INSTANCING 即可:

fd249e46890f69e46b4b3dcac5c448c0.png

但是由于 GPU Instancing 只对一个 Mesh 的多个实例有用,所以作用在「1锥体+1cube」上仍然还是3个 DrawCall,但若是「2cube」,开启 GPU Instancing 后则会合并成1个 DrawCall。

a7c1916dbf6884f507dbb055770042fe.png

「1锥体+1cube」不是同一个网格对象,不能合批

d361aa806119a7dcac8e76387d6c29f4.png

「2cube」是同一个网格对象,可以合批

四、优化 2D UI 物体 DrawCall

2D 是特殊的 3D,所以上述 3D 部分的合批手段在 2D 也都适用。

2D 的 DrawCall 优化会更简单些,只要 UI 元素使用同一个图集(同一个纹理)就可以合批。UI 元素使用的 Shader 都使用了 Buildin-Sprite Shader(包括 Label 也使用这个 Shader),所以是否「能够合批」就看 UI 物体是否为同一个图集。

精灵图集大家比较熟悉了,至于 Label 图集,引擎会自动生成,可以理解为 Label 的图集和精灵的图集不是同一个图集。如果开启了文本缓存模式,多个不同的 Label 文字就有可能被引擎生成到同一个图集。所以对于 2D 而言同一个图集的「能够合批」,Label 会中断打乱「精灵的合批」。

2D UI DrawCall 优化的核心就3点:

  1. 打图集,尽量将同一个界面的 UI 元素打入同一图集。

  2. 优化 2D 的节点组织层级,UI 是按照层级来渲染的,尽量防止不同图集的 UI 元素互相打乱、以及 Label 的打断。比如组织 UI 元素尽可能是:A1A2A3/B1B2B3/C1C2C3……A/B/C 不同的图集放在一起,避免以 A1B1C1A2B2C2A3B3C3……这样的方式来组织 UI 元素或图集。

  3. 注意 Label 打乱的 UI 物体的合批。


今天的内容就就到这里,感谢  Blake老师的分享,希望能对大家有所帮助!欢迎前往论坛专贴一起讨论交流,地址:

https://forum.cocos.org/t/topic/132490

往期精彩

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值