e-works数字化企业网  »  文章频道  »  基础信息化  »  移动应用

移动平台Unity3D应用性能优化

2017/6/25    来源:极客头条    作者:陈星百      
关键字:移动应用  Unity3D  
VR由于双眼double渲染的原因,对性能的优化要求比较高,在项目的进展过程中,笔者总结了一些关于移动平台上Unity3D的性能优化经验供大家分享。

    那么GC什么时候会触发呢?两种情况: 
 
    a、当我们的堆的内存不足时,会自动调用GC来回收内存。 
 
    b、手动的调用GC,用System.GC.Collect(),一般情况下,不建议手动去手动进行内存回收,因为容易出现问题。
 
    检查整个工程代码,关于减少GC这一方面的优化经验总结大概如下: 
 
    1、字符串连接的处理。因为将两个字符串连接的过程,其实是生成一个新的字符串的过程。而之前的旧的字符串自然而然就成为了垃圾。而作为引用类型的字符串,其空间是在堆上分配的,被弃置的旧的字符串的空间会被GC当做垃圾回收,可以使用StringBuilder来解决(注意:C#没有StringBuffer,Java里才有!!String 在进行运算时(如赋值、拼接等)会产生一个新的实例,而 StringBuilder 则不会。所以在大量字符串拼接或频繁对某一字符串进行操作时最好使用 StringBuilder,不要使用 String)。
 
    2、尽量不要使用foreach,而是使用for。foreach其实会涉及到迭代器的使用,而据传说每一次循环所产生的迭代器会带来24 Bytes的垃圾。那么循环10次就是240Bytes。
 
    3、不要直接访问gameobject的tag属性。比如if (go.tag ==“human”)最好换成if (go.CompareTag (“human”))。因为访问物体的tag属性(每次Object.name也会分配39B的堆内存.)会在堆上额外的分配空间。如果在循环中这么处理,留下的垃圾就可想而知了。
 
    4、不要实例化(Instantiate)和(Destroy)对象,事先建好对象池,以实现空间的重复利用。
 
    5、在某些可能的情况下,可以使用结构(struct)来代替类(class)。这是因为,结构变量主要存放在栈区而非堆区。因为栈的分配较快,并且不调用垃圾回收操作,所以当结构变量比较小时可以提升程序的运行性能。但是当结构体较大时,虽然它仍可避免分配/回收的开销,而它由于”传值”操作也会导致单独的开销,实际上它可能比等效对象类的效率还要低。所以要注意选择。
 
    6、场景切换时,可以主动进行垃圾回收(调用System.GC.Collect()),从而及时去除游戏中已经不必要地内存占用。
 
    Draw Call 的优化
 
    前面说过了,DrawCall是CPU调用底层图形接口的操作。比如有上千个物体,每一个的渲染都需要去调用一次底层接口,而每一次的调用CPU都需要做很多工作,那么CPU必然不堪重负。但是对于GPU来说,图形处理的工作量是一样的。
 
    我们先来看看Draw Call对CPU的消耗大概是一个什么级别的量: 
 
    NVIDIA 在 GDC 曾提出,25K batchs/sec 会吃满 1GHz 的 CPU,100%的使用率。有一个公式可以和清楚得计算出在给定的CPU资源 与 帧率的情况下,最多能有多少个DrawCall。 
 
    DrawCall_Num = 25K * CPU_Frame * CPU_Percentage / Framerate。 
 
    DrawCall_Num : DrawCall数量 
 
    CPU_Frame : CPU 工作频率(GHz单位) 
 
    CPU_Percentage:CPU 分配在DrawCall这件事情上的时间率(百分比)
 
    Framerate:希望的游戏帧率
 
    比如说我们使用一个高通820,工作频率在2GHz上,分配10%的CPU时间给DrawCall上,并且我们VR要求60帧,那么一帧最多能有83个DrawCall(由于双camera的存在,单眼DrawCall只能保证在41个以内)。其实,google官方的建议是单眼DrawCall不多于50个。
 
    所以对DrawCall的优化,主要就是为了尽量解放CPU在调用图形接口上的开销。所以针对drawcall我们主要的思路就是每个物体尽量减少渲染次数,多个物体最好一起渲染。那么DrawCall次数的优化有哪些方案呢?
 
    DC Batching(DC批处理) 
 
    batch即批处理,DrawCall batching即DC的批处理,即把多次DrawCall合并成一次DrawCall的方案。
 
    Dynamic Batching 动态批处理 
 
    Unity引擎对于使用相同材质的物体会自动进行批处理,相同材质意味着shader完全一样,这一部分主要是要注意那些破坏这一特性的人为因素,比如说: 
 
    1、批处理动态物体需要在每个顶点上进行一定的开销,所以动态批处理仅支持小于900顶点的网格物体,如果你的着色器使用顶点位置,法线和UV值三种属性,那么你只能批处理300顶点以下的物体(如果在这基础上还使用了UV2,则只能批处理180顶点以下的物体);请注意:属性数量的限制可能会在将来进行改变。
 
    2、使用不同的缩放比例的物体,unity将无法对这些物体进行批处理。比如(1,1,1)和(1,2,2)就不会动态批处理,但是(1,1,1)和(2,2,2)会动态批处理。
 
    3、拥有lightmap的物体含有额外(隐藏)的材质属性,比如:lightmap的偏移和缩放系数等。所以,拥有lightmap的物体将不会进行批处理(除非他们指向lightmap的同一部分)。接受实时阴影的物体也不会批处理。
 
    4、多通道的shader会中断批处理操作(为了达到特殊的渲染目的,可能某个物体要多遍渲染.这是就要多个通道)。
 
    5、在脚本中动态地指定了物体的材质,也不会进行批处理。
 
    Static Batching 静态批处理 
 
    动态批处理虽然是自动的,但是限制非常多,不小心就会打破批处理,所以unity在专业版中还提供了静态批处理,静态批处理要求是想批处理的物体一定是static的,静态的,不会改变位置和旋转角度以及缩放的,且必须材质一致。其原理是把物体的网格进行合并,变成一个静态的更大的网格物体,再使用一个统一的材质进行渲染。
 
    知道了它的原理,它的某些坑就比较清晰了: 
 
    1、在一个平行光、环境光下,没有问题,但是如果你使用了多个平行光,点光源,聚光灯这种复杂的光源去照射物体,那么静态批处理就会被打断。(项目中就遇到过,因为两边有两排英雄模型,所以场景中使用了两个不同平行光,场景中勾选的static物体并没有被合并drawcall,经过一番折磨才找到原因)。
 
    2、如果静态批处理前有一些物体共享了相同的网格,那么每一个物体都会有一个该网格的复制品(本来unity只会保留一份,但是静态批处理会生成新的一个大网格,所以会保留所有物体的网格,最后合并),即一个网格会变成多个网格被发送给GPU。这样会造成内存的使用变大,需要注意这个问题,但是一般场景中使用相同网格的物体会比较少。
 
    3、对于那些shader相同,纹理不同导致的不同材质无法进行批处理的物体(比如项目中的场景环境,基座,地面,其实都使用了unity自带的standard shader)可以通过纹理合并的方法来使得它们可以被静态批处理。这就引发了下面的事情: 
 
    BUS总线带宽 
 
    CPU完成一次DrawCall,除了需要发一个DrawCall的命令之外,还需要把内存中顶点数据、纹理贴图、shader参数通过bus总线拷贝到内存分配给GPU的显存之中,注意这是拷贝,不是指针传递,速度不快。如果一次drawcall传递的数据过大,带宽成为了瓶颈,那就会大大影响效率(其它的DrawCall无法出发,GPU又处于闲置)。这种情况最有可能出现在为了减少DrawCall,疯狂的合并纹理上。在项目中,UI的DrawCall调用占了很大一部分,也会最难优化的,为了减少drawcall ,我们把UI模块的静态部分(一些UI的底板,背景等不会发生变化的)全部合并成了一个纹理,最后导致了DrawCall下降了,但是帧率却也下降了,内存使用也增加了,原因就是这个。在项目中,不会同时出现的元素不要打包到一起,保证单张合并纹理不大于1024*1024一般就不会有问题了(王者荣耀最大纹理限制在了256*256)。
 
    DrawCall的优化大概就是这些,优化的目标其实是往一个目标上靠,cpu的DrawCall命令刚刚好能被GPU消化,不要让CPU等待(带宽限制),也不要让GPU闲置。如果即使做到了这个,应用帧率还是上不去,那么就只能去削减场景,做有损优化了。
 
    Physics
 
    Unity内置NVIDIA PhysX物理引擎,来模拟物理世界的一些效果,比如说重力、阻力、弹性、碰撞这些,其中使用了一些内置的组件来实现这些模拟,用的比较多的如:刚体(Rigidbody) 各种碰撞器(Collider) 恒力 (Constant Force) 物理材质(Physic Material)铰链关节(Hinge Joint)弹簧关节(Spring Joint)。
 
    unity除了提供了一些重要的组件之外,在unity脚本中的生命周期中提供了一个专门为物理计算的刷新方法: 
 
    FixedUpdate()。FixedUpdate跟Update的区别在于,这两个函数处于不同的“帧循环”中,FixedUpdate处于Physics循环中,而Update不是。所以这两个函数的使用也有了不同。Update的执行受场景GameObject的渲染的影响,三角形的数量越多,渲染所需要的时间也就越长。FixedUpate的执行则不受这些影响。所以,Update每个渲染帧之间的间隔是不相等的,而Fixedupdate在每个渲染帧之间的时间间隔是相等的。由于关系到物理模拟,所以一般涉及到物理组件,都需要放在Fixedupdate中进行计算。那么关于physics,一般的优化手段都有哪些呢?下面是一些经验及总结:
 
    1、将物理模拟时间步间隔设置到合适的大小。 Fixed Timestep是和物理计算有关的,所以若计算的频率太高,自然会影响到CPU的开销。同时,若计算频率达不到软件设计时的要求,有会影响到功能的实现,所以如何抉择需要具体分析,选择一个合适的值,一般大于16ms,小于30ms。可以通过Edit->Project Settings->Time来改变这个值。
 
    2、谨慎使用网格碰撞器(Mesh Collider),过于消耗性能,一般使用更简单的碰撞器,或者使用基本几何碰撞器合并的组合碰撞器。在这个项目中,把所有的网格碰撞体都抛弃了,都换成了box collider。
 
    3、真实的物理(刚体)很消耗,不要轻易使用,尽量使用自己的代码(数学计算)模仿假的物理。
 
    4、最小化碰撞检测请求(例如ray casts和sphere checks),尽量从每次检查中获得更多信息。 
 
    项目中涉及到物体的组件很少,关于physic的优化肯定还有很多可以说的,需要再去学习了。
 
    VSync
 
    简单地说,这是CPU优化的最直接的一个方法。
 
    科普:VSync垂直同步又称场同步(Vertical Hold),垂直同步信号决定了CRT从屏幕顶部画到底部,再返回原始位置的时间。从CRT显示器的显示原理来看,单个像素组成了水平扫描线,水平扫描线在垂直方向的堆积形成了完整的画面。显示器的刷新率受显卡DAC控制,显卡DAC完成一帧的扫描后就会产生一个垂直同步信号(决定于屏幕的刷新率)。我们平时所说的打开垂直同步指的是将该信号送入显卡3D图形处理部分,从而让显卡在生成3D图形时受垂直同步信号的制约(注意是制约)。
 
    如果我们选择等待垂直同步信号(也就是我们平时所说的垂直同步打开),那么在游戏中或许强劲的显卡迅速的绘制完一屏的图像,但是没有垂直同步信号的到达,显卡无法绘制下一屏,只有等垂直同步的信号到达,才可以绘制。这样FPS自然要受到操作系统刷新率运行值的制约。而如果我们选择不等待垂直同步信号(也就是我们平时所说的关闭垂直同步),那么游戏中作完一屏画面,显卡和显示器无需等待垂直同步信号就可以开始下一屏图像的绘制,自然可以完全发挥显卡的实力。但是不要忘记,正是因为垂直同步的存在,才能使得游戏进程和显示器刷新率同步,使得画面更加平滑和稳定。
 
    取消了垂直同步信号,固然可以换来更快的帧率,但是在图像的连续性上势必打折扣。
 
    四、Unity3D应用性能优化之GPU
 
    一般人说DC的优化占了unity3D软件优化的三分天下,那么GPU的优化也占了三分天下。在了解GPU优化都有哪些着手点之前,我们先了解一下GPU在3D软件渲染中做了啥事: 
 
    顶点着色器
 
    GPU接收顶点数据作为输入传递给顶点着色器。顶点着色器的处理单元是顶点,输入进来的每个顶点都会调用一次顶点着色器。(顶点着色器本身不可以创建或销毁任何顶点,并无法得到顶点与顶点之间的关系)。顶点着色器是完全可编程的,它主要完成的工作有:坐标变换和逐顶点光照。 坐标变换:就是对顶点的坐标进行某种变换—把顶点坐标从模型空间转换到齐次裁剪空间。顶点的多少直接决定了三角形面的多少,也直接决定了GPU的渲染流水线的工作量,所以减少顶点数是一个比较重要的优化点。那么减少顶点怎么操作呢,又有哪些途径? 
 
    1、优化基本几何体 
 
    3D软件都是从模型制作开始,在设计师建模的时候就要想到应该尽可能地减少顶点数,一些对于模型没有影响、或是肉眼非常难察觉到区别的顶点都要尽可能去掉。比如在项目中,对于用户背后的环境模型,一些树木和石头,视频背面永远无法看见的神庙,能削减的都已经削减了。
 
    2、使用LOD(Level of detail)技术 
 
    LOD技术有点类似于Mipmap技术,不同的是,LOD是对模型建立了一个模型金字塔,根据摄像机距离对象的远近,选择使用不同精度的模型。它的好处是可以在适当的时候大量减少需要绘制的顶点数目。它的缺点同样是需要占用更多的内存,而且如果没有调整好距离的话,可能会造成模拟的突变。
 
    3、使用遮挡剔除(Occlusion culling)技术 
 
    遮挡剔除是用来消除躲在其他物件后面看不到的物件,这代表资源不会浪费在计算那些看不到的顶点上,进而提升性能。刚才神庙后面的剔除就属于手动的遮挡剔除。
 
    遮挡剔除是一个PRO版才有的功能, 当一个物体被其他物体遮挡住而不在摄像机的可视范围内时不对其进行渲染。遮挡剔除在3D图形计算中并不是自动进行的。因为在绝大多数情况下离 camera 最远的物体首先被渲染,靠近摄像机的物体后渲染并覆盖先前渲染的物体(这被称为重复渲染”overdraw”). 遮挡剔除不同于视锥体剔除. 视锥体剔除只是不渲染摄像机视角范围外的物体而对于被其他物体遮挡但依然在视角范围内的物体,则不会被剔除. 注意当你使用遮挡剔除时,视锥体剔除(Frustum Culling)依然有效。
 
    中间操作
 
    1、曲面细分着色器:是一个可选的着色器,主要用于细分图元。
 
    2、几何着色器:是一个可选的着色器,可用于执行逐图元的着色操作,或者被用于产生更多的图元。
 
    3、裁剪:这一阶段是可配置的。目的是把那些不在视野内的顶点裁剪掉,并剔除某些三角形图元的面片。部分在视野内的图元需要做裁剪处理,在裁剪边缘产生新的顶点和三角形进行处理。
 
    4、屏幕映射:这一阶段是可配置和编程的,负责把每个图元的坐标(三维坐标系)转换成屏幕坐标(二维坐标系)。
 
    5、三角形设置:开始进入光栅化阶段,不再是数学上点了,而会把所有的点都映射到屏幕的具体像素坐标上,计算每条边上的像素坐标而得到三角形边界的表示方式即为三角形设置。
 
    6、三角形遍历:这一阶段会检查每个像素是否被一个三角风格所覆盖。如果覆盖的话,就会生成一个片元(一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。
 
    这些状态包括了屏幕坐标、深度信息,及从几何阶段输出的顶点信息,如法线和纹理坐标等。),这样一个查找哪些像素被三角形覆盖的过程就是三角形遍历。
 
    片元着色器
 
    片元着色器的输入就是上一阶段对顶点信息插值得到的结果,更具体点说,是根据从顶点着色器中输出的数据插值得到的。而这一阶段的输出是一个或者多个颜色值。这一阶段可以完成很多重要的渲染技术,如纹理采样,但是它的局限在于,它仅可以影响单个片元。片元着色器是比较花时间的,因为它是最终颜色的计算者,在某些情况下,例如复杂灯光环境下,片元着色器会出现GPU流水线主要的拖后腿的存在。为了让片元着色器的计算更加快,我们需要从很多方面进行提前的优化: 
 
    1、尽量减少overdraw 
 
    片元着色器最容易拖后腿的情况就是,overdraw!和Android app的开发一样,就是同一个像素点绘制了多次,某些情况会造成计算力的浪费,增加耗电量。前面提到的遮挡剔除有减少overdraw非常有用。在PC上,资源无限,为了得到最准确的渲染结果,绘制顺序可能是从后往前绘制不透明物体,然后再绘制透明物体进行混合。但是在移动平台上,对于不透明物体,我们可以设置从前往后绘制,对于有透明通道的物体(很多UI纹理就是含有透明通道的),再设置从后往前绘制。unity中shader设置为“Geometry” 队列的对象总是从前往后绘制的,而其他固定队(如“Transparent”“Overla”等)的物体,则都是从后往前绘制的。这意味这,我们可以尽量把物体的队列设置为“Geometry” 。对于GUI,尤其要注意和设计师商量,能用不透明的设计就用不透明的,对于粒子效果,也要注意不要引入透明值,多半情况下,移动平台的粒子效果透明值没有作用。
 
    2、减少实时光照 
 
    移动平台的最大敌人。一个场景里如果包含了三个逐像素的点光源,而且使用了逐像素的shader,那么很有可能将Draw Calls提高了三倍,同时也会增加overdraws。这是因为,对于逐像素的光源来说,被这些光源照亮的物体要被再渲染一次。更糟糕的是,无论是动态批处理还是动态批处理(其实文档中只提到了对动态批处理的影响,但不知道为什么实验结果对静态批处理也没有用),对于这种逐像素的pass都无法进行批处理,也就是说,它们会中断批处理。
 
    所以当你需要光照效果时,可以使用Lightmaps,提前烘焙好,提前把场景中的光照信息存储在一张光照纹理中,然后在运行时刻只需要根据纹理采样得到光照信息即可。当你需要金属性强(镜面)的效果,可以使用Light Probes。当你需要一束光的时候,可以使用体积光去模拟这个效果。
 
    3、不要使用动态阴影 
 
    动态阴影很酷,但是对于片元着色器来说是灾难,阴影计算是三角投影计算,非常耗性能。如果想要阴影,可以使用 a、简单的使用一个带阴影的贴图 b、烘焙场景,拿到lightmaps c、创建投影生成器的方法 d、使用ShadowMap的方法(目前还没有研究)。
 
    4、尽量使用简单的shader 
 
    a、建议尽量实用Unity自带mobile版本的(built-in)Shader,这些大大提高了顶点处理的性能。当然也会有一些限制。 
 
    b、自己写的shader请注意复杂操作符计算,类似pow,exp,log,cos,sin,tan等都是很耗时的计算,最多只用一次在每个像素点的计算,还有有些除法运算尽量该能乘法运算等。 
 
    c、避免透明度测试着色器,因为这个非常耗时,使用透明度混合的版本来代替。 
 
    d、浮点类型运算:精度越低的浮点计算越快。 
 
    e、不要在Shader中添加不必要的Pass.
 
    五、Unity3D应用性能优化之内存
 
    unity中有两类内存,一个是Mono托管的内存(相当于DVM的内存),一个是Unity3D使用的资源类类型的内存(Texture、Mesh这种)。
 
    Mono内存
 
    1、尽量不要动态的Instantiate和Destroy Object,使用Object Pool。
 
    2、不要动态的产生字符串,使用字符串的直接拼接,使用System.Text.StringBuilder代替。
 
    3、Cache一些东西,在update里面尽量避免search,如GameObject.FindWithTag(“”)、GetComponent这样的调用,可以在Start中预先存起来。
 
    4、尽量减少函数调用栈,用x = (x > 0 ? x : -x);代替x = Mathf.Abs(x)。
 
    5、定时重复处理用 InvokeRepeating 函数实现。
 
    6、减少GetComponent的调用,使用 GetComponent或内置组件访问器会产生明显的开销。您可以通过一次获取组件的引用来避免开销,并将该引用分配给一个变量(transform用的最多)。
 
    7、使用内置数组,内置数组是非常快的。ArrayList或Array类很容易使用,你能轻易添加元件。但是他们有完全不同的速度。 内置数组有固定长度,并且大多时候你会事先知道最大长度然后填充它。内置数组最好的一点是他们直接嵌入结构数据类型在一个紧密的缓存里,而不需要任何额外 类型信息或其他开销。因此,在缓存中遍历它是非常容易的,因为每个元素都是对齐的。
 
    Unity3D类的内存
 
    这类内存包括 
 
    1、AssetBundle 
 
    Unity3D 里有两种动态加载机制:一个是Resources.Load,另外一个通过AssetBundle,其实两者区别不大。 Resources.Load就是从一个缺省打进程序包里的AssetBundle里加载资源,而一般AssetBundle文件需要你自己创建,运行时 动态加载,可以指定路径和来源的。
 
    AssetBundle运行时加载:
 
    (1)来自文件就用CreateFromFile(注意这种方法只能用于standalone程序,就不提了)。
 
    (2)也可以来自Memory,用CreateFromMemory(byte[]),这个byte[]可以来自文件读取的缓冲,www的下载或者其他可能的方式。其实WWW的assetBundle就是内部数据读取完后自动创建了一个assetBundle而已,Create完以后,等于把硬盘或者网络的一个文件读到内存一个区域,这时候只是个AssetBundle内存镜像数据块,还没有Assets的概念。
 
    下图是AssetBundle的加载卸载示意图:
 
移动平台Unity3D 应用性能优化
 
    AssetBundle是如何加载的呢?用AssetBundle.Load(同Resources.Load) 这才会从AssetBundle的内存镜像里读取并创建一个Asset对象,创建Asset对象同时也会分配相应内存用于存放(反序列化)。异步读取用AssetBundle.LoadAsync,也可以一次读取多个用AssetBundle.LoadAll。
 
    AssetBundle如何释放呢?
 
    AssetBundle.Unload(flase)是释放AssetBundle文件的内存镜像,不包含Load创建的Asset内存对象。
 
    AssetBundle.Unload(true)是释放那个AssetBundle文件内存镜像和并销毁所有用Load创建的Asset内存对象。
 
    2、Texture 
 
    对于IOS选择使用 PVRTC压缩格式的,对于Android选择ETC压缩格式的,纹理可以节省大量内存和读取速度快,但是会有所降低图像的质量。
 
    2D纹理如果没有必要不要使用mimap(会约增加33%的内存开销),曾经在IOS上吃过亏。3D模型的纹理一般是需要mimap的,但是如果确定了3D模型距离摄像机的距离,在GPU分析器上确定了unity使用的纹理,就可以保留,关闭mimap(比如项目中的avatar)。
 
    3.Mesh 
 
    有Mesh合并和Mesh压缩(坑比较多,不建议使用)。
 
    4.Particle 
 
    粒子效果只要记住使用之后及时释放销毁就行。
 
    针对手游的性能优化,腾讯WeTest平台的Cube工具提供了基本所有相关指标的检测,为手游进行最高效和准确的测试服务,不断改善玩家的体验。
 
责任编辑:李欢
本文为授权转载文章,任何人未经原授权方同意,不得复制、转载、摘编等任何方式进行使用,e-works不承担由此而产生的任何法律责任! 如有异议请及时告之,以便进行及时处理。联系方式:editor@e-works.net.cn tel:027-87592219/20/21。
e-works
官方微信
掌上
信息化
编辑推荐
新闻推荐
博客推荐
视频推荐