移动游戏图形优化建议摘录

记录一些关于移动游戏图形向的优化建议

Posted by XStrachey on December 12, 2021

优化流程步骤拆解

优化流程包含下列步骤:

  • 使用分析器测量应用程序;
  • 分析数据,找到性能瓶颈;
  • 确定要运用的相关优化;
  • 验证优化是否达到预期;
  • 如果性能未达到预期,请返回至第 1 步,然后重复该流程。

优化是一项迭代流程,需要反复多次进行。

认识并调整Unity质量设置

在项目设置 > 质量中,有许多选项可以对游戏性能产生巨大影响:

像素光源数量

像素光源数量是指可以影响给定像素的光源数量。较高的像素光源数量需要大量的计算。大部分游戏即使使用极少量的动态实时光源,对图像质量的影响也微乎其微。如果光照引发了性能问题,请考虑在游戏中使用光照贴图和投影纹理等技术。

##纹理质量

纹理质量会给 GPU 带来负载,但通常不会引发性能问题。降低纹理质量会对游戏的画面质量产生不良影响,所以请只在必要的情况下降低纹理质量。在冰穴演示中,纹理质量设为全分辨率。

如果纹理引发了性能问题,可尝试使用 Mipmap 贴图。Mipmap 贴图可降低计算和带宽要求,同时不会影响图像质量。

##反锯齿

反锯齿是一项边缘平滑技术,该技术会混合三角形边周围的像素。这显著提高了游戏的画面质量。有多种反锯齿方式,但是本例中采用的是多重采样反锯齿 (MSAA)。4xMSAA 在 Mali GPU 上的运算量较低,应尽可能使用。

软粒子

软粒子需要渲染到深度纹理或在延迟模式下渲染。这会提高 GPU 负载,但可以获得逼真的粒子效果,因此值得采用。在移动平台上,渲染到深度纹理和从深度纹理读取会消耗掉宝贵的带宽,并且使用延迟路径进行渲染意味着你不能使用 MSAA。如果软粒子并非十分重要,则尽量不要在游戏中使用。

各向异性纹理

各向异性纹理技术可消除在高梯度下绘制的纹理的失真。这项技术可提高图像质量,但是非常消耗资源。除非失真特别明显,否则请避免使用此技术。

阴影

阴影具有高质量时,计算量较大。如果阴影引发了性能问题,请尝试采用简单的阴影或关闭阴影。如果阴影对于游戏非常重要,可以考虑使用简单的动态阴影技术,例如投影纹理。

硬阴影/硬加软阴影

软阴影看起来更为真实,但是计算时间较长。

阴影距离

阴影距离选项可定义与出现阴影的镜头的距离。增大阴影距离会增加可见阴影的数量,从而加大计算量。增大阴影距离还会增加用于阴影贴图中阴影的纹素数量,从而被动地提高阴影分辨率。

阴影分辨率

可通过选择调整阴影图分辨率来平衡质量和处理时间。

阴影级联

可以将它设置为零、二或四,级联阴影贴图用于定向光源,可获得非常好的阴影质量,尤其是视距较远时,级联数越高,质量越好,但会增加处理开销。

实时反射探针

实时反射探针选项对运行时性能存在显著的负面影响。在渲染反射探测器时,立方体贴图的每一面都由探测器原点处的镜头进行单独渲染。如果考虑相互反射,此过程会在每个反射反弹级别进行一次。在光泽反射情形中,立方体贴图 Mipmap 贴图也用于应用模糊处理。

下列因素影响立方体贴图的渲染:

立方体贴图分辨率

立方体贴图分辨率越高,渲染时间就越长。尽可能使用最低分辨率的立方体贴图来达到你需要的质量。

剔除遮罩

在渲染立方体贴图时使用剔除遮罩,从而避免对反射中任何无关几何体进行渲染。

立方体贴图更新

刷新模式选项定义立方体贴图的更新频率:

  • 每帧选项会逐帧渲染立方体贴图。这是计算成本最高的选项,所以非必要时请勿使用。
  • 唤醒时选项在场景启动时进行一次运行时立方体贴图渲染。
  • 借助脚本选项让你能控制立方体贴图更新的时间。使用此选项时,你可以指定更新条件,以此限制运行时的资源使用。

Unity性能定位工具

Unity Profile

关于Unity Profile主要摘录一个点:

