2021 LTS着色器构建时间和内存占用的改进

DAMIAN NACHMAN / UNITY TECHNOLOGIESSenior Technical Product Manager
Dec 28, 2022|13 Min
2021 LTS着色器构建时间和内存占用的改进
为方便起见,此网页已进行机器翻译。我们无法保证翻译内容的准确性或可靠性。如果您对翻译内容的准确性有疑问,请参阅此网页的官方英文版本。

随着可编程渲染管线(SRP)的功能集不断扩充,构建时需要处理和编译的着色器也越来越多。外加上对更多图形API和目标平台的支持,SRP的改进仍在持续进行。

着色器会在初始(“清洁”)构建时被编译和缓存,以加快后续的增量(“warm”)构建。清洁构建通常耗时最长,但冗长的warm构建时间才是项目开发和迭代的常见痛点。

项目构建时多种着色器的处理和编译

为了解决这个问题,Unity的Shader Management(着色器管理)团队努力制定了几种行之有效的可扩展解决方案,极大地减少了2021 LTS及以上版本的着色器构建时间和运行时内存占用。

要了解有关这些新优化的更多信息,包括受影响的版本、反向移植和内部测试数据,请直接跳转到有关着色器变量预过滤动态着色器加载 章节。在文末,我们将介绍着色器变体管理的整体改进计划,涉及创作、构建与运行时。

在深入探讨 Unity 着色器系统令人兴奋的改进之前,让我们也借此机会快速回顾一下条件着色器编译着色器变体着色器变体剥离的概念。

条件性着色器功能

着色器的条件性功能可让开发者与美术师借助脚本、材质设置、项目和图形设定,方便地控制或修改着色器的功能。此类功能的作用在于简化创作,减少需要编写及维护的着色器数量,让项目高效地完成扩展。

美术师可借助shader_feature关键词,在创作时启用Clear Coat(光滑表面)材质功能。

条件性着色器功能有以下几种实施方法:

  • 静态(编译时)分支
  • 着色器变体编译
  • 动态(运行时)分支

虽然静态分支可避免运行时与分支相关的着色器执行开销,但它是在编译时评估和锁定的,并不提供运行时控制。同时,着色器变量编译是一种静态分支形式,可提供额外的运行时控制。它会为静态分支的每一种组合编译一份独特的着色器程序(变体),以维持运行时的GPU性能。

通过shader_featuremulti_compileshader 关键字有条件地声明和评估着色器功能,可以创建此类变体。运行时会根据活动关键字和运行时设置加载正确的着色器变体。更多的着色器关键词会增加构建时间、文件大小及运行时的内存占用。

同时,动态(基于统一的)分支完全避免了着色器变量编译的开销,从而加快了构建速度,减少了文件大小和内存使用。这可以在开发过程中带来更顺畅、更快速的迭代。

另一方面,动态分支视着色器复杂程度和目标设备不同可对着色器的执行性能产生巨大影响。不对称分支,即分支的一侧比另一侧复杂得多,会对性能产生负面影响。这是因为执行较简单的路径仍然会产生较复杂路径的性能损失。

在向着色器添加条件性功能时,你必须时刻记住这些方法和方法的得失。更多详细信息,请参阅着色器条件着色器分支着色器变量文档。

着色器变体剥离

为了减少着色器处理和编译时间的增加,使用了着色器变量剥离技术。其目的是根据以下因素排除编译中不必要的着色器变体:

  • 所包含的材质与所启用的关键词
  • 项目和渲染管线设定
  • 可编程剥离

在枚举着色器变体时,编辑器会自动过滤掉任何用shader_feature声明的关键字,这些关键字未被构建中引用和包含的材质启用。因此,这些关键词不会产生任何额外的变体。

例如,如果使用 "复杂光源 URP 着色器"的任何材质都未启用 "透明涂层 "材质属性,那么所有实现 "透明涂层 "功能的着色器变体都将在构建时被安全删除。

与此同时,multi_compile关键字可促使开发人员和玩家在运行时根据可用的玩家设置和脚本自由控制着色器的功能。反过来说,编辑器无法像删除shader_feature关键字那样自动删除此类关键字。这就是为什么他们通常会生产更多的变体。

