随着Unity的可脚本渲染管道(SRP)的可用特性集的不断增长,在构建时处理和编译的着色器变量的数量也在不断增加。除了对更多图形api的持续支持和不断增长的目标平台选择外,SRP的改进还在继续扩展。
着色器在初始(“干净”)构建后被编译和缓存,从而加速进一步的增量(“温暖”)构建。虽然干净的构建通常需要最长的时间,但在项目开发和迭代过程中,冗长的温暖构建时间可能是一个常见的痛点。
图1: 在项目构建期间,着色器变体的处理和编译
为了解决这个问题,Unity的Shader管理团队一直在努力工作,提供有意义的和可扩展的解决方案。这大大减少了使用Unity 2021 LTS和更高版本创建的项目的着色器构建时间和运行时内存使用。
要关于这些新的优化,包括受影响的版本,后端端口,以及我们内部测试的数字,直接跳到涵盖着色器变量预过滤和动态着色器加载的部分。在这篇文章的最后,我们也谈到了我们未来的计划,进一步完善着色器变量管理作为一个整体-跨项目创作,构建和运行时。
在深入研究Unity着色器系统的令人兴奋的改进之前,让我们也利用这个机会快速回顾条件着色器编译、着色器变量和着色器变量剥离的概念。
条件着色器特性
条件着色器特性使开发人员和美工能够使用脚本、材质设置以及项目和图形设置方便地控制和改变着色器的功能。这样的条件特性有助于简化项目创作,允许项目通过最小化你必须创作和维护的着色器的数量来有效地扩展。
图2: 通过启用shader_feature关键字,艺术家在创作时启用了Clear Coat材质特性
条件着色器特性可以以不同的方式实现:
静态(编译时)分支
Shader变量编译
动态(运行时)分支
虽然静态分支避免了运行时与分支相关的着色器执行开销,但它在编译时被评估和锁定,并且不提供运行时控制。同时,Shader变体编译是一种静态分支形式,提供额外的运行时控制。这是通过为每个可能的静态分支组合编译一个唯一的着色器程序(变体)来工作的,以便在运行时保持最佳的GPU性能。
这样的变量是通过shader_feature和multi_compile着色器关键字有条件地声明和评估着色器功能来创建的。正确的着色器变量在运行时根据活动关键字和运行时设置加载。声明和计算额外的着色器关键字会导致构建时间、文件大小和运行时内存使用的增加。
同时,动态(基于统一的)分支完全避免了着色器变体编译的开销,导致更快的构建,并减少了文件大小和内存使用。这可以在开发过程中带来更顺畅和更快的迭代。
另一方面,基于着色器的复杂性和目标设备,动态分支可以对着色器的执行性能产生强烈的影响。不对称分支(分支的一边比另一边复杂得多)会对性能产生负面影响。这是因为执行较简单的路径仍然会导致较复杂路径的性能损失。
当你在自己的着色器中引入条件着色器特性时,应该记住这些方法和权衡。有关更详细的信息,请参阅着色器条件,着色器分支和着色器变量文档。
着色器变型剥离
为了减少着色器处理和编译时间的增加,使用了着色器变量剥离。它的目的是排除不必要的着色器变量从编译基于因素,如:
材料包括和关键字启用
项目和渲染管道设置
脚本化的剥离
当枚举着色器变量时,编辑器将自动过滤掉任何使用shader_feature声明的关键字,这些关键字不是由构建中引用和包含的材料启用的。因此,这些关键字不会产生任何额外的变量。
例如,如果Clear Coat材质属性没有被使用Complex Lit URP Shader的任何材质启用,那么所有实现Clear Coat功能的Shader变体都将在构建时被安全地剥离。
同时,multi_compile关键字提示开发人员和玩家在运行时根据可用的播放器设置和脚本自由控制着色器的功能。另一方面,编辑器不能像shader_feature关键字那样自动剥离这些关键字。这就是为什么它们通常会产生更多的变种。
可脚本化剥离是一个c# API,它允许你在构建时通过关键字和运行时不需要的组合来排除编译中的着色器变量。渲染管道利用脚本化剥离,根据项目的渲染管道设置和构建中包含的质量资产剥离不必要的变量。
Low quality | High quality | Variant multiplier | |
Main Light/Cast Shadows: | Off | On | 2x |
Main Light/Cast Shadows: | On | On | 1x |
Main Light/Cast Shadows: | Off | Off | 1x |
为了最大化编辑器的着色器变量剥离的效果,我们建议禁用所有与图形相关的功能和渲染管道设置在运行时不使用。请参考官方文档更多关于着色器变体剥离。
着色器变量预过滤
基于构建中的渲染管道质量资产等因素,着色器变量剥离大大减少了编译着色器变量的数量。然而,剥离目前是在着色器处理阶段的末尾执行的。简单地列举所有可能的变量仍然需要很长时间,而不考虑编译。
为了减少着色器变量处理(和项目构建)时间,我们现在引入了一个重要的优化引擎的内置着色器变量剥离。使用着色器变体预过滤,干净和温暖的构建时间都显著减少。
根据渲染管道设置驱动的预过滤属性,通过引入早期排除multi_compile关键字来优化工作。这减少了为潜在的剥离和编译枚举的变量的数量,从而减少了着色器处理时间——在最激烈的例子中,暖构建时间减少了高达90%。
Shader变体预过滤在2023.1.0a14首次登陆,并已后移植到2022.2.0b15和2021.3.15f1。
图3: 着色器处理时间减少
图4: 着色器处理时间减少
通过应用相同的原理,变体预过滤还有助于减少初始/干净的构建时间。
图5; Shader变体预过滤清洁项目的建造时间
图6:项目构建减少着色器处理时间
动态着色器加载
从历史上看,Unity运行时在场景和资源加载期间会将所有着色器对象从磁盘预先加载到CPU内存中。在大多数情况下,一个构建的项目和场景包含比应用程序运行时任何给定时刻所需的更多的着色器变体。对于使用大量着色器的项目,这通常会导致在运行时使用大量着色器内存。
动态着色器加载通过提供精细的用户控制着色器加载行为和内存使用来解决这个问题。这种优化有助于将着色器数据块流到内存中,以及根据用户控制的内存预算,删除运行时不再需要的着色器数据。这允许你在内存预算有限的平台上显著减少着色器的内存使用。
新的着色器变量加载设置现在可以从编辑器的播放器设置中访问。使用它们来覆盖加载的着色器块的最大数量和每个着色器块大小(MB)。
图7: Editor > Project Settings > Player > Shader Variant Loading Settings
现在有了下面的c# API,你可以使用编辑器脚本覆盖着色器变量加载设置,比如:
PlayerSettings。SetDefaultShaderChunkCount和PlayerSettings。
SetDefaultShaderChunkSizeInMB覆盖项目的默认着色器加载设置
PlayerSettings。SetShaderChunkCountForPlatform和PlayerSettings。
SetShaderChunkSizeInMBForPlatform在每个平台上覆盖这些设置
你也可以在运行时使用c# API通过着色器. maximumchunksoverride来覆盖加载着色器块的最大数量。这使你能够根据运行时查询的总可用系统和图形内存等因素重写着色器内存预算。
动态着色器加载在2023.1.0a11登陆,并已反向移植到2022.2.0b10, 2022.1.21f1和2021.3.12f。在通用渲染管道(URP)的BoatAttack的情况下,我们观察到着色器的运行时内存使用减少了78.8%,从315 MiB(默认)到66.8 MiB(动态加载)。您可以在官方公告中关于此优化的信息。
图8: URP Boat Attack中使用的动态着色器加载,导致运行时着色器内存使用减少了78.8%。
接下来是什么?
除了上面提到的关键变化,我们正在努力增强通用渲染管道的着色器变体的生成和剥离。我们还在研究Unity的着色器变体管理的其他改进。最终目标是促进引擎不断增加的功能集,同时确保最小的着色器构建和运行时开销。
我们正在进行的一些调查涉及在相似的变体中重复删除着色器资源,以及对着色器关键字和着色器变体收集api的整体改进。目的是提供更多的灵活性和控制着色器变量处理和运行时性能。
展望未来,我们也在探索在编辑器中跟踪和分析着色器变量的可能性,以提供以下着色器变量使用的细节:
哪些着色器和关键字产生最多的变体?
哪些变量被编译但在运行时未使用?
哪些变量被剥离,但在运行时被请求?