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.

减少着色器(管线)编译导致的卡顿

警告

本页内容仅适用于 Forward+(前向+)和 Mobile(移动端)渲染器,不适用于 Compatibility(兼容性)渲染器。超级着色器(Ubershaders)和管线预编译依赖于现代底层图形 API(如 Vulkan、Direct3D 12、Metal)才具备的功能。而兼容性渲染器在不同平台上使用的是 OpenGL 3.3、OpenGL ES 3.0 或 WebGL 2.0。这些旧版 API 缺乏有效实现超级着色器和管线预编译所需的功能。

为了避免在兼容性渲染器中出现着色器卡顿,你需要采用传统的做法:在关卡加载时,将材质、着色器和粒子效果显示在视锥体(也就是玩家的屏幕视野)内至少一帧,以此来达到预加载的目的。

管线编译(通常也称为着色器编译)是引擎为了能够使用 GPU 绘制任何内容而必须执行的一项开销较大的操作。

流程图展示了着色器的完整编译过程:从 VisualShader 和标准材质到 Godot 着色语言,再到 GLSL,然后到中间格式(SPIR-V),最后到管线。着色器编译是 GLSL 到中间格式的步骤。管线编译是中间格式到管线的步骤。

在 Godot 中,着色器和材质在被 GPU 运行之前需要经过几个步骤。

更准确地说,着色器编译涉及将 Godot 生成的 GLSL 代码转换为可以在不同系统之间共享的中间格式(例如在使用 Vulkan 时的 SPIR-V)。但是,这种格式不能直接被 GPU 使用。

管线编译是 GPU 驱动程序将中间着色器格式(着色器编译的结果)转换为 GPU 实际可用于渲染的内容的步骤。驱动程序通常会在系统中的某个位置存储管线的缓存,以避免每次运行游戏时重复此过程。当驱动程序更新时,此缓存通常会被删除。

管线包含的信息不仅仅是着色器代码,这意味着每个着色器可能有几十个甚至更多的管线!这使得引擎很难提前编译它们,因为这不仅会非常慢,而且会占用大量内存。此外,这个步骤只能在用户的系统上执行,并且很难在用户之间共享结果,除非他们拥有完全相同的硬件和驱动程序版本。

在 Godot 4.4 之前,除了在物体出现在相机视野中时生成管线外,没有其他解决方案来解决管线编译问题,这导致了臭名昭著的着色器卡顿或仅在第一次游玩时出现的卡顿。Godot 4.4 引入了新的机制来减轻管线编译导致的卡顿。

  • ubershaders:Godot 使用了特化常量(specialization constants),这一功能允许驱动程序根据一组参数(如光照、阴影质量等)优化管线的代码。特化常量通过限制不必要的功能来优化着色器。更改特化常量需要重新编译管线。Ubershaders 是着色器的一种特殊版本,能够在渲染时更改这些常量,这意味着 Godot 可以提前预编译一个管线,并在游戏过程中在后台编译更优化的版本。这大大减少了需要创建的管线数量。

  • 管线预编译:通过使用 ubershaders,引擎可以在多个地方提前预编译管线,例如在加载网格或向场景添加节点时。由于这是资源加载过程的一部分,管线甚至可以在加载界面期间或游戏过程中,在多个后台线程中进行预编译(如果可能的话)。

从 Godot 4.4 开始,Godot 将检测所需的管线并在加载时预编译它们。这个检测系统大多是自动的,但它依赖于 RenderingServer 在加载时看到所有着色器、网格或渲染功能的证据。例如,如果你在游戏运行时加载网格和着色器,那么该网格/着色器组合的管线将不会在网格/着色器加载之前编译。同样,在游戏运行时启用 MSAA 或实例化 VoxelGI 节点等操作将触发管线重新编译。

管道预编译监视器

提前编译管线是 Godot 用来缓解着色器卡顿的主要机制,但这并不是一个完美的解决方案。了解可能导致管线卡顿的情况会非常有帮助,而且与之前的版本相比,这些变通方法相当直接。随着时间的推移,随着更多检测技术的实现,在未来的 Godot 版本中,这些变通方法可能会变得不那么必要。

