Version: 2020.3
内置着色器变量
着色器数据类型和精度

着色器变体和关键字

可以编写着色器代码片段来共享通用代码,但在启用或禁用给定关键字时具有不同功能。Unity 编译这些着色器代码片段时,它将为已启用和已禁用关键字的不同组合创建单独的着色器程序。这些各个着色器程序被称为着色器变体。

由于项目工作流程的原因,着色器变体可能会很有用;可以将同一着色器分配给不同材质,但要为每种材质配置不同关键字。这意味着可以在同一个地方编写和维护着色器代码,并减少项目中的着色器资源。还可以使用着色器变体,通过启用或禁用关键字在运行时更改着色器行为。

具有大量变体的着色器被称为“大型着色器”或“超级着色器”。Unity 的标准着色器就是此类着色器的一个示例。

使用着色器变体和关键字

创建着色器变体

要创建着色器变体,请使用以下 pragma 指令之一:

  • #pragma multi_compile
  • #pragma multi_compile_local
  • #pragma shader_feature
  • #pragma shader_feature_local

可以将这些指令用于所有着色器源文件(包括表面着色器)和计算着色器

如果关键字仅影响单个着色器阶段,则可以向这些指令添加后缀以减少冗余的着色器编译工作。有关更多信息,请参阅特定于阶段的关键字指令

然后,Unity 使用不同的预处理器指令来多次编译同一着色器代码。

启用和禁用着色器关键字

要启用和禁用着色器关键字,请使用以下 API:

启用或禁用关键字时,Unity 会使用相应变体。

从您的构建中剥离着色器变体

如果您知道不需要着色器变体,则可以防止它们包含在您的构建中。这可以减少构建时间和文件大小。

要执行此操作,请使用以下 API:

IPreprocessShaders.OnProcessShader:在 Unity 将常规着色器编译为构建之前接收回调 ):在 Unity 将计算着色器编译为构建之前接收回调 IPreprocessComputeShaders.OnProcessComputeShader:在 Unity 将计算着色器编译为构建之前接收回调

有关此主题的更多信息,请参阅 Unity 博客文章剥离可编程着色器变体

multi_compile 的工作方式

指令示例:

# 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_SHADINGBETTER_SHADINGGOOD_SHADINGBEST_SHADING

为了生成未定义预处理器宏的着色器变体,请添加一个只有下划线 (__) 的名称。这是避免用完两个关键字的常用方法,因为在一个项目中可以使用的关键字数量有限(请参阅后面的关键字限制部分)。例如:

# pragma multi_compile __ FOO_ON

此指令生成两个着色器变体:一个未定义任何关键字 (__),另一个定义了 FOO_ON

shader_feature 与 multi_compile 之间的区别

shader_featuremulti_compile 非常相似。唯一的区别是 Unity 没有将 shader_feature 着色器的未用变体包含在最终构建中。因此,应该将 shader_feature 用于材质中设置的关键字,而 multi_compile 更适合通过代码来全局设置的关键字。

此外,有一个只包含一个关键字的速记符号:

# pragma shader_feature FANCY_STUFF

这只是 #pragma shader_feature _ FANCY_STUFF 的快捷方式。它会扩展为两个着色器变体(第一个没有定义;第二个有定义)。

合并多个 multi_compile 行

如果提供 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_featuremulti_compile 的主要缺点是其中定义的所有关键字均会影响 Unity 的全局关键字计数上限(384 个全局关键字,外加 64 个本地关键字)。为了避免此问题,可以使用不同的着色器变体指令:__shader_feature_local__ 和 multi_compile_local

  • shader_feature_local:__类似于 shader_feature__,但是枚举的关键字为本地关键字。
  • multi_compile_local:__类似于 multi_compile__,但是枚举的关键字为本地关键字。

本地指令将已定义的关键字保留在特定于该着色器的这些指令之下,而不是将这些关键字应用于整个项目。因此,应该使用本地关键字而不是全局关键字,除非计划通过全局 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 构建着色器时,它可以跳过多余的工作。

支持的图形 API

Unity 并不完全支持在所有图形 API 中使用特定于阶段的关键字指令。

  • 为 OpenGL 和 Vulkan 编译着色器时,编辑器会自动将任何特定于阶段的关键字指令恢复为常规关键字指令。
  • 为 Metal 编译着色器时,任何针对顶点阶段的关键字也会影响曲面细分阶段,反之亦然。

使用特定于阶段的关键字指令

可用的后缀是 _vertex_fragment_hull_domain_geometry_raytracing。您在关键字指令的末尾应用后缀,例如:multi_compile_fragmentshader_feature_local_vertex。要针对多个着色器阶段,您可以使用多个特定于阶段的关键字指令来声明同一个关键字。

注意:您应确保关键字仅用于指定的着色器阶段。

内置 multi_compile 快捷方式

在内置渲染管线中,有几个“快捷方式”符号用于编译多个着色器变体。这些变体主要处理 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

该指令会跳过包含 POINTPOINT_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 针对给定的 BuildTargetGraphicsTier 来覆盖 Unity 的内部 #define。

该功能仅与内置渲染管线兼容。它不兼容通用渲染管线 (URP)、高清渲染管线 (HDRP) 或自定义的可编程渲染管线。

请注意,如果您为给定的 BuildTarget 的不同 GraphicsTier 提供不同的 TierSettings 值,即使您未向着色器代码添加 #pragma hardware_tier_variants,Unity 也会为着色器生成层变体。

内置着色器变量
着色器数据类型和精度