Unity开发——CPU优化篇

Unity开发——CPU优化篇

  • 前言
    • 示意图
  • 一、渲染模块
    • 1.DrawCall
      • 1.1什么是DrawCall
      • 1.2为什么DC会有损耗
      • 1.3如何优化呢
        • Static batching
        • Dynamic batching
        • Static Batching In Runtime
        • GPU instancing
        • SkinnedMeshRenderer
        • Unity Batch Debugging
    • 2.简化资源
      • 2.1 Texture
      • 2.2 Mesh
      • 其他部分
    • 3.LOD
    • 4.MipMap
      • 5.Occlusion Culling
        • 5.1Culling Group
  • 二、UI模块
  • 三、加载模块

前言

本篇是关于CPU的优化,参考地址为UWA的CPU优化,但是UWA有些总结的比较精简,我会对一些展开解释和补充,下面就是按照示意图进行说明,有些篇幅大的会再开新的博客来说明。
好了,不多说,上干货。

示意图

Unity开发——CPU优化篇_第1张图片

一、渲染模块

1.DrawCall

1.1什么是DrawCall

Draw Call就是CPU调用图形编程接口,比如DirectX或OpenGL,来命令GPU进行渲染的操作。

1.2为什么DC会有损耗

在讲损耗之前,需要明白什么是Batch和合批处理
Batch:图形渲染及优化—Batch
合批技术:图形渲染及优化—Unity合批技术实践
从上面两个博客总结来说:
1.每一次Batch的提交就是一次Draw call调用。
2.损耗主要在:
命令从Runtime到Driver的过程中,CPU要发生从用户模式内核模式的切换。模式切换对于CPU来说是一件非常耗时的工作,所以如果所有的API调用Runtime都直接发送渲染命令给Driver,那就会导致每次API调用都发生CPU模式切换,这个性能消耗是非常大的。
3.那解决方法就是:
Runtime中的Command Buffer可以将一些没有必要马上发送给Driver的命令缓冲起来,在适当的时机一起发送给Driver,进而在Video Card执行。以这样的方式来寻求最少的CPU模式切换,提升效率。

1.3如何优化呢

针对上述方案:将命令缓存,在适当时间,统一发送给Driver。
大家肯定会想到的是Dynamic Batching动态批处理Static batching静态批处理,上面的合批技术也介绍了,我也不展开叙述。只说些注意点

Static batching

Static batching并不减少Draw call的数量,是预先把所有的子模型的顶点变换到了世界空间下,并且这些子模型共享材质,所以在多次Draw call调用之间并没有渲染状态的切换,渲染API会缓存绘制命令,起到了渲染优化的目的。
需要静态批处理的模型会合并到一个新的网格结构中,这意味着模型不能移动,但由于它只需要一次合并操作,所以比动态批处理更高效。
Unity入门精要也有写关于静态批处理的缺陷:

  1. 不能移动,时刻保持静止。
  2. Static batching会导致应用打包之后体积增大,应用运行时所占用的内存体积也会增大。
  3. 需要更多的内存,因为需要储存合并后的几何结构。如果每个物体都共享了相同的网格,那么每一个物体都会对应一个该网格的复制品,即一个网格就会变成多个网格再发送给GPU。举个例子:1000棵树木使用了相同树的模型,再使用了静态批处理,就会多1000倍的内存,这就会成为了一个性能瓶颈。解决办法就是要么忍受牺牲内存换性能,要么不使用静态批处理,转而使用动态批处理。
    Unity开发——CPU优化篇_第2张图片Unity开发——CPU优化篇_第3张图片

Dynamic batching

