英文原文:
https://blog.unity.com/technology/stripping-scriptable-shader-variants
通过允许开发人员控制 Unity Shader 编译器处理哪些 Shader 变体并包含在 Player 数据中,大大减少了 Player 构建时间和数据大小。
由于着色器变体数量的增加,player构建时间和数据大小会随着项目的复杂性而增加。
借助 2018.2 测试版中引入的可编写脚本的着色器变体剥离,您可以管理生成的着色器变体的数量,从而大大减少player构建时间和数据大小。
此功能允许您去除具有无效代码路径的所有着色器变体,去除未使用功能的着色器变体或创建着色器构建配置,例如“调试”和“发布”,而不会影响迭代时间或维护复杂性。
在这篇博文中,我们首先定义了我们使用的一些术语。然后我们关注着色器变体的定义来解释为什么我们可以生成这么多。接下来是对自动着色器变体剥离的描述以及如何在 Unity 着色器管道架构中实现可编写脚本的着色器变量剥离。然后,在讨论 Fountainbleau 演示的结果之前,我们先查看可编写脚本的着色器变体剥离 API,并总结一些编写剥离脚本的技巧。
学习可脚本化着色器变体剥离并非易事,但它可以大大提高团队效率!
要了解可编写脚本的着色器变体剥离功能,准确理解所涉及的不同概念非常重要。
在 Unity 中,超级着色器由 ShaderLab 子着色器、通道和着色器类型以及 #pragma multi_compile 和 #pragma shader_feature 预处理器指令管理。
要使用可编写脚本的着色器变体剥离,您需要清楚地了解什么是着色器变体,以及着色器构建管道如何生成着色器变体。生成的着色器变体数量与构建时间和player着色器变体数据大小成正比。着色器变体是着色器构建管道的一个输出。
着色器关键字是导致着色器变体生成的元素之一。不加考虑地使用着色器关键字会很快导致着色器变体数量激增,从而导致构建时间极长。
要查看着色器变体是如何生成的,下面的简单着色器计算了它产生了多少着色器变体:
Shader "ShaderVariantsStripping"
{
SubShader
{
Pass
{
Name "ShaderVariantsStripping/Pass"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile COLOR_ORANGE COLOR_VIOLET COLOR_GREEN COLOR_GRAY
#pragma multi_compile OP_ADD OP_MUL OP_SUB
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 get_color()
{
#if defined(COLOR_ORANGE)
return fixed4(1.0, 0.5, 0.0, 1.0);
#elif defined(COLOR_VIOLET)
return fixed4(0.8, 0.2, 0.8, 1.0);
#elif defined(COLOR_GREEN)
return fixed4(0.5, 0.9, 0.3, 1.0);
#elif defined(COLOR_GRAY)
return fixed4(0.5, 0.9, 0.3, 1.0);
#else
#error "Unknown 'color' keyword"
#endif
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 diffuse = tex2D(_MainTex, i.uv);
fixed4 color = get_color();
#if defined(OP_ADD)
return diffuse + color;
#elif defined(OP_MUL)
return diffuse * color;
#elif defined(OP_SUB)
return diffuse - color;
#else
#error "Unknown 'op' keyword"
#endif
}
ENDCG
}
}
}
项目中着色器变体的总数是确定性的,由以下等式给出:
以下简单的 ShaderVariantStripping 示例使这个等式变得清晰。它是一个单一的着色器,可将等式简化如下:
类似地,这个着色器有一个子着色器和一个通道,这进一步将方程简化为:
等式中的关键字指的是平台和着色器关键字。图形层是特定的平台关键字集组合。
ShaderVariantStripping/Pass 有两个多重编译指令。第一个指令定义了 4 个关键字(COLOR_ORANGE、COLOR_VIOLET、COLOR_GREEN、COLOR_GRAY),第二个指令定义了 3 个关键字(OP_ADD、OP_MUL、OP_SUB)。最后,pass 定义了 2 个着色器阶段:一个顶点着色器阶段和一个片段着色器阶段。
此着色器变体总数是针对单个受支持的图形 API 给出的。但是,对于项目中每个支持的图形 API,我们需要一组专用的着色器变体。例如,如果我们构建一个同时支持 OpenGL ES 3 和 Vulkan 的 Android 播放器,我们需要两组着色器变体。因此,播放器构建时间和着色器数据大小与支持的图形 API 数量成正比。
Unity 中的着色器编译流水线是一个黑匣子,项目中的每个着色器都在其中被解析以提取着色器片段,然后再收集变体预处理指令,例如 multi_compile 和 shader_feature。这会生成一个编译参数列表,每个着色器变体一个。
这些编译参数包括着色器片段、图形层、着色器类型、着色器关键字集、通道类型和名称。每个设置的编译参数都用于生成单个着色器变体。
因此,Unity 基于两个启发式执行自动着色器变体剥离通道。首先,剥离基于项目设置,例如,如果禁用虚拟现实支持,则系统地剥离 VR 着色器变体。其次,自动剥离是基于 Graphics Settings 的 Shader Stripping 部分的配置。
自动着色器变体剥离基于build时间限制。 Unity 无法在构建时仅自动选择必要的着色器变体,因为这些着色器变体依赖于运行时 C# 执行。例如,如果一个 C# 脚本添加了一个点光源,但在构建时没有点光源,那么着色器构建管道就无法确定播放器需要一个着色器变体来进行点光源着色。
以下是启用了自动剥离的关键字的着色器变体列表:
Lightmap modes: LIGHTMAP_ON, DIRLIGHTMAP_COMBINED, DYNAMICLIGHTMAP_ON, LIGHTMAP_SHADOW_MIXING, SHADOWS_SHADOWMASK
Fog modes: FOG_LINEAR, FOG_EXP, FOG_EXP2
Instancing Variants: INSTANCING_ON
Furthermore, when Virtual Reality support is disabled, the shader variants with the following built-in enabled keywords are stripped:
STEREO_INSTANCING_ON, STEREO_MULTIVIEW_ON, STEREO_CUBEMAP_RENDER_ON, UNITY_SINGLE_PASS_STEREO
完成自动剥离后,着色器构建管道使用剩余的编译参数集来并行调度着色器变体编译,启动与平台具有 CPU 核心线程一样多的同时编译。
在 Unity 2018.2 beta 中,着色器管道架构在着色器变体编译调度之前引入了一个新阶段,允许用户控制着色器变体编译。这个新阶段通过 C# 回调向用户代码公开,并且每个回调在每个着色器片段中执行。
例如,以下脚本可以剥离与“DEBUG”配置相关联的所有着色器变体,由开发player构建中使用的“DEBUG”关键字标识。
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Rendering;
using UnityEngine;
using UnityEngine.Rendering;
// 剥离调试构建配置的简单示例
class ShaderDebugBuildProcessor : IPreprocessShaders
{
ShaderKeyword m_KeywordDebug;
public ShaderDebugBuildProcessor()
{
m_KeywordDebug = new ShaderKeyword("DEBUG");
}
// 可以实现多个回调。
// 第一个执行的是 callbackOrder 返回最小数字的那个。
public int callbackOrder { get { return 0; } }
public void OnProcessShader(
Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> shaderCompilerData)
{
// 在开发中,不要剥离调试变量
if (EditorUserBuildSettings.development)
return;
for (int i = 0; i < shaderCompilerData.Count; ++i)
{
if (shaderCompilerData[i].shaderKeywordSet.IsEnabled(m_KeywordDebug))
{
shaderCompilerData.RemoveAt(i);
--i;
}
}
}
}
OnProcessShader 在着色器变体编译的调度之前被调用。
Shader、ShaderSnippetData 和 ShaderCompilerData 实例的每个组合都是着色器编译器将生成的单个着色器变体的标识符。要去除该着色器变体,我们只需将其从 ShaderCompilerData 列表中删除。
着色器编译器应生成的每个着色器变体都将出现在此回调中。在编写着色器变体剥离脚本时,您需要首先确定需要删除哪些变体,因为它们对项目没有用处。
可编写脚本的着色器变体剥离的一个用例是系统地剥离渲染管道的无效着色器变体,因为着色器关键字的各种组合。
HD 渲染管道中包含的着色器变体剥离脚本允许您使用 HD 渲染管道系统地减少项目的构建时间和大小。此脚本适用于以下着色器:
HDRenderPipeline/Lit
HDRenderPipeline/LitTessellation
HDRenderPipeline/LayeredLit
HDRenderPipeline/LayeredLitTessellation
该脚本产生以下结果:
Unstripped Stripped
Player Data Shader Variant Count 24350 (100%) 12122 (49.8%)
Player Data Size on disk 511 MB 151 MB
Player Build Time 4864 seconds 1356 seconds
此外,Unity 2018.2 的轻量级渲染管道有一个 UI 可以自动提供一个剥离脚本,该脚本可以自动剥离多达 98% 的着色器变体,我们预计这对移动项目特别有价值。
另一个用例是一个脚本,用于剥离渲染管道中未用于特定项目的所有渲染功能。使用轻量级渲染管道的内部测试演示,我们得到了整个项目的以下结果:
Unstripped Stripped
Player Data Shader Variant Count 31080 7056
Player Data Size on disk 121 116
Player Build Time 839 seconds 286 seconds
正如我们所看到的,使用可脚本化着色器变体剥离可以产生显着的结果,并且在剥离脚本上进行更多工作,我们可以走得更远。
一个项目可能会很快遇到着色器变体数量爆炸,导致编译时间和player数据大小不可持续。可编写脚本的着色器剥离有助于解决这个问题,但您应该重新评估如何使用着色器关键字来生成更相关的着色器变体。我们可以依靠#pragma skip_variants 来测试编辑器中未使用的关键字。
例如,在 ShaderStripping/Color Shader 中,预处理指令使用以下代码声明:
#pragma multi_compile COLOR_ORANGE COLOR_VIOLET COLOR_GREEN COLOR_GRAY // color keywords
#pragma multi_compile OP_ADD OP_MUL OP_SUB // operator keywords
这种方法意味着将生成颜色关键字和操作符关键字的所有组合。
假设我们要渲染以下场景:
首先,我们应该确保每个关键字实际上都是有用的。在这个场景中,从未使用过 COLOR_GRAY 和 OP_SUB。如果我们可以保证这些关键字永远不会被使用,那么我们应该删除它们。
其次,我们应该组合有效地产生单一代码路径的关键字。在此示例中,“Add”操作始终与“ORANGE”颜色一起使用。所以我们可以将它们组合在一个关键字中并重构代码,如下所示。
#pragma multi_compile ADD_COLOR_ORANGE MUL_COLOR_VIOLET MUL_COLOR_GREEN
#if defined(ADD_COLOR_ORANGE)
#define COLOR_ORANGE
#define OP_ADD
#elif defined(MUL_COLOR_VIOLET)
#define COLOR_VIOLET
#define OP_MUL
#elif defined(MUL_COLOR_GREEN)
#define COLOR_GREEN
#define OP_MUL
#endif
当然,重构关键字并不总是可能的。在这些情况下,可编写脚本的着色器变体剥离是一个有价值的工具!
对于每个片段,都会执行所有着色器变体剥离脚本。我们可以通过对 callbackOrder 成员函数返回的值进行排序来排序脚本的执行。着色器构建管道将按照 callbackOrder 递增的顺序执行回调,因此最低的在前,最高的在后。
使用多个着色器剥离脚本的一个用例是按目的分离脚本。例如:
着色器变体剥离非常强大,但需要大量工作才能获得良好的效果。