1 前言
看的一个性能优化课程的笔记,课程内容还不错,可以学习一下。课程链接:《Unity性能优化》课程-Metaverse大衍神君。
2 笔记
2.1 静态资源优化

从这五部分入手优化。

部分内部创建资源也会受到导入设置的影响。
2.1.1 Audio导入设置检查与优化
- Unity资源工作流程与Asset checker的使用
具体参考本篇:【Unity】Unity性能分析解决方案——UPR-CSDN博客
-
资源检查报告——Audio部分问题解读
-
根据平台选择合理的音频设置,原始音频资源尽量采用未压缩WAV格式。
-
移动平台对音乐音效统一采用单通道设置(Force to Mono),并将音乐采样频率设置为22050Hz(经验值)。Mono意为单声道。
-
移动平台大多数声音尽量采用Vorbis压缩设置,IOS平台或不打算循环的声音可以选择MP3格式,对于简短、常用的音效,可以采用解码速度快的ADPCM格式(压缩比不是最好,但解码速度快),PCM为未压缩格式。
-
音频片段加载类型说明
-
简短音效导入后小于200kb,采用Decompress on Load模式
-
对于复杂音效,大小大于200kb,长度超过5秒的音效采用Compressed In Memory模式
-
对于长度较长的音效或背景音乐则采用Streaming模式,虽然会有CPU额外开销,但节省内存并且加载不卡顿
-
-
当实现静音功能时,不要简单的将音量设置为0,应销毁音频(AudioSource)组件,将音频从内存中卸载。
-
-
优化前后真机对比(Android)
-
Apk包体由原来的560.7M下降到544.6M
-
Audio部分的内存由76.1M下降到6.9M
-
CPU开销由2.5%左右上升到5%左右,这是由于部分音频资源才用了Streaming模式加载
-
2.1.2 Model导入设置检查与优化
- Unity模型导入流程

-
DCC中模型导出(美术)
-
Unity 支持多种标准和专有模型文件格式(DCC)。Unity 内部使用 .fbx 文件格式作为其导入链。最佳做法尽可能使用 .fbx 文件格式,并且不应在生产中使用专有文件格式。
-
优化原始导入模型文件,删除不需要的数据
-
统一单位
-
导出的网格必须是多边形拓扑网格,不能是贝塞尔曲线、样条曲线、NURBS、NURMS、细分曲面等
-
烘培Deformers,在导出之前,确保变形体被烘培到网格模型上,如骨骼形变烘培到蒙皮权重上
-
不建议模型使用到的纹理随模型导出
-
如果你需要导入blend shape normals,必须要指定光滑组smooth groups
-
DCC导出面板设置, 不建议携带场景信息导出,如不建议导出摄像机、灯光、材质等信息,因为这些的信息与Unity内默认都不同。除非你自己为某DCC做过自定义导出插件。
-
-
-
原始模型文件对性能的影响点(美术)
-
最小化面数,不要使用微三角形,分布尽量均匀
-
合理的网络拓扑和平滑组,尽可能是闭包(即面闭合,否则可能烘焙出问题)。
-
尽量少的使用材质个数
-
尽可能少的使用蒙皮网格
-
尽可能少的骨骼数量
-
FK与IK节点分离,导出时删除IK骨骼节点。(Unity不支持导入的IK骨骼,所以删除)
-
-
模型优化
-
尽可能的将网格合并到一起
-
尽可能使用共享材质
-
不要使用网格碰撞体
-
不必要不要开启网格读写
-
使用合理的LOD级别
-
Skin Weights受骨骼影响个过多
-
合理压缩网格
-
不需要rigs和BlendShapes尽量关闭
-
如果可能,禁用法线或切线
-
多套模型
-
-
资源检查报告——FBX部分问题解读
-
其中两项建议与模型动画有关,而测试项目中所有模型资源都不涉及动画,可以将Rig标签下的Animation Type设置为None,并关闭Animation标签下的Import Animations选项,设置Materials标签中的Material Creation Mode为None.
-
开启Project Settings——>Player——>Optimization下的Vertex Compression与Optimize Mesh Data选项
-
-
优化成果
-
包体大小前后没变化,依然是544.6M
-
运行时模型内存优化前为422.9M,优化后为400.5M,相差的22M来自于运行时CombinedMesh开销的节省。后期运行时模型资源优化才是我们的重点。
-
2.1.3 纹理的基础概念
纹理类型
-
Default:默认的纹理类型格式
-
Normal map:法线贴图,可将颜色通道转换为适合实时法线贴图格式
-
Editor GUI and Legacy GUI:在编辑器GUI控件上使用纹理请选择此类型
-
Sprite(2D and UI):在2D游戏中使用的精灵(Sprite)或UGUI使用的纹理请选择此类型
-
Cursor:鼠标光标自定义纹理类型
-
Cookie:用于光照Cookie剪影类型的纹理
-
Lightmap:光照贴图类型的纹理,编码格式取决于不同的平台
-
Single Channel:如果原始图片文件只有一个通道,请选择此类型
纹理大小
选择合适纹理大小应尽量遵循以下经验:
-
不同平台、不同硬件配置选择不同的纹理大小,Unity下可以采用bundle变体设置多套资源、通过Mipmap限制不同平台加载不同level层级的贴图。
-
根据纹理用途的不同选择不同的纹理加载方式,如流式纹理加载Texture Streaming、稀疏纹理Sparse Texture、虚拟纹理VirtualTexture等方式。
-
不能让美术人员通过增加纹理大小的方式增加细节,可以选择细节贴图DetailMap或增加高反差保留的方式。
-
在不降低视觉效果的情况下尽量减小贴图大小,最好的方式是纹理映射的每一个纹素的大小正好符合屏幕上显示像素的大小,如果纹理小了会造成欠采样,纹理显示模糊,如果纹理大了会造成过采样,纹理显示噪点。这一点做到完美平衡很难保障,可以充分利用Unity编辑器下SceneView->DrawMode->Mipmap来查看在游戏摄像机视角下哪些纹理过采样,哪些纹理欠采样(红:过采样;蓝:欠采样),进而来调整纹理大小。
纹理颜色空间
默认大多数图像处理工具都会使用sRGB颜色空间处理和导出纹理。但如果你的纹理不是用作颜色信息的话,那就不要使用sRGB空间,如金属度贴图、粗糙度贴图、高度图或者法线贴图等。一旦这些纹理使用sRGB空间会造成视觉表现错误。
纹理压缩
纹理压缩是指图像压缩算法,保持贴图视觉质量的同时,尽量减小纹理数据的大小。默认情况下我们的纹理原始格式采用PNG或TGA这类通用文件格式,但与专用图像格式相比他们访问和采样速度都比较慢,无法通用GPU硬件加速,同时纹理数据量大,占用内存较高。所以在渲染中我们会采用一些硬件支持的纹理压缩格式,如ASTC 、ETC、ETC2、DXT等。
如下图为未压缩、ETC2、ASTC6x6三种格式文件大小对比,我们可以看到在质量相差不大的情况下ASTC6x6压缩下大小可以减少到接近未压缩的十分之一。

纹理图集
纹理图集是一系列小纹理图像的集合,
-
优点:
一是采用共同纹理图集的多个静态网格资源可以进行静态合批处理,减少DrawCall调用次数。
二是纹理图集可以减少碎纹理过多,因为他们打包在一个图集里,通过压缩可以更有效的利用压缩,降低纹理的内存成本和冗余数据。
-
缺点
美术需要合理规划模型,并且要求模型有相同的材质着色器,或需要制作通道图去区分不同材质。制作和修改成本较高。
纹理过滤
-
Nearest Point Filtering:临近点采样过滤最简单、计算量最小的纹理过滤形式,但在近距离观察时,纹理会呈现块状。
-
Bilinear Filtering:双线性采样过滤会对临近纹素采样并插值化处理,对纹理像素进行着色。双线性过滤会让像素看上去平滑渐变,但近距离观察时,纹理会变得模糊。
-
Trilinear Filtering:三线性过滤除与双线性过滤相同部分外,还增加了Mipmap等级之间的采样差值混合,用来平滑过度消除Mipmap之间的明显变化。
-
Anisotropic Filtering:各向异性过滤可以改善纹理在倾斜角度下的视觉效果,跟适合用于地表纹理。
纹理Mipmap
-
Mipmap纹理
逐级减低分辨率来保存纹理副本。相当于生成了纹理LOD,渲染纹理时,将根据像素在屏幕中占据的纹理空间大小选择合适的Mipmap级别进行采样。
-
优点:
GPU不需要在远距离上对对象进行全分辨率纹理采样,因此可以提高纹理采样性能。
同时也解决了远距离下的过采样导致的噪点问题,提高的纹理渲染质量。
-
缺点:
由于Mipmap纹理要生成低分辨率副本,会造成额外的内存开销。
2.1.4 纹理导入设置检查与优化

Texture Shape
-
2D 最常用的2D纹理,默认选项
-
Cube 一般用于天空和与反射探针,默认支持Default、Normal、Single Channel几种类型纹理,可以通过Assets > Create > Legacy > Cubemap生成,也可以通过C#代码 Camera.RenderToCubemap在脚本中生成
-
2D Array 2D纹理数组,可以极大提高大量相同大小和格式的纹理访问效率,但需要特定平台支持,可以通过引擎SystemInfo.supports2DArrayTextures 接口运行时查看是否支持。
-
3D 通过纹理位图方式存储或传递一些3D结构话数据,一般用于体积仿真,如雾效、噪声、体积数据、距离场、动画数据等信息,可以外部导入,也可运行时程序化创建。
Alpha Source
默认选择Input Texture Alpha就好,如果确定不使用原图中的Alpha通道,可以选择None。另外From Gray Scale我们一般不会选用
Alpha Is Transparency
指定Alpha通道是否开启半透明,如果位图像素不关心是否要半透明可以不开启此选项。这样Alpha信息只需要占1bit。节省内存
Ignore Png file gamma
是否忽略png文件中的gamma属性,这个选项是否忽略取决于png文件中设置不同gamma属性导致的显示不正常,一般原图制作流程没有特殊设置,这个选项一般默认就好。
Read/Write
开启此选项会导致内存量增加一倍,默认我们都是不开启,除非你的脚本逻辑中需要动态读写该纹理时需要打开此选项。
Streaming Mipmaps(Texture Streaming部分讲解)
Virtual Texture Only(虚拟部分讲解)
Generate Mip Maps
什么时候不需要生成MipMaps?
-
2D场景
-
固定视角,摄像机无法缩放远近
-
Border Mip Maps 默认不开启,只有当纹理的是Light Cookies类型时,开启此选项来避免colors bleeding现象导致颜色渗透到较低级别的Mip Level纹理边缘上
-
MipMap Filtering
-
Box 最简单,随尺寸减小,Mipmap纹理变得平滑模糊
-
Kaiser,避免平滑模糊的锐化过滤算法。
-
-
Mip Maps Preserve Coverage,只有需要纹理在开启mipmap后也需要做Alpha Coverage时开启。默认不开启。
-
Fadeout MipMaps, 纹理Mipmap随Mip层级淡化为灰色,一般不开启,只有在雾效较大时开启不影响视觉效果。
选择合适纹理过滤的最佳经验:
-
使用双线性过滤平衡性能和视觉质量。
-
有选择地使用三线性过滤,因为与双线性过滤相比,它需要更多的内存带宽。
-
使用双线性和 2x 各向异性过滤,而不是三线性和 1x 各向异性过滤,因为这样做不仅视觉效果更好,而且性能也更高。
-
保持较低的各向异性级别。仅对关键游戏资源使用高于 2 的级别。

