Technology Performance - chaolunner/CloudNotes GitHub Wiki
DrawCall
DrawCall就是一个命令,从CPU发起,GPU接收。这个命令指向一个需要被渲染的图元(primitives)列表,告诉GPU开始进行一个渲染过程。相当与OpenGL中的glDrawElements命令,DirectX中的DrawIndexedPrimitive命令。 数据(准备渲染对象,然后将渲染对象从硬盘加载到内存,然后从内存加载到显存,进而方便GPU高速处理)、状态(设置每个对象的渲染状态,也就是设置对象的材质、纹理、着色器等)、命令(输出渲染图元,然后向GPU发送DrawCall命令,并将渲染图元传递给GPU)等,需要做很多工作。而GPU的渲染速度往往快于CPU提交命令的速度。如果DrawCall太多,CPU就会把大量时间花在提交DrawCall上,造成CPU过载,而GPU空闲。
MipMaps 纹理贴图金字塔
用于优化实时3D引擎的性能。远离相机的对象使用较小的纹理版本。使用mipmap会占用33%以上的内存,但不使用它们会导致巨大的性能损失。对于游戏中的纹理,应该始终使用mipmap。唯一例外的是永远不会变小的纹理(例如GUI纹理、Skybox、光标和cookie)。mipmap对于避免许多形式的纹理混叠和闪烁也是必不可少的。
Level of Detail(LOD)模型金字塔
用于优化实时3D引擎的性能。远离相机的对象使用较低细节的模型版本。使用LOD会占用更多的内存,但不使用它们会导致巨大的性能损失。对于游戏中的模型,如果该模型顶点数较多而且在游戏中时而远离时而靠近摄像机,则应该考虑使用LOD。
Static Batching 静态批处理
-
静态批处理的时间点
在游戏导出的时候,在Player Setting中勾选Static Batching,这样在导出包的时候就进行批处理,导出来的包会比较大。
在游戏场景中,勾选场景物体的static选项,在加载该场景的时候,会进行一次静态批处理的合并,这样导出来的包不大,但是在加载的时候会使得内存变大。
-
静态批处理的基本原理
场景中有4个物体,ABCD,如果都勾选静态选项,在进行静态批处理的时候,引擎会判断这四个物体是否共用同一渲染材质。
如果共用同一渲染材质,则会将这四个物体视为可以批处理的对象,引擎会基于单个渲染对象的大小拷贝出3个,总共变为4个mesh,此时这4个mesh会存在一个index buffer中,此时会让资源占用的内存变大4倍。
-
为什么要用静态批处理?
在游戏的运行中,有时候CPU的瓶颈也会至关重要。如果CPU的运行速度较慢,则GPU会出现等待CPU的情况,此时游戏主要受到CPU的限制。
CPU在游戏中的主要分工,主要分为两个部分:设置渲染状态和调用DrawCall。 其中设置渲染状态属于比较重要的分工,对于加载到游戏中的资源和对象等,CPU需要计算其顶点相关的矩阵,渲染所用的贴图,渲染所用到的材质和Shader,渲染所用到的灯光等。
如果每个物体的材质和贴图等都不一样,此时CPU的主要工作就是设置这些物体的渲染状态(当然调用DrawCall也会更多,但此时渲染状态的改变更消耗性能,也就是setPass),游戏的运行会比较缓慢。所以在常见的游戏中,对于大量的不需要改变位置的物体,都会采用静态批处理的方式来解决渲染状态的瓶颈。
采用批处理的方式,对于相同渲染材质的物体,会合并成一个更大的渲染对象Mesh来进行渲染,这时候设置合并后的渲染对象的渲染状态,与设置合并前的多个渲染对象的渲染状态相比会大大减小次数。
此外对于大部分的渲染对象,主要的判断依据就是渲染状态和位置矩阵相关的参数。如果渲染状态一致,则基本可以视为同一个批处理的对象,然后对于位置矩阵进行单独的设置即可。其实在引擎的内部,对于需要渲染的对象也会进行一个渲染排序,会优先将渲染状态相同的排在一起进行设置,这样渲染状态的切换就不会过于频繁。
通过批处理的方式来降低渲染状态的切换次数,可以极大的优化CPU的渲染瓶颈,所以在很多时候会采用静态批处理的方式来优化CPU的瓶颈。
-
对于静态批处理后的物体,如何决定其可视?
对于静态批处理后的物体,比如ABCD,那么如何在实际的游戏中去具体的渲染ABCD中的哪几个可见(位于相机的视锥体内)? 这需要解释一下批处理的合并方式:在Unity5中,会构建一个更大的内存buffer空间,依次存放ABCD的数据。
在渲染A的时候,会调用DX的接口来取这份buffer中的指定起始点和长度的数据出来,传递到GPU中进行渲染。如果需要渲染AD两个对象,则会在前面的基础上,再将整体取出来(因为D存在末尾),然后传递到GPU中进行渲染。
注意此时是整体传递A或者ABCD的数据,不会在CPU进行裁剪的工作。具体的A的哪部分可见,是在GPU的顶点裁剪过程中进行的。关于GPU的渲染原理详见CG
此时相当于要进行两次渲染,渲染的次数增大了,这样当然会带来一定的性能损耗,但是相对于渲染状态的设置改变带来的性能损耗,是可以接受的。游戏中如果大量的物体都采用静态批处理,此时会出现很大的内存buffer,如果渲染头和尾部的物体,则会使得渲染数据过大(CPU传递给GPU),带来较大性能损耗,所以可以在游戏中对静态批处理对象进行一个分块 StaticBatchingUtility.Combine() 的处理。
将场景中的对象分成多个块,每个块的大小可以依据一个经验值来设置,此时就会出现多个静态批处理的操作,而不是统一的一个静态批处理操作。具体的分块操作取决于具体的项目的场景大小,可以多次测试得到一个经验值来进行设置。
Dynamic Batching 动态批处理
Unity将尝试自动批量移动物体到一个Draw Call中。要使物体可以被动态批处理,它们应该共享相同的材质,但是还有一些其他约束条件:
- 批处理动态物体需要在每个顶点上进行一定的开销,所以动态批处理仅支持小于900顶点的网格物体;如果你的着色器使用顶点位置,法线和UV值三种属性,那么你只能批处理300顶点以下的物体;如果你的着色器需要使用顶点位置,法线,UV0,UV1和切向量,那你只能批处理180顶点以下的物体。
- 尽量不要使用缩放尺度(scale)。分别拥有缩放尺度(1,1,1)和(2,2,2)的两个物体将不会进行批处理;统一缩放尺度的物体不会与非统一缩放尺度的物体进行批处理。使用缩放尺度(1,1,1)和 (1,2,1)的两个物体将不会进行批处理,但是使用缩放尺度(1,2,1)和(1,3,1)的两个物体将可以进行批处理。
- 拥有光照贴图的物体有其他渲染器参数,例如光照贴图索引或光照贴图的偏移与缩放。一般来说,动态光照贴图的游戏对象应该指向完全相同的光照贴图的位置。
- 多通道(Pass)的Shader会妨碍批处理操作。比如,几乎Unity中所有的着色器在前向渲染中都支持多个光源,并为它们有效地开辟多个通道,这就会要求额外的渲染次数,所以绘制 "额外的每像素灯" 时不会被批处理。
蒙皮骨骼
- CPU Skinning (效率比较低)
- GPU Skinning (技术方案还不太成熟) 由于CPU需要拿到GPU蒙皮后的骨骼点信息(挂点)Unity采用OpenGL ES3.0 API Transform Feedback (IOS也有类似功能) GPU蒙皮后再将骨骼点信息回传到CPU中
Animation-Instancing
- 在GPU上做骨骼变换,以及骨骼蒙皮
- 在编辑模式下先将动画每帧所有骨骼节点以及蒙皮信息都烘培在一张图中
- 运行时在Shader中先计算出动画当前播放的帧取出骨骼节点和蒙皮信息,最后在VS里计算每个顶点的位置实现骨骼动画效果,通过GPU Instancing一次将大量动画渲染出来
- 如果需要CPU挂点(节点下挂特效需求),可以在编辑器模式下预先将挂点每帧的坐标记录下来,运行时直接查表就可以了
GPU骨骼动画隐患
- GPU骨骼动画虽然降低了CPU的开销,但是增加了GPU的开销,而且还会额外增加内存(因为需要一张额外的贴图,动画数量越大内存也就越大)
- 原本的动画文件只是保存了关键帧,每帧的骨骼变换都是拟合出来的,所以内存远远小于烘培出来的动画贴图
- 并不是所有手机的GPU性能都很好,有些手机设计上并不是为了玩游戏,所以可能CPU比较好,GPU非常弱
- 如果是这样的情况下GPU骨骼动画性能还不如CPU做骨骼动画,因为Unity默认CPU骨骼动画是用Job多线程做的,如果CPU足够好性能也很强
- 所以如果采用GPU动画,一定要同时支持CPU动画和GPU动画的切换,在某些GPU性能较差的设备上,fallback成CPU动画
Job 动画
- 如果你的项目采用CPU动画,Unity还提供了Job骨骼动画
- 它的功能就是在播放动画的时候可以开启Job,并且在Job中读取动画流信息
- 比如我们想动画播放时修改骨骼某个节点的朝向,这在之前是无法做到的
- 另外利用AnimationScriptPlayable还可以做到播放动画不用创建AnimationController文件,直接播放动画文件,可以享受新版Animator动画带来效率的提升
Canvas 优化
- 一个Canvas下的所有UI元素都是合在一个Mesh中的,过大的Mesh在更新时开销很大 建议每个较复杂的UI界面,都自成一个Canvas(可以是子Canvas),在UI界面很复杂时,甚至要划分更多的子Canvas
- 动静分离 Canvas又不能细分的太多,因为会导致DrawCall的上升,所以静态(位置、形状不会时常发生改变)的UI元素可以考虑放在一个Canvas下面。
- 把一个面板下的UI元素放到一个图集里,来减少DrawCall
- 背景大图不要和小图放在一个图集里
降低UGUI填充率
-
滥用不可见组件
用不可见的Image作为交互响应的控件,这些东西虽然画上去没有效果,依然占用了显卡资源。
解决办法:实现一个只在逻辑上响应Raycast但是不参与绘制的组件即可。
using UnityEngine; using System.Collections; namespace UnityEngine.UI { public class Empty4Raycast : MaskableGraphic { protected Empty4Raycast() { useLegacyMeshGeneration = false; } protected override void OnPopulateMesh(VertexHelper toFill) { toFill.Clear(); } } }
Order of Execution for Event Functions
-
FixedUpdate 并不是真的有固定的更新频率!
如果你设置FixedUpdate的更新频率是50FPS(0.02s),而我们设定的更新频率为30FPS,那么每次Update调用之前都会调用1~2次FixedUpdate方法。也就是说当你的设备帧率过低时,Unity会通过多次执行FixedUpdate方法来模拟真实的物理时间。