复习Unity优化技巧
一、引言
又要找工作了,现在的公司项目组被砍了,很不幸,我也被砍了。那么久需要复习下,准备下阶段找工作了。于是,决定弄个文章来记录下。被砍是因为政策原因。并没有做什么违法的事情,跟公司性质有关系。
新的格局
不太想做游戏开发了,这一年多搞了Linux跟各种游戏开发软件结合,如Unity、Cocos、Electron,希望未来能找一个开发游戏但偏Linux方向的。
有推荐的吗?
欢迎大家在评论区告诉我。谢谢。
二、优化
按照Unity的官方文档来吧。不懂的加备注。
第一章、优化图形性能
1、分析:
A、GPU通常受填充率或者内存带宽制约。
填充率
是指图形处理单元在每秒内所渲染的像素数量,单位是MPixel/s(百万像素每秒)或者GPixel/s(十亿像素每秒)。像素的填充率等于显示核心的渲染管线数量×核心频率。
渲染管线
渲染管线(Rendering Pipeline)其实就是GPU渲染流程。你可以这样理解渲染,在一个三维坐标系下,给定一个视点(即摄相机),给定三维物体、光源以及照明模式、纹理等信息,如何绘制一幅呈现在视点画面中的二维图像的过程。
尝试解决办法:降低显示分辨率并运行游戏。如果显示分辨率降低后游戏运行更快,表明 GPU 填充率可能是限制因素。
B、CPU 通常受到需要渲染的批次数的限制。
尝试解决办法:检查 Rendering Statistics 窗口中的“batches”。渲染的批次越多,CPU 成本越高。
C 、不太常见的瓶颈。
a、GPU 有太多顶点需要处理。
可接受的能确保良好性能的顶点数量取决于 GPU 和顶点着色器的复杂程度。一般来说,移动端应不超过 100,000 个顶点。另一方面,即使有数百万个顶点,PC 也能管理到位,不过最好还是通过优化尽可能减少此数量。
b、CPU 有太多顶点需要处理。
这些顶点可能位于蒙皮网格、布料模拟、粒子或其他游戏对象和网格中。如上所述,通常较好的做法是在不影响游戏质量的情况下尽可能降低此数量。
c. 其他问题。
如果渲染在 GPU 或 CPU 方面不是问题,则可能在其他地方存在问题,例如在脚本或物理系统中。
2、优化
A、GPU:优化模型几何体
优化模型几何体有两个基本规则:
-
除非必要,否则不要使用三角形(尽量减少模型的三角面数和顶点数)
-
尽可能降低 UV 贴图接缝和硬边(双倍顶点)的数量
GPU优化: -
模型优化,尽量减少模型的三角面数和顶点数。
-
保持尽量少的材质数目,便于Unity进行批处理。
-
使用纹理图集(一张大贴图中包含多个子贴图)来替代一系列单独的小贴图。
-
使用代码操作材质时,尽量使用renderer.shareMaterial代替renderer.Material,因为后者的每一次改动都会创建一个新的材质。
-
使用Mip Map(Mip Map在贴图的Import Setting中设置),但同时会增加内存。
-
使用LOD,遮挡剔除(Occlusion culling)等技术。
-
光照优化,尽量使用烘培灯光,预先烘培好场景的Lightmap。控制灯光的数量并且谨慎使用产生实时阴影的光。
B、GPU:光照性能
速度最快的方案是始终创建根本不需要计算的光照。要做到这一点,使用光照贴图只需一次“烘焙”静态光照,而无需每帧计算。生成光照贴图环境的过程只比在 Unity 场景中放置光源稍久一点,但是:
- 运行速度要快得多(每像素 2 个光源的情况下,速度快 2–3 倍)
- 视觉效果要好得多,因为可以烘焙全局光照,使光照贴图显得更平滑
在许多情况下,可运用简单的技巧,无需添加多个额外的光照。例如,无需添加直接照入摄像机的光源来提供__边缘光照__效果,而是直接在着色器中添加专用的 Rim Lighting 计算(请参阅表面着色器示例以了解如何执行此操作)。
前向渲染中的光照
对于所有像素,动态光照会为每个受影响的像素增加渲染工作,可能导致对象在多个 pass 中被渲染。避免在性能较弱的设备(如移动端或低端 PC GPU)上使用多个__像素光照__来照射单个对象,应使用光照贴图实现静态对象的光照,而不是每帧计算其光照。每顶点动态光照可能会为顶点变换增加显著的工作量,因此尽量避免多个光源照射单个对象的情况。
避免组合距离足够远而需要受到不同像素光照影响的网格。使用像素光照时,每个网格必须渲染多次,因为只要发生像素光照就要进行渲染。如果组合两个相距很远的网格,则会增加组合对象的有效大小。照射该组合对象任何部分的所有像素光照在渲染期间都要考虑在内,因此需要创建的渲染 pass 的数量可能增加。通常情况下,为渲染组合对象而必须创建的 pass 数为每个单独对象的 pass 数之和,因此进行网格组合并不会获得任何好处。
在渲染过程中,Unity 会查找网格周围的所有光源,并计算出哪些光源对网格的影响最大。使用 Quality 窗口上的设置可修改多少个光源用于像素光照以及多少个用于顶点光照。每个光源根据它与网格的距离以及它的光照强度来计算其重要性;纯粹从游戏背景而言,有些光源比另一些光源更重要。鉴于此原因,每个光源都有 Render Mode 设置,可设置为 Important 或 Not Important__;标记为 Not Important__ 的光源具有较低的渲染开销。
示例:假设有一个驾驶游戏,玩家的汽车在黑暗中行驶,前照灯已打开。前照灯可能是游戏中视觉上最重要的光源,因此它们的 Render Mode 应设置为 Important。游戏中可能还有其他不太重要的光源,比如其他汽车的尾灯或远处的灯柱,这些光源不能通过像素光照来大幅改善视觉效果。这种情况下,可放心地将这些光源的 Render Mode 设置为 Not Important,从而避免将渲染能力浪费在无用之处。
通过优化每像素光照可以节省 CPU 和 GPU 工作量:CPU 的绘制调用将减少,而 GPU 要处理的顶点将减少,同时为所有其他对象渲染栅格化的像素也将减少。
C、GPU:纹理压缩和 Mipmap
使用压缩纹理可减小纹理的大小。这种做法可加快加载时间、减小内存占用并显著提高渲染性能。与未压缩的 32 位 RGBA 纹理所需的内存带宽相比,压缩纹理使用的内存带宽要小得多。
纹理 Mipmap
对于 3D 场景中使用的纹理,应始终启用 Generate mipmaps 选项。Mipmap 纹理使 GPU 能够为较小的三角形使用较低分辨率的纹理。这一点类似于纹理压缩可以帮助限制 GPU 渲染时传输的纹理数据量。
此规则的唯一例外是当已知纹理像素将 1:1 映射到渲染的屏幕像素时(与 UI 元素或在 2D 游戏中一样)。
D、GPU:LOD(细节级别)和每层剔除距离
剔除对象涉及使对象不可见。这是减轻 CPU 和 GPU 负载的有效方法。
在许多游戏中,在不影响玩家体验的情况下快速有效地执行此操作的方法是,相对于大对象,更激进地剔除小对象。例如,可让远处的小岩石和碎片不可见,而大型建筑物仍然保持可见。
有多种方式实现此目标:
- 使用细节级别系统
- 手动设置摄像机上的每层剔除距离
- 将小对象放入单独一层,并使用 Camera.layerCullDistances 脚本函数设置每层剔除距离
E、GPU:实时阴影:
实时阴影很不错,但它们对性能有很大影响,同时会增加 CPU 的绘制调用次数和 GPU 的处理量。
F、GPU:编写高性能着色器的技巧
不同的平台具有截然不同的性能;与低端移动端 GPU 相比,高端 PC GPU 在图形和着色器方面的处理能力要高得多。即使在单一平台上也是如此;快速的 GPU 比慢速的集成 GPU 快几十倍。
移动平台和低端 PC 上的 GPU 性能可能远低于开发机器上的 GPU 性能。建议手动优化着色器以减少计算和纹理读取,从而在低端 GPU 机器上获得良好的性能。例如,某些内置的 Unity 着色器具有速度快得多但存在一些限制或近似处理的“移动端”等效项。
以下是移动端和低端 PC 显卡的一些指导原则:
复杂的数学运算
超越数学函数(例如 pow、exp、log、cos、 sin、tan)都很消耗资源,所以尽量避免使用它们。如果可能,请尽量考虑使用查找纹理作为复杂数学计算的替代方法。
避免编写自己的运算(如 normalize、dot、inversesqrt)。Unity 的内置选项确保驱动程序可以生成好得多的代码。请记住,Alpha 测试 (discard) 运算通常会使片元着色器变慢。
浮点精度
虽然浮点变量的精度(float 与 half 与 fixed) 在桌面平台 GPU 上很大程度上会被忽略,但在移动端 GPU 上 对于获得良好性能非常重要。
有关着色器性能的更多详细信息,请参阅着色器性能页面。
E、CPU:优化方向:
为了在屏幕上渲染对象,CPU 需要做很多处理工作:确定哪些光源影响该对象,设置着色器和着色器参数,向图形驱动程序发送绘制命令,而图形驱动程序随后将准备发送到显卡的命令。
所有这种基于“每个对象”的 CPU 使用率都是非常消耗资源的,所以如果有很多可见对象,影响就会累加起来。例如,如果有一千个三角形,如果它们都在一个网格中,而不是每个三角形在一个网格中(这种情况下加起来就有 1000 个网格),则 CPU 处理起来就比较容易。两种方案的 GPU 成本非常相似,但 CPU 完成渲染一千个对象(而不是一个)的工作要高得多。减少可见对象数量。要减少 CPU 需要执行的工作量,请执行以下操作:
- 通过手动方式或使用 Unity 的绘制调用批处理将近处对象组合在一起。
- 通过将单独的纹理放入更大的纹理图集,在对象中使用更少的材质。
- 减少可能导致对象多次渲染的因素(例如反射、阴影和每像素光照)。
将对象组合在一起,使每个网格至少有几百个三角形,并使整个网格只使用一种材质。请注意,组合两个不共享材质的对象根本不会提高性能。需要多种材质的最常见原因是两个网格不共享相同的纹理;为了优化 CPU 性能,请确保组合的所有对象共享相同的纹理。
F、用于提高游戏运行速度的简单核对表
- 在针对 PC 平台进行构建时,保持顶点数量低于 200K 和 3M/帧(具体值取决于目标 GPU)。
- 如果要使用内置着色器,请从 Mobile 或 Unlit 类别中选取。这些类别也适用于非移动平台,但它们 - - 是更复杂着色器的简化和近似版本。
- 保持每个场景使用较少的不同材质,并尽可能在不同对象之间共享材质。
- 在非移动对象上设置 Static 属性以便允许内部优化,如静态批处理。
- 只有一个(最好是方向性的)pixel light 影响几何体(而不是有多个)。
- 烘焙光照而不是使用动态光照。
- 尽可能使用压缩纹理格式,并使用 16 位纹理而非 32 位纹理。
- 尽可能避免使用雾效。
- 如果复杂的静态场景具有大量遮挡,使用遮挡剔除减少可见几何体数量和绘制调用次数。设计关卡 - - 时注意遮挡剔除。
- 使用天空盒“伪造”远处的几何体。
- 使用像素着色器或纹理组合器来混合多个纹理而不是使用多 pass 方法。
- 尽可能使用 half 精度变量。
- 最大限度减少在像素着色器中使用复杂的数学运算,例如 pow、sin 和 cos。
- 每个片元使用更少的纹理。
第二章、绘制调用批处理
1、基本概念
A、UV
完整的说,其实应该是UVW(因为XYZ已经用过了,所以另选三个字母表示)。U和V分别是图片在显示器水平、垂直方向上的坐标,取值一般都是0~1,也 就是(水平方向的第U个像素/图片宽度,垂直方向的第V个像素/图片高度)。那W呢?贴图是二维的,何来三个坐标?嗯嗯,W的方向垂直于显示器表面,一般 用于程序贴图或者某些3D贴图技术(记住,确实有三维贴图这种概念!),对于游戏而言不常用到,所以一般我们就简称UV了。
B、光栅化
光栅化就是将一个图元转变为一个二维图像的过程,光栅化会根据三角形顶点的位置,来确定需要多少个像素点才能构成这个三角形。总的来说就是将几何信息转换成一个个的栅格组成的图像的过程,把顶点投影到屏幕空间进行渲染。
C、渲染管道、主要步骤
渲染管道是显示器上为了显示出图像而经过的一系列的必要操作,将几何物体从一个坐标系中变换到另一个坐标系中去。
主要步骤:本地坐标->视图坐标->光照->裁剪->投影->视图变换->光栅化
2、分析
要在屏幕上绘制游戏对象,引擎必须向图形 API(例如 OpenGL 或 Direct3D)发出绘制调用。绘制调用通常为资源密集型操作,图形 API 为每次绘制调用执行大量工作,从而导致 CPU 端的性能开销。此开销的主要原因是绘制调用之间的状态变化(例如切换到不同材质),而这种情况会导致图形驱动程序中执行资源密集型验证和转换步骤。
Unity 使用两种方法来应对此情况:
动态批处理:对于足够小的网格,此方法会在 CPU 上转换网格的顶点,将许多相似顶点组合在一起,并一次性绘制它们。
静态批处理:将静态(不移动)游戏对象组合成大网格,并以较快的速度渲染它们。
与手动合并游戏对象相比,内置批处理有几个好处;最值得注意的是,仍然可以单独剔除游戏对象。但是,也有一些缺点;静态批处理会导致内存和存储开销,动态批处理会产生一些 CPU 开销。
3、批处理的材质设置
- 只有共享相同材质的游戏对象才可一起接受批处理。因此,如果想要实现良好批处理,应在尽可能多的不同游戏对象之间共享材质。
- 如果两种相同材质仅在纹理上不同,可将这些纹理组合成单个大纹理。此过程通常称为纹理镶嵌。一旦纹理位于相同图集中,即可使用单个材质。
- 如果需要从脚本访问共享材质属性,必须注意,修改 Renderer.material 将创建该材质的副本。应改用 Renderer.sharedMaterial 来保留共享的材质。
- 阴影投射物即使材质不同,通常也可以在渲染时接受批处理。Unity 中的阴影投射物即使具有不同材质也可以使用动态批处理,只要阴影 pass 所需材质中的值相同即可。例如,许多板条箱可能使用具有不同纹理的材质,但是由于渲染纹理的阴影投射物不相关,所以在此情况下,它们可以一起接受批处理。
4、动态批处理(网格)
如果移动的游戏对象共享相同材质并满足其他条件,则 Unity 可自动在同一绘制调用中批处理这些游戏对象。动态批处理是自动完成的,无需您进行任何额外工作。
-
批处理动态游戏对象在每个顶点都有一定开销,因此批处理仅会应用于总共包含不超过 900 个顶点属性且不超过 300 个顶点的网格。
如果着色器使用顶点位置、法线和单个 UV,最多可以批处理 300 个顶点,而如果着色器使用顶点位置、法线、UV0、UV1 和切线,则只能批处理 180 个顶点。
注意:将来可能会更改属性数量限制。
这个地方为什么是300和180,按照900个顶点属性计算,900/(顶点位置、法线和单个 UV)=300。同样的,900/(顶点位置、法线、UV0、UV1 和切线)=180。 -
如果游戏对象在变换中包含镜像,则不会对这些对象进行批处理(例如,具有 +1 缩放的游戏对象 A 和具有 –1 缩放的游戏对象 B 无法一起接受批处理)。即分别拥有缩放尺度(1,1,1)和(2,2,2)的两个物体将不会进行批处理。统一缩放尺度的物体不会与非统一缩放尺度的物体进行批处理。使用缩放尺度(1,1,1)和 (1,2,1)的两个物体将不会进行批处理,但是使用缩放尺度(1,2,1)和(1,3,1)的两个物体将可以进行批处理。
DrawCall分析流程来分析一下:
1、渲染A,使用材质1
2、渲染B,使用材质1
3、渲染C,使用材质2
在这种情况下是2个DrawCall,在下面这种情况下,则是3个DrawCall
1、渲染A,使用材质1
2、渲染C,使用材质2
3、渲染B,使用材质1 -
即使游戏对象基本相同,使用不同的材质实例也会导致游戏对象不能一起接受批处理。例外情况是阴影投射物渲染。
-
带有光照贴图的游戏对象具有其他渲染器参数:光照贴图索引和光照贴图偏移/缩放。通常,动态光照贴图的游戏对象应指向要批处理的完全相同的光照贴图位置。
-
多 pass 着色器会中断批处理。
- 几乎所有的 Unity 着色器都支持前向渲染中的多个光照,有效地为它们执行额外 pass。“其他每像素光照”的绘制调用不进行批处理。
- 旧版延迟(光照 pre-pass)渲染路径会禁用动态批处理,因为它必须绘制两次游戏对象。
因为动态批处理的工作原理是将所有游戏对象顶点转换到 CPU 上的世界空间,所以仅在该工作小于进行绘制调用的情况下,才有优势。绘制调用的资源需求取决于许多因素,主要是使用的图形 API。例如,对于游戏主机或诸如 Apple Metal 之类的现代 API,绘制调用的开销通常低得多,通常动态批处理根本没有优势。
5、动态批处理(粒子系统、线渲染器、轨迹渲染器)
动态批处理在用于具有 Unity 动态生成的几何体的组件时,其工作方式与用于网格时不同。
- 对于每个兼容的渲染器类型,Unity 将所有可批处理的内容构建为 1 个大型顶点缓冲区。
- 渲染器设置材质状态以用于批处理。
- Unity 将顶点缓冲区绑定到图形设备。
- 对于批处理中的每个渲染器,Unity 将偏移更新到顶点缓冲区中,然后提交新的绘制调用。
在衡量图形设备调用的成本时,渲染组件时的最慢部分是材质状态的设置。相比之下,将不同偏移处的绘制调用提交到共享顶点缓冲区中的速度非常快。
这种方法与 Unity 在使用静态批处理时提交绘制调用的方式非常相似。
6、静态批处理
使用静态批处理,引擎可减少任何大小的几何体的绘制调用,但前提是它共享相同材质并且不移动。这种处理方式通常比动态批处理更高效(它不会在 CPU 上转换顶点),但是使用更多内存。
为了利用静态批处理,您需要显式指定某些游戏对象是静态对象且不会在游戏中移动、旋转或缩放。为此,请使用 Inspector 中的 Static 复选框,将游戏对象标记为静态。
在内部,静态批处理的工作原理是将静态游戏对象转换到世界空间并为它们构建一个共享的顶点和索引缓冲区。如果已启用 Optimized Mesh Data__(在 Player__ 设置中),则 Unity 会在构建顶点缓冲区时删除任何着色器变体未使用的任何顶点元素。为了执行此操作,系统会进行一些特殊的关键字检查;例如,如果 Unity 未检测到 LIGHTMAP_ON 关键字,则会从批处理中删除光照贴图 UV。然后,针对同一批次中的可见游戏对象,Unity 会执行一系列简单的绘制调用,每次调用之间几乎没有状态变化。在技术上,Unity 不会减少 API 绘制调用,而是减少它们之间的状态变化(这正是消耗大量资源的部分)。在大多数平台上,批处理限制为 64k 个顶点和 64k 个索引(OpenGLES 上为 48k 个索引,在 macOS 上为 32k 个索引)。
7、提示
当前,仅对网格渲染器、轨迹渲染器、线渲染器、粒子系统和精灵渲染器进行批处理。这意味着不会对蒙皮网格、布料和其他类型的渲染组件进行批处理。
渲染器仅与其他相同类型的渲染器一起接受批处理。
半透明着色器通常要求游戏对象按照从后到前的顺序进行渲染,从而实现透明性。Unity 首先按此顺序对游戏对象排序,然后尝试对它们进行批处理,但是因为必须严格满足顺序,所以这通常意味着可以实现比不透明游戏对象更少的批处理。
手动组合彼此接近的游戏对象可以是绘制调用批处理的极好替代方法。例如,一个带有大量抽屉的静态橱柜通常只需在 3D 建模应用程序中或者使用 Mesh.CombineMeshes 来组合成一个网格。
第三章、角色建模的性能优化
1、使用单个带蒙皮的网格渲染器
对于每个角色,仅使用单个蒙皮网格渲染器。Unity 使用可见性剔除和包围体更新来优化动画,这些优化仅在您将单个动画组件和单个蒙皮网格渲染器组合使用时才会生效。若使用两个蒙皮网格,模型的渲染时间大概会是单个网格的两倍,并且这样做很少能带来实际的意义。
2、使用尽可能少的材质
您同样应该尽可能减少每个网格上的材质数量。只有在需要为角色的不同的部分使用不同着色器(例如,眼睛的特殊着色器)时,您才应该考虑使用多种材质。其余大多数情况下,每个角色有两到三种材质就应该足够了。
3、使用尽可能少的骨骼
典型的桌面游戏中的骨骼层级视图大概拥有 15 到 60 根骨骼。骨骼的数量越少,性能就越好。在使用大约 30 根骨骼的情况下,桌面平台上可以获得极佳质量,移动平台上也能获得比较好的质量。理想情况下,移动设备游戏的骨骼数量应保持在 30 根以下,桌面游戏的骨骼数量也不要超出 30 根太多。
4、多边形数量
您应该使用的多边形数量取决于您需要达到的质量,以及您的目标平台。对于移动设备而言,每个网格 300 到 1500 个多边形就可以取得良好的效果,而对于桌面平台,理想的数量范围大约为 1500 到 4000。如果游戏经常会同屏内出现大量角色,那么您可能需要减少每个网格的多边形数量。
5、正向和反向动力学保持分离
在导入动画时,模型的反向动力学 (IK) 节点将被烘焙为正向动力学 (FK),因此 Unity 根本不需要 IK 节点。但是,如果它们留在了模型中,那么即使它们不影响动画,也会让 CPU 产生额外开销。您可以根据自己的偏好,删除 Unity 或建模工具中的冗余 IK 节点。理想情况下,应在建模期间保留单独的 IK 和 FK 层级视图,以便在必要时更轻松地删除 IK 节点。
A、反向动力学(IK)
反向动力学,就是子骨骼节点带动父骨骼节点运动。一般在模型制作的时候删除。导入Unity如何删除暂时没有找到方法。
第四章、最佳实践指南
1、性能分析
-[UnityAppController startUnity]
|- [UnityInitApplicationGraphics]
|- [UnityLoadApplication]
UnityInitApplicationGraphics 执行大量内部工作,例如设置图形设备和初始化 Unity 的大量内部系统。此外,它还初始化资源系统 (Resources system)。为此,它必须加载资源系统包含的所有文件的索引。
每个“Resources”文件夹中的每个资源文件 (1)(注意: 这仅适用于项目“Assets”文件夹中名为“Resources”的文件夹,以及这些“Resources”文件夹中的所有子文件夹。)都作为资