其他可能有问题的纹理类型
-
纹理图集大小设置不合理,图集利用率低
-
同一图集中,纹理的生命周期应尽可能相近
-
大量只有颜色差异的图片(可以采用分离变化区域贴图)
-
UI背景贴图而不采用9宫格缩放的图
-
纯色图没有使用Single Channel
-
不合理的半透明UI,占据大量屏幕区域,造成Overdraw开销
-
大量2D序列帧动画,而且图片大,还不打图集
-
不合理的通道图利用方案
-
大量渐变色贴图,没有采用1像素过渡图,也不采用Single Channel, 粒子特效中较为常见。(还可替换为曲线,单像素梯度纹理等)
优化前后对比:
纹理内存使用减少了170M左右, APK包体减少了140M左右
2.1.5 动画导入设置检查与优化
Rig标签页

Animation Type
-
None 无动画
-
Legacy 旧版动画,不要用
-
Generic 通用骨骼框架
-
Humanoid 人形骨骼框架
选择原则:
-
无动画选择None
-
非人形动画选择Generic
-
人形动画
-
人形动画需要Kinematices或Animation Retargeting功能,或者没有有自定义骨骼对象时选择Humanoid Rig
-
其他都选择Generic Rig,在骨骼数差不多的情况下,Generic Rig会比Humanoid Rig省30%甚至更多的CPU的时间。
-
Skin Weights
蒙皮顶点最多同时受几根骨骼的影响。默认4根骨头,但对于一些不重要的动画对象可以减少到1根,节省计算量
Optimize Bones
建议开启,在导入时自动剔除没有蒙皮顶点的骨骼
Optimize Game Objects
在Avatar和Animatior组件中删除导入游戏角色对象的变换层级结构,而使用Unity动画内部结构骨骼,消减骨骼transform带来的性能开销。可以提高角色动画性能, 但有些情况下会造成角色动画错误,这个选项可以尝试开启但要看表现效果而定。注意如果你的角色是可以换装的,在导入时不要开启此选项,但在换装后在运行时在代码中通过调用AnimatorUtility.OptimizeTransformHierarchy接口仍然可以达到此选项效果。
Animation标签页

Resmple Curves
将动画曲线重新采样为四元数数值,并为动画每帧生成一个新的四元数关键帧,仅当导入动画文件包含尤拉曲线时才会显示此选项
Anim.Compression
-
Off 不压缩,质量最高,内存消耗最大
-
Keyframe Reduction 减少冗余关键帧,减小动画文件大小和内存大小。
-
Keyframe Reduction and Compression 减小关键帧的同时对关键帧存储数据进行压缩,只影响文件大小。内存和Keyframe Reduction减少后的一致。
-
Optimal,仅适用于Generic与Humanoide动画类型,Unity决定如何进行压缩。
Animation Custom Properties
导入用户自定义属性,一般对应DCC工具中的extraUserProperties字段中定义的数据
动画曲线数据信息

-
Curves Pos: 位置曲线
-
Quaternion: 四元数曲线 Resample Curves开启会有
-
Euler: 尤拉曲线
-
Scale: 缩放曲线
-
Muscles: 肌肉曲线,Humanoid类型下会有
-
Generic: 一般属性动画曲线,如颜色,材质等
-
PPtr:精灵动画曲线,一般2D系统下会有
-
Curves Total: 曲线总数
-
Constant: 优化为常数的曲线(一般动画压缩选项中Error越大,这个越多。常数曲线一般不参与采样,效率更好。)
-
Dense: 使用了密集数据(线性插值后的离散值)存储
-
Stream: 使用了流式数据(插值的时间和切线数据)存储
动画文件导入设置优化后信息查看原则
-
一看效果差异(与原始制作动画差异是否明显)
-
二看曲线数量(总曲线数量与各种曲线数量,越少越好;常量曲线比重大更好)
-
三看动画文件大小(以移动平台为例,动画文件在小几百k或更少为合理,超过1M以上的动画文件考虑是否进行了合理优化)
2.2 Unity工作流
2.2.1 工程目录与Assets目录设置
Unity工程目录结构及用途
-
Asset文件夹:用来存储和重用的项目资产
-
Library文件夹:用来存储项目内部资产数据信息的目录**
-
Packages文件夹:用来存储项目的包文件信息
-
Project Settings文件夹:用来存储项目设置的信息
-
UserSettings文件夹:用来存储用户设置信息
-
Temp文件夹:用来存储使用Unity编辑器打开项目时的临时数据,一旦关闭Unity编辑器也会被删除
-
Logs文件夹:用来存储项目的日志信息(不包含编辑器日志信息)
Unity Assets目录中的特殊文件夹及用途
-
Editor文件夹(可以多个)
-
Editor Default Resources文件夹(根目录唯一)
-
Gizmos文件夹(根目录唯一,存放编辑器中的特殊对象图标,可标记特殊对象或位置,还可添加到Scene视图中)
-
Plugins文件夹(2019后已无,但仍可使用,仍能保障其中代码编译的优先顺序)
-
Resources文件夹(可以多个,强烈建议正式项目中一定不要有此文件夹)
-
Standard Assets文件夹(根目录唯一,其中代码编译优先)
-
StreamingAssets文件夹(根目录唯一)
-
忽略导入的文件夹
- 隐藏的文件夹
- 以"."开头的文件和文件夹
- 以"~"结尾的文件和文件夹
- 扩展名为cvs的文件和文件夹
- 扩展名为.tmp的文件夹
Assets目录结构设计(仅个人建议,不作为标准)
一级目录设计原则:
-
目录尽可能少
-
区分编辑模式与运行模式
-
区分工程大版本
-
访问场景文件、全局配置文件便捷
-
不在一级目录做资源类别区分,只有Video类视频建议直接放到StreamAssets下
二级目录设计原则:
-
只区分资源类型
-
资源类型大类划分要齐全
-
不做子类型区分
-
不做功能区分
-
不做生命周期区分
三级目录设计原则:
-
Audio/Texture/Models三级目录做子类型区分
-
其他类型资源可按功能模块/生命周期区分
四级目录设计原则:
-
只有Audio/Texture/Models做四级目录,可按工呢个模块/生命周期划分
2.2.2 资源导入工作流
资源导入工作流的三种方案
1. 手动编写工具
优点:根据项目特点自定义安排导入工作流,并且可以和后续资源制作与打包工作流结合
缺点:存在开发和维护成本,会让编辑器菜单界面变得复杂,对新人理解工程不友好
适合类型:大型商业游戏团队
AssetPostprocessor:
编写编辑器代码继承AssetPostprocesser对象自定义实现一些列OnPreprocessXXX接口修改资源导入设置属性
伪代码:
public class XXXAssetPostprocessor : AssetPostprocessor
{
public void OnPreprocessXXXAsset()
{
XXXAssetImporter xxxImporter = (XXXAssetImporter)assetImporter;
xxxImporter.属性 = xxx
...
xxxImporter.SaveAndReimport();
}
}
参考资料:资源审核 - Unity 手册
AssetsModifiedProcessor (新试验接口):
资源被添加、删除、修改、移动时回调该对象的OnAssetsModified接口
void OnAssetsModified(string[] changedAssets, string[] addedAssets, string[] deletedAssets, AssetMoveInfo[] movedAssets)
{
...
}
2. 利用Presets功能
结合上面的AssetsModifiedProcessor就可以实现资源导入、变动时,自动检查并按Preset资源进行导入设置。具体见官方文档。
优点:使用简单方便,只需要Assets目录结构合理规范即可
缺点:无法和后续工作流整合,只适合做资源导入设置。
适合类型:小型团队或中小规模项目
参考资料:Applying default presets to Assets by folder - Unity 手册
3. 利用AssetGraph工具
优点:功能全,覆盖Unity资源工作流全流程,节点化编辑,直观
缺点:有一定上手成本,一些自定义生成节点也需要开发,不是Unity标准包(日本Unity内部的工具),Unity新功能支持较慢。
适合类型:任何规模项目和中大型团队
AssetGraph仓库地址:GitHub - Unity-Technologies/AssetGraph: Visual Workflow Automation Tool for Unity.
2.3 编辑器创建资源优化
2.3.1 场景
场景结构设计原则
-
合理设计场景一级节点的同时,避免场景节点深度太深,一些代码生成的游戏对象如果不需要随父节点进行Transform的,一律放到根节点下。(太深的话,在查找时存在效率问题。)
-
尽量使用Prefab节点构建场景,而不是直接创建的GameObject节点。(Prefab节点在多引用时只占用一份内存。)
-
避免DontDestroyOnLoad节点下有太多生命周期过长或引用资源过多的复杂节点对象。Additve模式添加的场景要尤为注意。
-
最好为一些需要经常访问的节点添加tag,静态节点一定要添加Static标记。
注意:复杂场景中,对于设置好Tag的节点,使用FindGameObjectWithTag方法取查找该节点更高效。对比的是Find、FindGameObjectWithTag、FindObjectOfType。最慢的是Type。

