跳到主要内容

基本性能优化

简介

在一个理想的世界里,计算机将以无限的速度运行。我们唯一的限制就是我们的想象力。 然而,在现实世界中,制造出能让最快的计算机也屈服的软件实在是太不容易了。 因此,设计游戏和其他软件是在我们希望可能的情况下,和在保持良好性能的前提下,能够实际实现的情况之间的折中选择。
在用户看来,项目缓慢问题往往被归纳在一起。 但实际上,缓慢问题有几种不同的类型:

  • 每一帧都发生的缓慢过程,导致持续的低帧率。
  • 一个断断续续的过程,造成缓慢的到达 "巅峰", 导致停滞不前。
  • 在正常游戏之外发生的缓慢进程,例如加载关卡时。

每一种都会给用户带来烦恼,但方式不同。
主要影响计算机性能的3个系统:CPU、GPU、内存。

tip

GPU 与 CPU 的优化策略大不相同(甚至相反;例如,通常在优化 CPU 时让 GPU 做更多工作,反之亦然)。

原则

开发者的时间是有限的。与其盲目地试图加快一个程序的所有方面,应该集中精力在真正重要的方面。
在优化方面的努力,最终往往会得到比非优化代码更难阅读和调试的代码。将这种情况限制在真正受益的领域更符合我们的利益。

tip

程序员浪费了大量的时间去考虑或者担心程序中非关键部分的速度,如果考虑到调试和维护, 这些提高效率的尝试实际上会产生强烈的负面影响。 我们应该忘掉小效率,比如说97%左右的时间: 过早的优化是万恶之源。然而不应该放弃那关键的3%的机会。

性能瓶颈

对于优化来说, 最重要的工具可能是衡量性能的能力--找出瓶颈所在, 并衡量我们突破瓶颈的尝试是否成功。

tip

瓶颈是指程序中最慢的部分,限制了所有事情的进展速度。专注于瓶颈,可以让我们集中精力优化, 给我们带来最大速度提升的地方,而不是花大量时间去优化那些能带来微小性能提升的功能。

有几种衡量性能的方法,包括:

  • 在功能代码块前后放置一个 开启/停止 的计时器

  • 使用IdeaXR分析器

tip

检查帧速率(禁用垂直同步)。垂直同步:将画面的帧率调整到和显示器一样的刷新率。

众所周知,相同的功能模块,在不同硬件上的性能,可能会有所不同,所以在多个设备上进行测试,是最好的方案。

对CPU性能的优化

描述

为了在屏幕上渲染对象,CPU 需要做很多处理工作:

  • 确定哪些光源影响该对象;
  • 设置着色器和着色器参数;
  • 向图形驱动程序发送绘制命令;
  • ...

而图形驱动程序随后将准备发送到显卡的命令。

对于 CPU 来说,找出瓶颈的最简单方法就是使用性能分析器。

CPU分析器

性能分析器与程序一起运行,并进行时间测量,以计算出每个功能所花费的时间比例。
IdeaXR集成开发环境有一个方便的内置性能分析器。
它不会在每次启动项目时运行:必须手动启动和停止。这是因为,与大多数分析器一样,记录这些时序测量会大大减慢你的项目速度。 分析后,你可以回看一帧的结果。

Docusaurus Plushie

当一个项目运行缓慢时,你经常会看到一个明显的功能或流程比其他功能或流程花费更多的时间。 这是你的主要瓶颈,你可以通过优化这个领域来提高速度。

CPU优化

基于“每个对象”的 CPU 使用率都是非常消耗资源的,所以如果有很多可见对象,影响就会累加起来。
例如,如果有一千个三角形,如果它们都在一个网格中,而不是每个三角形在一个网格中(这种情况下加起来就有 1000 个网格),则 CPU 处理起来就比较容易。
为了优化 CPU 性能,请减少可见对象数量,减少 CPU 需要执行的工作量。

  • 通过将单独的纹理放入更大的纹理图集,在对象中使用更少的材质。

  • 减少可能导致对象多次渲染的因素(例如反射、阴影和每像素光照)。

  • 将对象组合在一起,使每个网格至少有几百个三角形,并使整个网格只使用一种材质,请确保组合的所有对象共享相同的纹理。

tip

请注意,组合两个不共享材质的对象不会提高性能。需要多种材质的最常见原因是,两个网格不共享相同的纹理;

对GPU性能的优化

描述

