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 批处理

In 2D, the costs of treating each item individually can be prohibitively high - there can easily be thousands of them on the screen. This is why 2D batching is used with OpenGL-based rendering methods. Multiple similar items are grouped together and rendered in a batch, via a single draw call, rather than making a separate draw call for each item. In addition, this means state changes, material and texture changes can be kept to a minimum.

基于Vulkan的算绘方法尚不使用2D批次。由于与OpenGL相比,Vulkan的绘制呼叫要便宜得多,因此不太需要2D批次(尽管在某些情况下它仍然是有益的)。

3D 批处理

In 3D, we still aim to minimize draw calls and state changes. However, it can be more difficult to batch together several objects into a single draw call. 3D meshes tend to comprise hundreds or thousands of triangles, and combining large meshes in real-time is prohibitively expensive. The costs of joining them quickly exceeds any benefits as the number of triangles grows per mesh. A much better alternative is to join meshes ahead of time (static meshes in relation to each other). This can be done by artists, or programmatically within Godot using an add-on.

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

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

Reuse shaders and materials

The Godot renderer is a little different to what is out there. It’s designed to minimize GPU state changes as much as possible. StandardMaterial3D does a good job at reusing materials that need similar shaders. If custom shaders are used, make sure to reuse them as much as possible. Godot’s priorities are:

  • 复用材质:场景中不同的材质越少, 渲染的速度就越快. 如果一个场景有大量的物体(数以百计或数以千计), 可以尝试重复使用这些材质. 在最坏的情况下, 使用图集来减少纹理变化的数量.

  • Reusing Shaders: If materials can’t be reused, at least try to reuse shaders. Note: shaders are automatically reused between StandardMaterial3Ds that share the same configuration (features that are enabled or disabled with a check box) even if they have different parameters.

If a scene has, for example, 20,000 objects with 20,000 different materials each, rendering will be slow. If the same scene has 20,000 objects, but only uses 100 materials, rendering will be much faster.

像素成本与顶点成本

你可能听说过, 一个模型中的多边形数量越少, 它的渲染速度就越快. 这其实是 相对的 , 取决于许多因素.

在现代PC和控制台, 顶点成本很低.GPU最初只渲染三角形. 这意味着每一帧:

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

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

Nowadays, all this is handled inside the GPU, greatly increasing performance. 3D artists usually have the wrong feeling about polycount performance because 3D modeling software (such as Blender, 3ds Max, etc.) need to keep geometry in CPU memory for it to be edited, reducing actual performance. Game engines rely on the GPU more, so they can render many triangles much more efficiently.

在移动设备上, 情况则不同. 个人电脑和控制台的GPU是粗暴的怪物, 可以从电网中获取所需的电力. 移动GPU被限制在一个很小的电池里, 所以它们需要更高的功率效率.

为了提高工作效率, 移动GPU试图避免 overdraw . 当屏幕上的同一个像素被渲染了不止一次时, 就会出现Overdraw. 想象一下, 一个有几座建筑的小镇. 在绘制之前,GPU不知道哪些是可见的, 哪些是隐藏的. 例如, 一栋房子可能被画出来, 然后在它前面又画了一栋房子(这意味着同一像素的渲染发生了两次).PC GPU通常不怎么关心这个问题, 只是把更多的像素处理扔给硬件以提高性能(这也会增加功耗).

在移动设备上使用更多的电力是不可能的,所以移动设备使用一种叫做基于图块的渲染的技术,将屏幕划分为一个网格。每个单元格都保存着绘制的三角形列表,并按深度进行排序,以尽量减少过度绘制。这种技术提高了性能,降低了功耗,但对顶点性能造成了影响。因此,可以处理更少的顶点和三角形进行绘制。

一般来说, 这并不是那么糟糕, 但在移动设备上有一个必须避免的特殊情况, 即在屏幕的一小部分内具有大量几何形状的小物体. 这迫使移动GPU在单个屏幕单元上用很大的力气, 大大降低了性能(因为所有其他单元必须等待它完成才能显示该帧).

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

使用时要注意额外的顶点处理:

  • 蒙皮(骨骼动画)

  • 变形(形态键)

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

In contrast to vertex processing, the costs of fragment (per-pixel) shading have increased dramatically over the years. Screen resolutions have increased: the area of a 4K screen is 8,294,400 pixels, versus 307,200 for an old 640×480 VGA screen. That is 27 times the area! Also, the complexity of fragment shaders has exploded. Physically-based rendering requires complex calculations for each fragment.

你可以很容易地测试一个项目是否受到填充率限制. 关闭V-Sync以防止每秒帧数的上限, 然后比较使用大窗口运行时的每秒帧数和使用非常小的窗口运行时的帧数. 如果使用阴影, 你也可以从同样减少阴影贴图大小中获益. 通常, 你会发现使用小窗口的FPS会增加不少, 这说明你在某种程度上受到了填充率的限制. 另一方面, 如果FPS几乎没有增加, 那么你的瓶颈就在其他地方.

You can increase performance in a fill rate-limited project by reducing the amount of work the GPU has to do. You can do this by simplifying the shader (perhaps turn off expensive options if you are using a StandardMaterial3D), or reducing the number and size of textures used. Also, when using non-unshaded particles, consider forcing vertex shading in their material to decrease the shading cost.

参见

在支持的硬件上,可以使用 可变速率着色 降低着色过程的损耗,并且不影响最终图片边缘的锐度。

在针对移动设备时, 考虑使用你能合理负担得起的最简单的着色器.

读取纹理

片段着色器的另一个因素是读取纹理的成本。读取纹理是一项昂贵的操作,尤其是在一个片段着色器中从多个纹理中读取时。另外,考虑到过滤可能会进一步减慢它的速度(mipmap 之间的三线性过滤,以及平均)。读取纹理在功耗方面也很昂贵,这在手机上是个大问题。

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

纹理压缩

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

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

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

作为警告, 大多数Android设备不支持具有透明度的纹理的纹理压缩(仅不透明), 因此请记住这一点.

备注

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

后期处理和阴影

就片段着色活动而言, 后期处理效果和阴影也可能很昂贵. 始终测试这些对不同硬件的影响.

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

透明度和混合

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

透明对象的填充率也特别差, 因为每一个项目都要绘制, 即使之面会在上面绘制其他透明对象.

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

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

多平台建议

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

In general, you should design your game for the lowest common denominator, then add optional enhancements for more powerful platforms. For example, you may want to use the Compatibility rendering method for both desktop and mobile platforms where you target both.

移动端和图块渲染

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

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