可脚本剥离是一种C# API,可让你在编译时通过运行时不需要的关键字和组合排除着色器变体。渲染管道利用脚本剥离功能,根据项目的渲染管道设置和构建中包含的质量资产,剥离不必要的变体。

低质量 高质量 可变乘数 主光/投射阴影:关 开 2x 主灯/投射阴影:开 1x 主灯/投射阴影:关闭 关闭 1x

为了最大化着色器剥离的效果,我们推荐禁用所有运行时用不到的图形相关功能和渲染管线设定。有关着色器变量剥离的更多信息,请参阅官方文档。

着色器变体预筛选

根据构建中的渲染管道质量资产等因素,着色器变体剥离可大大减少编译着色器变体的数量。不过,剥离目前是在着色器处理阶段结束时进行的。无论编译与否,仅仅枚举所有可能的变体仍然需要很长时间。

为了降低变体处理(及项目构建)的时间,我们为引擎内置的剥离功能推出了重大优化。着色器变体预过滤可以显著降低清洁和热构建的耗时。

根据渲染管道设置驱动的 "预过滤属性",通过提前排除multi_compile 关键字进行优化。这就减少了枚举可能被剥离和编译的变体数量,反过来又减少了着色器处理时间--在最显著的例子中,暖构建时间最多可减少 90%。

着色器变体预过滤首次出现在2023.1.0a14 中,并已回传至2022.2.0b152021.3.15f1

URP《Boat Attack》项目热构建时所减少的着色器处理时间
URP《Terrain Demo》项目热构建时所减少的着色器处理时间

变体预过滤能以同样的原理削减初次/清洁构建的耗时。

URP《Terrain Demo》项目清洁构建时所减少的着色器处理时间
URP《Terrain Demo》项目热构建时所减少的着色器处理时间
动态着色器加载

以前,Unity运行时会在场景和资源加载阶段将磁盘上的所有着色器对象前台加载到CPU内存里。大部分时候,项目和场景在任意时刻都会有大量多余的着色器变体。如果项目有着许多的着色器,这些变体会占用大量的内存。

动态着色器加载可以解决这个问题。它支持精准控制着色器的加载行为和内存占用,辅助着色器数据块到内存的流传输,还可根据内存预算释放多余的数据,让你在内存有限的平台上大幅降低项目的内存占用。

现在可通过编辑器的 "播放器设置 "访问新的着色器变体加载设置。可用于覆写着色器数据块的最大数量以及每个着色器的数据块大小(MB)。

Editor > Project Settings > Player > Shader Variant Loading Settings

有了以下C# API,你还能用编辑器脚本覆写Shader Variant Loading Settings:

您还可以在运行时使用 C# API,通过 Shader.maximumChunksOverride.这样就可以根据运行时查询的可用系统图形内存 总量等因素,覆盖着色器内存预算。

动态着色器加载在2023.1.0a11 中出现,并已回传至2022.2.0b102022 .1.21f12021.3.12f。Universal Render Pipeline(URP) 的Boat Attack 中,我们观察到着色器的运行时内存使用量减少了 78.8%,从 315 MiB(默认)减少到 66.8 MiB(动态加载)。有关此次优化的更多信息,请参阅官方公告

在《Boat Attack》里使用动态着色器加载使得运行时的着色器内存占用降低了78.8%。
未来计划

除了以上提到的重大改动,我们还将提高通用渲染管线的着色器变体生成与剥离,并且还在考虑改进总的着色器变体管理方案,并最终辅助引擎不断扩展功能集,最小化着色器构建和运行时的开销。

我们正在进行的一些研究涉及重复利用类似变体的着色器资源,以及着色器关键字和着色器变体收集 API 的整体改进。以做到更灵活、全面地控制着色器变体的处理和运行时性能。

往后,我们还将试着开发编辑器内的工具,用以跟踪和分析着色器变体,解答以下变体使用相关的细节问题:

  • 哪些着色器和关键词产生了最多的变体?
  • 哪些变体编译后未在运行时使用?
  • 哪些变体剔除后又被运行时调用?

您的反馈是我们调整工作优先级、制定有效方案的风向标。请查看我们的公开路线图,对最符合您需求的功能进行投票。如果您希望看到其他更改,请随时提交功能请求,或直接在此着色器论坛与团队联系。