对新的图形功能和进步的需求几乎可以保证你必会遇到图形瓶颈,有些瓶颈可能出现在CPU端,
例如在IdeaXR引擎内部的计算中,为渲染准备对象。
瓶颈也可能发生在CPU上的图形驱动中,它将指令分类传递给GPU,以及这些指令的传输过程。
最后,瓶颈也会发生在GPU本身。渲染中的瓶颈发生在哪里,高度依赖于硬件。
了解和调查GPU瓶颈与CPU上的情况略有不同。这是因为,通常情况下,你只能通过改变你给GPU的指令来间接改变性能。
另外,测量起来可能更困难。在许多情况下,衡量性能的唯一方法是通过检查每帧渲染时间的变化。

GPU优化

绘制调用、状态更变、API

IdeaXR通过图形API(OpenGL, OpenGL ES或Vulkan)向GPU发送指令。所涉及的通信和驱动指令可能会导致性能严重损耗,尤其是在OpenGL和OpenGL ES中。如果我们能减少这些指令,就能大大提高性能。

OpenGL中几乎每一个API命令都需要一定的验证, 以确保GPU处于正确的状态。即使是看似简单的命令, 也会导致一连串的幕后工作。 因此, 我们的目标是将这些指令减少到最低限度, 并尽可能地将相似的对象分组, 以便它们可以一起渲染, 或者以最少的数量进行这些昂贵的状态变化。

2D 批处理

屏幕上很容易出现数千个 2D UI,单独处理每个UI会造成cpu和gpu极大的消耗。 这就是使用 2D 批处理的原因。 多个相似的控件通过单个绘图调用分组在一起,并在批处理中呈现,而不是为每个控件进行单独的绘图调用。 此外,这意味着状态变化、材料和纹理变化可以保持在最低限度。

3D 批处理

在 3D 中,我们的目标仍然是尽量减少绘制调用和状态更改。但是,将多个对象批处理到一个绘图调用中,可能会比 2D 批处理 更加困难。 3D 网格往往包含成百上千个三角形,实时组合大型网格会造成CPU和GPU严重的消耗。随着每个网格的三角形数量增加,加入它们的成本很快就超过了带来的好处。一个更好的选择是网格提前做好分组(静态的模型放在一个网格内)。这可以由设计师完成,也可以在 IdeaXR 中以编程方式完成。

在 3D 中将对象批处理在一起也是有成本的。多个对象渲染成一个,就不能单独剔除。如果将屏幕外的整座城市与屏幕上的一片草地连接在一起, 那么它仍然会被渲染。 因此, 当试图将3D对象批量连接在一起时。 应该始终考虑到对象的位置和剔除。 尽管如此, 加入静态对象的好处往往大于其他考虑因素, 特别是对于大量的远距离或低多边形物体。

重复使用着色器和材质

IdeaXR渲染器旨在尽可能减少 GPU 状态变化。
SpatialMaterial 在重用需要类似着色器的材质方面做得很好。如果使用自定义着色器,请确保尽可能多地重复使用它们。 优先事项是:

  • 重用材质: 场景中不同的材质越少,渲染的速度就越快。如果一个场景有大量的物体(数以百计或数以千计),可以尝试重复使用这些材质。在最坏的情况下,使用图集来减少纹理变化的数量。
  • 重用着色器: 如果材质不能被重复使用,至少要尝试重用着色器。注意:在共享相同配置(可用复选框启用或禁用该功能)的SpatialMaterials之间会自动重用着色器,即使它们有不同的参数。

例如,如果一个场景有20,000个物体,每个物体有20,000种不同的材质,渲染会很慢。如果同一个场景有20,000个物体,但只使用100种材料,渲染就会快很多。

像素成本与顶点成本

你可能听说过,一个模型中的多边形数量越少,它的渲染速度就越快。这其实是相对的,取决于许多因素。
在现代PC和控制台,顶点成本很低。GPU最初只渲染三角形。这意味着每一帧:

  • 所有顶点都必须由CPU进行转换(包括剪裁)。
  • 所有顶点都必须从主RAM发送到GPU内存。

