一.定义着色器变体
定义一个着色器变体(Shader Variant)从概念和实现上讲,主要包括以下几个核心部分
1.使用预编译指令来声明变体关键字
关键字是驱动变体生成的“开关”。它们是简单的字符串标识符,用于在 Shader 代码中标记不同的功能路径。
以multi_compile类型为例在ShaderLab中声明自定义变体关键字:
#pragma multi_compile _ _CustomKeyWord
这里用到了关键字的组合写法(下划线+自定义关键字名),默认会使用第一个变体(这里的 _代表的是 默认状态或无关键字激活的状态 )
(*)使用下划线组合写法的好处
清晰性:它明确指出了当该组功能都未开启时的 Shader 行为。这使得 Shader 代码更易读、更易于理解。
智能裁剪:对于 shader_feature,Unity 在构建游戏时会执行 Shader Stripping(着色器裁剪)。如果你的项目中,没有任何一个材质勾选了“开启功能 A”,那么与 MY_FEATURE_ON
对应的变体就不会被编译和打包。但由于有 _
的存在,默认的、不带该功能的变体始终会被包含(只要这个 Shader 被使用了)。
优化包体:这意味着你不需要为每个可选功能都强制打包“开启”和“关闭”两个版本。如果“开启”版本根本没用,它就不会被打包,从而节省了存储空间。
注意:Unity有变体数量的限制
(*)使用multicompile时可以不明确使用 _
隐含的默认状态:当 multi_compile 列表中没有 _
且没有其他关键字被激活时,Unity 仍然会生成一个变体,这个变体是 没有任何该 multi_compile 行中关键字定义的。这其实就等同于“默认”或“无宏定义”的状态。但如果使用了 _ ,那么监视器面板里默认显示为未激活状态。
总结:使用下划线可以节省变体数量
(*)multi_compile与shader_feature的区别
1.概述
#pragma multi_compile指令会强制编译所有可能的关键字组合对应的 Shader 变体,无论这些变体是否在你的项目中被实际使用。
#pragma shader_feature指令会按需编译 Shader 变体,它只会编译和包含那些在你的项目中被 材质实际使用 的关键字组合对应的变体。
2.区别
功能独立:它们各自执行的任务是独立的。shader_feature 的核心是裁剪(stripping),而 multi_compile 的核心是强制编译所有变体。一个不能替代另一个的功能。
目的不同:
shader_feature 主要服务于优化构建包体大小和编译时间,针对的是那些通过材质属性控制的“可选”功能。
multi_compile 主要服务于保证运行时功能的完整性,针对的是那些引擎内部控制的“必需”功能。
2.静态分支代码块
在 Shader 源代码内部,需要使用 条件编译宏(#ifdef / #ifndef / #else
) 来包裹那些受关键字控制的代码逻辑。这些宏在编译时根据关键字的激活状态,决定哪些代码会被包含在最终的变体中,哪些会被剔除.
一般在顶点或片元着色器中常使用如下格式分别书写变体分支的逻辑:
#ifdef _CustomKeyword_ONxxxxxxxxxxx;#elsexxxxxxxxxxx;#endif
3.关键字与材质属性的关联(可选,但常用)
为了方便美术师和设计师在 Unity 编辑器中控制这些变体,通常需要将 Shader 关键字与材质的 Properties
块中的属性 关联起来。这通过使用像 [Toggle]
、[KeywordEnum]
这样的属性修饰符来实现。
变体开关的实现
1.ShaderLab属性块
可以发现在Property中命名相同的属性会指向ShaderLab中的同一变体关键字。
注意命名的匹配规则
SubShader中的关键字命名:(全大写)自定义关键字名"+" _ON"
#pragma shader_feature _CustomKeyword_ON
Proprty中声明的属性名命名:(大小写可混搭,但要与SubSahder中关键字的字符组成保持一致)自定义关键字名
[Toggle]_CustomKeyword_ON ("Toggle MyOn", Float) = 1
另外,如果你使用不同形式(如[Toggle]和[MaterialToggle])定义了属性名完全相同的变量开关,在监视器面板中,它们将同时切换状态。
2.C#代码
1.单个材质实例
(1)启用关键字:Material.EnableKeyword(string keyword)
作用: 激活此 Material
实例上 Shader 的特定关键字
(2)禁用关键字:Material.DisableKeyword(string keyword)
作用: 禁用此 Material
实例上 Shader 的特定关键字。
(3)检查关键字状态:Material.IsKeywordEnabled(string keyword)
作用: 检查此 Material
实例上 Shader 的某个关键字是否处于激活状态。
2.全局材质
(1)启用关键字:Shader.EnableKeyword(string keyword)
作用: 激活一个全局着色器关键字。一旦激活,所有使用该关键字的 Shader 都会切换到对应的变体(如果该变体存在)。
(2)禁用关键字:Shader.DisableKeyword(string keyword)
作用: 禁用一个全局着色器关键字。
(3)检查关键字状态:Shader.IsKeywordEnabled(string keyword)
作用: 检查一个全局着色器关键字当前是否处于激活状态。
(4)重置所有全局关键字:Shader.DisableAllKeywords()
作用: 禁用所有当前激活的全局着色器关键字。这是一个比较少用的操作,因为它会影响所有 Shader 的行为。
二.着色器变体的限制
Unity中的着色器变体也存在着一些限制。
1.变体数量限制与关键字的关系
Unity 的变体数量限制主要体现在 关键字数量的限制 上,而不是直接的“变体”数量限制。这是因为每一个变体都是由一套激活的关键字组合来定义的。
(1)全局关键字限制:
Unity 对项目中所有着色器使用的全局关键字总数有一个限制。这个限制是 256 个。Unity 引擎自身也会使用大约 60 多个内部关键字(如用于光照、阴影、雾效等),这会进一步减少你可以使用的自定义关键字数量。无论是 multi_compile
还是 shader_feature
定义的关键字,只要它们是全局的(即没有使用 _local
后缀),都会计入这 256 个全局关键字的限制。
(2)局部关键字限制(Local Keywords):
从 Unity 2019.1 版本开始,引入了 局部关键字 的概念,通过 #pragma shader_feature_local
和 #pragma multi_compile_local
来声明。每个独立的 Shader 文件可以有最多 64 个独特的局部关键字。
局部关键字不计入全局关键字的 256 个限制。这是解决全局关键字限制的一种重要方式,尤其适用于那些只在该特定 Shader 内部使用的功能。注意:局部关键字不能与 Shader.EnableKeyword
或 CommandBuffer.EnableShaderKeyword
等全局关键字 API 一起使用。
2.变体爆炸 (Shader Variant Explosion)
虽然有关键字数量的限制,但更直接的问题是 “变体爆炸”。即使你没有达到关键字数量的硬性限制,但如果你使用了过多的 multi_compile 指令,或者在shader_feature中定义了太多未优化的组合,仍然会导致天文数字的变体数量:
变体数量是关键字组合的乘积。 例如,如果你有 10 个multi_compile 指令,每个指令定义了两个关键字,那么总的变体数量可能是 210=1024 个。如果其中一些是三个或更多关键字的组合,这个数字会呈指数级增长。
本篇完