Shader Variant

基础知识

什么是 Shader Variant

在写 Shader 时,往往会在 Shader 中定义多个宏,并在 Shader 代码中控制开启宏或关闭宏时物体的渲染过程。最终编译的时候也是根据这些不同的宏来编译生成多种组合形式的 Shader 源码。其中每一种组合就是这个 Shader 的一个 变体

Material Shader Keywords 与 Shader Variant

Material 所包含的 Shader Keywords 表示启用 Shader 中对应的宏,Unity 会调用当前宏组合所对应的变体来为 Material 进行渲染

  • 在 Editor 下,可以通过将 Material 的 Inspector 调成 Debug 模式来查看当前 Material 定义的 Keywords,也可以在此模式下直接定义 Keywords,用空格分隔 Keyword
  • 在代码中,可用 Material.EnableKeyword()Material.DisableKeyword()Shader.EnableKeyword()Shader.DisableKeyword() 来启用 / 禁用相应的宏
  • Enable函数应与 Disable 函数相对应。若一个宏由 Material.EnableKeyword() 开启,则应由 Material.DisableKeyword() 关闭,Shader.DisableKeyword()无法关闭这个宏

multi_compileshader_feature

multi_compile 的用法解析
1
#paragma multi_compile OFF ON

表示定义了两个变体,OFFON。在运行时,其中的一个将被激活,根据材质或者全局着色器关键字(#ifdef OFF 之类的宏命令也可以)来确定激活哪个。若两个关键词都没有启用,那么将默认使用前一个选项,也就是OFF

也可以同时创建多个变体:

1
#paragma multi_compile A B C

还可以使用多行指令对变体进行组合,但是这样做的话,会导致变体数量成倍的增长,如果使用下面的代码生成变体,会得到 中不同的变体:

1
2
#paragma multi_compile A B C
#paragma multi_compile D E
shader_feature用法简析

shader_feature 的用法与 multi_compile 大致相同,唯一的区别在于 shader_feature 不会将不用的 Shader 变体添加到程序中去。shader_feature更适用于材质的关键字,而 multi_compile 更适用于代码的全局关键字

1
#paragma shader_feature A

其实 #paragma shader_feature A#paragma shader_feature _ A的简写,下划线表示未定义宏(NoKeyword)。因此此时 Shader 其实对应了两个变体,一个是 NoKeyword,一个是定义了宏 A。而#paragma multi_compile A 并不存在简写这一说,所以 Shader 此时只对应 A 这个变体。若要表示未定义任何变体,则应写为#paragma multi_compile _ A

如何控制项目中 Shader 变体的生成

生成方式 优点 缺点
Shader 与材质打在一个包中 变体根据材质中的 keywords 自动生成
  1. 多个不同的材质包中可能存在相同的 Shader 变体,造成资源冗余
  2. 若在程序运行时动态改变材质的 keyword,使用 shader_feature 定义的宏,其变体可能并没有被生成
Shader 单独打包,使用 multi_compile 定义全部宏 全部变体都被生成,不会发生需要的变体未生成的情况
  1. 生成的变体数量庞大,严重浪费资源
Shader 单独打包,shader_feature(需要使用 ShaderVariantCollection 生成变体)与multi_compile(还是会生成所有变体)结合使用 能够有效控制 shader_feature 变体数量
  1. 如何确定哪些变体需要生成
  2. 容易遗漏需要生成的变体,特别是需要动态替换的变体

使用 shader_feature 的解决方案:ShaderVariantCollection

ShaderVariantCollection 介绍

ShaderVariantCollection 的其中一个作用就是用来记录 Shader 中使用shader_feature 定义的宏产生的变体。能够设置生成任何变体,从而避免生成不必要的变体;Shader 不必和材质打在一个包中,避免了多个包中存在相同的变体资源;明确直观的显示了哪些变体是需要生成的。

在 Unity 中可以通过 Create->Shader->Shader Variant Collection,就可以新建一个 ShaderVariantCollection 文件

ShaderVariantCollection 生成通过 shader_feature 定义的变体规则
  • 必定生成首个宏定义开启所对应的变体

    • pragma shader_feature A:除了生成 A 的变体,nokeyword 所对应的变体也会被生成
    • pragma shader_feature A B C:ShaderVariantCollection 中即使未添加变体 A,这个变体也会被生成
  • Shader 中又多个 Pass 时变体的生成规则

    1. 读取 ShaderVariantCollection 中已存在的变体,获取他们的 keywords

    2. 将这些 keywords 分别与每个 Pass 的多组 keywords 列表求交集,取交集中 keywords 数量最多的那组

    3. 用得到的 keywords 与对应 PassType 生成 Shader 变体,并添加到 ShaderVariantCollection 中

    4. 若得到的交集中有新的 keywords,则回到 2

      例如 Shader 中有 ForwardBase、ForwardAdd、Normal 三种 PassType,定义的宏如下:

      ForwardBase ForwardAdd Normal
      #pragma shader_feature A
      #pragma shader_feature B
      #pragma shader_feature C
      #pragma shader_feature A
      #pragma shader_feature E
      #pragma shader_feature A
      #pragma shader_feature B
      #pragma shader_feature E

      此时若 ShaderVariantCollection 中包含的变体是 ForwardBase ABC,ForwardAdd AE。则此时生成的变体为:这三种 PassType 的默认定义的宏 (nokeyword) 所对应的变体(3 个)以及原先直接包含的 ForwardBase ABC、ForwardAdd AE。除此之外 Unity 还会额外生成 ForwardAdd A、ForwardBase A、Normal A、Normal AB、 ForwardBase AB、Normal AE 这 6 个变体

      1
      2
      3
      4
      5
      6
      ABC ∩ Add AE -> Add A (A is NewKeyword)
      A ∩ Base ABC -> Base A
      A ∩ Normal ABE -> Normal A
      ABC ∩ Normal ABE -> Normal AB (AB is NewKeyword)
      AB ∩ Base ABC -> Base AB
      AE ∩ Normal ABE -> Normal AE
变体的调用规则

当 ShaderVariantCollection 将变体准确生成后,便能在运行时通过修改材质中的 keywords 来实现对不同变体的调用。假设某 collection 生成的变体只有 Forward ABC,Forward ABE,Forward nokeyword 这三种,则此时调用关系如下:

Material 中的 Keywords 调用的变体 解释
A B C Forward A B C 正常匹配
A B Forward nokeyword 没有匹配的变体,调用首个被定义的宏 所对应的变体
A B C D Forward A B C 调用交集中 keyword 数量多的变体 ABCD ∩ ABC = ABC ABCD ∩ ABE = AB
A B C E Forward A B C 交集中 keyword 数量相同,在 collection 中谁在前就调用谁
A B E C Forward A B C 与在 material 中的定义顺序无关

项目中变体的添加

  • 遍历每个材质,提取其 Shader keywords
  • 将获得的 keywords 与 Shader 的每个 PassType 所包含的宏定义做交集,并将其结果添加到 ShaderVariantCollection 中

变体代码在 Shader 中编写规范

  • 建议使用 shader_feature 时将定义语句写成完整模式,并且不要在一个语句中定义多个宏
    • #pragma shader_feature _ A,不建议写成#pragma shader_feature A
  • 若在 Shader 中使用shader_feature,请为这个 Shader 指定一个 CustomEditor

Shader Variant
https://silhouettesforyou.github.io/2022/08/06/93ff363a107d/
Author
Xiaoming
Posted on
August 6, 2022
Licensed under