2.3.2 预制体
预制体Prefab
Unity中的预制体是用来存储游戏对象、子对象及其所需组件的可重用资源,一般来说预制体资源可充当资源模版,在此模版基础上可以在场景中创建新的预制体实例。
使用预制体的好处
-
由于预制体系统可以自动保持所有实例副本同步,因此可以比单纯地简单复制粘贴游戏对象做到更好的对象管理。
-
此外通过预制体嵌套(Nested Prefabs)可以将一个预制体嵌套到另一个预制体中,从而创建多个易于编辑的复杂游戏对象层级视图。
-
可以通过覆盖各个预制体实例的设置来创建预制体变体(Prefabs Variant),从而可以将一系列覆盖组合在一起形成有意义预制体的变化。
嵌套预制体与单预制体相比的优点与缺点
-
优点:
-
嵌套预制体方便预制体管理,方便资源重复利用,易于统计场景复杂度
-
美术制作时可以比较合理的分配UV,和贴图利用率
-
方便关卡设计人员发挥,充分合理利用资源
-
嵌套预制体比较方便利用工具做LOD,LOD效果也比较好
-
嵌套预制体修改方便,只需修改子预制体就可以做到所有嵌套预制体同步
-
比较方便做场景遮挡剔除,可以做到精细的遮挡剔除优化效果
-
-
缺点:
-
手动做Bundle依赖时要按Scene方式处理,依赖关系较为复杂
-
可能会增加材质数量与Drawcall数量
-
不太适合做大规模远景对象。
-
美术与关卡设计人员要充分考虑组合复杂度与特例场景显示,避免重复性和单一性,需要更多的沟通成本
-
使用Prefab变体的一些限制
-
不能改变本体Prefab游戏对象 (GameObject)层级
-
不能删除本体Prefab中的游戏对象,但可以通过Deactive游戏对象来达到与删除游戏对象同样的效果
-
对于Prefab变体要保持其Override属性的变化,不能通过Apply to base把这些变化应用到本体Prefab上,这样会破坏基础Prefab的结构和功能。
2.3.3 UGUI(上)
Unity UI性能的四类问题
-
Canvas Re-batch 时间过长
-
Canvas Over-dirty, Re-batch次数过多。(UI遮挡,将增加batch数。)
-
生成网格顶点时间过长
-
Fill-rate overutilization
Canvas画布
Canvas负责管理UGUI元素,负责UI渲染网格的生成与更新,并向GPU发送DrawCall指令。
Canvas Re-batch过程
-
根据UI元素深度关系进行排序
-
检查UI元素的覆盖关系
-
检查UI元素材质并进行合批
UGUI渲染细节
-
UGUI中渲染是在Transparent半透明渲染队列中完成的,半透明队列的绘制顺序是从后往前画,由于UI元素做Alpha Blend,我们在做UI时很难保障每一个像素不被重画,UI的Overdraw(同一像素绘制多次)太高,这会造成片元着色器利用率过高,造成GPU负担。
-
UI SpriteAtlas图集利用率不高的情况下,大量完全透明的像素被采样也会导致像素被重绘,造成片元着色器利用率过高;同时纹理采样器浪费了大量采样在无效的像素上,导致需要采样的图集像素不能尽快的被采样,造成纹理采样器的填充率过低,同样也会带来性能问题。
Re-Build过程
-
在WillRenderCanvases事件调用PerformUpdate::CanvasUpdateRegistry接口
-
通过ICanvasElement.Rebuild方法重新构建Dirty的Layout组件
-
通过ClippingRegistry.Cullf方法,任何已注册的裁剪组件Clipping Compnents(Such as Masks)的对象进行裁剪剔除操作
-
任何Dirty的 Graphics Compnents都会被要求重新生成图形元素
-
-
Layout Rebuild(触发情况)
-
UI元素位置、大小、颜色发生变化
-
优先计算靠近Root节点,并根据层级深度排序的Transform操作
-
-
Graphic Rebuild(触发情况)
-
顶点数据被标记成Dirty
-
材质或贴图数据被标记成Dirty
-
使用Canvas的基本准则:
-
将所有可能打断合批的层移到最下边的图层,尽量避免UI元素出现重叠区域
-
可以拆分使用多个同级或嵌套的Canvas来减少Canvas的Rebatch复杂度
-
拆分动态和静态对象放到不同Canvas下。
-
不使用Layout组件
-
Canvas的RenderMode尽量Overlay模式,减少Camera调用的开销
UGUI射线(Raycaster)优化:
-
必要的需要交互UI组件才开启“Raycast Target”
-
开启“Raycast Targets”的UI组件越少,层级越浅,性能越好
-
对于复杂的控件,尽量在根节点开启“Raycast Target”
-
对于嵌套的Canvas,OverrideSorting属性会打断射线监测,避免射线向上一层级的Canvas传递,这样可以降低UI层级遍历的成本。
2.3.3 UGUI(下)
UI字体
-
避免字体框重叠,造成合批打断
-
字体网格重建
-
UIText组件发生变化时
-
父级对象发生变化时
-
UIText组件或其父对象enable/disable时
-
-
TrueTypeFontImporter
-
支持TTF和OTF字体文件格式导入
-

-
动态字体与字体图集
-
运行时,根据UIText组件内容,动态生成字体图集,只会保存当前Actived状态的 UIText控件中的字符
-
不同的字体库维护不同的Texture图集
-
字体Size、大小写、粗体、斜体等各种风格都会保存在不同的字体图集中(有无必要,影响图集利用效率,一些利用不多的特殊字体可以采用图片代替或使用Custom Font,Font Assets Creater创建静态字体资源)
-
当前Font Texture不包含UIText需要显示的字体时,当前Font Texture需要重建
-
如果当前图集太小,系统也会尝试重建,并加入需要使用的字形,文字图集只增不减(2的幂尺寸)
-
利用Font.RequestCharacterInTexture可以有效降低启动时间
-
-
UI控件优化注意事项
-
不需要交互的UI元素一定要关闭Raycast Target选项
-
如果是较大的背景图的UI元素建议也要使用Sprite的九宫格拉伸处理,充分减小UI Sprite大小,提高UI Atlas图集利用率
-
对于不可见的UI元素,一定不要使用材质的透明度控制显隐,因为那样UI网格依然在绘制,也不要采用active/deactive UI控件进行显隐,因为那样会带来gc和重建开销,尽量通过Canvas控件的激活和关闭来控制。
-
使用全屏的UI界面时,要注意隐藏其背后的所有内容,给GPU休息机会。
-
在使用非全屏但模态对话框时,建议使用OnDemandRendering接口,对渲染进行降频。(类似那种降帧的感觉,不过这个是只降渲染频率,不降输入频率。还有另外的接口是两一起降。)
-
优化裁剪UI Shader,根据实际使用需求移除多余特性关键字。
-
-
滚动视图Scroll View优化
-
使用RectMask2d组件裁剪
使用基于位置的对象池作为实例化缓存
-
2.3.4 物理
Unity中的物理解决方案
-
Box2D
-
Nvidia PhysX
-
Unity Physics
-
Havok Physics for Unity
Unity中的物理组件Collider部分的优化
-
Trigger与Collider
-
Trigger对象的碰撞会被物理引擎所忽略,通过OnTriggerEnter/Stay/Exit函数回调
-
Collider对象由物理引擎触发碰撞,通过OnCollisionEnter/Stay/Exit函数回调
-
Collider对象必须至少有一个Collider对象有RigidBody组件,Trigger同理
-
Trigger对象更高效
-
Unity中的物理组件Collider部分的优化
-
尽量少使用MeshCollider,可以用简单Collider代替,即使用多个简单Collider组合代替也要比复杂的MeshCollider来的高效
-
MeshCollider是基于三角形面的碰撞
-
MeshCollider生成的碰撞体网格占用内存也较高
-
MeshCollider即使要用也要尽量保障其是静态物体
-
可以通过PlayerSetting选项中勾选Prebake Collision Meshes选项来在构建应用时预先Bake出碰撞网格。
Unity中的物理组件RigidBody部分的优化
-
Kinematic与RigidBody
-
Kinematic对象不受物理引擎中力的影响,但可以对其他RigidBody施加物理影响。
-
RigidBody完全由物理引擎模拟来控制,场景中RigidBody数量越多,物理计算负载越高
-
勾选了Kinematic选项的RigidBody对象会被认为是Kinematic的,不会增加场景中的RigidBody个数
-
场景中的RigidBody对象越少越好
-
Unity中的RayCast与Overlap部分的优化
-
Unity物理中RayCast与Overlap都有NoAlloc版本的函数,在代码中调用时尽量用NoAlloc版本,这样可以避免不必要的GC开销
-
尽量调用RayCast与Overlap时要指定对象图层进行对象过滤,并且RayCast要还可以指定距离来减少一些太远的对象查询
-
此外如果是大量的RayCast操作还可以通过RaycastCommand的方式批量处理,充分利用JobSystem来分摊到多核多线程计算。
2.3.5 动画
Animation的一些细节
-
播放单个AnimationClip速度,Legacy Animation系统更快,因为老系统是直接采样曲线并直接写入对象Transform
-
针对动画的缩放曲线比位移、旋转曲线开销更大
-
常数曲线不会每帧写入场景,更高效
Animator的一些细节
-
不要使用字符串来查询Animator
-
使用曲线标记来处理动画事件
-
使用Target Marching函数来协助处理动画
-
将Animator的CullingMode设置成Based On Renderers来优化动画,并禁用SkinMesh Renderer的Update When Offscreen属性来让角色不可见时动画不更新
Internal Animation Update