Godot 调试器提供了用于跟踪游戏创建的管线数量及其触发编译步骤的监视器。你可以在游戏运行时密切关注这些监视器,以识别潜在的着色器卡顿来源,而无需每次测试时都清除驱动程序缓存。在加载界面之外,这些值的突然增加可能会在玩家首次在他们的系统上运行游戏时表现为游戏过程中的卡顿。建议你查看这些监视器以识别可能导致玩家卡顿的来源,因为在不删除驱动程序缓存或在性能较弱的系统上测试的情况下,你自己可能无法体验到这些问题。

Godot 管线编译监视器的截图

演示项目的管线编译。

备注

我们可以看到在游戏过程中编译的管线,并验证哪些步骤可能导致卡顿。注意:这些值只会增加而不会减少,因为这些监视器不会跟踪已删除的管线,而且管线可能会在游戏过程中被删除并重新创建。

  • Canvas:在绘制 2D 节点时编译。引擎目前不支持 2D 元素的预编译,因此在首次绘制 2D 节点时会出现卡顿现象。

  • Mesh:在加载 3D 网格时进行编译,并识别可以根据其属性预编译的管线。如果在游戏过程中加载网格,可能会导致卡顿,但如果使用后台线程加载网格,则可以缓解这种情况。作为节点一部分的修改器(例如材质覆盖)无法在此步骤进行编译

  • Surface:当一帧即将绘制且场景树中首次实例化了 3D 对象时进行编译。这也可能包括对场景树中不可见节点的编译。卡顿只会在节点首次加入场景的那一帧发生,如果恰好在加载界面之后发生,则不会造成明显的卡顿。

  • Draw:当需要绘制 3D 对象且未提前预编译 ubershader 时,按需编译。由于触发了尚未覆盖的情况或对引擎代码进行了修改,引擎无法预编译此管线。这会导致游戏过程中出现卡顿。这与 4.4 版本之前的 Godot 相同。如果你在此处看到编译,请 告知开发者 ,因为在使用 Ubershader 系统时,这种情况本不应该发生。请确保在报告时附上一个最小化的重现项目。

  • Specialization:在游戏过程中在后台编译以优化帧率。不会导致卡顿,但如果每帧发生多次,则可能会导致帧率降低。

管道预编译功能

Godot 提供了许多渲染功能,但并非每个游戏都会使用这些功能。遗憾的是,管线预编译无法提前知道某个特定功能是否会被项目使用。其中一些功能只有在用户向场景中添加节点或在项目或环境中切换特定设置时才能被检测到。管线预编译系统会在首次遇到这些功能时进行跟踪,并为之后创建的任何网格或表面启用这些功能的预编译。

如果你的游戏使用了这些功能,请确保尽早加载一个使用这些功能的场景,然后再加载大部分资源。这个场景可以非常简单,只要它使用了游戏计划使用的功能即可。如果需要,它甚至可以在屏幕外渲染至少一帧,例如通过使用 ColorRect 节点覆盖它,或者使用位于窗口边界之外的 SubViewport

你还需要注意,在游戏过程中更改这些特性会导致立即的卡顿。确保只在必要时从配置界面更改这些特性,并在应用更改时插入加载界面和提示信息。

  • MSAA 等级:当在项目设置中更改 3D MSAA 等级时启用。不幸的是,在不同的视口上使用不同的 MSAA 等级会导致卡顿,因为引擎一次只能跟踪一个等级来执行预编译。

  • 反射探针:当场景中放置了 ReflectionProbe 节点时启用。

  • Separate Specular:在使用次表面散射或依赖直接从屏幕采样高光效果的合成器效果时启用。

  • Motion Vectors:当使用需要运动矢量的效果(如 TAA、FSR2 或合成器效果(如运动模糊))时启用。

  • Normal and Roughness:当使用 SDFGI、VoxelGI、屏幕空间反射、SSAO、SSIL,或在自定义着色器或 CompositorEffect 中使用 normal_roughness_buffer 时启用。

  • Lightmaps:当场景中存在 LightmapGI 节点且某个节点使用了烘焙光照贴图时启用。

  • VoxelGI:当场景中放置了 VoxelGI 节点时启用。

  • SDFGI:当 WorldEnvironment 启用 SDFGI 时启用。

  • Multiview:在 XR 项目中启用。

  • 16/32-bit Shadows:当在项目设置中更改了阴影贴图的深度精度配置时启用。

  • Omni Shadow Dual Paraboloid:当全向光源投射阴影并使用双抛物面模式时启用。

  • Omni Shadow Cubemap:当全向光源投射阴影并使用立方体贴图模式(默认模式)时启用。

