赞
踩
本篇是关于CPU的优化,参考地址为UWA的CPU优化,但是UWA有些总结的比较精简,我会对一些展开解释和补充,下面就是按照示意图进行说明,有些篇幅大的会再开新的博客来说明。
好了,不多说,上干货。
Draw Call就是CPU调用图形编程接口,比如DirectX或OpenGL,来命令GPU进行渲染的操作。
在讲损耗之前,需要明白什么是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模式切换,提升效率。
针对上述方案:将命令缓存,在适当时间,统一发送给Driver。
大家肯定会想到的是Dynamic Batching动态批处理和Static batching静态批处理,上面的合批技术也介绍了,我也不展开叙述。只说些注意点:
Static batching并不减少Draw call的数量,是预先把所有的子模型的顶点变换到了世界空间下,并且这些子模型共享材质,所以在多次Draw call调用之间并没有渲染状态的切换,渲染API会缓存绘制命令,起到了渲染优化的目的。
需要静态批处理的模型会合并到一个新的网格结构中,这意味着模型不能移动,但由于它只需要一次合并操作,所以比动态批处理更高效。
Unity入门精要也有写关于静态批处理的缺陷:
Dynamic batching在进行场景绘制之前将所有的共享同一材质的模型的顶点信息变换到世界空间中,然后通过一次Draw call绘制多个模型,达到合批的目的。模型顶点变换的操作是由CPU完成的,所以这会带来一些CPU的性能消耗。
经过动态批处理的物体仍然是可以移动的,这是因为在处理每帧时,Unity都会重新合并一次网格。
动态批处理的好处多,但是限制也多:
总结: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更加高效。所以我们在实践中可以根据具体的场景灵活地平衡两种合批技术的使用。
这篇博客对于静态批处理和运行时批处理解释的很清楚:解决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开销。
Unity在5.4版本及之后,新增了一项功能,那就是GPU Instancing。GPU Instancing的出现,给我们提供了新的思路,对于大场景而言将所有的场景物件一次性都加载,对内存来说是很有压力的,我们可以将这些静态的物件如植被等全部从场景中剔除,而保存其位置、缩放、uv偏移、lightmapindex等相关信息,在需要渲染的时候,根据其保存的信息,通过Instance来渲染,这能够减少那些因为内存原因而不能合批的大批量相同物件的渲染时间。
具体如何使用可以看下这篇:U3D优化批处理-GPU Instancing了解一下
现在大部分都是翻译了unity官方案例,源地址:GPU instancing
这篇文章主要介绍GPU的开启条件和shader写法(网上很多教程都是说shader需要支持GPU Instancing,但并没有解释如何实现)。
还有一篇雨松大大写的博客: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) 和动画的计算。
但是很多时候我们都需要生成大量的角色(例如小兵、小怪),使用Animator来管理角色的动画,而角色也必须使用SkinnedMeshRender来进行渲染。1个2个没关系,1w个小兵呢?答案是性能肯定炸了!!!
大家可以看陈嘉栋大佬写的这篇博客:利用GPU实现大规模动画角色的渲染
从博客里的示意图可以看出:
主要会有以下两个巨大的开销:
那么如何解决呢?
就是将CPU的运算移动到GPU来运算,比如把动画烘培成贴图,用顶点动画的方式来处理。
思路主要是:我们按照固定的频率对角色动画取样并记录取样点时刻角色网格上各个顶点的位置信息,并利用贴图的纹素的颜色属性(Color(float r, float g, float b, float a))保存对应顶点的位置(Vector3(float x, float y, float z))。
这样该贴图就记录了整个动画时间内角色网格顶点在各个取样点时刻的位置,这个贴图我把它称为AnimMap。
在实际工程中,AnimMap是这个样子的。水平方向记录网格各个顶点的位置,垂直方向是时间信息。
具体代码可以下载陈嘉栋大佬的博客里面的demo,如何制作AnimMap。
过程是将角色的Animator或Animation去掉,将SkinnedMeshRender更换为一般的Mesh Render,只使用AnimMap利用vs来随时间修改顶点坐标实现的动画效果。
到这里我们就完成了将动画效果的实现从CPU转移到GPU运算的目的,在CPU的开销统计中没有了动画相关的内容。但是在渲染的统计中,Draw Call并没有减少,此时渲染8个角色的场景内仍然有10个Draw Call的开销。下一步再利用GPU Instancing技术减少Draw Call。
再次感谢陈嘉栋大佬写的关于SkinnedMeshRenderer的优化,受益匪浅。
Stats Pane
通过Stats pane我们可以获得两个数据:
1.Batches – 目前绘制场景可视区域的Batch数量。
2.Saved by batching – 通过合批渲染我们减少的Batch(Draw call)数量。
Profiler
这里我们可以获得比Stats pane更加详细的数据。我们能够看到Static Batching和Dynamic Batching的具体执行情况,每种类型的合批技术处理的三角形及顶点的数量。
Frame Debugger
通过Window -> Frame Debugger我们可以打开Unity的Frame Debugger调试工具:
点击“Enable”按钮就可以开始调试场景的Frame渲染了。
通过Frame Debugger我们可以获得比Unity Profiler更加详细的批次渲染数据。我们可以查看每一个批次的渲染顺序,批次内合并了哪些内容,并且可以看出是什么因素导致了合批的失败而开启了一个新的批次。
简化资源是非常行之有效的优化手段。在大量的移动游戏中,其渲染资源其实是“过量”的,过量的网格资源、不合规的纹理资源等等。比如Texture的内存占用、大小、压缩格式;mesh的顶点是否过多之类的。借用UWA的一张图:不过这是要付费购买的服务,这边介绍一个运行时查看贴图暂用内存的大小及引用关系——profile的Memory!如下图:
通过对当前帧的采样(比如运行卡顿的时候),查看当前内存的使用(后面会介绍Unity的内存优化),Assets可以查看当前的Texture2D、Mesh等比较消耗内存的贴图等。肯定不如UWA的好用,但是已经是非常好的工具了。
举个例子,上述Texture是分别设置MaxSize为2048,512的大小差别,所以选择正确的纹理大小、压缩格式非常的重要,不仅减少了内存的占用,也减少了CPU的压力。
主要压缩 Size、Format。根据实际的状况来调整这两块的数值。如果不需要细节的模型,贴图可以不产生 MipMap。
更细节的注意事项可以看这篇:Unity游戏开发图片纹理压缩方案
大家有兴趣的可以看看,讲的非常到位!我这边给个总结:
所以在一个商业项目,混搭多种纹理格式是在所难免的事情。把项目纹理划分成高、中、低三种质量需求,节省带宽。
还有一个mipmap技术会在后面介绍。
这边我也不过多叙述,陈嘉栋大佬这边总结的很好:Unity的Mesh压缩:为什么我的内存没有变化?
建议:事实上,期望开启Mesh Compression后Mesh所占用的内存降低,是对Mesh Compression的作用的误解,这个选项的开启是对模型进行压缩的意思。但是实际上开启这个选项只会减小mesh在硬盘的存储大小,在runtime时vertex使用format并没有被改变,仍然是Float。因此也就无法实现Runtime时内存的优化。所以如果为了优化Mesh的内存开销,不要开启Mesh Compression,以避免Vertex Compression的失效。
剩下的一些就是比较老生常谈的,比如模型顶点个数即尽可能减少模型中三角面片的数目,对与GPU来说,它本质上只关心有多少个顶点。所以这边的建议是:移除不必要的硬边及纹理衔接,避免边界平滑和纹理分离。
层级细节(Level of Detail ),根据物体在游戏画面中所占视图的百分比来掉调用不同复杂度的模型的。
Unity官方:Level of Detail (LOD) - Unity - Manual
用法和教程:Unity LOD-Level of Detail(多层次细节)用法教程
Mipmap技术有点类似于LOD技术,但是不同的是,LOD针对的是模型资源,而Mipmap针对的纹理贴图资源。
参考博客:Unity中关于 Mipmap
这边也提下:可以在Scene视图中可视化mipmap级别
可视化mipmap级别 :
红
高绘图密度
纹理分辨率大于所需的
蓝色
低绘图密度
可以增加纹理分辨率
博客里还介绍了如何可视化MipMap、场景物体显示MipMap以及在Game画面和Scene视图中显示mip地图级别。有兴趣和需要的可以看看。
具体使用可以看这篇博客:Unity_遮挡剔除
注意点:
LOD和遮挡剔除完成的操作都是静态的物体,但是在我们的实际项目开发中,NPC,Monster,建筑物等都是动态生成的。这种情况肯定就无法烘焙成静态的。
这篇有介绍:如何动态设置LOD和遮挡剔除
思路是通过射线或者距离来设置物体MeshRender组件失活。
官方说明:
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)让物体执行在可见范围内该有的行为。
应用场景有:
这篇博客对Culling Group说明的很到位:利用Culling Group实现LOD和剔除逻辑
还有一个注意事项:Unity什么时候应该手动进行视域Culling?
大部分的Renderer都提供了是否Cull的选项,而且都是默认是激活的(而MeshRenderer和SpriteRenderer则是想关掉Cull都做不到)。
需要手动Culling的时候:
由于篇幅太长,新写了一篇博客,地址:CPU优化之UI模块
同上,地址:CPU优化之加载模块
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。