Profiler.BeginSample() 和 Profiler.EndSample() 方法

Unity 分析器可让你采用 Profiler.BeginSample() 和 Profiler.EndSample() 方法以在脚本中标记一个区域,然后附上自定义标签,此区域将作为单独的条目出现在分析器层级中。通过执行此操作,可以获取特定代码的信息,而无需采用深度分析选项,从而节省计算和内存消耗。

void Update()
{
	Profiler.BeginSample("ProfiledSection");
	[...]
	Profiler.EndSample();
}

Unity Frame Debugger

Unity Frame Debugger 可以方便地检视以下数据:

  • Draw Call 数量;
  • Draw Call 顺序;
  • 每一Draw Call对应的Shader、Pass、关键字(对应Sahder变体);
  • 每一Draw Call所使用的变量(包括矩阵、纹理、向量等);
  • 每一Draw Call后的实际效果。

实际上,Unity Frame Debugger实际展示并非以Draw Call为单位梳理,而是以Batch为单位。

Mali Offline Compiler

这里记录下 Mali Offline Compiler 数据分析上的一些应注意点:

Mali 离线着色编译器产生的统计信息提供着色器所要求的每一顶点或像素的周期数的测量结 果,结果分成三行:

  • 合计;
  • 最短路径;
  • 最长路径。

分别查看代码中采取/不采取分支的效果之后衡量得出最短和最长路径,这样可以估计执行周期 数的最小和最大值。

对于算数操作的分析值,第一排中的测量结果除以算术流水线的数目(该数值为一、二或三,具体取决于 Mali GPU)。

第二列和第三列用于加载/存储和纹理流水线,但它们没有考虑缓存未命中情况,所以最好将这些 数字乘以 1.5,从而获得更加真实的估计结果。

Mali 离线着色编译器提供每一流水线中使用的周期数。测量出的周期数最高的流水线其速度最慢。应将流水线中速度最慢的作为优化目标,优化着色器。

Unity GPU 常见优化手段

使用静态批处理

静态批处理是一种常见的优化技术,可以减少绘制调用数量,从而降低应用处理器的使用率。静态批处理可用于大量顶点组成的对象,但是经过批处理的对象在渲染过程中不得移动、旋转或放大。

使用 4x MSAA

可以在 Unity 质量设置中启用 4x MSAA, Arm Mali GPU 能够以极低的计算开销执行 4x 多重采样反锯齿。

使用LOD Group

Unity 引擎可以使用 LOD Group 比较方便地完成对于同一物体的渲染根据距离镜头的远近去调整网格的任务。

减少高消耗数学函数的使用

在编写自定义着色器时,请避免使用计算量大的内置数学函数,例如:

  • pow().
  • exp().
  • log().
  • cos().
  • sin().
  • tan().

ASTC纹理压缩格式

ASTC 纹理压缩是 OpenGL 和 OpenGL ES 图形 API 的官方扩展。ASTC 可以减小应用程序所需的内存以及 GPU 需要的内存带宽。

ASTC 提供的纹理压缩质量高、比特率低,而且控制选项也很多。它具有下列特性:

  • 比特率从 8 位每像素 (bpp) 到小于 1 bpp 不等,这允许开发者微调文件大小与质量,以在两者间取得平衡;
  • 支持 1 至 4 个颜色通道;
  • 同时支持低动态范围 (LDR) 和高动态范围 (HDR) 图像;
  • 支持 2D 和 3D 图像;
  • 支持选择不同的特性组合。

ASTC 相较于 ETC 具有以下感受区别:

  • 内存大小相同时,ASTC 比 ETC 质量更高;
  • 相应的,ASTC 能够用更少的内存达到和 ETC 相同的效果;
  • ASTC 的编码时间比 ETC 长,而且会增加游戏打包的时间,如果在意它的话,那么最好只在最终游戏打包时使用 ASTC;
  • ASTC 允许通过设置块的大小来对质量进行更多的控制,虽然块大小没有一个理想的默认值,但一开始最好将块大小设置为 5x5 或 6x6。

ASTC 设置窗口中有多个块大小选项,可以从中选择与资源最匹配的块大小,块大小越大,提供的压缩率越高。

下表显示了,针对一幅大小为 4 MB,像素分辨率为 1024x1024,格式为 RGBA(8 位每通道)的纹理而言,Unity 中所有可用的 ASTC 块大小所对应的压缩比率:

