GPU优化

简介

对新的图形功能和进步的需求几乎可以保证你必会遇到图形瓶颈. 有些瓶颈可能出现在CPU端, 例如在Godot引擎内部的计算中, 为渲染准备对象. 瓶颈也可能发生在CPU上的图形驱动中, 它将指令分类传递给GPU, 以及这些指令的传输过程. 最后, 瓶颈也会发生在GPU本身.

渲染中的瓶颈发生在哪里, 高度依赖于硬件. 特别是移动GPU可能会在桌面上轻松运行的场景中挣扎.

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

绘制调用, 状态更变和api

注解

以下部分与最终用户无关, 但对于提供与后面章节相关的背景信息是有用的.

Godot通过图形API(OpenGL, OpenGL ES或Vulkan)向GPU发送指令. 所涉及的通信和驱动活动可能非常昂贵, 尤其是在OpenGL和OpenGL ES中. 如果我们能以驱动和GPU喜欢的方式提供这些指令, 就能大大提高性能.

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

2D批处理

在2D中, 单独处理每个项目的成本可能会非常高--屏幕上很容易有成千上万的项目. 这就是为什么使用2D 批处理 的原因. 多个类似的项目被归为一组, 并通过一个单一的绘制调用进行批量渲染, 而不是对每个项目进行单独的绘制调用. 此外, 这意味着状态变化, 材质和纹理变化可以保持在最低限度.

关于2D批处理的更多信息, 参见 使用批处理优化 .

3D批处理

在3D中, 我们的目标仍然是尽量减少绘制调用和状态变化. 然而, 将多个对象批量合并到一个绘图调用中可能比较困难.3D网格往往由数百个或数千个三角形组成, 而实时组合大型网格的成本非常高. 随着每个网格的三角形数量的增加, 加入它们的成本很快就超过了带来的好处. 一个更好的选择是 提前加入网格 (静态网格之间的关系). 这可以由设计师完成, 或者在Godot中以编程方式完成.

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

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

重复使用着色器和材质

Godot 渲染器和其它的渲染器不同,是以尽量减少 GPU 状态更改为目标的。 SpatialMaterial 可以在所需着色器相似时很好地复用材质。如果是用自定义着色器,那么请尽量进行复用。Godot 的优先级是:

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

  • 重用着色器Reusing Shaders: 如果材质不能被重复使用,至少要尝试重用着色器。注意:在共享相同配置(可用复选框启用或禁用该功能)的SpatialMaterials之间会自动重用着色器,即使它们有不同的参数。

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

像素成本与顶点成本

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

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

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

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

现在, 所有这些都在GPU内部处理, 大大提高了性能. 三维艺术家通常对多维性能有错误的感觉, 因为三维DCC(如Blender, Max等)需要将几何图形保存在CPU内存中进行编辑, 从而降低了实际性能. 游戏引擎更依赖GPU, 所以它们可以更有效地渲染许多三角形.

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

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

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

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

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

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

  • 蒙皮(骨骼动画)

  • 变形(形态键)

  • 顶点照明对象(在移动设备上很常见)

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

与顶点处理相比, 片段着色器(每像素)的成本在这些年里急剧增加. 屏幕分辨率提高了(4K屏幕的面积是829400像素, 而老式640×480 VGA屏幕的面积是307200, 是27倍), 但片段着色器的复杂度也爆炸式增长. 基于物理的渲染需要对每个片段进行复杂的计算.

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

你可以通过减少GPU的工作量来提高填充率限制项目的性能. 你可以通过简化着色器(如果你使用的是 :ref:`SpatialMaterial <class_SpatialMaterial> `, 也许可以关闭昂贵的选项), 或者减少使用的纹理数量和大小来实现.

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

读取纹理

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

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

纹理压缩

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

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

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

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

注解

即使在3D中,"像素艺术 "纹理也应该禁用VRAM压缩, 因为这会对其外观产生负面影响, 而不会因为其低分辨率而显著提高性能.

后处理和阴影

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

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

透明度和混合

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

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

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

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

多平台建议

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

一般来说, 你应该从最底的共性设计你的游戏, 然后为更强大的平台添加可选的增强功能. 例如, 你可能希望在同时针对桌面和移动平台的情况下, 同时使用GLES2后台.

移动端和图块渲染

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

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