Unity性能优化之Draw Call
一、Draw Call的基本概念
(一)Unity画面生成过程
Unity(实际上基本所有图形引擎)生成一帧画面的处理过程可简化描述如下:引擎首先进行简单的可见性测试,确定摄像机能够看到的物体。接着,准备这些物体的相关数据,包括顶点(如本地位置、法线、UV等)、索引(顶点如何组成三角形)、变换(物体的位置、旋转、缩放以及摄像机位置等)、相关光源、纹理和渲染方式(由材质/Shader决定)。之后,引擎通知图形API(可简单看作通知GPU)开始绘制。GPU基于这些数据进行一系列运算,在屏幕上绘制出成千上万的三角形,最终构成一幅图像。
(二)Draw Call的定义
在Unity中,每次引擎准备数据并通知GPU的过程被称为一次Draw Call。这个过程是逐个物体进行的。对于每个物体,不仅GPU的渲染操作耗时,引擎重新设置材质/Shader也是一项非常耗时的操作。因此,每帧的Draw Call次数是一项极其重要的性能指标。对于iOS平台,应尽量将Draw Call次数控制在20次以内,该数值可以在编辑器的Statistic窗口中查看。
从技术角度严格定义,“一个Draw Call,等于呼叫一次DrawIndexedPrimitive (DX) 或 glDrawElements (OGL),等于一个Batch”。对于熟悉DirectX或OpenGL的开发者来说,对DrawIndexedPrimitive和glDrawElements这两个API一定不陌生。当我们准备好数据(通常为三角面的顶点信息)并要让GPU进行绘制时,就必须调用这些函数。例如,在画面上有一张“木”椅子和一张“铁”桌子,由于这两个物件使用了不同的材质球或者不同的Shader,理论上就会产生两个Draw Call。在DirectX或OpenGL中,对不同物件指定不同贴图或不同Shader的描述,就需要调用两次Draw Call,示例代码如下:
SetShader( “Diffuse” );
SetTexture( “铁” );
DrawPrimitive( DeskVertexBuffer );
SetShader( “VertexLight” );
SetTexture( “木” );
DrawPrimitive( ChairVertexBuffer );
每次对Shader的更改或者贴图的更改,本质上是对Rendering Pipeline的设置进行修改,所以需要不同的Draw Call来完成物件的绘制。这也解释了为什么UNITY官方文件总是建议尽量使用相同的材质球,以减少Draw Call数量。
二、Draw Call对性能的影响
Draw Call数量主要影响的是CPU效能而非GPU。可以将每次的Draw Call看作产生一个Batch,Batch中存储的是物件顶点资料。CPU通过“驱动程序”将顶点资料送往GPU,GPU接手后将物件绘制在画面上。因此,Draw Call越多,CPU就越忙碌。NVIDIA在GDC曾提出,25K batchs/sec会使1GHz的CPU达到100%的使用率,并推出了一条公式来预估游戏中大概可以运行的Batch数量。例如,如果目标是游戏以30FPS运行、使用2GHz的CPU,且将20%的工作量分配给Draw Call,那么每秒可以有333 Batchs/Frame,计算方式为:333 Batchs/Frame = 25K 2 (0.2/30)。
三、Draw Call Batching技术
(一)技术原理
Unity内置了Draw Call Batching技术,其主要目标是在一次Draw Call中批量处理多个物体。只要物体的变换和材质相同,GPU就可以按完全相同的方式进行处理,即可以把它们放在一个Draw Call中。该技术的核心是在可见性测试之后,检查所有要绘制的物体的材质,把相同材质的物体分为一组(一个Batch),然后将它们组合成一个物体(统一变换),这样就可以在一个Draw Call中处理多个物体(实际上是组合后的一个物体)。
(二)技术缺陷
Draw Call Batching存在一个缺陷,它需要把一个Batch中的所有物体组合到一起,相当于创建了一个与这些物体加起来一样大的物体,与此同时需要分配相应大小的内存。这不仅会消耗更多内存,还需要消耗CPU时间。特别是对于移动的物体,每一帧都得重新进行组合,因此需要进行权衡,否则可能得不偿失。但对于静止不动的物体来说,只需要进行一次组合,之后就可以一直使用,效率要高得多。
(三)两种实现方式
Unity提供了Dynamic Batching和Static Batching两种方式:
- Dynamic Batching:完全自动进行,不需要也无法进行任何干预。对于顶点数在300以内的可移动物体,只要使用相同的材质,就会组成Batch。
- Static Batching:需要把静止的物体标记为Static,然后无论物体大小,都会组成Batch。显然,Static Batching比Dynamic Batching要高效得多,但该功能是收费的。
四、有效利用Draw Call Batching的方法
(一)减少材质数量
尽量减少场景中使用的材质数量,即尽量共享材质。对于仅纹理不同的材质,可以把纹理组合到一张更大的纹理中,这种方法称为Texture Atlasing。
(二)标记静止物体
把不会移动的物体标记为Static,以利用Static Batching提高效率。
(三)手动组合物体
可以通过CombineChildren脚本(Standard Assets/Scripts/Unity Scripts/CombineChildren)手动把物体组合在一起。但需要注意的是,这个脚本会影响可见性测试,因为组合在一起的物体始终会被看作一个物体,从而会增加GPU要处理的几何体数量,因此要谨慎使用。
(四)设计遮挡剔除算法
对于复杂的静态场景,还可以考虑自行设计遮挡剔除算法,减少可见的物体数量,同时也可以减少Draw Call。
五、总结
在Unity开发中,理解Draw Call和Draw Call Batching的原理至关重要。开发者需要根据场景特点设计相应的方案来尽量减少Draw Call次数。开发游戏时,可打开Game窗口里的Stats查看Draw Call与Batched的数字。总之,根据实际情况灵活运用各种优化方法,才是提升Unity性能的关键。