适用于 Unity 中可用 ASTC 块大小的压缩率

ASTC 块大小 大小 压缩比率
	   4x4 1 MB 4.00
	   5x5 655 KB 6.25
	   6x6 455 KB 9.00
	   8x8 256 KB 16.00
	   10x10 164 KB 24.97
	   12x12 144 KB 35.93

Mipmap 贴图

Mipmap 贴图是不同大小的纹理的预计算版本。每个生成的纹理称为一个层级,它们的宽度和高度是前一个纹理的一半。Unity 能够自动生成完整的层级,从原始尺寸的第一层级到 1x1 像素版本。

  • 如果纹理没有 Mipmap 贴图层级,当具有纹理的表面覆盖的区域(以像素表示)小于纹理尺寸时,GPU 会将纹理缩小至适合更小的区域。但是,此过程中会丧失部分准确性,即便使用滤波器插值像素颜色。

  • 如果纹理添加了 Mipmap 贴图层级,GPU 将从最接近对象大小的层级中提取像素数据以渲染纹理。这可提高图像质量并降低带宽,因为 GPU 为获得更高质量已经离线扩展了层级,并且 GPU仅从恰当的层级中提取纹理数据。Mipmap 贴图的劣势在于它额外需要 33% 的内存来存储纹理数据。

通常不需要对 2D UI 中所用的纹理添加 Mipmap 贴图。UI 纹理通常不需要放大即可在屏幕上渲染,它们仅使用 Mipmap 贴图链中的第一层级。

阴影

可以使用阴影距离较小且分辨率较高的硬阴影。这会在距离镜头较远范围内产生不是很复杂且质量较高的阴影。

进行光照贴图的对象不会产生实时阴影,你在场景中烘焙的静态阴影越多,GPU 执行的实时计算就越少。

在移动平台上,要尽量限制仅包含实时阴影的光源数量,并尽量使用光照贴图。

遮挡剔除

当对象完全退出镜头视锥体时,Unity 将自动执行视锥剔除,同时 Unity 包含称之为 Umbra 的遮挡剔除系统。

指定渲染顺序

自 Mali -T600 系列起,Arm Mali GPU 上用于减少计算浪费的硬件技术中就包含了 Early-Z,但在一些情形中会自动禁用。

例如:如果片段着色器通过写入 gl_FragDepth 变量修改深度,片段着色器将调用 discard;或者,如果为透明物体等对象启用了混合或 Alpha 测试。为帮助此系统达到最高效率,请确保从前往后渲染不透明物体。

按照从前往后的顺序渲染每一帧成本可能会很高昂,如果同一通道中还渲染透明物体,这也可能会不正确。自 T620 起,Arm Mali GPU 提供了一种称为前像素终止 (PFK) 的机制。 Mali GPU已流水化,可以为同一像素同时执行多个线程。当某一线程完成了其执行时,如果当前线程遮挡了该像素的所有其他线程时,PFK 系统会将它们全部停止。这起到了减少计算浪费的效用。

Pre-Z Pass

Shader 中的 Pre-Z Pass 是类似于如下示例的 Pass ,其目的可以类比于用户侧的一次手动的 Early Z :

// extra pass that renders to depth buffer only
Pass {
	ZWrite On
	ColorMask 0
}

资源优化杂项

禁用静态纹理读取/写入

如果不动态修改纹理,请确保检视面板中的读取/写入已启用选项已被禁用。

合并网格以减少绘制调用数量

为减少渲染所需的绘制调用数量,可以使用 Mesh.CombineMeshes() 方法将多个网格合并为一个网格。如果所有网格的材质相同,请将 mergeSubMeshes 参数设置为 true,以便它可以根据合并组中的每个网格生成单一子网格。把多个网格合并为单个较大的网格将可以:

  • 创建更高效的遮挡器;
  • 将多个基于图块的资源转变为大型、无缝、实心的单一资源。

避免读取/写入网格

如果实时修改了模型,Unity 会在保留原始数据的同时在内存中另外保存一份复制的网格数据对其进行修改。

如果运行时未修改模型(即使准备缩放),也请在导入设置的模型选项卡中禁用读取/写入已启用选项。这样不需要另外保存一份待修改的副本以节省内存。

请勿在非动画 FBX 网格模型上导入动画数据