如果你在游戏过程中遇到卡顿现象,并且监视器报告在 Surface 步骤期间编译次数突然增加,这很可能是某个功能没有提前启用。在加载游戏时确保启用此效果可能会缓解该问题。

管线预编译实例化

游戏中卡顿的一个常见原因是,某些效果只有在游戏过程中发生交互时才会在场景中实例化。例如,如果你有一个粒子效果,只有当玩家执行某个操作时才会通过脚本添加到场景中。即使场景是预加载的,引擎也可能无法预先编译管线,直到该效果至少被添加到场景中一次。

幸运的是,在 Godot 4.4 及更高版本中,只要场景至少被实例化过一次,即使完全不可见或在摄像机视图之外,也可以预编译这些管线。

隐藏节点效果的示例截图

在某个演示项目中附加到玩家身上的隐藏子弹节点。这有助于引擎提前预编译效果的管线。

如果你知道在游戏过程中会动态添加到场景中的某些效果,并且当这些效果出现时在编译监视器上看到突然增加的情况,一个解决方法是将一个隐藏版本的效果附加到保证会显示的某个地方。

例如,如果玩家角色能够引发某种爆炸效果,你可以将特效作为一个不可见的节点附加到玩家节点下。确保禁用附加到隐藏节点上的脚本,或者隐藏可能引起问题的其他节点,这可以通过在该节点上启用可编辑子节点来实现。

着色器烘焙器

从 Godot 4.5 开始,你可以选择在导出时烘焙着色器以缩短初始启动时间。这通常不会解决现有的卡顿问题,但会减少首次加载游戏所需的时间。使用 Direct3D 12 或 Metal 时尤其如此,由于需要转换步骤,它们的初始着色器编译时间明显慢于 Vulkan。Godot 自己的着色器使用 GLSL 和 SPIR-V,但 Direct3D 12 和 Metal 使用不同的格式。

备注

着色器烘焙器只能将源代码烘焙为中间格式(Vulkan 使用 SPIR-V,Direct3D 12 使用 DXIL,Metal 使用 MIL),无法将中间格式进一步烘焙为最终的渲染管线,因为这依赖于 GPU 驱动程序和硬件。

着色器烘焙器并非用于替代管线预编译,而是旨在对其进行补充。

启用后,着色器烘焙器会将已编译的着色器代码打包进 PCK 文件中,从而完全跳过运行时的着色器编译步骤。缺点是导出过程会稍慢一些,且 PCK 文件体积会增加数兆字节。

着色器烘焙器默认处于禁用状态,但您可以在导出对话框的每个导出预设中,通过勾选着色器烘焙器 > 启用导出选项来启用它。

请注意,着色器烘焙只能导出当前编辑器运行平台所支持的驱动程序对应的着色器:

  • 在 Windows 上运行的编辑器可以导出适用于 Vulkan 和 Direct3D 12 的着色器。

  • 在 macOS 上运行的编辑器可以导出适用于 Vulkan 和 Metal 的着色器。

  • 在 Linux 上运行的编辑器仅能导出适用于 Vulkan 的着色器。

  • 在 Android 上运行的编辑器仅能导出适用于 Vulkan 的着色器。

着色器烘焙器仅会导出与目标平台的渲染/渲染设备/驱动项目设置相匹配的着色器。

备注

着色器烘焙器仅支持 Forward+ 和 Mobile 渲染器。如果项目使用的是 Compatibility 渲染器,或因硬件不支持 Forward+ 或 Mobile 渲染器而回退到 Compatibility,则着色器烘焙器将不会生效。

这也意味着 Web 平台不支持着色器烘焙,因为该平台仅支持 Compatibility 渲染器。

Additionally, the shader baker is not supported when exporting a project using the --headless command line argument, as Godot cannot access the GPU when running in headless mode.