Dynamic batching在进行场景绘制之前将所有的共享同一材质的模型的顶点信息变换到世界空间中,然后通过一次Draw call绘制多个模型,达到合批的目的。模型顶点变换的操作是由CPU完成的,所以这会带来一些CPU的性能消耗
经过动态批处理的物体仍然是可以移动的,这是因为在处理每帧时,Unity都会重新合并一次网格
动态批处理的好处多,但是限制也多:

  1. 能进行Dynamic batching的模型最高能有900个顶点属性。这里注意不是900个顶点,而是900个定点属性。如果我们在Shader中使用了Vertex Position,Normal and single UV,那么能够进行Dynamic batching的模型最多只能够有300个顶点。如果我们在Shader中使用了Vertex Position、Normal、UV0、UV1 and Tangent那么顶点的数量就减少到180个。(未来可能因为硬件升级,取消这些限制,那就是非常完美了!!!)
  2. 很多地方都会写模型缩放会影响动态批处理,但是在unity5之后,这个限制已经没有了。
  3. 多Pass的shader会中断批处理。因为Multi-pass Shader通常会导致一个物体要连续绘制多次,并切换渲染状态。这会打破其跟其他物体进行Dynamic batching的机会。如果一个GameObject接受一个以上的per-pixel light那么就会为每一个per-pixel light产生多余的模型提交和绘制。我们可以在Quality Settings中将的per-pixel light数量限制为1.这样设置之后Unity通过各种条件判断只会选择出一个Light执行per-pixel lighting。GameObject的绘制在一个Pass内就可以完成,避免了附加的Pass,提高了可合批的概率。
  4. 在Unity的渲染管线中无论采用Forward Rendering或者是Deferred Rendering,半透明物体的渲染都要在最后采用Forward Rendering的方式渲染(Deferred Rendering无法支持半透明渲染)。所有的半透明物体在绘制之前要进行严格的排序,绘制操作按序执行。因为物体绘制的顺序被严格限定,所以物体间能够进行动态合批的概率很小。
  5. 进行合批的前提是多个GameObject共享同一材质,但是对于Shadow casters的渲染是个例外。仅管Shadow casters使用不同的材质,但是只要它们的材质中给Shadow Caster Pass使用的参数是相同的,他们也能够进行Dynamic batching。

总结:Dynamic batching在降低Draw call的同时会导致额外的CPU性能消耗,所以仅仅在合批操作的性能消耗小于不合批,Dynamic batching才会有意义。而新一代图形API( Metal、Vulkan)在批次间的消耗降低了很多,所以在这种情况下使用Dynamic batching很可能不能获得性能提升。Dynamic batching相对于Static batching不需要预先复制模型顶点,所以在内存占用和发布的程序体积方面要优于Static batching。但是Dynamic batching会带来一些运行时CPU性能消耗,Static batching在这一点要比Dynamic batching更加高效。所以我们在实践中可以根据具体的场景灵活地平衡两种合批技术的使用。

Static Batching In Runtime

这篇博客对于静态批处理和运行时批处理解释的很清楚:解决Batching Static静态合并网格的容量问题
照上面静态批处理所说,如果想用Batching Static静态合并,似乎就必须增大AssetBundle的容量。或者换个说法,如果想AssetBundle的容量小,似乎就没有办法使用Batching Static静态合并?
事实上Unity是提供了API可以在运行的时候设置Batching的,StaticBatchingUtility类,具体的API为:
public static void Combine(GameObject staticBatchRoot);
public static void Combine(GameObject[] gos, GameObject staticBatchRoot);
这就是说,你可以选择把所有物体放在一个父节点下面,然后调用第一个API,这样Unity就会自动帮你设置静态合并,也可以使用第二个API自己组装GameObject的数组,来控制哪些GameObject是组合在一起的。
使用这种方法我们可以避免最终打包的应用体积增大,但是由于在运行时通过CPU做模型的合并,会到来一次性的运行时内存和CPU开销

GPU instancing

