这篇文章转载自 Unity 社区大佬 ForgemasterGua。全方位解读 Unity Shader 变体的由来、编译流程、剔除流程、加载流程等。ForgemasterGua 在 Unity 中国开发者社区持续更新技术内容中,点击阅读原文,前往 ForgemasterGua 的社区主页,阅读更多干货文章。
Shader 变体的由来
首先引用官方对 Shader 的定义,下图:
以下为官方对 shader 变体定义的说明:
虽然 shader 变体解决了 shader 并行计算的问题,节省了性能开销,但是也带来了额外的问题。比如随着项目的 shader 越来越多,变体越来越多,导致内存直线增长,顺带着构建过程越来越久,也就是我们常说的变体爆炸。我们通过下图来直观的感受一下变体爆炸的威力:
你没有看错,如果不进行任何的变体收集和剔除,单单一个 urp 自带的 lit shader,变体数量就能达到 355W 个!如果开启选择 skip shader features 呢?如下图,竟也还有 6w 多个变体。
我们接着测试,把 Graphics 下 Shader Stripping 的剔除条件都关闭,再来看一下 lit shader 的变体数量,只剩 593 个了。
关键字 Keyword
Shader 变体编译流程
构建完成后,我们可以在 Editor.log 文件中搜索 shader 变体的打包信息,可以直接在 log 中搜索 "Compiling shader"。
上述只是记录了打包的 shader 变体,为了了解在运行时实际为 GPU 编译了哪些着色器,可以在 Project Settings > Graphics 下启用 Log Shader Compilation 选项。
这将导致游戏在运行时会编译着色器变体显示在玩家日志中。它仅适用于开发版本和 Debug 模式,如工具提示中所述。如下图所示,在 android studio 中搜索 Compiled shader 可以看到进入场景时,编译的 shader 变体都有哪些:
最后,您可以使用 Memory Profiler 包在游戏运行时拍摄游戏的快照,然后大致了解内存中当前加载的着色器及其大小。按大小排序通常可以很好地指示哪些着色器引入的变体最多,并且值得优化。
现在 2021、2022、2023 的 LTS 版本另外还支持 Shader 变体的动态压缩,更灵活地控制 shader 内存。构建期间的数据块大小(以 MB 为单位)和在运行时为每个着色器同时保持解压缩的最大数据块数。这两个设置都可以全局配置,并针对每个平台进行覆盖。默认值为每个数据块 16 MB,数据块数量不受限制。如果是 0 个块则视为无限制。
Shader 变体剔除流程
利用引擎剔除
如果将它们设为 Automatic,Unity 就可以根据构建中包含的场景来确定要剥离的变体。
如果您不确定自己正在使用哪些功能,还可以使用 Import from Current Scene 按钮让 Unity 找出您需要的功能。当然,仅当所有场景都使用相同的设置时,这才有用,因此请确保在使用此选项时选择代表性场景。
如果您使用的是 URP,则其中一些选项将被隐藏。相反,您可以直接在 Pipeline Settings (工作流设置) 资源中定义游戏所需的功能。例如,禁用 Terrain Holes 将导致所有 Terrain Holes Shader 变体被剥离,从而缩短构建时间。
URP 对要包含在游戏中的功能提供了更精细的控制,从而有可能产生更优化的版本和更少的未使用变体。
基于图形层的剥离
注意:这仅在使用 Built-in Render Pipeline 时相关。使用可编程渲染管道(如 URP)时,将忽略这些设置。
图形层用于根据运行游戏的硬件应用不同的图形设置(不要与 Quality Settings 混淆)。游戏启动时,Unity 将根据硬件功能、图形 API 和其他因素确定您的设备图形层。
可以在 Project Settings > Graphics > Tier Settings 中设置它们。
基于这些,Unity 会将以下三个关键字添加到所有着色器中:
UNITY_HARDWARE_TIER1
UNITY_HARDWARE_TIER2
UNITY_HARDWARE_TIER3
然后,它会为定义的每个图形层生成着色器变体。如果不使用图形层并希望避免使用它们的相关变体,则需要确保所有图形层都设置为完全相同的设置,以便 Unity 跳过这些变体。
如前所述,Unity 将尝试删除相同的重复变体,因此,例如,如果三个层中的两个具有相同的设置,这将导致磁盘大小减小,即使仍会生成所有变体。您可以选择强制 Unity 为给定的着色器和图形渲染器 API 生成层变体,使用如下所示hardware_tier_variants:
// Direct3D 11/12
基于图形 API 的剥离,Unity 会为构建中包含的每个图形 API 编译一组着色器变体,因此在某些情况下,手动选择 API 并排除不需要的 API 是有益的。为此,请转到 Project Settings > Player。默认情况下,Auto Graphics API 处于选中状态,Unity 将包含一组内置图形 API,并在运行时根据设备功能选择一个。例如,在 Android 上,Unity 将首先尝试使用 Vulkan,如果设备不支持它,则引擎将回退到 GLES3.2、GLES3.1 或 GLES3.0(尽管这些 GLES 版本的变体是相同的)。相反,请为相关平台禁用 Auto Graphics API,并手动选择要包含的 API。然后,Unity 将优先考虑列表中的第一个 ID。
脚本自定义剔除
public class StripDebugVariantsPreprocessor : IPreprocessShaders
{
public int callbackOrder => 0;
ShaderKeyword keywordToStrip;
public StripDebugVariantsPreprocessor()
{
keywordToStrip = new ShaderKeyword("DEBUG");
}
public void OnProcessShader(Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> data)
{
if (EditorUserBuildSettings.development)
{
return;
}
for (int i = data.Count - 1; i >= 0; i--)
{
if (data[i].shaderKeywordSet.IsEnabled(keywordToStrip))
{
data.RemoveAt(i);
}
}
}
}
Shader 变体加载流程
当一个游戏对象的材质需要使用特定的着色器时,Unity 会确定所需的着色器变体。每个材质可能需要一个具体的变体,这取决于材质的属性和所启用的特性。着色器变体是由关键字(keywords)决定的。关键字可以开启或关闭特定的着色器代码块,生成不同的变体。每种关键字组合代表一个不同的变体。
Shader Variant Collection
SVC 是 Unity 提供的 shader 变体收集工具,使用 SVC 可以帮助我们灵活控制 shader 变体。
Shader Variant Collection 提供了优化着色器管理和性能的有效手段。它通过预编译、优化内存使用、提升构建和加载速度、提供更好的调试工具、增强控制和灵活性、减少冗余工作,以及改进整体性能,帮助开发者实现更高效、更流畅的游戏体验。
既然好处这么多,一旦项目中 Shader 体量过大,那么就很有必要来通过 SVC 控制项目当中的变体了,具体要怎么用呢?有 2 种办法(更推荐第 2 种):
方法一
在编辑器中运行时,Unity 会记录场景中当前使用的着色器和变体,并允许将其导出到 SVC 中。可以 Project Settings>Graphics 。在底部,你会看到 "着色器加载 "部分,显示当前有多少个着色器处于活动状态。
确保事先点击 "清除 "以获得更准确的样本,然后进入 "播放 "模式并进入场景,确保遇到需要特定着色器的所有游戏元素。这将增加跟踪计数器。然后,按下 "保存到资产... "按钮,将所有这些保存到 Collections 资产中。
虽然这样做很方便,但根据我的经验,这样做也很容易出错。很难确保在一次游戏中遇到所有需要的变体,而且有些功能可能只能在特定情况下在设备上加载,因此列表并不一定准确。当游戏发生变化,关卡中添加了新元素或材质发生变化时,就需要更新这些 Collections。
方法二
https://github.com/tuyoogame/YooAsset
Unity 中文社区持续征集内容投稿,欢迎与 Unity 官方分享你的技术笔记、项目 demo、行业经验、有趣案例,加入社区建设,繁荣内容生态,带领百万 Unity 中文开发者一同学习。
方式二:联系邮箱 learn-cn@unity.cn,投稿技术内容。
点击“阅读原文”,访问博主更多技术分享