剥离可编写脚本的着色器变体

允许开发人员控制由 Unity 着色器编译器处理并包含在播放器数据中的着色器变体,从而大幅减少播放器的构建时间和数据大小。
由于着色器变体的数量不断增加,播放器的构建时间和数据大小会随着项目的复杂性而增加。
利用2018.2 测试版中引入的可编写脚本的着色器变体剥离功能,您可以管理生成的着色器变体数量,从而大幅减少播放器的构建时间和数据大小。
利用该功能,您可以剥离所有代码路径无效的着色器变体,剥离未使用功能的着色器变体,或创建 "调试 "和 "发布 "等着色器构建配置,而不会影响迭代时间或维护复杂性。
在本博文中,我们将首先定义我们使用的一些术语。然后,我们将重点讨论着色器变体的定义,以解释为什么我们可以生成如此多的着色器变体。接下来将介绍自动着色器变体剥离以及如何在 Unity 着色器流水线架构中实现可编写脚本的着色器变体剥离。然后,我们将介绍可编写脚本的着色器变体剥离 API,然后讨论 Fountainbleau 演示的结果,最后介绍编写剥离脚本的一些技巧。
学习可编写脚本的着色器变体剥离并非易事,但它能大大提高团队效率!
要了解可编写脚本的着色器变量剥离功能,必须准确理解其中涉及的不同概念。
- 着色器资产:包含属性、子着色器、通道和 HLSL 的完整文件源代码。
- 着色器片段带有单个着色器阶段依赖关系的 HLSL 输入代码。
- 着色器阶段:GPU 渲染管道中的一个特定阶段,通常是顶点着色器阶段和片段着色器阶段。
- 着色器关键字:用于跨着色器编译时分支的预处理器标识符。
- 着色器关键字集:一组特定的着色器关键字,用于识别特定的代码路径。
- 着色器变体Unity 着色器编译器生成的特定平台的着色器代码,用于特定图形层、通量、着色器关键字集等的单个着色器阶段。
- Uber 着色器可生成多种着色器变体的着色器源。
在 Unity 中,uber 着色器由ShaderLab子着色器、传递和着色器类型以及 #pragma multi_compile 和 #pragmashader_feature 预处理器指令管理。
要使用可编写脚本的着色器变体剥离,你需要清楚地了解什么是着色器变体,以及着色器构建管道是如何生成着色器变体的。生成的着色器变量数量与构建时间和播放器着色器变量数据大小成正比。着色器变量是着色器构建流水线的输出之一。
着色器关键字是生成着色器变体的要素之一。如果不加考虑地使用着色器关键字,很快就会导致着色器变体数量激增,从而导致极长的构建时间。
要了解着色器变体是如何生成的,下面这个简单的着色器会计算它生成了多少个着色器变体:
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)。最后,该通道定义了 2 个着色器阶段:顶点着色器阶段和片段着色器阶段。
此着色器变体总数是针对单个受支持的图形 API 而给出的。不过,对于项目中每个受支持的图形 API,我们都需要一套专用的着色器变体。例如,如果我们制作的 Android 播放器同时支持 OpenGL ES 3 和 Vulkan,我们就需要两套着色器变体。因此,播放器的构建时间和着色器数据大小与支持的图形 API 数量成正比。

自动着色器变量剥离基于构建时间限制。Unity 无法在构建时自动只选择必要的着色器变体,因为这些着色器变体取决于运行时的 C# 执行。例如,如果 C# 脚本添加了一个点光源,但在构建时并没有点光源,那么着色器构建流水线就无法计算出播放器需要一个能进行点光源着色的着色器变体。
下面列出了带有启用关键字的着色器变体,这些关键字会被自动剥离:
光图模式LIGHTMAP_ON, DIRLIGHTMAP_COMBINED, DYNAMICLIGHTMAP_ON, LIGHTMAP_SHADOW_MIXING, SHADOWS_SHADOWMASK
雾化模式FOG_LINEAR, FOG_EXP, FOG_EXP2
实例变量:INSTANCING_ON
此外,当禁用虚拟现实支持时,带有以下内置启用关键字的着色器变体会被删除:
STEREO_INSTANCING_ON, STEREO_MULTIVIEW_ON, STEREO_CUBEMAP_RENDER_ON, UNITY_SINGLE_PASS_STEREO
自动剥离完成后,着色器构建流水线会使用剩余的编译参数集并行安排着色器变体的编译,根据平台的 CPU 内核线程数量同时启动编译。
下面是这一过程的直观展示:

