变体与 keyword:multi_compile vs shader_feature
一个 shader 文件编译出来往往不是「一个程序」,而是几十上百个程序——每个 keyword 组合一个变体。变体数量决定包体大小、编译时长、首帧卡顿,是 shader 工程里最容易被忽视的成本项。
变体是什么
#pragma multi_compile _ _NORMALMAP 声明一个 keyword:编译器生成两份完整代码(开/关各一份),运行时按 keyword 状态选用哪份。这不是运行时分支——每个变体是独立编译的程序,切换 keyword 只是切换程序,没有分支代价(对比见 /notes/unity/principles-4/)。
N 个布尔 keyword = 2^N 个变体,再乘以每个平台(DX11、Vulkan、GLES3……各编译一遍)。10 个 keyword × 5 平台 = 5120 份程序——这就是「一个 shader 导入半分钟、包体里 shader 占几百 MB」的来源。
multi_compile 与 shader_feature 的区别
multi_compile A B | shader_feature A B | |
|---|---|---|
| 保留哪些变体 | 全部保留 | 只保留场景里真有材质用到的 |
| 谁控制开关 | 引擎/代码全局设置(Shader.EnableKeyword) | 材质勾选/材质参数 |
| 典型用途 | 引擎级状态:阴影模式、lightmap 开关 | 材质级特性:法线贴图、自发光 |
| 风险 | 变体爆炸主力 | 运行时才开 keyword 的材质 → 变体已被裁掉,静默失效 |
URP 内置的 multi_compile _ _MAIN_LIGHT_SHADOWS ... 属引擎状态,必须全保留;自己加材质级开关时默认用 shader_feature。
_local:变体爆炸的第一解药
keyword 是全局字符串:两个不相干 shader 都定义 _ENABLE_X,Unity 会视为同一 keyword,变体组合互相牵连。shader_feature_local / multi_compile_local 把 keyword 作用域缩到单个 shader,互不污染——新代码一律用 _local 版本,这是减少全局变体污染最便宜的改动。
查看与裁剪
- Shader Inspector 直接显示「Compiles into X variants」;Compile and show variants 按钮逐变体列出 keyword 组合与每平台指令数/采样数/临时寄存器——这份数据同时是性能优化的输入(见 /notes/unity/performance-1/)。
- 引擎自动裁剪:URP asset 按质量档裁高特性变体(低档不编译级联阴影变体);未用的
shader_feature变体构建时剥离。 - 自查清单:把
multi_compile换shader_feature_local;删掉没人用的 Properties+keyword 对;一个 keyword 只控制一个特性(别用三个 keyword 表达一个三态选择——用一个枚举 keyword 组multi_compile A B C是 3 变体而不是 8)。
运行时 keyword 的坑
shader_feature的 keyword 在构建时按当时场景里的材质裁剪;AssetBundle/资源包里动态EnableKeyword打开一个构建时没用的组合 → 没有对应变体,效果静默不出现。资源包项目要么用Always Included Shaders/shadecabor 收集,要么改multi_compile。MaterialPropertyBlock传参数没问题,但不能跨变体切 keyword——它只改 uniform。- 首帧卡顿:变体在首次使用时才上传 GPU 编译(异步编译可缓解,预热用 Shader Variant Collection)。
决策速查
| 要加的开关 | 用什么 |
|---|---|
| 材质面板勾选(法线贴图、rim) | shader_feature_local |
| 引擎/管线状态(阴影、lightmap) | multi_compile(照抄 URP 库现成的) |
| 运行时按 Draw 变化的连续量 | 不是 keyword——uniform + 数学(lerp/step),见 /notes/unity/principles-4/ |
| 运行时切换的离散模式(少) | multi_compile_local + keyword 切换 |
自检三问
- 10 个布尔 keyword 为什么不是 10 个变体而是 1024 个?
- AssetBundle 里动态开 keyword 效果没出现,最可能缺什么?
- 三态选择用两个布尔 keyword 和用一个三值 keyword 组,变体数各是多少?