Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

GPU 优化

介绍

对新图形功能和进展的需求意味着你几乎必定会遇到图形瓶颈。其中一些瓶颈可能出现在 CPU 端,例如 Godot 引擎内部为渲染准备对象的计算过程。瓶颈也可能出现在 CPU 上的图形驱动程序中,它负责排序要传递给 GPU 的指令,以及这些指令的传输过程。最后,瓶颈也可能出现在 GPU 本身。

渲染瓶颈出现在何处,很大程度上取决于硬件。尤其是移动 GPU,在桌面上能轻松运行的场景中可能会举步维艰。

理解和调查 GPU 瓶颈与 CPU 瓶颈的情况略有不同。这是因为,通常你只能通过改变发送给 GPU 的指令来间接影响性能。另外,测量起来可能更困难。在许多情况下,衡量性能的唯一方法是检查每帧渲染时间的变化。

绘制调用、状态更改和 API

备注

以下部分与最终用户无关,但对于提供后续章节相关的背景信息很有帮助。

Godot 通过图形 API(Vulkan、OpenGL、OpenGL ES 或 WebGL)向 GPU 发送指令。其中涉及的通信和驱动程序活动可能会产生相当大的开销,使用 OpenGL、OpenGL ES 和 WebGL 时尤为明显。如果我们能以驱动程序和 GPU 更喜欢的方式提供这些指令,就可以大大提升性能。

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

2D 批处理

在 2D 中,单独处理每个对象的成本可能高得令人望而却步——屏幕上很容易出现数千个对象。这就是为什么需要使用 2D批处理。多个相似的对象会被分组在一起,通过单个绘制调用进行批量渲染,而不是为每个对象单独进行绘制调用。此外,这意味着状态更改、材质和纹理更改可以保持在最低限度。

3D 批处理

在 3D 中,我们的目标仍然是尽量减少绘制调用和状态更改。但是,将多个对象合并到单个绘制调用中可能会更加困难。3D 网格通常包含数百或数千个三角形,实时组合大型网格的成本非常高。随着每个网格的三角形数量增加,合并它们的成本很快就会超过所带来的收益。更好的替代方案是提前合并网格(相对于彼此静态的网格)。这可以由美术师完成,也可以使用插件在 Godot 中以编程方式完成。

在 3D 中将物体批处理在一起也是有成本的。作为整体渲染的多个对象无法被单独剔除。如果屏幕外的整座城市与屏幕上的一根草连接在一起,那么整座城市都会被渲染。因此,在尝试将 3D 对象批量合并在一起时,应始终考虑对象的位置和剔除。尽管如此,合并静态对象的好处往往大于其他考虑因素,特别是对于大量的远距离或低多边形物体。

有关特定于 3D 的优化的更多信息,请参阅优化 3D 性能

复用着色器和材质

Godot 渲染器与其他渲染器略有不同。它的设计目标是以尽量减少 GPU 状态更改。StandardMaterial3D 可以在所需着色器相似时很好地复用材质。如果使用自定义着色器,请尽量复用它们。Godot 的优先级是:

  • 复用材质:场景中不同的材质越少,渲染速度就越快。如果一个场景有大量的物体(数以百计或千计),可以尝试复用材质。实在不行,可以使用图集来减少纹理更换的次数。

  • 复用着色器:如果材质无法复用,至少要尝试复用着色器。注意:即使 StandardMaterial3D 具有不同的参数,共享相同配置(通过勾选框启用或禁用的功能)的 StandardMaterial3D 之间也会自动复用着色器。

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

像素成本与顶点成本

你可能听说过,模型中的多边形数量越少,渲染速度就越快。但这其实非常相对,取决于许多因素。

在现代 PC 和游戏机上,顶点成本很低。GPU 最初只渲染三角形。这意味着每一帧:

  1. 所有顶点都必须由 CPU 进行变换(包括裁剪)。

  2. 所有顶点都必须从主内存发送到 GPU 内存。

如今,所有这些都由 GPU 内部处理,大大提高了性能。3D 美术师通常对多边形数量性能有着错误的认识,因为 3D 建模软件(如 Blender、3ds Max 等)需要将几何体保留在 CPU 内存中以便编辑,从而降低了实际性能。游戏引擎更多地依赖 GPU,因此它们可以更高效地渲染大量三角形。

在移动设备上,情况则不同。PC 和主机的 GPU 是暴力计算怪兽,可以从电网获取所需的任意电量。而移动 GPU 受限于小小的电池,因此需要高得多的能效。

为了提高效率,移动 GPU 试图避免过度绘制(overdraw)。当屏幕上的同一个像素被渲染多次时,就会出现过度绘制。想象一个有几座建筑的城镇。GPU 在绘制之前不知道哪些是可见的,哪些是隐藏的。例如,可能会先绘制一栋房子,然后在它前面再绘制另一栋房子(这意味着同一像素被渲染了两次)。PC GPU 通常不太关心这个问题,只是通过向硬件增加更多像素处理器来提高性能(这也会增加功耗)。

在移动设备上无法使用更多电量,因此移动设备使用一种称为基于图块的渲染(tile-based rendering)的技术,将屏幕划分为网格。每个单元格保存绘制到其中的三角形列表,并按深度对它们进行排序,以尽量减少过度绘制。这种技术提高了性能并降低了功耗,但对顶点性能造成了影响。因此,能够处理进行绘制的顶点和三角形数量更少。

此外,当屏幕的一小部分区域内存在包含大量几何体的小物体时,基于图块的渲染会遇到困难。这会迫使移动 GPU 对单个屏幕图块施加很大压力,从而显著降低性能,因为所有其他单元格必须等待该单元格完成后才能显示当前帧。