Unity在5.4版本及之后,新增了一项功能,那就是GPU Instancing。GPU Instancing的出现,给我们提供了新的思路,对于大场景而言将所有的场景物件一次性都加载,对内存来说是很有压力的,我们可以将这些静态的物件如植被等全部从场景中剔除,而保存其位置、缩放、uv偏移、lightmapindex等相关信息,在需要渲染的时候,根据其保存的信息,通过Instance来渲染,这能够减少那些因为内存原因而不能合批的大批量相同物件的渲染时间。
具体如何使用可以看下这篇:U3D优化批处理-GPU Instancing了解一下
现在大部分都是翻译了unity官方案例,源地址:GPU instancing
这篇文章主要介绍GPU的开启条件和shader写法(网上很多教程都是说shader需要支持GPU Instancing,但并没有解释如何实现)。
Unity开发——CPU优化篇_第4张图片
还有一篇雨松大大写的博客:Unity3D研究院之Lightmap支持GPU Instancing(一百零七)
大家想学习如何使用可以在这里学习下,但前提是要会写一些基础的shader。

与Static batching和Dynamic batching的关系:
1.Static batching的优先级要比Instancing的优先级高,如果一个GameObject被标记为static物体并且在Build阶段成功地执行了静态合批,那么如果这个物体还要使用Instancing Shader渲染的话,Instancing会失效。
2.Dynamic batching的优先级要低于Instancing。如果一个GameObject使用Instancing渲染的话,那么对于它的Dynamic batching会失效。

注意SkinnedMeshRenderer会使得GPU instancing和Dynamic batching失效。原因是大量的 skinning(蒙皮)计算发生在 CPU 中,然后相关顶点数据流逐个地被提交到 GPU 再进行渲染计算。一般情况下,CPU是无法一口气把所有的角色数据提交到渲染管线。当一个场景中有大量挂有SkinnedMeshRenderer的对象时,将会产生大量的Draw Call(简称DC) 和动画的计算。

SkinnedMeshRenderer

但是很多时候我们都需要生成大量的角色(例如小兵、小怪),使用Animator来管理角色的动画,而角色也必须使用SkinnedMeshRender来进行渲染。1个2个没关系,1w个小兵呢?答案是性能肯定炸了!!!
大家可以看陈嘉栋大佬写的这篇博客:利用GPU实现大规模动画角色的渲染
从博客里的示意图可以看出:
Unity开发——CPU优化篇_第5张图片
主要会有以下两个巨大的开销:

  • CPU在处理动画时的开销。
  • 每个角色一个Draw Call造成的开销。

那么如何解决呢?
就是将CPU的运算移动到GPU来运算,比如把动画烘培成贴图,用顶点动画的方式来处理。

思路主要是:我们按照固定的频率对角色动画取样并记录取样点时刻角色网格上各个顶点的位置信息,并利用贴图的纹素的颜色属性(Color(float r, float g, float b, float a))保存对应顶点的位置(Vector3(float x, float y, float z))。
这样该贴图就记录了整个动画时间内角色网格顶点在各个取样点时刻的位置,这个贴图我把它称为AnimMap。
在实际工程中,AnimMap是这个样子的。水平方向记录网格各个顶点的位置,垂直方向是时间信息。

具体代码可以下载陈嘉栋大佬的博客里面的demo,如何制作AnimMap。
Unity开发——CPU优化篇_第6张图片
过程是将角色的Animator或Animation去掉,将SkinnedMeshRender更换为一般的Mesh Render,只使用AnimMap利用vs来随时间修改顶点坐标实现的动画效果。

到这里我们就完成了将动画效果的实现从CPU转移到GPU运算的目的,在CPU的开销统计中没有了动画相关的内容。但是在渲染的统计中,Draw Call并没有减少,此时渲染8个角色的场景内仍然有10个Draw Call的开销。下一步再利用GPU Instancing技术减少Draw Call。

再次感谢陈嘉栋大佬写的关于SkinnedMeshRenderer的优化,受益匪浅。

Unity Batch Debugging

Stats Pane
通过Stats pane我们可以获得两个数据:
1.Batches – 目前绘制场景可视区域的Batch数量。
2.Saved by batching – 通过合批渲染我们减少的Batch(Draw call)数量。
Unity开发——CPU优化篇_第7张图片
Profiler
Unity开发——CPU优化篇_第8张图片
这里我们可以获得比Stats pane更加详细的数据。我们能够看到Static Batching和Dynamic Batching的具体执行情况,每种类型的合批技术处理的三角形及顶点的数量。