现在,所有这些都在GPU内部处理,大大提高了性能。 三维设计师通常对多维性能有错误的感觉,因为三维DCC(如Blender,Max等)需要将几何图形保存在CPU内存中进行编辑,从而降低了实际性能。 游戏引擎更依赖GPU,所以它们可以更有效地渲染许多三角形。
在移动设备上,情况则不同。移动GPU被限制在一个很小的电池里,所以它们需要更高的功率和效率。
为了提高工作效率,移动GPU试图避免Overdraw。当屏幕上的同一个像素被渲染了不止一次时,就会出现Overdraw。 想象一下,一个有几座建筑的小镇。在绘制之前,GPU不知道哪些是可见的,哪些是隐藏的。例如,一栋房子可能被画出来,然后在它前面又画了一栋房子(这意味着同一像素的渲染发生了两次)。PC GPU通常不怎么关心这个问题,只是把更多的像素处理扔给硬件以提高性能(这也会增加功耗)。
移动设备使用一种叫做基于瓷砖的渲染的技术,将屏幕划分为一个个单元格。每个单元格都保存着绘制的三角形列表,并按深度进行排序,以尽量减少过度绘制。这种技术提高了性能,但降低了功耗,对顶点性能造成了影响。
一般来说,这并不是那么糟糕,在移动设备上有一个必须避免的特殊情况,即在屏幕的一小部分内具有大量几何形状的小物体。这迫使移动GPU在每个 屏幕单元 上需要做大量的工作,导致大大降低了性能(因为所有其他单元必须等待它完成才能显示该帧)。
总而言之,在移动端不用担心顶点数量,但避免顶点集中在屏幕的一小部分。如果一个角色,NPC,车辆等离得很远(这意味着它看起来很小),就使用一个较小的细节级别模型(LOD)。即使在桌面GPU上,最好也不要让三角形小于屏幕上一个像素的大小。
使用时要注意额外的顶点处理:

  • 骨骼动画
  • 变形(形态键)
  • 顶点照明对象(在移动设备上很常见)

对内存性能的优化

描述

在一个较为复杂的大中型项目中,资源的内存占用往往占据了总体内存的70%以上。
因此,资源使用是否恰当直接决定了项目的内存占用情况。
一般来说,项目的资源主要可分为如下几种:纹理、网格、动画片段、音频片段、材质、着色器、字体资源以及文本资源等等。其中,纹理、网格、动画片段和音频片段则是最容易造成较大内存开销的资源。

内存优化

纹理

纹理资源可以说是几乎所有项目中占据最大内存开销的资源。一个6万面片的场景,网格资源最大才不过10MB,但一张 2048x2048 的纹理,可能直接就超过了10MB。
因此,项目中纹理资源的使用是否得当,会极大地影响项目的内存占用。

纹理格式

纹理格式是研发团队最需要关注的纹理属性。因为它不仅影响着纹理的内存占用,同时还决定了纹理的加载效率。
一般来说,建议根据硬件的种类选择硬件支持的纹理格式,比如Android平台的ETC、iOS平台的PVRTC、Windows PC上的DXT等等。
在使用硬件支持的纹理格式时,你可能会遇到以下几个问题:

  • 色阶问题
    由于ETC、PVRTC等格式均为有损压缩,因此,当纹理色差范围跨度较大时,均不可避免地造成不同程度的“阶梯”状的色阶问题。因此使用RGBA32/ARGB32格式来实现更好的效果。但是,这种做法将造成很大的内存占用。比如,同样一张1024x1024的纹理,如果不开启Mipmap,并且为PVRTC格式,则其内存占用为512KB,而如果转换为RGBA32位,则很可能占用达到4MB。所以,研发团队在使用RGBA32或ARGB32格式的纹理时,一定要慎重考虑,更为明智的选择是尽量减少纹理的色差范围,使其尽可能使用硬件支持的压缩格式进行储存。
  • ETC1 不支持透明通道问题
    在Android平台上,对于使用OpenGL ES 2.0的设备,其纹理格式仅能支持ETC1格式,该格式有个较为严重的问题,即不支持Alpha透明通道,使得透明贴图无法直接通过ETC1格式来进行储存。对此,建议将透明贴图尽可能分拆成两张,即一张RGB24位纹理记录原始纹理的颜色部分和一张Alpha8纹理记录原始纹理的透明通道部分。然后,将这两张贴图分别转化为ETC1格式的纹理,并通过特定的Shader来进行渲染,从而来达到支持透明贴图的效果。该种方法不仅可以极大程度上逼近RGBA透明贴图的渲染效果,同时还可以降低纹理的内存占用,非常推荐的使用方式。

纹理尺寸

一般来说,纹理尺寸越大,则内存占用越大。所以,尽可能降低纹理尺寸,如果512x512的纹理对于显示效果已经够用,那么就不要使用1024x1024的纹理,因为后者的内存占用是前者的四倍。

网格

网格资源在较为复杂的项目中,往往占据较高的内存。
Mesh资源的数据中经常会含有大量的Color数据和Normal数据等。这些数据的存在将大幅度增加Mesh资源的文件体积和内存占用。其中,Color数据和Normal数据主要为3DMax、Maya等建模软件导出时设置所生成,而Tangent一般为导入引擎时生成。
更为麻烦的是,如果项目对Mesh进行Draw Call Batching操作的话,那么将很有可能进一步增大总体内存的占用。
比如,100个Mesh进行拼合,其中99个Mesh均没有Color、Tangent等属性,剩下一个则包含有Color、Normal和Tangent属性,那么Mesh拼合后,CombinedMesh中将为每个Mesh来添加上此三个顶点属性,进而造成很大的内存开销。