可以编写着色器代码片段来共享通用代码,但在启用或禁用给定关键字时具有不同功能。Unity 编译这些着色器代码片段时,它将为已启用和已禁用关键字的不同组合创建单独的着色器程序。这些各个着色器程序被称为着色器变体。
由于项目工作流程的原因,着色器变体可能会很有用;可以将同一着色器分配给不同材质,但要为每种材质配置不同关键字。这意味着可以在同一个地方编写和维护着色器代码,并减少项目中的着色器资源。还可以使用着色器变体,通过启用或禁用关键字在运行时更改着色器行为。
具有大量变体的着色器被称为“大型着色器”或“超级着色器”。Unity 的标准着色器就是此类着色器的一个示例。
要创建着色器变体,请使用以下 pragma 指令之一:
#pragma multi_compile
#pragma multi_compile_local
#pragma shader_feature
#pragma shader_feature_local
可以将这些指令用于所有着色器源文件(包括表面着色器)和计算着色器。
如果关键字仅影响单个着色器阶段,则可以向这些指令添加后缀以减少冗余的着色器编译工作。有关更多信息,请参阅特定于阶段的关键字指令 。
然后,Unity 使用不同的预处理器指令来多次编译同一着色器代码。
要启用和禁用着色器关键字,请使用以下 API:
CommandBuffer
来启用全局关键字CommandBuffer
来禁用全局关键字启用或禁用关键字时,Unity 会使用相应变体。
如果您知道不需要着色器变体,则可以防止它们包含在您的构建中。这可以减少构建时间和文件大小。
要执行此操作,请使用以下 API:
IPreprocessShaders.OnProcessShader:在 Unity 将常规着色器编译为构建之前接收回调 ):在 Unity 将计算着色器编译为构建之前接收回调 IPreprocessComputeShaders.OnProcessComputeShader:在 Unity 将计算着色器编译为构建之前接收回调
有关此主题的更多信息,请参阅 Unity 博客文章剥离可编程着色器变体 。
指令示例:
# pragma multi_compile FANCY_STUFF_OFF FANCY_STUFF_ON
此指令示例生成两个着色器变体:一个定义了 FANCY_STUFF_OFF
,另一个定义了 FANCY_STUFF_ON
。在运行时,Unity 根据材质或全局着色器关键字来激活其中一个变体。如果这两个关键字均未启用,则 Unity 使用第一个关键字(在此示例中为 FANCY_STUFF_OFF
)。
可以在 multi_compile 行中添加两个以上的关键字。例如:
# pragma multi_compile SIMPLE_SHADING BETTER_SHADING GOOD_SHADING BEST_SHADING
此指令示例生成四个着色器变体:SIMPLE_SHADING
、BETTER_SHADING
、GOOD_SHADING
和 BEST_SHADING
。
为了生成未定义预处理器宏的着色器变体,请添加一个只有下划线 (__
) 的名称。这是避免用完两个关键字的常用方法,因为在一个项目中可以使用的关键字数量有限(请参阅后面的关键字限制部分)。例如:
# pragma multi_compile __ FOO_ON
此指令生成两个着色器变体:一个未定义任何关键字 (__
),另一个定义了 FOO_ON
。
shader_feature
与 multi_compile
非常相似。唯一的区别是 Unity 没有将 shader_feature
着色器的未用变体包含在最终构建中。因此,应该将 shader_feature
用于材质中设置的关键字,而 multi_compile
更适合通过代码来全局设置的关键字。
此外,有一个只包含一个关键字的速记符号:
# pragma shader_feature FANCY_STUFF
这只是 #pragma shader_feature _ FANCY_STUFF
的快捷方式。它会扩展为两个着色器变体(第一个没有定义;第二个有定义)。
如果提供 multi_compile
行,Unity 将会针对所有可能的行组合来编译生成的着色器。例如:
# pragma multi_compile A B C
# pragma multi_compile D E
这会为第一行生成三个变体,为第二行生成两个变体。总共生成六个着色器变体(A+D、B+D、C+D、A+E、B+E、C+E)。
每个 multi_compile
行可以视为用于控制单个着色器“功能”。请记住,着色器变体的总数会以这种方式急速增长。例如,十个各有两个选项的 multi_compile
功能总共生成 1024 个着色器变体。
使用着色器变体时,Unity 中的关键字数量上限是 384,Unity 将大约 60 个关键字保留供内部使用(因此降低了可用上限)。关键字会在整个 Unity 项目中全局启用,因此在多个不同着色器中定义多个关键字时,请注意不要超过限制。
shader_feature 和 multi_compile 的主要缺点是其中定义的所有关键字均会影响 Unity 的全局关键字计数上限(384 个全局关键字,外加 64 个本地关键字)。为了避免此问题,可以使用不同的着色器变体指令:__shader_feature_local__ 和 multi_compile_local。
本地指令将已定义的关键字保留在特定于该着色器的这些指令之下,而不是将这些关键字应用于整个项目。因此,应该使用本地关键字而不是全局关键字,除非计划通过全局 API 来启用这些特定关键字。
开始使用本地关键字时,您可能会发现性能有变化,但是此差异取决于项目的设置方式。每个着色器的本地和全局关键字总数会影响性能:在理想设置中,多用本地关键字和少用全局关键字可以减少每个着色器的关键字总数。
如果全局关键字和本地关键字同名,Unity 会优先考虑本地关键字。
不能将本地关键字与进行全局关键字更改的 API 一起使用(例如 Shader.EnableKeyword 或 CommandBuffer.EnableShaderKeyword)。
每个着色器最多有 64 个唯一性的本地关键字。
如果材质启用了本地关键字,并且其着色器变为不再声明的着色器,Unity 将创建新的全局关键字。
# pragma multi_compile_local __ FOO_ON
此指令生成两个着色器变体:一个未定义任何关键字 (__
),另一个定义了 FOO_ON
(本地关键字)。
启用本地关键字的过程与启用全局关键字的过程相同:
public Material mat;
Private void Start()
{
mat.EnableKeyword("FOO_ON");
}
创建着色器变体时,Unity 编辑器的默认行为是在每个变体中生成着色器程序的每个阶段。例如,如果您的着色器程序包含一个顶点阶段和一个片元阶段,Unity 会为每个关键字组合生成一个顶点阶段和一个片元阶段。
如果关键字不影响所有阶段,则此默认行为会导致冗余工作。例如,如果关键字仅影响片元阶段,则编辑器会为每个变体生成相同的顶点阶段。Unity 会在之后识别并删除重复项,因此这种冗余工作不会影响构建大小或运行时性能;但是,如果您有很多阶段和/或变体,着色器编译期间浪费的时间会非常明显。
为避免此问题,您可以使用特定于阶段的关键字指令。这些是应用于常规关键字指令的后缀。它们告诉编辑器给定关键字影响哪个着色器阶段,因此在为支持的图形 API 构建着色器时,它可以跳过多余的工作。
Unity 并不完全支持在所有图形 API 中使用特定于阶段的关键字指令。
可用的后缀是 _vertex
、_fragment
、_hull
、_domain
、_geometry
和 _raytracing
。您在关键字指令的末尾应用后缀,例如:multi_compile_fragment
或 shader_feature_local_vertex
。要针对多个着色器阶段,您可以使用多个特定于阶段的关键字指令来声明同一个关键字。
注意:您应确保关键字仅用于指定的着色器阶段。
在内置渲染管线中,有几个“快捷方式”符号用于编译多个着色器变体。这些变体主要处理 Unity 中的不同光源、阴影和光照贴图类型。请参阅有关渲染路径和着色器的文档以了解详细信息。
multi_compile_fwdbase
编译 PassType.ForwardBase 所需的所有变体。这些变体处理不同的光照贴图类型以及启用或禁用的方向光主要阴影。multi_compile_fwdadd
编译 PassType.ForwardAdd 的变体。这将编译变体来处理方向光、聚光灯或点光源类型,以及它们带有剪影纹理的变体。multi_compile_fwdadd_fullshadows
- 与 multi_compile_fwdadd
相同,但还能够让光源具有实时阴影。multi_compile_fog
扩展为多个变体以处理不同的雾效类型 (off/linear/exp/exp2)。大多数内置快捷方式会产生许多着色器变体。如果知道项目不需要这些变体,可以使用 #pragma skip_variants
来跳过对其中一些变体的编译。例如:
# pragma multi_compile_fwdadd
# pragma skip_variants POINT POINT_COOKIE
该指令会跳过包含 POINT
或 POINT_COOKIE
的所有变体。
在运行时,Unity 会检查 GPU 的功能并确定其对应的图形层。在内置渲染管线中,可以为每个图形层自动创建一组着色器变体;为此,请使用 #pragma hardware_tier_variants
指令。
该功能仅与内置渲染管线兼容。它不兼容通用渲染管线 (URP)、高清渲染管线 (HDRP) 或自定义的可编程渲染管线。
要启用此功能,请添加 #pragma hardware_tier_variants renderer
,其中 renderer
是有效的图形 API,如下所示:
# pragma hardware_tier_variants gles3
除了其他所有关键字,Unity 还为每个着色器生成三个着色器变体。每个生成的变体都有以下定义命令之一,它们对应于 GraphicsTier 枚举的相同编号值:
UNITY_HARDWARE_TIER1
UNITY_HARDWARE_TIER2
UNITY_HARDWARE_TIER3
您可以使用它们为更低端或更高端硬件编写条件性回退或额外功能。
Unity 首次加载应用程序时,它会检测到 GraphicsTier
并将结果存储在 Graphics.activeTier 中。要覆盖 Graphics.activeTier
的值,请直接设置该值。请注意,必须在 Unity 加载您要更改的任何着色器之前执行此操作。一个非常适合设置此值的位置是在加载主场景之前的预加载场景中。
为了帮助尽可能降低这些变体的影响,Unity 在播放器中只加载一组着色器。相同的着色器(例如,如果您只为 TIER1
编写了专用版本而其他所有版本都相同)将不占用磁盘上的任何额外空间。
要在 Unity Editor 中测试图形层,请导航至 Edit > Graphics tier,然后选择您希望 Unity Editor 使用的层。
请注意,图形层与质量设置无关。它们是此设置的补充。
在内置渲染管线中,可以使用 EditorGraphicsSettings.SetShaderSettingsForPlatform API 针对给定的 BuildTarget 和 GraphicsTier 来覆盖 Unity 的内部 #define。
该功能仅与内置渲染管线兼容。它不兼容通用渲染管线 (URP)、高清渲染管线 (HDRP) 或自定义的可编程渲染管线。
请注意,如果您为给定的 BuildTarget
的不同 GraphicsTier
提供不同的 TierSettings
值,即使您未向着色器代码添加 #pragma hardware_tier_variants
,Unity 也会为着色器生成层变体。