Frame Debugger
通过Window -> Frame Debugger我们可以打开Unity的Frame Debugger调试工具:
点击“Enable”按钮就可以开始调试场景的Frame渲染了。
Unity开发——CPU优化篇_第9张图片
通过Frame Debugger我们可以获得比Unity Profiler更加详细的批次渲染数据。我们可以查看每一个批次的渲染顺序,批次内合并了哪些内容,并且可以看出是什么因素导致了合批的失败而开启了一个新的批次。

2.简化资源

简化资源是非常行之有效的优化手段。在大量的移动游戏中,其渲染资源其实是“过量”的,过量的网格资源、不合规的纹理资源等等。比如Texture的内存占用、大小、压缩格式;mesh的顶点是否过多之类的。借用UWA的一张图:Unity开发——CPU优化篇_第10张图片不过这是要付费购买的服务,这边介绍一个运行时查看贴图暂用内存的大小及引用关系——profile的Memory!如下图:
Unity开发——CPU优化篇_第11张图片
通过对当前帧的采样(比如运行卡顿的时候),查看当前内存的使用(后面会介绍Unity的内存优化),Assets可以查看当前的Texture2D、Mesh等比较消耗内存的贴图等。肯定不如UWA的好用,但是已经是非常好的工具了。

2.1 Texture

在这里插入图片描述在这里插入图片描述
举个例子,上述Texture是分别设置MaxSize为2048,512的大小差别,所以选择正确的纹理大小、压缩格式非常的重要,不仅减少了内存的占用,也减少了CPU的压力
Unity开发——CPU优化篇_第12张图片
主要压缩 Size、Format。根据实际的状况来调整这两块的数值。如果不需要细节的模型,贴图可以不产生 MipMap。
更细节的注意事项可以看这篇:Unity游戏开发图片纹理压缩方案
大家有兴趣的可以看看,讲的非常到位!我这边给个总结:

  • 高清晰无压缩 - RGBA32
    RGBA32等同于原图了,优点是清晰、与原图一致,缺点是内存占用十分大;对于一些美术要求最好清晰度的图片,是首选。
  • 中清晰中压缩 - RGBA16 + DitheringRGBA16 + Dithering
    RGBA16的优点:内存占用是RGBA32的1/2,搭配上Dithering抖动,在原尺寸下看清晰度一模一样。
    缺点:Unity原生不支持Dithering抖动,需要自己做工具对图片做处理。
  • 低清晰高压缩 - ETC1+Alpha/PVRTC4
    为什么游戏开发中经常看到一些图片,需要设置成2的次方?因为像ETC1、PVRTC4等这类在内存中无需解压、直接被GPU支持的格式,占用内存极低,而且性能效率也是最好的。但是,相对RGBA32,还是能肉眼看出质量有所下降的。

所以在一个商业项目,混搭多种纹理格式是在所难免的事情。把项目纹理划分成高、中、低三种质量需求,节省带宽。
还有一个mipmap技术会在后面介绍。

2.2 Mesh

这边我也不过多叙述,陈嘉栋大佬这边总结的很好:Unity的Mesh压缩:为什么我的内存没有变化?
建议:事实上,期望开启Mesh Compression后Mesh所占用的内存降低,是对Mesh Compression的作用的误解,这个选项的开启是对模型进行压缩的意思。但是实际上开启这个选项只会减小mesh在硬盘的存储大小,在runtime时vertex使用format并没有被改变,仍然是Float。因此也就无法实现Runtime时内存的优化。所以如果为了优化Mesh的内存开销,不要开启Mesh Compression,以避免Vertex Compression的失效。

其他部分

剩下的一些就是比较老生常谈的,比如模型顶点个数即尽可能减少模型中三角面片的数目,对与GPU来说,它本质上只关心有多少个顶点。所以这边的建议是:移除不必要的硬边及纹理衔接,避免边界平滑和纹理分离。

3.LOD

层级细节(Level of Detail ),根据物体在游戏画面中所占视图的百分比来掉调用不同复杂度的模型的。
Unity官方:Level of Detail (LOD) - Unity - Manual
用法和教程:Unity LOD-Level of Detail(多层次细节)用法教程