当导入不包含任何动画数据的 FBX 网格时,应当在导入设置的装置选项卡中将动画类型设置为无。这样设置后,将网格置于层级时 Unity 不会生成未使用的动画组件。

针对 Mali™ GPU 流水线进行优化

Mali GPU 包含三种类型的处理流水线:

  • 算术流水线;
  • 加载/存储流水线;
  • 纹理流水线。

算术流水线

所有算术运算消耗算术流水线的周期。

可以将负载移到其他流水线中,从而降低对算术管道的负载:

  • 将矩阵作为统一变量传递,而不要计算它们,这可将负载移到加载/存储流水线;
  • 使用纹理来存储代表正弦或余弦等函数的一组预计算值,这可将负载移到纹理流水线。

加载/存储流水线

加载/存储流水线用于读取统一变量、写入变量,以及访问着色器中的缓冲区,如统一缓冲区对象或着色器存储缓冲区对象。

如果发现应用处于加载/存储流水线 bound 的状态,可以尝试下列方法改善:

  • 着色器中通过使用纹理而非缓冲区对象来读取数据;
  • 使用算术运算计算数据;
  • 压缩或减少统一缓冲区和变量的使用。

纹理流水线

访问纹理时会使用纹理流水线中的周期,也会使用内存带宽。使用大纹理可能有害,因为缓存未 命中的几率更高,而且这样可能导致多个线程因为等待数据而停滞。

要提高纹理流水线的性能,可进行如下尝试:

  • 使用 Mipmap 贴图,Mipmap 贴图可提高缓存命中率,因为它根据纹理坐标变化选用纹理的最佳分辨率;
  • 使用纹理压缩,这也有益于降低内存带宽并提高缓存命中率,每个压缩的块都包含至少一个以上的纹素,所以其访问变得更容易缓存;
  • 避免三线性或各向异性过滤,三线性和各向异性过滤将增加获取纹素所需的操作数,若非绝对必要,请避免使用。

减少流水线周期的其他方法

避免寄存器溢出

Mali 离线着色编译器可指示你的着色器是否溢满寄存器。造成寄存器溢出的原因通常是线程中包含大量变量,它们无法完整装入寄存器集中。

寄存器溢出通常由包含大量以下对象的线程造成:

  • 输入统一变量;
  • 变量;
  • 临时变量;
  • 如果变量使用高精度,也可能会发生寄存器溢出。

寄存器溢出会强制 Mali GPU 从内存读取一些统一变量,这会加重加载/存储单元的负载并降低性能。要解决此问题,可尝试减少向着色器提供的统一变量数量并降低其精度。

Mali Offline Compiler 在编译寄存器溢出的shader时会给出提示语句:spilling used.

降低变量和Uniform变量的精度

使用半浮点数有几个好处:

  • 带宽用量减少;
  • 算术流水线中使用的周期数减少,因为着色器编译器可以优化Shader代码以提高并行化程度;
  • 要求的Uniform变量寄存器数量减少,这反过来又降低了寄存器溢出风险;
  • 使用半浮点数时生成的代码大小也小于使用浮点数生成的代码,这可提高 Mali GPU 上的缓存命中率,从而提升了性能。

将世界空间法线贴图用于静态对象

对于静态对象,可以选择使用局部空间法线贴图或世界空间法线贴图。使用局部空间法线贴图可减少着色器中执行的计算数量,但是必须对采样的法线应用模型上的变换。世界空间法线贴图不需要任何变换,但它们是静态的,并且对象无法移动。

美术资源的产出优化

顶点与三角

在移动设备上,每个 3D 对象或网格最多可以使用 65535 个顶点,因此,使用的数量必须小于这个数。使用 Mali-400 等一些早期 GPU 的 Android 设备只能支持这个数量的顶点,这些设备不会渲染具有更多顶点的 3D 对象。

关于微型三角形

微型三角形是微小的三角形,对对象或场景的最终显示效果没有多大贡献。具有大量多边形的 3D 对象远离镜头移动时,就会出现微型三角形问题。微型三角形通常是指设备上大小在 1 到 10 像素之间的三角形。微型三角形不太好用,因为 GPU 必须处理所有这些三角形,但是,由于它们太小而看不到,它们对最终图像的贡献不大。