总之,在移动端不要担心顶点数量,但避免顶点集中在屏幕的一小部分。如果角色、NPC、车辆等离得很远(这意味着它看起来很小),就使用细节层次(LOD)较低的模型。即使在桌面 GPU 上,最好也避免让三角形小于屏幕上一个像素的大小。

使用以下内容时,请注意额外的顶点处理需求:

  • 蒙皮(骨骼动画)

  • 变形(形态键)

  • 顶点光照物体(在移动设备上常见)

像素/片段着色器和填充率

与顶点处理相比,片段(每像素)着色的成本历年来急剧增加。屏幕分辨率已经提高:4K 屏幕的面积为 8,294,400 像素,而旧式 640×480 VGA 屏幕的面积为 307,200 像素。面积是 27 倍!此外,片段着色器的复杂性也呈爆炸式增长。基于物理的渲染需要对每个片段进行复杂的计算。

你可以很容易地测试项目是否受到填充率限制。关闭垂直同步(V-Sync)以移除帧率上限,然后比较使用大窗口运行时的帧率和使用非常小的窗口运行时的帧率。如果使用阴影,你同样可以通过减小阴影贴图大小来提升性能。通常,你会发现使用小窗口时帧率会大幅提升,这说明你在某种程度上受到了填充率限制。另一方面,如果帧率几乎没有提升,那么你的瓶颈就在其他地方。

你可以通过减少 GPU 的工作量,来提升填充率受限项目的性能。具体做法包括:简化着色器(如果你使用的是标准 StandardMaterial3D,可以尝试关闭一些高开销的选项),或者减少所用贴图的数量和尺寸。此外,在使用带光照的粒子系统时,可以考虑在材质中强制开启顶点着色,以此来降低着色成本。

参见

在支持的硬件上,可以使用可变速率着色降低着色处理开销,并且不影响最终图像边缘的锐度。

在针对移动设备时,考虑使用你能够合理承受的范围内最简单的着色器。

读取纹理

片段着色器的另一个因素是读取纹理的成本。读取纹理是一项昂贵的操作,尤其是在单个片段着色器中从多个纹理读取时。另外,请注意过滤可能会进一步减慢速度(mipmap 之间的三线性过滤以及平均处理)。就功耗而言,读取纹理也很昂贵,这在移动设备上是个大问题。

如果你使用第三方着色器或编写自己的着色器,请尽量使用纹理读取需求次数尽可能少的算法。

纹理压缩

默认情况下,Godot 在导入 3D 模型时使用显存(VRAM)压缩来压缩纹理。显存压缩在存储时不如 PNG 或 JPG 高效,但在绘制足够大的纹理时,会极大地提高性能。

这是因为纹理压缩的主要目标是减少内存和 GPU 之间的带宽需求。

在 3D 中,物体的形状更多取决于几何体而非纹理,所以压缩通常不明显。在 2D 中,压缩更多取决于纹理内部的形状,所以 2D 压缩产生的伪影比较明显。

需要注意的是,大多数 Android 设备不支持具有透明度的纹理的纹理压缩(仅支持不透明纹理),请记住这一点。

备注

即便在 3D 中,“像素画”纹理也应该禁用 VRAM 压缩,因为压缩会对外观产生负面影响,且由于像素画较低的分辨率,性能也无法得到显著提升。

后期处理和阴影

就片段着色活动而言,后期处理效果和阴影也可能很昂贵。请务必测试这些效果在不同硬件上的影响。

减少阴影图的大小可以提高性能,无论是在写入还是读取阴影贴图方面。除此之外,提高阴影性能的最佳方法是关闭尽可能多的灯光和物体的阴影。较小或较远的泛光灯/聚光灯通常可以禁用它们的阴影,这对视觉效果影响很小。

透明度和混合

透明物体给渲染效率带来了特殊问题。不透明对象(尤其是在 3D 中)基本上可以以任意顺序渲染,Z 缓冲区将确保只有最前面的对象得到着色。透明或混合对象则不同。在大多数情况下,它们不能依赖 Z 缓冲区,必须以“画家顺序”(即从后到前)渲染才能看起来正确。

透明对象对填充率的影响也特别大,因为每个对象都要绘制,即使之后会在上面绘制其他透明对象。

不透明对象不需要这样做。它们通常可以利用 Z 缓冲区,先只向 Z 缓冲区写入数据,然后只对“胜出”的片段执行片段着色器,也就是在某一像素处于最前面的对象。

在多个透明对象重叠的情况下,透明度的代价特别高。通常情况下,应使用尽可能小的透明区域以尽量降低这些填充率要求,尤其是在移动端,填充率代价非常高。事实上,在很多情况下,渲染更复杂的不透明几何体最终可能比使用透明度来“作弊”更快。

多平台建议

如果你的目标是在多个平台上发布,请尽早并在所有平台上经常性进行测试,尤其是移动平台。在桌面上开发游戏,但在最后一刻才试图将其移植到移动设备,这是灾难的根源。

一般来说,你应该针对最小公约数设计游戏,然后为更强大的平台添加可选的增强功能。例如,你可能希望在同时针对桌面和移动平台的情况下,使用兼容性渲染方法。

移动端/图块渲染器

如上所述,移动设备上的 GPU 与桌面上的 GPU 工作方式有很大不同。大多数移动设备使用图块渲染器。图块渲染器将屏幕分割成规则大小的图块,这些图块可以放入超快的缓存中,从而减少了对主内存的读写操作次数。

不过也有一些缺点。图块渲染会让某些技术变得更加复杂,执行起来也更加昂贵。依赖于不同图块渲染的结果,或者依赖于早期操作的结果被保存的图块可能会非常慢。要非常小心地测试着色器、视图纹理和后期处理的性能。