4.MipMap

Mipmap技术有点类似于LOD技术,但是不同的是,LOD针对的是模型资源,而Mipmap针对的纹理贴图资源。
参考博客:Unity中关于 Mipmap
这边也提下:可以在Scene视图中可视化mipmap级别
Unity开发——CPU优化篇_第13张图片
可视化mipmap级别 :


  • 高绘图密度
    纹理分辨率大于所需的

  • 蓝色
    低绘图密度
    可以增加纹理分辨率

博客里还介绍了如何可视化MipMap、场景物体显示MipMap以及在Game画面和Scene视图中显示mip地图级别。有兴趣和需要的可以看看。

5.Occlusion Culling

具体使用可以看这篇博客:Unity_遮挡剔除

注意点:
LOD和遮挡剔除完成的操作都是静态的物体,但是在我们的实际项目开发中,NPC,Monster,建筑物等都是动态生成的。这种情况肯定就无法烘焙成静态的。

这篇有介绍:如何动态设置LOD和遮挡剔除
思路是通过射线或者距离来设置物体MeshRender组件失活。

5.1Culling Group

官方说明:
https://docs.unity3d.com/2018.2/Documentation/Manual/CullingGroupAPI.html
大量对象移动和剔除群组

如同 Transform Manipulation 那节所述,移动有超大层级结构的 Transform 对象会造成很大的 CPU 消耗。但在现实的环境中,通常不可能将对象结构精简到最少的 GameObjects。

同时,如果可以最好在玩家不发现的前提下,删除玩家看不到的行为。例如,在有大量角色的场景时,只针对屏幕可见范围内的角色计算网格蒙皮(Mesh-skinning)和处理角色动作等等。不需要浪费CPU的资源在计算屏幕外看不到的角色行为。

这两个问题都可以透过 Unity 5.1 导入的 API 来完美解决:CullingGroups。

与其直接操作场景中一大群的 GameObject,而是改变系统操作 CullingGroup 里的一组 BoundingSpheres 的Vector3 参数。每个 BoundingSphere 作为这些 GameObject 在游戏世界中的代表,当 CullingGroup 接近或进入CullingGroup 设定的主镜头的锥体范围内时成员才会收到 callback。然后这些 callback 就可以用来执行启用/停用的程序代码或组件(例如Animators)让物体执行在可见范围内该有的行为。

应用场景有:

  • 粒子当前不可见时,将其暂停
  • 粒子与相机或主角在不同距离阶段时,使用不同的简化版粒子。
  • ai不在视野内时停止更新

这篇博客对Culling Group说明的很到位:利用Culling Group实现LOD和剔除逻辑
还有一个注意事项:Unity什么时候应该手动进行视域Culling?

大部分的Renderer都提供了是否Cull的选项,而且都是默认是激活的(而MeshRenderer和SpriteRenderer则是想关掉Cull都做不到)。
需要手动Culling的时候:

  1. 粒子系统必须做手动Cull。
    Unity开发——CPU优化篇_第14张图片
  2. SkinMeshRenderer
    SkinMeshRenderer默认情况是用面板上设置的固定Bounds来进行Cull的,而这个默认Bounds是根据原始Mesh的数据来决定的,是一个固定值,和动作无关,所以模型如果做出一些类似“橡胶手枪”的动作,超出Bounds范围,就会出现Cull错误。
    想解决这个问题,比较方便的做法是在做这些动作的时候设置updateWhenOffscreen为true(粒子系统也给这样一个选项就好了),这样它就会动态地计算bounds,可以让Renderer被正确裁剪并减少多边形和DC,但同时,在任何时候都要计算蒙皮,而且联带着导致Animation系统也必须始终计算。

二、UI模块

由于篇幅太长,新写了一篇博客,地址:CPU优化之UI模块

三、加载模块

同上,地址:CPU优化之加载模块

你可能感兴趣的:(技术分享,unity,游戏开发,Unity,优化,CPU)