在Unity 2018.2 测试版中,着色器流水线架构在着色器变体编译调度之前就引入了一个新阶段,允许用户控制着色器变体编译。这个新阶段通过 C# 回调暴露给用户代码,每个回调在每个着色器片段中执行。
例如,以下脚本可剥离与 "DEBUG "配置相关的所有着色器变量,这些变量由开发播放器构建时使用的 "DEBUG "关键字标识。
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Rendering;
using UnityEngine;
using UnityEngine.Rendering;
// Simple example of stripping of a debug build configuration
class ShaderDebugBuildProcessor : IPreprocessShaders
{
ShaderKeyword m_KeywordDebug;
public ShaderDebugBuildProcessor()
{
m_KeywordDebug = new ShaderKeyword("DEBUG");
}
// Multiple callback may be implemented.
// The first one executed is the one where callbackOrder is returning the smallest number.
public int callbackOrder { get { return 0; } }
public void OnProcessShader(
Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> shaderCompilerData)
{
// In development, don't strip debug variants
if (EditorUserBuildSettings.development)
return;
for (int i = 0; i < shaderCompilerData.Count; ++i)
{
if (shaderCompilerData[i].shaderKeywordSet.IsEnabled(m_KeywordDebug))
{
shaderCompilerData.RemoveAt(i);
--i;
}
}
}
}
OnProcessShader 会在着色器变量编译调度之前调用。
着色器、着色器片段数据(ShaderSnippetData)和着色器编译器数据(ShaderCompilerData)实例的每个组合都是着色器编译器将生成的单个着色器变体的标识符。要剥离该着色器变量,我们只需将其从 ShaderCompilerData 列表中移除即可。
着色器编译器应生成的每个着色器变量都将出现在此回调中。在编写着色器变体剥离脚本时,首先要弄清楚哪些变体需要删除,因为它们对项目没有用处。
可脚本着色器变体剥离的一个用例是系统地剥离渲染管道中因着色器关键字的不同组合而产生的无效着色器变体。
高清渲染管道中包含的着色器变量剥离脚本可让您使用高清渲染管道系统地减少项目的构建时间和大小。此脚本适用于以下着色器:
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。如果我们能保证这些关键词从未被使用过,那么我们就应该删除它们。
其次,我们应该结合关键词,有效地生成单一的代码路径。在本例中,"添加 "操作始终只使用 "橙色"。因此,我们可以将它们合并为一个关键字,并重构代码,如下所示。
#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 递增的顺序执行回调,即先执行最低的,后执行最高的。
使用多个着色器剥离脚本的一个用例是将脚本按用途分开。例如:
- 脚本 1:系统删除所有代码路径无效的着色器变体。
- 脚本 2:删除所有调试着色器变量。

- 脚本 3:删除代码库中当前项目不需要的所有着色器变量。
- 脚本 4:记录剩余的着色器变量,并将其全部剥离,以加快剥离脚本的迭代时间。
着色器变量剥离功能非常强大,但需要大量工作才能实现良好效果。
在项目视图中,筛选所有着色器。
选择一个着色器,在 "检查器 "中单击 "显示",打开该着色器的关键字/变体列表。将有一个始终包含在构建中的关键字列表。
确保您了解项目使用的具体图形功能。
检查是否在所有着色器阶段都使用了关键字。对于不使用这些关键词的阶段,只需要一个变体。
在脚本中剥离着色器变量。
验证构建中的视觉效果。
对每个着色器重复步骤 2 - 6。
下载示例项目
用于说明本博文的示例项目可在此处下载。它需要 Unity2018.2.0b1。
来参加 Jonas Echterhoff 6 月 21 日的讲座,了解所有能让您更好地控制构建内容的新工具!