可以采取以下几个步骤来缓解这一问题:

  • 对于会改变与镜头距离的对象,请使用细节层次 (LOD)。使用正确的 LOD,可以简化距离较远的对象,并降低三角形的使用数量;
  • 在背景对象上使用较少的三角形;
  • 为细节部分建模时,应避免布置密集的三角形网格,从建议上来讲,精细部位应使用法线贴图来实现精细化效果;
  • 合并所有太小而无法在屏幕上看到的或者对于最终画质没有太大价值的顶点或三角形;
  • 尝试将区域内的三角形尺寸保持在 10 像素以上。

关于细长三角形

细长的三角形由那些在最终图像中渲染时,小于 10 个像素,并沿屏幕延伸的顶点构成。细长三角形之所以不好是因为它们要比普通三角形消耗更多的 GPU 算力。

可以采取以下几个步骤来缓解这一问题:

  • 尽可能从所有对象中移除任何细长的三角形,虽然在某些情况下这是不可能的,但最好的解决方法是完全去除细长的三角形;
  • 避免在有细长三角形的对象上使用有光泽的材质,因为这会导致对象闪烁;
  • 在对象离屏幕较远时,使用细节层次 (LOD) 并去除细长三角形;
  • 从技术上讲,保持三角形接近等边更好,所以请确保三角形内部相对于边而言具有更大的面积。

LOD

确定每个 LOD 中的三角形数量时,需要考虑如下关键点:

  • 各个 LOD 层级之间值得按照 50% 的幅度递减三角形数量;
  • 请勿在较低的 LOD 上密集使用三角形。这些只有当对象离得更远时才能被看到;
  • 在 LOD 应当要被看到的相对于镜头的正确距离上,检查 LOD 的呈现状态,较低的 LOD 虽然近距离看起来很糟糕,这没关系,因为它们本就不适合近景查看。

纹理

纹理滤波

纹理滤波是一种用于改善场景中纹理质量的方法,Unity 针对纹理过滤提供了几个选项:

最近/点过滤

当近距离观察时,近点过滤会让纹理显示为像素块。这是最简单,也是成本最低的纹理过滤。

双线性过滤

双线性过滤会模糊近处的纹理。GPU 会对四个最接近的纹素进行采样,然后取平均值为主像素上色。与近点过滤不同,在双线性过滤下,像素的渐变更平滑,像素块更少。

三线性过滤

三线性过滤类似于双线性过滤,但会在两个 Mipmap 贴图层级之间添加混合效果。通过在 Mipmap 贴图之间实现平滑过渡,三线性过滤可以抹除贴图之间明显的界限。

各向异性过滤

各向异性过滤能够提高倾斜纹理的画面质量。

纹理滤波设置的优化性建议

Arm 所推荐的纹理过滤小技巧:

  • 若要在性能和画面质量之间寻求平衡点,可以使用双线性过滤;
  • 有选择性地使用三线性过滤,因为相较于双线性过滤,它占据的内存带宽更多;
  • 与其使用三线性/1x 各向异性过滤的组合,不妨使用双线性/2x 各向异性过滤的组合,这样能同时兼顾视效和性能;
  • 始终采用较低的各向异性值,仅对游戏中的关键素材使用高于 2 的值。

纹理大小

减少某些需要较少细节的纹理的大小有助于降低带宽级别。例如,可以将漫反射纹理设置为 1024x1024,将粗糙或金属贴图设为 512x512。

UV展开

UV 展开是创建 UV 贴图的过程,其最佳做法是保持 UV island 尽可能直。

原因如下:

  • 使得 UV island 更容易打包,浪费的空间更少;
  • 直的 UV 有助降低纹理上发生的阶梯效应;
  • 在移动平台上,纹理空间是有限的,因为纹理大小通常比游戏主机或电脑上显示的要小。良好的 UV 打包可确保纹理获得更高分辨率;
  • 为保持 UV 笔直从而获得整体质量更好的纹理,让 UV 出现稍许扭曲也是值得的。

其他的建议包括将 UV 接缝放在不太显眼的地方,这样做有利于视效,因为纹理接缝在模型上看起来可能不太美观。因此,在边缘清晰处分离 UV island,之间稍微保持一点距离,这有助于后续通过烘焙过程创建更好的法线贴图。

纹理通道打包

在使用通道合并的方式减少纹理占用、节省带宽时,有一个注意点是建议使用绿色通道存储更重要的遮罩,绿色通道通常具有更多位。这是由于人们的眼睛对绿色更敏感,而对蓝色不那么敏感。