CPU
游戏线程
Animation
- EventGraph的执行需要调用虚拟机,性能较低,建议尽量避免这类使用,可以考虑多用Fast Path+多线程异步计算来提升性能[1]
- LOD不仅可以用于mesh的面片,还可以用于模型的动画,屏占比较低的蒙皮模型可以考虑降低其骨骼动画计算频率(中间通过插值来过渡),从而降低计算消耗,这种技术叫做URO(Update Rate Optimization)[1],这里除了对全局更新频率进行控制之外,还可以在AnimGraph中设置某级LOD开始,哪些节点就不需要更新。
- Character的动画消耗相对于其他动画消耗要高很多,因为需要进行一系列的规则判断,比如碰撞、斜坡检测、跨越高度检测等,因此通常会考虑只对主角进行一些复杂计算,其他玩家的角色则会考虑根据服务器下发的数据进行插值处理,绝大部分情况下表现不会有太大差异[1]
GamePlay
- 蓝图中的Tick调用是通过Event Graph来实现的,前面说过,Event Graph的调用会增加虚拟机的调用开销,当次数多了就会影响性能,因此可以考虑尽量避免这种方式,常用的策略有转移到C++,或者采用直接换成其他的不需要Tick的方案(比如风车的转动可以考虑使用顶点动画?)[1]
Physics
- 一个物理场景通常有两棵树,一棵负责Query,一棵负责Simulation,要想得到较好的性能,一个方面是减少注册到场景中的物理物件数目,另一个方面是降低物理对象的空间复杂度(面数)[1]
- 可以考虑共享异步物理场景的数据,这种共享可以通过较小的内存消耗来实现,从而在进行物理模拟的时候,同时允许异步线程进行物理查询[1]
- 视野范围比物理模拟范围通常要大很多,而UE中Mesh只要加载到内存中就会自动将物理对象注册到物理场景中,从而导致物理对象占用的内存与计算消耗都高很多,可以通过将这两种数据进行解耦的方式来降低消耗[1]
Draw Commit
- UE中静态物件是在添加到场景中就确定其渲染顺序,添加到对应的Draw Policy中,而动态创建的单位(这里指的应该是动态的物体,比如蒙皮模型等)则会在每帧InitViews阶段才能获得对应的数据,因此其渲染顺序是动态计算的,渲染效率较低,对于这种情况,其实是可以做区分处理的,将一些短期内不变的物件添加到一个StaticRenderPath中来提升渲染性能[1]
- UE的Texture Streaming每帧会在CPU中计算每张贴图渲染时候所需要的精度(游戏线程),之后按需提交给GPU,而分析画面的Wanted Mip的计算量是不低的,因此,在游戏线程性能吃紧的时候,可以考虑降低Wanted Mip的计算频率。[1]
Transform
- SceneComponent是场景中有坐标的物件都会携带的一个组件,这个组件的Transform更新是在Game线程中进行的,当场景中物件较多的时候,这个更新的消耗会占用较多的性能,虽然这个计算过程可以考虑放在异步线程中完成,还是会有较多的消耗[1]
- SceneComponent位置变化的时候,会触发Overlap检查,如果每帧Transform变化数过多,就会导致这个计算消耗变高,因此可以考虑关闭不必要的overlap[1]
UI
- UI层级复杂,就会导致位置更新计算消耗高,可以考虑通过SlatLayoutCaching和Invalidation Box对数据进行Cache处理来避免不必要的Transform更新计算[1]
- 尽量将Widget进行合批处理,引擎有一些自动合批的策略,比如自带的一些控件如Horizental和Vertical Box,Grid等,其下的子控件是属于同一层的,因此引擎会考虑作为一个Draw Call绘制出来,而其他的灵活控件其下的子控件由于引擎无法自动识别顺序,会默认对新增的子控件的Z-Order加一,通过手动调整这个Order可以实现合批以降低消耗,当然,合批的前提是多个UIWidget使用的是同一个材质且贴图是同一张。[1]
音频
- 音频开销很大,《堡垒之夜》中移动端上Sound Source的总数是16,主机上则是32[1]
特效
- 特效的消耗在于OverDraw,UE提供了一种对贴图进行自动裁剪的方案,这种方案可以通过八边形而非四边形来最大化贴图的有效面积率以降低overdraw,这种方案在支持SRV的设备上可以打开[1]
Level Streaming
- Level Streaming整个过程包含三个步骤,分别是IO,数据反序列化以及PostLoad,在Event Driven模式下,IO跟反序列化可以并行进行,且打开s.AsyncLoadingThreadEnabled之后还可以将反序列化放在异步线程完成,而PostLoad由于需要注册对象到游戏线程,因此整个过程是放在主(游戏)线程完成的,为了提升性能,UE这边将一些不需要游戏线程参与的内容挪到了异步线程,从而减轻游戏线程的负担
渲染线程
- 场景遍历消耗,场景过于复杂会导致场景遍历需要较高的消耗,从而导致渲染Draw调用时间延后影响整体性能,这就是为什么会有加速遍历(如四叉树等加速结构以及UE的多线程并行遍历)策略的原因
- 剔除消耗,剔除是为了最快速度的移除不需要绘制的物件,常见的剔除有视锥剔除、遮挡剔除等,想要提升剔除的精确性,就要增加计算的复杂度,二者不可兼得,因此这里会存在一个平衡。
- Draw Call消耗,每个Draw Call在提交的时候都有不低的消耗,尤其是当渲染状态发生变化时,消耗尤其之高,这就是为什么需要减少Draw Call以及对Draw Call进行排序(DrawPolicy)的原因,常见的策略是合批,包括HISM等GPU Instancing方案,以及HLOD等静态合批方案。
卡顿优化
- level streaming在启用异步加载的时候,网络同步Spawn Actor需要依赖加载数据的时候,可能会因为flush导致卡顿
- ShaderCache/ShaderPipelineCache,提供了离线状态搜集需要Compile的shader信息,将之写入文件,在首次启动的时候分多次预先进行Compile以减少运行时的Compile消耗
- SpawnActor导致的卡顿,解决方式有,减少每个Actor挂接的Components的数量,尽量使用C++ Components而非蓝图Components,Components注册到游戏线程可以分时完成,对于同类Actor,如果有大量Spawn需求,建议使用Actor Pool等
- GC卡顿,GC主要分成两个阶段,分别是扫描与标记阶段,以及清理阶段,后者可以分帧完成,对于一些渲染线程依然访问的资源,会通过render fence标记出来,当渲染线程释放后,在下一帧的主线程再进行清理,前面的扫描标记阶段(也称为引用分析阶段)则需要在一帧之内完成,这是GC卡顿的主要原因,UE的优化方法有:多线程并行处理、跳过常驻对象、对象Clustering处理(一荣俱荣的处理方式来增大处理粒度)等
GPU
- PS Bound
- 改分辨率
- Quality Switch实现不同设备的计算LOD
- 带宽Bound
- 调整SceneColor的格式
- 地形Bound
- LOD策略从基于距离更改为基于屏占比
- 材质建议不超过三层,三层材质情况中,两张weight map可以跟normal共用一张贴图,比较有优势
- Discard优化,像素discard会导致early-z失效,从而导致overdraw增加,性能下降较为明显,解决方案可以考虑增加一个depth的prepass,这个pass会将depth写入到RT,之后在base pass通过Depth Equal来模拟early z。此外,对于LOD切换而言,通常不是直接切换(太生硬了),而是将两个LOD都绘制一遍,通过一个mask控制两级LOD的平滑过渡(比如某个像素的mask为1,那么就第一级LOD绘制,第二级LOD不画,反之亦然,这种方式称为dither),而通过alpha test开启的mask会导致discard,为了避免discard,可以考虑用stencil test替代alpha test
内存优化
- Shared Shader Code有助于降低Shader代码导致的内存消耗
- Shader Library,将Shader代码存入Library,MaterialInstance只存ID,以避免Shader代码的重复
- 由于CDO的存在导致用普通指针引用的UTexture指针无法被GC掉,这种问题可以通过weakptr的方式来解决
- UI Slate Atlas是为了进行UI绘制的合批,提升性能,不过Atlas Size过大也会导致一些空间占用的浪费,而Atlas Size太小又会导致合批效果变差,这里需要做一个平衡
- GPU Particle功能开启的时候,会使用两张分辨率为1024而每个像素占用128位的贴图来存储particle的位置以及速度数据,因此在不用开启GPU Particle的时候可以把fx.AllowGPUParticles关掉
- FSlateRHIResoureceManage,FrenderTargetPool等Pool中建议根据需要主动调用资源释放接口,从而释放一些短期内不需要用到的,但是在之前的使用过程中膨胀大的Pool空间占用
除此之外,UE针对内存消耗还有一些其他的优化点:
网友评论