上图属于Animator::UpdateAvatars。
- 白色步骤:动画系统各个时段的回调。
- 灰色步骤:动画系统更新的关键步骤。
Animator VS Animation
-
Animation可以将任何对象属性制作成Animation Clip, Animator是将Animaiton Clip组织到状态机流程图中使用
-
Animation与Animator播放动画时的效率是有个临界点的,这个临界点是根据动画曲线条数来的,当动画曲线条数小于这个临界点时Animation快,当动画曲线条数大于这个临界点时Animator快
-
当Cpu核数较少时,Animation播放动画有优势,当Cpu核数较多时,Animator表现会更好
-
Animator Controller Graph中的所有动画节点的Animation Clip都会载入到内存中,当有海量动画状态机节点时,内存开销较大。可以使用Playable API来解决。
Playable API VS Animator
Unity提供了Playable API的可视化工具PlayableGraph Visualizer。地址:https://github.com/UnityTech/graph-visualizer。
Playable API非常值得学习。
Playable API优点
-
支持动态动画混合,可为场景中的对象提供自己的动画,并可以动态添加到PlayableGraph当中使用
-
允许创建播放单个动画,而并不会产生创建和管理AnimatorController资源所涉及的开销,可更加灵活的控制PlayableGraph的数据流,可以插入自定义的AimationJob。
-
可以控制动画文件加载策略,按需加载、异步加载等
-
允许用户动态创建混合图,并直接逐帧控制混合权重(甚至可以混合AniationClip与AnimatorController动画)
-
可以运行时动态创建,根据条件添加可播放节点。而不需要提前提供一套PlayableGraph运行时启动和禁用节点,可以做到自由度更高的override机制
-
可加载自定义配置数据,更加方便的和其他游戏系统整合
Playable API缺点
-
没有直接使用Animator直观
-
混合模式没有现成的,需要自己实现
-
需要开发更多的配套工具
-
有一定学习成本
解决方案选择
-
一些简单、少量曲线动画可以使用Animation或动画区间库如Dotween\iTween等完成,如UI动画,Transform动画等。
-
角色骨骼蒙皮动画如果骨骼较少,Animation Clip资源不多,对动画混合表现要求不高的项目可以采用Legacy Animation。注意控制总体曲线数量
-
一些角色动画要求与逻辑有较高的交互、并且动画资源不多的项目可以直接用Animator Graph完成
-
一些动作游戏,对动画混合要求较高、有一些高级动画效果要求、动画资源量庞大的项目,建议采用Animator+Playable API扩展Timeline的方式完成。(比如:Animator状态机种只提供角色基础状态机节点,如走、跑、跳等。技能、持不同武器、过场动画等特殊动画则是由Playable API扩展Timeline的方式完成。)
2.4 特别篇-性能优化之道
性能优化问题的本质
-
慢与快的问题
-
前提
-
稳定性:不能因优化造成稳定性变差
-
兼容性:不能因优化导致兼容性变差
-
性价比:优化要有度,考虑成本与复杂度
-
性能优化的流程
-
发现问题(什么平台、什么操作系统、什么情况下出现问题,一般问题还是特例问题等)
-
定位问题(什么地方造成的性能问题,我们要用什么工具、什么方法确定瓶颈)
-
研究问题(确定用什么方案处理这个问题,要考虑性能优化的前提)
-
解决问题(按问题研究的结论去实际处理,并验证处理结果与预期的一致性)
影响性能的四大类问题
-
CPU
-
GPU
-
带宽
-
内存
隐藏的几类小问题
-
功耗比
-
填充率
-
发热量
性能问题可能的情况
-
瓶颈可能性按由高到低的顺序排列(个人经验总结)
-
CPU利用率
-
带宽利用率
-
CPU/GPU强制同步
-
片元着色器指令
-
几何图形到CPU到GPU的传输
-
纹理CPU到GPU的传输
-
顶点着色器指令
-
几何图形复杂性
-
经常用的优化思路
-
升维与降维
-
维度转换,如空间与时间,量纲转换
2.5 性能优化实战
2.5.1 性能总览与瓶颈定位
《文章复制》
《参考文章:Unity性能优化学习笔记(1)性能总览与瓶颈定位》
本篇文章主要是对移动平台上的项目上的帧率进行优化,一般移动平台主要是 iOS 与安卓平台。我这里建议的优化顺序是,先 iOS 在安卓,一是苹果在 iOS 平台上有比较强大的性能分析工具。二是iOS 平台的设备较少,不像安卓那样硬件千差万别,各家的操作系统也五花八门。一般共性的性能问题, iOS 平台优化好了,安卓平台也就差不多七七八八了,只需要再对特殊的硬件和系统做兼容性优化处理即可。
在优化一个项目前,我们需要对整个项目有一个整体性能的了解。一般我们可以通过 Unity 自带的 profile frame、debugger、 memory profiler 以及 profile analyze 进行。如果是安卓平台,推荐使用 Unity 中国这边提供的 UPR 工具,它提供了更多的工具,一般用它来做整体状况分析时会比较方便,而且会自动保留历史记录,方便我们对多次优化结果进行对比。
我们先要对示例项目分别构建 iOS 与安卓应用程序,通过 Unity 自带的性能分析工具和 UPR 工具分别对 iOS 平台与安卓平台做一个整体的性能检测,并根据结果粗略判断下我们的测试项目中的性能瓶颈所在。构建项目工程时,需要在 build Settings 中勾选 development build 与 auto connect profile 选项。
我们先来看 iPhone 手机上 Unity profile 的性能测试结果。我们可以看到一帧内有 70% 的时间都发生在 GFX 点 wait for present on GFX THREAD 上,这是 GPU 与 CPU 之间的同步阻塞,主线程在等待渲染线程的结束。通过这一点,我们可以初步认定 iOS 上的主要性能瓶颈可能发生在 GPU 上,或者 CPU 与 GPU 之间的带宽堵塞上。我们通过 profile 的时间线视图可以进一步发现,一帧的渲染时间大概在 70 毫秒左右,但渲染线程花费了接近 50 毫秒,其中 40 多毫秒都发生在 post process 渲染过程中。
接下来我们再来看安卓平台上 profile 的验证结果,我们可以看到,在小米 11 Ultra 上,每帧的时间消耗大概在 87 毫秒左右,比 iPhone 8 上还要慢。这可能是由于渲染分辨率不同或系统差异造成的,后续我们也会对此进行详细的验证。我们还看到在渲染线程上, GFX present frame 的开销在 74 毫秒左右,这里与 iPhone 上显示的 post process 的消耗不同,但可以确定的一点是,原因都是由于渲染线程慢造成主线程阻塞带来的。
说到这里,我要提一下 Unity 下常见的一些等待函数,你会经常在 profile 中看到这些标记,WaitForTargetFPS是主线程等待达到目标,帧率主要出现在垂直同步的状态下,一般这种情况 CPU 与 GPU 都没什么负载问题。
第二类是WaitForCommand,这类标记代表渲染线程已经准备接受新的渲染命令,这时一般瓶颈在CPU。第三类是 WaitForPresent 标记,它代表主线程在等待渲染线程绘制完成,一般瓶颈在 GPU 端。第四类是WaitForJobGroupID,这类标记代表等待工作线程完成,一般瓶颈也在 CPU 端。了解 profile 中这些经常出现的标记有助于我们快速定位应用程序的性能瓶颈在哪里?
接下来我们再来看 UPR 下的性能测试,你可以通过 UPR 提供的工具进行真机联机测试,也可以使用 UPR 提供的云真机功能进行测试, Android 平台下云真机会更方便一些,下面我们来看一下 iOS 与 Android 平台下生成的真机性能测试报告。我们看到 iOS 下的平均帧率只有 16 帧,纹理资源、模型资源仍然超出标准好多,DC更是达到了接近 3000 次,网格数量也超过了 110 万。概况中还分裂了各个模块的耗时占比与最耗时的TOP30。
今天我们通过 profile 与 UPR 的测试报告,大体上我们可以判定我们的视力工程的性能瓶颈主要在 GPU 端或者 CPU 与 GPU 之间的通讯带宽上。那么接下来我们可以按以下顺序去验证我们的判断,首先我们需要检查渲染流程与使用到的效果是否存在冗余与不合理的地方。其次,我们需要检查渲染中生成的资源,判断有没有冗余和不合理的地方。第三,我们通过报告看到 draw call 与 set pass call 的数量较高,需要进一步优化。第四,我们需要检查工程内使用的 shader 的片源着色器是否存在效率问题,有没有优化的空间。如果上述的优化点我们都做了,依然满足不了我们的性能优化的需求,我们再去考虑优化三角形数。
2.5.2 渲染流程分析
《复制文章》
《参考文章:Unity性能优化学习笔记(2)渲染流程分析》
基于上篇文章,要进一步定位 GPU 端到底哪里存在性能瓶颈,优化 GPU 端的性能,我们需要了解项目的整体渲染流程。在 Unity 编辑器下,我们可以通过 Unity 自带的FrameDebug工具抓取一帧,来看具体的渲染流程。
抓取后,我们可以清晰地看到主要部分分为三块儿, URP.RenderSingleCamera 是场景主相机的渲染, UGUI Rendering.RenderOverlays 为UGUI 部分的渲染,而 GUITexture.Draw 是 IMGUI 的渲染。三个部分后边的数字代表调用 GFX 渲染指令的数量。当我们看到Render GBuffer 与DeferredPass两个流程,基本上我们就可以确定这是个延时渲染流程了。
把主相机渲染流程转换成流程图可以看到,该项目的主相机渲染主要被划分为 12 个部分。首先是主光源的阴影渲染,这部分的渲染指令较多。接下来是渲染 depth与normal的prepass 阶段,接下来是渲染 ColorGrading 的 lookup texture阶段,然后是渲染G buffer 阶段,拷贝场景深度以及真正计算延时光照于着色的Deferred Pass 阶段。接下来是一部分仍然使用前向渲染的特殊的不透明物体,之后视作屏幕空间下的环境遮蔽效果,渲染天空盒,拷贝场景颜色、渲染半透明物体以及最后的渲染后处理特效。
虽然从Frame Debug 中我们可以查看到各个渲染阶段的情况与当前材质的参数,着色器的变量等,但并不能看到每个阶段的具体渲染耗时以及该渲染阶段资源是否冗余的问题。这里重点推荐的工具XCode 中的 metal capture 功能,虽然这个功能运行在 metal API 下,但在分析渲染流程的瓶颈与问题时非常直观,是我们优化移动平台渲染时的最重要的工具。使用时,我们需要打开 Xcode 工程中的编译面板,在 option 标签下打开 GPU frame capture 功能。注意这里不要选用 auto 选项,需要选择 metal 选项之后我们需要勾选profile。
GPU track after capture 选项完成后,我们点击运行,把工程稍录到手机上。当程序在手机上运行起来后,我们在左侧窗口中切换顶部按钮到ShowDebugNavigator 上。这时我们会发现左侧窗口上会出现几个标签,分别是 CPU、Memory、EnergyImpact、Disk、Network 以及 FPS。这里的 FPS 标签在我们不开启 GPU frame capture 功能时是不会出现的。
FPS 标签显示了当下 GPU 的概况以及设备整体顶点着色器、片元着色器的负载情况。可以看到我们的应用程序在所有负载方面都比较高。以上标签显示了我们的应用程序在运行时的整体状况。接下来我们通过下面窗口的 GPU Frame Capture 按钮。
summary 显示了当前应用程序 GPU 端的整体状况,包括性能、显存以及可能与显存带宽性能有关的问题。需要查看具体显存与性能问题,可以切换到具体的标签下查看。我们再来看渲染指令队列部分,这里我们可以通过左下角的小旗子按钮和三角叹号按钮来过滤周扣指令与有问题的渲染指令,并且每个渲染子流程后都有这条渲染指令执行的时间。所以说这个工具提供的信息会比 frame debug 下提供的更加直观。我们可以看到具体哪一步产生了渲染瓶颈。
我们可以看到,当前的一帧 GPU 共花费了 60 毫秒左右,其中主光源阴影花费了 4. 7 毫秒, depth normal Pre pass 花费了 1. 63 毫秒,渲染 color grading lookup texture 花费了 0. 1 毫秒,渲染 g buffer 花费了 3. 4 毫秒, copy depth 拷贝场景深度图花费了 0. 7 毫秒, deferred pass 花费了 8. 94 毫秒, SSAO 花费了 24. 4 毫秒。前向渲染特殊的不透明物体花费了 1 毫秒, copy color 拷贝场景颜色贴图花费了 0. 4 毫秒,渲染半透明物体花费了 2. 2 毫秒,后处理效果整体花费了 11. 8 毫秒。
按我个人的优化经验来说,任何超过 1 毫秒的渲染步骤都是值得我们关注的,尽量将 GPU 一帧的耗时控制在 10 毫秒之内是比较合理的。我们可以看到 SSAO 和后处理是渲染耗时的大头,估计是作者为了表现堆砌了一些效果所致。
2.5.3 SSAO优化
具体看视频。
《复制文章》
《参考文章:Unity性能优化学习笔记(3)SSAO优化》
在完成SSAO优化前,先介绍一下什么是AO和SSAO,简单来说, Ambient Occlusion(以下简称"AO")是一种基于全局照明中的环境光(Ambient Light)参数和环境几何信息来计算场景中任何一点的光照强度系数的算法。 AO描述了表面上的任何一点所接受到的环境光被周围几何体所遮蔽的百分比, 因此使得渲染的结果更加富有层次感, 对比度更高。
具体来说,SSAO的实现分为以下几个步骤:
- 渲染当前的场景到一张RT中,该RT中的每个像素存储了当前渲染像素的法向和深度(均为相机坐标系下)。
- 利用上一步所生成的法向深度图,采样生成一个初步的SSAO RT。
- 对上一步生成的SSAO RT进行blur处理,使其更为平滑。
- 将最终生成的SSAO图作为Ambient项的参数应用到渲染过程中。
Unity下实现 AO 的默认三种方案中SSAO 可以提供实时的 AO 渲染效果,但正是由于它不是烘焙的,在性能上会增加 GPU 的开销。在篇文章通过X code 的 GPUFrameCapture抓取了示例工程中的渲染流程。通过分析,我们看到了渲染过程中耗时最高的正是 SSAO 的渲染。在 iPhone 8X上渲染一帧整体花费在 60毫秒,而SSAO的渲染就占了 24- 25 毫秒左右,这肯定是影响渲染性能的最主要瓶颈。通过进一步展开 SSAO 的渲染过程,我们发现 SSAO 渲染共分为五步,分别对应了 URP 管线下 SSAO shader代码中的5个pass。
分别是基于相机的深度贴图与法线贴图采样,并进行环境光遮蔽计算。然后是分别在水平方向与垂直方向上进行模糊处理,生成最终模糊后的 AO 图,然后和不透明物体渲染结果进行混合。在这里我们可以发现第一步就占了二十多毫秒。显然第一步是我们优化的重点。通过进一步展开第一步的 draw primitive 命令,我们可以看到这一个 DP 命令所绑定的attachment,以及这一步渲染引用到的纹理资源、中间纹理、 G Buffer 以及 shader 等。我们可以看到引用的纹理资源和中间纹理都是手机全分辨率,1688 *1242 大小,其中一些临时的 buffer 甚至是 4096 大小的。由于贴图与中间纹理在一个普通的移动设备上,可能会存在纹理采样瓶颈与带宽的瓶颈,这里将是我们一个需要优化的点。
我们继续往下看渲染管线分析与性能分析的标签。在渲染管线分析标签中,我们可以看到 20 多毫秒主要发生在 ALU time与Wait time 中,这与我们判断认为片源处理像素过多以及带宽性能瓶颈的判断是一致的。而在性能分析标签下,我们可以看到 GPU 消耗时间具体的分布,这里包括各个模块指令单元具体的占比消耗。我们可以看到数值高的部分依旧在片源指令处理,包括片源处理指令数过高、片源纹理缓存命中率低等。
在整体的Performance 标签下,我们可以通过一帧的时间轴看到更直观的数据情况。这里包括顶点与片源的占用率情况、读写带宽情况、 ALU 逻辑处理单元情况、纹理读写情况、 catch 缓存命中率情况等信息。
我们再来看 SSAO feature 中可以调整的参数。
首先是DownSample 这个选项,选择这个选项可以降低生成中间纹理的分辨率以降低纹理采样数,这对 SSAO 整体效率提升是非常明显的。默认在 URP 下只提供降低 1/ 2 分辨率的设置。可以通过修改 URP 源码来调整这个值,以降低更大倍数的采样。可以尝试修改为原始分辨率的 1/ 4 大小,这样整体纹理采样率就会降低到原来的 1/ 16。此外, URP 中只对AO第一张中间纹理做了降采样处理。我们还可以对其他模糊过程中的中间纹理进行降采样,以降低纹理采样的开销。当然,这样降采样会带来视觉表现上的损失。可以通过扩展 SSAO 参数,为每张生成的中间纹理指定降采样系数,并将其暴露到编辑器上。这样通过可视化调整达到既保障效率又满足视觉表现的平衡。
After Optical 选项是将计算和应用 SSAO 放到不透明物体渲染之后,以改善在GPU 上的效率。考虑到是移动端的优化,虽然它会造成物理理论上的一些不精确,但还是建议开启。接下来的 source 选项是灰的。这是由于我们示例工程是延时渲染的,所以默认就会生成 depth 和 normal 的信息。用这两个信息可以生成更精确的AO。当我们修改渲染为前向渲染时,我们可以只使用 depth 来生成 normal 并计算AO信息,这时我们可以调整生成 normal的精度质量。
再接下来是AO强度的设置,这个参数只影响混合系数,通过混合系数调整最终 AO 的混合强度,这个值一般不会影响效率,但我们可以通过它和其他选项配合一起做优化强度。下面的是 AO 信息计算的采样半径,这个值越大, GPU 开销也就越大。一般我们要在可接受的视觉效果范围内,通过调低采样半径,调大 AO 强度来优化 AO 显示效果。接下来的参数是直接光影响的强度,这部分一般是指受直接光光照的像素来混合这个影响 AO 的系数,以此来调整表现效果。这个参数并不影响渲染效率,可以采用默认值。最后一个参数是采样次数,这个值直接影响采样的循环次数,这个值越大,性能开销也就越大,虽然表现效果越好,但在性能上是得不偿失的。一般在移动设备上要尽量保证最小采样次数下来调整 AO 的效果。
下面给出我优化后的 SSAO 参数,其中对 AO 的中间纹理采用了 4 倍降采样,同时应用到了模糊过程中的中间纹理,但为了效果没有屏蔽模糊过程。
我们再来对比优化前后的效果,可以看到细微之处还是可以看到差异,但差异表现并不算大,甚至某些细节处如墙缝,我认为优化后的表现效果更好。
我们再来看x code 中基于优化后的程序,使用 GPUFrameCapture 抓取的数据,可以看到一帧的渲染流程时间从之前的 60 毫秒优化到了 25 毫秒,而 SSAO 过程从之前的 24. 42 毫秒优化到了现在只有1.8毫秒,并且 SSAO 五步都有性能的提高。
此外, 1. 8 毫秒的 SSAO 渲染对于整体的渲染流程依然有优化空间,作为进一步优化,你可以尝试采用采样更少的 HBAO 与基于HBAO魔改的采样更少但AO效果更好的 GTAO 来替换 SSAO 方案。另外,还可以从 AO shader 的指令上做进一步优化。作为一些更低端的移动设备上,针对于我们这个示例项目,只是静态场景的话,可以采用烘焙 AO 到光照贴图的方案来替代 SSAO 方案。
2.5.4 AA反走样优化
《复制文章》
《参考文章:Unity性能优化学习笔记(4)AA反走样优化》
经过上节课我们优化 SSAO 后,我们看到渲染性能开销最高的已经变成了渲染后处理过程,展开后,我们看到后处理渲染的开销7.26 毫秒中有 3. 24 毫秒是 SMAA 导致的,其他开销是运动模糊与 Bloom 效果所致。那么今天就先来说说反走样方案的选择与优化。
反走样主要是为了解决采样不足导致的问题,一般方案选择需要兼顾画面质量与渲染效率权衡的前提下对图像进行增强。反走样方案经过了从第一代全屏超级采样抗锯齿SSAA 到第二代的MSAA、FXAA、SMAA 目前逐步被第三代 TAA 以及未来的第四代 DLSS 所取代。目前 Unity 的 URP 下 AA 的方案主要还停留在第二代,目前 URP 的官方默认TAA 方案还在开发中,而 DLSS 需要特殊硬件支持,目前 Unity 下只有 HDRP管线下的 PC 平台才支持。
为了方便大家理解,我这边列出了 URP 管线下的一些方案选择以及优缺点对比。
我们先来看 URP 默认支持的MSAA,这个 AA 方案是绝大多数显卡硬件默认支持的,反走样效果也比较好。一般情况下静态画面要比动态画面效果更好,但其只支持前向渲染,在 MRT 情况下占用内存与带宽也较高,效率也较差。另外,其仅能消除Geometry边缘的反走样,对于高光像素部分就显得无能为力了,某些情况下也会出现物体边缘暗边的情况。但总体而言, MSAA 是一般前向渲染游戏中最常使用的反走样方案,某些情况下还可以结合其他 AA 方案一起使用。
接下来是 FXAA 与 SMAA 方案, URP 下可以通过相机的 AA 设置启用。
FXAA 与 SMAA 都可以归类为形变 AA 的解决方案,都是通过后处理方式进行支持的。不同的是, FXAA 只需要一遍pass,通过检测像素的亮度差来判断高反差边缘,并计算混合方向与混合权重系数进行混合来完成反走样。其特点是开销非常小,并且开销固定,适合移动端游戏。但由于没有额外的像素辅助,在一些高频颜色变化过快的边缘或动态场景下会出现闪烁状况。另外,由于 FXAA 是对图形所有颜色边缘进行柔化处理,这也会导致整体画面相对模糊。SMAA 方案是使用三次 pass 完成边缘检测,形变与混合开销相对于 FXAA 要高一些。三变 pass 导致 RT 的切换与带宽的。开销可能在手机端造成更严重的性能问题。
虽然 SMAA 在边缘处理上比 FXAA 更精细一些,但模糊效果与闪烁的问题依旧存在,不过会比 FXAA 要好一些。总之,我认为 FXAA 更适合一些对低端硬件有要求,对画面模糊不敏感的移动游戏,而 SMAA 更适合一些低端硬件设备的 PC 游戏。接下来的 TAA 方案如我们前面所说,是最新反走样比较流行的方案,支持延迟渲染,反走样效果好的同时性能开销也可以被平摊到多帧,这样总体性能开销较小,但其也有一些缺点,比如动态场景下低帧率下会出现鬼影的情况,无法处理半透明物体与序列帧动画物体的反走样。另外需要额外的内存开销,需要Motion Vector的支持等。
目前可以将URP中的AA做两个维度的比较
在效率上看FXAA>SMAA>TAA>MSAA
在质量上看MSAA>SMAA>TAA>FXAA
接下来看看项目案例中的AA情况
可以看到案例中使用的是SMAA,从GPUFrameCapture抓取数据中可以看到,在 iPhone 8xs上 SMAA 的耗时为3.24 毫秒,三个pass 中边缘检测的 pass 耗时1.18 毫秒计算混合权重系数的 pass 开销1.29 毫秒,最后混合结果的 pass 开销0.77 毫秒。
我们查看场景中的相机 AA 设置,可以看到 SMAA 下有高、中、低三档质量,当我们设置质量为 low 时,重新用 GPU frame capture 抓取后SMAA 的开销下降到2.52 毫秒,三个 pass 的开销分别降低到1.1毫秒、0.7 毫秒与 0.71毫秒,可以判断开销的降低主要发生在第二个pass。
通过查看 URP中SMAA 对应的 shader 源码,可以看到三档质量的设置,最低档与最高档只是 SMAA 的阈值与计算权重的步长的差异。如果我们想继续降低计算的开销,可以继续调高 SMAA 的阈值与调低计算的步长,但这样带来 AA 效果,肯定会有进一步的损失。
接下来对SMAA 三遍pass中的中间纹理带宽开销可以利用metal的片上内存来优化。通过调整源码带 LSRT 的 memorize 模式与存储的action,我们对带宽进行优化后抓取数据,可以看到 SMAA 的开销进一步降低一帧GPU所用时间。下面是我切换到一遍pass的FXAA,与 MSAA 不同,可以看到 FXAA 是在最后渲染 final passplit 中完成的,耗时1.78 秒,相对 SMAA 从效率上有一定的改进,但 1. 78 毫秒对于移动设备上 GPU 10 毫秒的渲染目标仍然是较重的开销。
这时我们要评估我们的游戏是否需要AA了。如果我们游戏模型本身建模较细,色彩过度不是太跳的情况下,由于手机屏幕本身DPI较大,AA 的开启与关闭对画面变化不大。肉眼不易察觉的情况下,我们可以选择舍弃 AA 的渲染流程。当然,如果未来从别的渲染流程中节省出额外事件,我们仍然可以选择开启。
下面说一下AA反走样优化的总结。由于我们的示例工程是延时渲染,不能采用 MSAA 的方式,因此在一些高端移动设备上,我们可以采用优化后的 SMAA 和 FXAA 来做反走样,未来 Unity 的 TAA 解决方案在性能与表现均衡的情况下也可以采用,而在一些中低端移动设备上,建议关闭 AA 渲染。
2.5.5 PostProcess后处理优化
《复制文章》
《参考文章:Unity性能优化学习笔记(5)PostProcess优化》
Unity URP 管线下的后处理大概可以分为三大类,第一类是用来做色彩校正与增强的,如Channel Mixer、Color Adjustment(颜色调整)、Color Curves(颜色曲线).Lift,Gamma,and Gain、Tonemapping、Shadows/Midtones/Highlights、White Balance(白平衡)、Split Toning(拆分着色)。这类后处理一般开销较低,但若胡乱堆砌依然会产生较大的性能开销。
第二类后处理效果是画面增强类,如Bloom(泛光)、Chromatic Aberration(散色相差)、Depth Of Field(景深)、Film Grain(膜颗粒)、Motion Blur(运动模糊)等,这类后处理效果一般都需要额外的Render pass 处理,涉及RT的切换与采样,开销都比较高,因此整体性能开销也比较大,所以尽量不要叠加使用。如果非要使用,建议不要采用global volume 方式一直开启,而是采用local volume方式在不同逻辑下进行开启。比如我们的色散分离只在机器受伤类情况下开启,景深只在过场动画时开启等。
第三类后处理效果是镜头效果类,如Lens Disortion(镜头失真)、Panini Projection(Panini投影)、Vignette(渐晕)、Lens Flare(镜头光晕),这类效果一般是shader,计算量比较大,也有一些纹理采样的开销,总体开销介于前两类之间。使用这类镜头特效应与第二类特效一样,建议通过local volume设置或通过逻辑开启。
下面是我对 URP 下所有后处理特效的一个列表总结,包括他们在移动端的开销、常用性以及可能的优化方式。
效果描述 性能开销和常用性 优化方式 Channel Mixer(通道混合器) 性能开销非常低、常用性低 几乎无 Color Adjustment(颜色调整) 性能开销中等、常用性高 ColorGradingMode(HDR or LDR) Color Curves(颜色曲线) 性能开销低、常用性低 几乎无 Lift,Gamma,and Gain(提升与增益) 性能开销非常低、常用性低 几乎无 Tonemapping(色调映射) 性能开销中等、常用性中 几乎无 White Balance(白平衡) 性能开销较低、常用性低 几乎无 Shadows/Midtones/Highlights 性能开销较低、常用性低 几乎无 Split Toning(拆分着色) 性能开销非常低、常用性低 几乎无 Bloom(泛光、镜头污垢) 性能开销较高、常用性高 降采样、降低迭代次数、替换两边模糊pass算法 Chromatic Aberration(散色相差) 性能开销中等、常用性低 几乎无 Depth Of Field(景深) 性能开销非常高、常用性较低 切换Gaussian与Bokeh的景深模式 Film Grain(膜颗粒) 性能开销中等、常用性低 几乎无 Motion Blur(运动模糊) 性能开销高、常用性较低 MotionBlur质量分级、Intensity强度、Clamp摄像机旋转产生的速度可以具有最大长度 Lens Disortion(镜头失真) 性能开销较低、常用性低 几乎无 Panini Projection(Panini投影) 性能开销中等、常用性低 几乎无 Vignette(渐晕) 性能开销中等、常用性低 开销与Intensity和Smoothness的设置有关 Lens Flare(镜头光晕) 性能开销中等、常用性中 开销与其组件 occlusion 设置和光晕数量有关 从我们目前的示例项目可以看到,项目中使用了全局volume,定义了一系列后处理特效,包括Tonemapping、白平衡、Color Adjustments、Lift Gamma Gain、 Bloom、MotionBlur等等。
我个人认为一些效果是可以被舍弃或替代的,而另外一些效果是可以通过设置来做优化的。这里我将对画面影响效果不大的白平衡、散色相差、不太适合游戏正式画面的Vegnette,以及开销过大的Motion Blur移除。如果Motion Blur在其他游戏中一定要使用的话,建议将质量设置为 low 的同时将Intensity值和clamp值在不影响效果的情况下设置的越小越好。另外值得注意的是,尽量将不需要的效果直接删除,而不是通过复选框禁用。因为有些效果即使你禁用了,它的一些渲染资源依旧会被预先绑定,会导致内存的浪费。删除不必要的优化后如图所示。
目前剩下的 4 类特效只有Color Adjustments与Bloom两个有优化的空间。Color Adjustments优化选项来自 URP Assets中对PostProcess字段的设置。目前示例项目中的GradingMode是HDR模式,此模式更适合电影制作工作流中的高精度分级。如果是游戏,可以采用 LDR 模式。另外,LookUpTexture的纹理大小也可以调整, HDR 模式下默认是32。在平衡效率与质量时,我们还可以在 LDR 模式下使用 16 位模式。在优化时,我们评估低端设备上画面损失与性能提升的性价比,可以参考勾选Fast sRGB/Linear conversions。但要说明一点的是,如果支持浮点精度纹理的平台或设备,Color GradingMode选HDR 模式效率会更高,优化安卓某些不支持浮点精度纹理的设备,我们在做LDR与LUT大小调整。
下面我们来看Bloom的优化,查看URP下Bloom shader,可以发现Bloom是通过 4 个pass来完成的,分别是Bloom prefilter做downscale水平与垂直两边模糊pass,以及upscale sample与color图像的合成。我们的优化可以来自第一个pass的当scale降采样阶段与接下来的两遍模糊pass,其中在第一个Pass Profilter过程,我们可以通过视频中代码位置调整开始的采样分辨率以及最大downscale 的迭代次数。默认Bloom是从 1/ 2 分辨率开始 downscale的,我们可以调整为1/ 4开始默认downscale迭代最大次数为16次,实际根据分辨率大小做了 8 次,这里我们可以强制调整为4次。当然这个数据是我根据调整前后对比实际画面差异,在不太影响整体效果的范围内作出的。
我这边就仅对开始的当scale采样大小与迭代次数作出了优化调整。下面我们来看Postprocess整体优化前后通过 GPUFrameCapture抓取的性能数据对比。我们可以看到Postprocess整体过程由优化 AA 后的3. 99 毫秒优化到目前的1. 61毫秒,其中移除Motion Blur节省了 1. 96 毫秒,Bloom优化节省了0. 21毫秒,其他效果移除和优化节省了0. 13毫秒。
到目前为止做的优化都是只与设备、分辨率相关的 GPU 开销部分的优化,都是一些全屏AO、AA 、后效这类。接下来我们会优化与场景复杂度相关的 GPU 开销。结尾感谢up主Metaverse大衍神君的高质量性能优化课程,大家可以多去看看这个宝藏up主。
2.5.6 Culling、Simplization和Batching
《复制文章》
《文章参考:Unity性能优化学习笔记(6)Culling、Simplization和Batching》
本篇文章是对于性能优化里的三个重点环节做一个介绍,分别是Culling剔除、Simplization简化与 Batching合批。他们是渲染前的优化的重要且必要的环节。
一、Culling
先来看Culling剔除优化部分的内容。Unity 引擎URP下关于Culling剔除做了很多工作,这里我把这些Culling工作分为四类,分别是像素剔除、网格剔除、灯光剔除和场景剔除。
首先是像素级别的剔除,包括摄像机平节头体剔除、背面剔除、Early-z 与Pre-Z pass。其中摄像机剔除、背面剔除与Early-z都是渲染库或硬件直接支持的部分,而Pre-Z pass是针对于前向渲染下而Early-z 失效的情况下,通过Pre-Z pass方式提前获取场景深度,后续绘制像素时根据场景深度外壳进行像素着色计算的剔除。
接下来是网格对象级别的剔除。 Unity 提供了Layer Mask、可视距离对象剔除与Occlusion Culling的方案,前两种都可以通过简单的设置完成。而Occlusion Culling是一种CPU加烘焙的方案,在某些OverDraw严重而又存在大面积建筑或遮挡体类型的游戏中可以起到加速的效果。但Occlusion Culling本身是一把双刃剑。由于烘焙会有额外的内存开销,而所有关于遮挡剔除的计算又在CPU端会有额外的CPU的开销,在用之前需要判断剔除给GPU端的优化是否能弥补CPU端的开销,测试后在使用。
接下来是灯光剔除,这部分需要依赖特殊的图形库和硬件的架构完成,比如Tile-Based Deferred Rendering,也就是我们通常所说的TBDR管线和Forward+渲染管线,都是针对于多实时光源在游戏项目上的光源剔除优化。在灯光处理上会为每个Tile建立可用的灯光列表,这时不能影响该Tile内像素的灯光将会被剔除在列表之外。这样在计算该Tile中的像素着色时,可以大大节省像素光照着色的开销。对比而言,即使Tile-Based延时渲染在带宽与内存上做了优化,但仍会比Forward+要高,而Forward+的性能瓶颈依然在场景对象复杂度上。因此如何选择管线要根据自己的游戏类型和目标设备来进行选择。
接下来的场景剔除是针对于大场景多场景拼接的地图,这时我们可以通过 Unity Additive的场景根据逻辑来做异步的加载和卸载,以实现场景的动态剔除。这也算是另类的Culling的一种了。
除 Unity 提供的一些Culling的优化方案外,用户也可以扩展自己的Culling优化方案,如默认Unity下没有场景数据结构管理,可以通过添加各种场景结构来对场景中的对象进行管理,如OcTree、BSP Tree、Portal、SDF等,这些数据结构都是为了做Culling加速或其他功能的必要数据结构。另外还有一些利用 GPU 加速的算法,如通过 Hierarchical-Z pass,利用上一帧的深度图和摄像机矩阵做剔除,利用Temporal Reprojection Culling的算法来对当前帧的场景做剔除。还有比较热门的GPU Driven Pipeline中的构建Visible Buffer以及基于Cluster剔除的算法,这些都可以在Unity的管线中进行扩展集成。
一般而言,基于GPU的Culling方案在移动端都会面临兼容性的问题,而基于CPU的方案往往都会存在可能变成负优化的双刃剑。如果想要扩展自己的Culling方案,往往需要代码实现的工程能力要大于理解算法本身的能力。
二、Simplization
接下来是Simplization简化部分,通常情况下,我们最常用的简化手段就是分级配置各种LOD,结合一些场景数据结构做简化,当然还有一些替代体的方案,下面我们来看一下,Unity下具体有哪些简化的手段。这里仅列出了一些我认为比较常用的手段。
首先是ProjectSetting下的Quality优化分级设置,通过这个设置,你可以自定义在不同平台设备下Unity现有的一些功能的设置分级,你可以预先设置在哪些平台下开启或不开启某些功能,包括分辨率、贴图、分辨率、阴影质量等的一系列设置。
接下来是在静态灯光场景下,可以使用烘焙的光照方案替代实时光照方案。若整体场景上的动态物体、动态光源非常少,完全可以使用烘焙方案替代实时光源。
接下来是一些替代方案来做的简化,比如用Bonding Box或简化替代体代替Mesh碰撞体来做碰撞,用Local volume代替Global volume来做特效以及后效的区分,用多条Raycast射线检测方式来代替开销比较高的Sphere Cast 或 Capsual Cast,以及用纹理字体代替系统字体等。这些简化替代方案还有很多,大家一定要知道你在使用引擎某些功能时,使用哪种方案在对应平台上开销较小,带来的负面影响较小,并可以替代或近似替代你需要的功能,这样你才能去做某些简化的优化。
接下来是一系列的LOD简化功能,比如Mesh LOD根据距离采用不同复杂度级别的Mesh进行渲染,以达到不影响视觉表现,同时带来更小的开销。还有通过Shader LOD功能来做多平台或低端设备上的兼容性,尤其是一些 Shader效果需要图形 API 版本要求时。HLOD是 Unity 针对于大世界提出的一种简化方案,它可以在长视句下用单个静态网格组合替代多个静态网格对象,有助于减少场景渲染对象的个数,同时减少DC调用次数来做场景渲染优化。不光模型,动画方面也可以尝试做LOD,比如做动画频率的LOD,可以根据视距降低远处角色的动画频率或使用骨骼 LOD 为远距的角色,采用另一套骨骼较少的骨骼。
Eu:通过Camera Override代替URP管线中的一些通用设置;各种OnDemand更新或分级设置接口。
三、Batching
Batching其本质就是按需求组织数据,从CPU发的给GPU的过程,这些数据包括网格、纹理、shader、变量、材质属性等等。那么这些数据怎么组织,以什么结构组织,以什么频率发送,以及每次发送多少,就是Batching研究的问题。
下面我们还是先来看一下哪些内容是需要Batching的。首先是资源的Batching,这里包括网格、纹理、shader变量、材质属性的Batching,这些资源的合批是做后续某些Batching优化的前提。接下来是DrawCall Batching,主要是为了降低 GPU的DC操作,能更少的调用绘制命令接口。Unity在这里为我们提供了Static Batching 与 Dynamic Batching 功能。接下来是GPUInstancing,这也是一种绘制优化的手段,用于绘制多个副本网格对象。 Unity 提供了直接绘制、间接绘制与程序化间接绘制Instances的三种方法接口。接下来是SetPassCallBatching。之前DrawCall Batching是为了减少渲染操作调用的次数,而SetPassCallBatching是为了减少渲染状态切换的次数。Unity为我们提供了减少SetPass Call次数的SRP Batching的功能。接下来我们看一下它们的具体原理与注意事项。
3.1 资源的Batching
资源的Batching包括网格、纹理、shader变量、材质属性的Batching。先来说网格资源,我们可以将邻近且不移动的网格对象通过Mesh.CombineMesh合并到一起,合并后用一次网格的渲染调用代替每个网格的渲染调用。但这种方案也有一定的弊端,一旦合并的网格儿对象较大时,可能造成摄像机剔除不掉的问题以及Overdraw的问题,另外在内存上也会增加一定的开销。CombineMesh主要是针对于静态网格对象,如果是动态 SkinMesh对象,往往由于美术为了材质表现效果,会通过Material ID标记多维子材质,这样导入后会形成多个SubMesh,每个不同材质的SubMesh会增加一次Draw Call,所以在做平台移植或低端设备兼容时可以合并多个材质与贴图,并通过通道图方式标记模型不同部位的材质变化。
接下来是纹理贴图资源,Unity默认提供了Sprite Atlas贴图,开发者也可以自己通过DCC合并同一模型上多张贴图到一张贴图上,以达到纹理合并的方式。另外还可以通过TextureArray纹理数组方式向GPU同时传递多张设置相同的贴图资源。
Shader变量与材质属性在默认渲染管线下,Unity是通过Material Property Block完成合批的,而在SRP管线下则是通过Const buffer来实现的,并且通过定义不同的Const buffer来控制提交到 GPU 上的频率。
3.2 DrawCall Batching
DrawCall Batching主要讲Static Batching与Dynamic Batching,与手动CombineMesh合并静态网格不同, Static Batching是引擎在构建时自动将邻近可合并的静态网格对象合并到一起,并将合并后的网格转换到世界空间下,并用他们的顶点信息构建一个共享的顶点缓冲与索引缓冲区,然后对可见网格进行简单的绘制调用。Static Batching功能依旧会有额外的内存开销,所有和批的网格对象都会在内存中保留一份额外的拷贝,是一个典型的空间换时间的优化方案。
Dynamic batching是对移动的游戏对象进行绘制批处理的手段,以减少绘制调用。 Unity 在运行时动态网格与动态生成几何体上的动态合批处理方式不同, Unity会将可动态合批的对象构建到一块大的顶点缓冲区中,并根据合批后的数据设置渲染器材质状态,然后将缓冲区绑定到 GPU 上。对于每个Mesh Renderer是通过缓冲区偏移量来更新提交绘制内容的。
Dynamic batching 主要是为了一些低端旧设备性能优化考虑的。在现代消费级硬件上,Dynamic batching在 CPU 上的调用开销可能会更大,因此是否开启动态合批选项还需要在目标设备上测试使用,并不是无脑开启的。总之,无论是 static batching 还是 Dynamic batching,在使用上都有一些使用限制,稍不注意就会造成无法合批的现象。具体有哪些使用限制和合批失败会稍后在解释。
3.3 GPU Instance
GPU实例化也是一种batching,用于渲染网格的多个副本。它是将基础网格对象传递给GPU后,充分利用 instances buffer的方法传递多个网格实例位置、朝向、颜色等其他属性构成的 instances buffer到GPU,避免了反复传递多个基础网格对象在世界空间下变换后的各种顶点数据和其他额外数据了,所有的实例都会引用同一个基础网格对象,非常适合创建植被、石头等场景中大量重复的网格对象。GPUInstances在Unity默认渲染管线与 SRP管线下都支持,只不过在 SRP 管线中无法与我们接下来要讲的 SRP batching兼容,二者只能选择一个开启。需要开启 GPU instancing 时,我们需要保障对应网格对象的材质中Enable GPU Instances选项的开启。值得注意的一点是,如果基础网格对象顶点数较少时,由于无法充分利用GPU资源,可能会导致性能不够理想。因此对于顶点数较少的网格,我们需要反复测试。
3.4 Set Pass Call Batching
最后是Set Pass Call Batching。这里只有一类SRP Batching,顾名思义,其仅能在SRP管线下开启,它可以显著减少Unity未使用相同着色器材质,其准备和调度绘制的CPU 时间开销。其核心是需要图形API支持的Const Buffer,这个Const Buffer中放什么每个元素大小,Const Buffer总体大小以,及什么时机以什么频率将 Const Buffer中的内容提交给GPU,都是会影响最终性能的。
好在Unity已经为我们定义好了一些Const Buffer类型,如上图所示,上边几类SRP已经定义好的Const Buffer,包含了引擎内置的一些shader变量与属性,同时也指定了提交到GPU上的频率。而需要开发者定义的只有Unity,Per Material这类的Const Buffer,这类Const Buffer只有在其定义属性发生变化时才会被提交到GPU 上。我们需要将自定义的着色器变量与材质属性添加到这类的Const Buffer中,不能有在Const Buffer之外定义的其他属性或uniform变量。确保我们SRP Batching字段是Compatible 的,这一点非常重要。在 SRP 管线下强烈建议开启 SRP Batching功能。如果不使用此功能,我个人觉得完全没有必要升级到URP或HDRP管线。
最后我这边给出这几类batching的优化顺序及优先级资源的batching优化是后续batching优化的基础,必须优先做。其次需要做的是SRP batching与Static batching。这两类batching一般在所有游戏下都会开启,之后根据场景类型和需求做GPU Instance,最后的Dynamic batching根据开启和关闭时的性能比较结果选择使用。
3.5 合批的限制
首先说Static Batching,总体来说,Static Batching限制较少,主要有三点,其中产生额外的内存开销、可能影响Culling剔除的结果、合并后的静态对象网格顶点数最大不超过64000个。
其次动态合批的限制就很多了,首先是合批后的对象不能超过900个顶点属性。注意这里是顶点属性而不是顶点。比如,假如你的基础顶点有位置、颜色、法线、 UV 坐标 4 个属性时,那么最大的顶点数就不超过 900 / 4 = 225 个顶点。第二个限制是除了渲染阴影对象外,相同的材质、不同的材质实例的对象也不能进行动态合批。第三个限制是具有光照贴图的游戏对象一般不能进行动态合批。第四点是着色器具有多 pass 的材质,对象无法动态合批。第五点是受多个光源影响的游戏对象,即使满足动态合批条件,合批后也只会受到一个光源影响。第六点是延时渲染下不支持动态合批。最后,动态合批在现代硬件架构下,CPU 开销可能会更大,需要测试后再做开启选择。
接下来GPU instancing的限制主要在需要图形API版本与 shader 版本的支持,另外GPU instancing与SRP batching不兼容。此外三种Instances实例绘制API在参数与实例绘制个数上也有差异,使用时需要注意。最后是渲染顶点数较少的网格实例时,GPU instancing的效率可能会比较差,这点和动态合批一样,需要测试后在做选择。
最后SRP Batching功能,同样也需要图形API对Const Buffer的支持,而且需要在SRP渲染管线下shader是 compatible时才能生效。另外粒子对象不能做 SRP batching,同样使用MaterialPropertyBlocks的游戏对象也不能做SRP Batching。
此外如果要看合批失败的原因汇总可以看这篇文章合批(Batching)的限制与失败原因汇总这篇文章的作者也是Unity中国Solution部门的高级程序,无脑推荐。
2.5.7 场景简化总览与远景简化
《复制文章》
《参考文章:Unity性能优化学习笔记(7)场景简化总览与远景简化》
上一篇文章介绍了Culling、Simplization和Batching做渲染前优化的理论基础,后面的学习笔记则是运用这些知识对示例工程进行进一步优化。我们先来看一下我们示例工程上一阶段后处理优化后的性能数据,以此来作为下一阶段优化的对比数据。我们还是以XCode截取数据为基础。我们先来看 Summary 下的数据,目前当前画面下的DrawCall有 2347 个, GPU 花费时间仍有 21 毫秒,渲染顶点数也有 287 万之多。内存上占用 687 兆左右,其中纹理占用 353 兆,其他 8 份占用 333 兆左右。
Performance 标签下显示的时序图中我们可以看到各个阶段各个指标下的开销,其中 Deferred Shading 阴影渲染、 G buffer 渲染与 Depth Normal Pass 阶段是目前渲染主要开销所在,其中顶点阶段的 Shadow 渲染开销最高, G buffer 渲染与 Deferred Shading 次之,分别处理了 187 万、 70 万、 18 万个顶点。而像素阶段 shadow 的渲染调用了 1355 次DrawCall, Depth Normal Pass 调用了 247 次 DrawCall,G buffer 396 次DrawCall, Deferred Shading 323 次 DrawCall。
回到我们的示例项目中,我们打开编辑器下的统计信息界面来看一下统计信息。当前视角下渲染批次有 5400 个左右,顶点 280 万个,三角形面片 190 万个, SetPass Call 大概调用 350 次左右,投影体 2700 个左右,这些数据也是我们做优化前后对比的显著指标。
既然要做简化,我们就先来看一下场景总览。通过编辑器下的线框模式,我们大概可以看一下整个场景中的三角形与顶点分布,可以看到大面积的植被、树木与房屋,是顶点三角形分布的重点。另外我们还可以看到远处的山峰也是由真实模型构建的,这让我们做场景简化、 LOD 简化有了必要的理由。
![]()
标题编辑器下的线框模式 SRP 渲染管线下,通过 Rendering Debug 工具,我们可以看到整个场景的 Overdraw 信息,可以看到场景的 overdraw 还是比较高的,这也是我们要做剔除的必要理由。通过 Rendering Debug,我们还可以看到其他渲染调试信息与调试视图,可以帮助大家找到优化的方向与思路。
![]()
标Rendering Debug 工具下的 Overdraw 信息题 通过阴影调试图,我们可以看到该项目中的级联阴影可视视距设置还是很远的,因此投影体有 2700 多个也就不足为奇了。将视角拉高,从空中看级联阴影的级别范围也可以验证这一点,因此做投影体简化也是很有必要的。
一般情况下,游戏中我们不是通过传送就能走到的远景,一般都会选用模型,而不能走到的远景一般都是使用天空壳完成的,下面我们来看一下简化过程。只需要先将场景中除远景外的对象先隐藏,然后在场景中添加一个反射探针,烘焙 Cube map 即可。注意这里要选择反射探针的类型为bake,当然 custom 也可以选择后需要开启 static 选项中的 Reflection Probe Static 选项,之后取消 Windows Rendering Lighting 下边的 auto generate 复选框,这时候我们会发现添加的反射探针 Inspector 面板中会出现 baker 按钮。
接下来要根据实际远景裁剪距离设置裁剪平面。示例项目中如果使用默认的裁剪距离会烘焙不完整,需要调整裁剪远平面到 3000 或者更大 5000 同时可以提高渲染分辨率。我这里设置了1024,因为这个大小与实际模型表现的质量最为接近。之后点击烘焙按钮,会烘焙出对应的 Cube map。
接下来我们要创建天空盒材质。首先创建一个新材质,选择它的 shader 为 Skybox/Cube map,并在贴图中指定我们刚刚生成的 Cube map。之后我们需要替换 Windows rendering Lighting 中的 environment 标签下的天空盒材质为我们刚才创建的材质。
这样我们就将远景模型替换成天空盒的简化做好了。我们可以调整天空盒材质的曝光系数来控制远景天空盒的明暗,来模拟天空的变化,也可以通过旋转系数控制远景贴图的旋转显示角度。附上简化后的统计信息界面结果。
2.5.8 中景简化与LOD策略
《复制文章》
《参考文章:Unity性能优化学习笔记(8)中景简化与LOD策略》
接着上一篇文章继续进行分析,在完成远景简化后,我们就可以可以对游戏物体进行LOD简化,一般LOD简化可以遵循以下的策略,如果对于需要带远景、中景、近景都需要出现的物体,我们可以将其 LOD 分为四级, 零级为近景,一级为中景,二级为远景,三级则为剔除。
而只需要出现在近景和中景中的物体,我们可以设置 LOD 为三级, 二级为剔除。还有一些只需要在近景出现的物体,我们可以设置为两级,零级为近景,一级为简化模型或剔除。一般情况下,我们的 LOD 级别设置最好不要超过五级,在五级情况下,零级为原始模型,一级为一级简化模型,二级为二级简化模型,三级为替代体,四级为剔除。如果我们不需要使用替代体技术的话,我们可以选择不超过四级的 LOD 设置。
我们示例工程中所有模型都设置了 LOD group,并且都是美术制作的 LOD 模型资源,这样我们就不需要再采用一些工具去做简化了。但通过查看,我们可以看到原始工程下的一些物体的 LOD 设置并不合理,比如一个罐子,它只会出现在室内场景,但它的 LOD 裁剪层级与简化模型层级级别设置都非常远,不能做到合理的简化与剔除,这显然会造成一些不必要的绘制浪费。这一点在所有室内装饰物的 LOD 设置上都多多少少有些问题,我们可以通过 Rendering Debug 工具 Overdraw 模式下看到这一点,我们可以看到,即使摄像机到了屋外很远的地方,室内的一些小装饰物依旧在绘制,一方面是由于场景没有做遮挡剔除,另一方面是由于这些物体的 LOD 剔除级别设置太远。
此外,场景中的房屋建筑是靠小模型块拼接而成的,但他们第一层级 LOD 设置也距离太远,很大范围内用到的都是第 0 级的原始模型。一些植被也存在同样的问题,这也是我们在这个项目的编辑器默认视角下会有 5000 多个渲染批次, 280 万个顶点、 190 万个三角形面片的原因了。因此,我们必须按实际情况调整 LOD 层级设置。另外,项目中的对象投影体个数也有 2700 多个,这显然会造成阴影绘制阶段的压力。因此我们对场景中一些不必要的 LOD 级别上的模型不显示投影,比如桌子上放置的器具、一些装饰物等等,只在 LOD 0 级别原始模型下显示投影,其他 LOD 层级下的模型不显示投影。这样在中景范围下只有较大的模型对象 LOD 层级别才做投影。还有一些静态光照下的对象已经在阴影体内了,也没有必要再进行投影,毕竟我们还可以通过 SSAO 来显示接触面的暗部。
![]()
阴影内的静态物体不显示投影标题 此外对于PC端和移动端,我们可以采用不同的LOD Bias,比如可以PC端我们可以将LOD Bias设置为1以上,对于移动端将LOD Bias设置为1以内,LOD Bias是根据对象在屏幕上的大小选择LOD级别。当大小在两个LOD级别之间时,可以偏向于两个可用模型中细节级别更低或更高者。此属性设置为0到+无穷大之间的值。设置为0到1之间时,裁示倾向于更少细节。超过1的设置表示倾向于更多细节。例如,将LOD Bias设置为2,并使其在50%距离处变化,LOD实际上仅基于25%变化。
下面是我给大家看展示前后的对比效果。大家可以看到 LOD 层级与可视化范围我都做了相应调整,而从场景移动测试画面来看,也看不出明显的差异,这就达到了我们要求不影响整体画面表现的要求。而从数据对比结果上看,无论是从渲染批次、 SetPass Call 数量以及投影体个数都降低到原来 50% 左右,而三角形数、顶点数下降比较小的原因是我们对近景小物体对象调整幅度较大,而对远中近景都要显示的大物体对象调整幅度要小一点所致。
PS:Unity Quality设置中的Log Base属性要特别注意。
2.5.9 遮挡剔除与光影剔除优化
《文章复制》
《参考文章:Unity性能优化学习笔记(9)遮挡剔除和光照剔除优化》
遮挡剔除发生在应用程序阶段,由游戏引擎实现,运行在CPU上。需要根据场景中Static物体的位置预先生成场景Occlusion Culling数据,运行时就可以剔除对应静态物体之后的其他物体。遮挡剔除是减少渲染消耗的有效手段之一,可以和视椎体剔除同时生效,进一步减少渲染的消耗。此外入口剔除Portal Culling也是遮挡剔除的一种,它可以配合动态物体进行动态剔除,这两种手段在 Unity 中都原生提供,他们都是为了防止那些被完全遮挡的游戏对象进行渲染。
其实在示例工程延迟渲染下,不透明对象已经做了像素的深度检测,会剔除那些被遮挡的像素进入渲染管线,但在原始渲染下的投影阶段与半透明物体的绘制仍然是前向渲染的。因此遮挡剔除与窗口剔除这些 CPU 端的 Culling 主要是为了优化这部分的。注意需要在应用遮挡剔除的相机上开启Occlusion Culling选项,该相机下才能起作用。
Occlusion Culling对场景中设置成静态的遮挡体和被遮挡体起作用。那么当我们游戏中有动态开启的,比如门窗这类的遮挡体该怎么办?这时Portal Culling就能派上用场了。对于门窗这类遮挡体,我们不需要设置成静态遮挡体或被遮挡体属性,我们只需要给它们添加 Occlusion Portal Component 组件,并根据门窗的实际大小设置入口区域,重新烘焙场景即可,然后通过脚本实际控制入口遮挡的开启与关闭。在我们的示例工程中只设置了遮挡剔除,因此,我们需要对示例项目中的一些门的 Prefabs 添加 Occlusion Portal Component,并在开门和关门时添加动态开关逻辑。
接下来看光影剔除,由于游戏中的地形对象本身相对比较平坦,也没有需要投影的表现,因此地形对象上的投影体属性也可以关闭,更多的是接受其他物体在地形上的投影,而不是自投影。此外,我们的场景中还有一些篝火灯光与路灯,以及非常多的室内照明、蜡烛、吊灯等灯光对象,这些都是实时带投影的点光源。虽然在Tile-Based 延时渲染下对灯光影响对象做了剔除,光照计算也有所优化,但实时灯光依然会产生很多投影体,所以在该项目中我们依然需要对光影进行剔除优化。
最合理的方法是充分利用 SRP 下的 Light layer 功能来做光影剔除,解释一下 Light Layers是用来控制灯光对物体的影响的,如果物体灯光在某个Light Layers中,而物体的Rendering Layers Mask不包含Light Layers,则这个灯光将无法影响到这个物体。
但由于目前项目中并没有开启 light layer 功能,对场景中的物体也没有区分室内室外物体的 layer mask,如果做 light layer 的修改会涉及大量资源设置的变动,比较麻烦,所以还是选择通过脚本逻辑来控制光影的剔除。主要算法就是通过脚本判断灯光与摄像机的距离来控制灯光组件的开启与关闭,同时控制同一室内场景中只有一盏点光源做室内区域投影的主光源。当然这个方法依然是个 CPU 的光影剔除方法,会有一定的 CPU 开销,在 CPU 是性能瓶颈的情况下,这种方法就不太适合了。
另外,关于光影的剔除,我们还可以从 QualitySettings 中着手,默认示例项目中对于所有平台的灯光和阴影只有一套配置。为了对比,我创建了一个新的 URP 管线的设置,在保证不太影响视觉效果的范围内,调整了部分灯光与投影的参数设置。
首先是灯光部分,对于主光源,由于地形范围较大,投影距离为 150 米,所以主光源依旧采用 4096 阴影贴图大小,以保证室外的阴影质量。而对于室外的额外灯光,我首先调整了每个对象最大能接收到几盏灯光的影响,这里我从 8 改到了4。而额外光源投影的 Shadow Atlas Resolution 从 4096 调整到了2048。因为室内距离较小,稍微调低了阴影贴图的 Atlas 与 Shadow Resolution Tiers 并不影响太大的视觉表现。
此外,我在 Cascade Shadow 的分级与分级距离设置上也做了调整,在保证主光源投影距离不变的情况下,降低了 Cascade shadow 的分级,从四级降到了二级,这样可以减少远距离上细小投影体的个数,而视觉效果上也没有过大的差异。总之,这些调整是根据目标平台的不同而进行不同的设置的,主观因素较大,应尽量遵守不影响整体渲染效果的前提下进行调整,既不造成性能的浪费,也不要有过大的视觉表现性的差异。
下面是编辑器模式下非运行时剔除优化前后的性能数据对比。接下来是编辑器模式下运行时剔除优化前后的性能数据对比。
最后我还是在真机上通过XCode 抓取了一帧,进行了优化前后的对比。左边是没有做 Simplization 与Cullinng之前的数据,右边是我们优化完成后的数据。可以看到在编辑器端性能数据有了很大的改善,但在真机上的优化效果真的不是很明显。
主要原因是我们的一些简化与剔除的优化,示例工程的原作者都已经做了,我们只是做了一些更深入的优化调整。在渲染光源阴影阶段,我们的优化还是有 1ms 的成果,但后面的一些渲染阶段的数据,有的还没有优化之前高,这可能是 GPU 负载不均衡导致的前后差异。在没有真正解决主要瓶颈之前,这个数据可能是不准确的。所以我认为接下来的 Batching 优化可能也不会带来本质上的性能提升。所以后续会优先优化渲染管线解决主要的性能瓶颈,再看看性能方面有无质的提升。
2.5.10 渲染流程的精简与优化
直接看课程:渲染流程的精简与优化
2.5.11 Terrain地形优化
直接看课程:Terrain地形优化
2.5.12 主光源级联阴影优化
直接看课程:主光源级联阴影优化
2.6 内存优化
2.6.1 Unity中的内存概述与工具方法
直接看课程:Unity中的内存概述与工具方法
2.6.2 Native内存分配器详解
直接看课程:Native内存分配器详解
2.6.3 内存指标术语与进程内存介绍
直接看课程:内存指标术语与进程内存介绍
2.6.4 项目设置与内存优化
直接看课程:项目设置与内存优化
2.6.5 Shader与托管内存优化
直接看课程:Shader与托管内存优化
2.7 渲染优化
2.7.1 Shader指令优化
直接看课程:Shader指令优化
2.8 发布前优化
2.8.1 耗电量与发热量优化
直接看课程:耗电量与发热量优化
给出了调低帧率的合理方式,可以参考。
2.8.2 启动时间优化
直接看课程:启动时间优化
优化主要分两阶段,启动到Unity Log/自定义 Log阶段,从Log阶段到第一个场景的阶段。视频中给出了各阶段的优化建议。
2.8.2 打包优化与课程总结
直接看课程:打包优化与课程总结
3 后记
后面几节没记笔记,直接看视频吧,一集也就10分钟左右。


















































1万+

被折叠的 条评论
为什么被折叠?



