#pragma multi_compile是 Unity Shader 里用来生成**多个 shader 变体(variants)**的机制。理解它,能帮你搞懂"为什么一个 shader 文件能应对开关阴影、开关雾效、切换光照模式等无数种情况"。
一、先搞懂核心问题:为什么需要 shader 变体?
假设你写一个 shader,想支持"雾效开/关"两种情况。最笨的办法是写两个 shader 文件。但这样太蠢了——代码几乎一样,只差几行。
更聪明的做法是在一份代码里用条件编译:
#ifdef FOG_ON color = ApplyFog(color); // 开雾时才执行 #endif问题是:FOG_ON这个宏,是"开"还是"关"?
如果写死,那就只能编译出一种情况。我们希望的是——同一份代码,编译出"开雾"和"关雾"两个版本,运行时按需切换。
这就是multi_compile干的事:让编译器把一份代码,按关键字的不同取值,编译成多个版本(变体)。
二、基本用法
#pragma multi_compile FOG_OFF FOG_ON这行的意思是:
定义一组关键字
FOG_OFF和FOG_ON,编译器会分别编译两个版本:
- 版本 1:定义了
FOG_OFF- 版本 2:定义了
FOG_ON
配合#ifdef使用:
#pragma multi_compile FOG_OFF FOG_ON // ... 顶点/片元着色器里 ... fixed4 frag(...) : SV_Target { fixed4 color = tex2D(_MainTex, i.uv); #ifdef FOG_ON color = ApplyFog(color); // 只有 FOG_ON 版本会包含这段 #endif return color; }编译结果:Unity 生成了两个 shader 变体。运行时通过代码决定用哪个:
// C# 端切换material.EnableKeyword("FOG_ON");// 用开雾版本material.DisableKeyword("FOG_ON");// 用关雾版本(回到 FOG_OFF)三、几种写法及其含义
1. 普通多选一
#pragma multi_compile _A _B _C生成3 个变体,分别定义_A、_B、_C。
2. 用下划线表示"什么都不定义"
#pragma multi_compile _ FANCY_EFFECT- 第一个
_是个占位符,代表不定义任何关键字(默认版本) - 第二个是
FANCY_EFFECT
生成 2 个变体:一个啥也没定义,一个定义了FANCY_EFFECT。
这是开关型关键字的常见写法:
_= 关,FANCY_EFFECT= 开。
3. 多组关键字(会相乘!)
#pragma multi_compile _ SHADOWS_ON #pragma multi_compile _ FOG_ON这不是 4 个变体,而是 2×2 = 4 个组合变体:
① 无阴影 + 无雾 ② 有阴影 + 无雾 ③ 无阴影 + 有雾 ④ 有阴影 + 有雾⚠️关键警告:多组关键字是相乘关系。每加一组,变体数量翻倍。这会导致后面要讲的"变体爆炸"问题。
四、multi_compile vs shader_feature(重要区别)
Unity 里有两个很像的指令,新手常搞混:
#pragma multi_compile _ MY_KEYWORD #pragma shader_feature _ MY_KEYWORD它们语法一样,行为也几乎一样,唯一区别在打包(build)时:
multi_compile | shader_feature | |
|---|---|---|
| 打包时 | 所有变体都保留 | 只保留"实际被用到"的变体 |
| 适用场景 | 运行时动态切换的关键字 | 只在材质面板上勾选、且打包后不变的关键字 |
| 未使用的变体 | 仍打进包里 | 被剔除,节省包体 |
通俗理解
shader_feature:如果没有任何材质用到MY_KEYWORD这个版本,打包时就把它扔掉。适合"美术在 Inspector 里勾选的开关"——因为最终用哪些是确定的。multi_compile:无论如何都保留所有变体。适合"运行时用 C# 代码EnableKeyword动态切换"的情况——因为编译器不知道运行时会用哪个,只能全留着。
一句话选择
运行时代码动态开关关键字 → multi_compile(必须全保留) 美术在材质面板勾选、打包后固定 → shader_feature(可剔除未用的,省包体)五、变体爆炸(Variant Explosion)——最大的坑
前面说过,多组关键字是相乘的。看这个例子:
#pragma multi_compile _ A B // 3 种 #pragma multi_compile _ C D // 3 种 #pragma multi_compile _ E F G // 4 种 #pragma multi_compile _ H // 2 种总变体数 = 3 × 3 × 4 × 2 =72 个变体!
而 Unity 自带的一些内置multi_compile(如阴影、光照贴图、实例化)也会叠加进来。真实项目里,一个 shader 编译出上千甚至上万个变体是常事。
变体爆炸的危害
变体太多 → ① 打包时间暴增(编译上万个变体,几十分钟起步) → ② 包体变大(每个变体都是一段编译后的代码) → ③ 运行时加载/切换变体有卡顿如何缓解
1. 优先用shader_feature(能自动剔除未用变体)
2. 用_local变体(Unity 2019.1+):
#pragma multi_compile_local _ MY_KEYWORD_local表示这个关键字只在本 shader 内部有效,不占用全局关键字槽位(Unity 全局关键字数量有限,早期是 256 个上限)。
3. 主动剔除不需要的组合:用 Unity 的IPreprocessShaders接口在打包时过滤掉用不到的变体。
4. 减少关键字组数:能合并的尽量合并,别滥用。
六、Unity 内置的常用 multi_compile
Unity 提供了一些快捷指令,一行展开成一整组内置关键字:
#pragma multi_compile_fog // 展开雾效相关关键字(FOG_LINEAR/EXP/EXP2) #pragma multi_compile_fwdbase // 前向渲染 base pass 需要的一整套 #pragma multi_compile_fwdadd // 前向渲染 additive pass #pragma multi_compile_instancing // GPU Instancing 支持 #pragma multi_compile_shadowcaster // 阴影投射 pass例如#pragma multi_compile_fog等价于展开雾效的多种模式变体,你不用手写。
七、C# 端如何控制关键字
// 针对某个材质material.EnableKeyword("FOG_ON");material.DisableKeyword("FOG_ON");boolisOn=material.IsKeywordEnabled("FOG_ON");// 全局开关(影响所有用到该关键字的 shader)Shader.EnableKeyword("GLOBAL_EFFECT");Shader.DisableKeyword("GLOBAL_EFFECT");URP/HDRP 里更推荐用
LocalKeyword结构体(配合_local变体),性能和管理更好。
八、完整示例
Shader "Custom/MultiCompileDemo" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag // 声明关键字组:开关雾效 #pragma multi_compile _ FOG_ON // 声明关键字组:三选一的光照模式 #pragma multi_compile LIGHT_SIMPLE LIGHT_NORMAL LIGHT_ADVANCED #include "UnityCG.cginc" sampler2D _MainTex; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert(appdata_base v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord; return o; } fixed4 frag(v2f i) : SV_Target { fixed4 color = tex2D(_MainTex, i.uv); // 根据光照模式关键字走不同分支 #if defined(LIGHT_ADVANCED) color.rgb *= 1.5; // 高级光照 #elif defined(LIGHT_NORMAL) color.rgb *= 1.0; // 普通光照 #else // LIGHT_SIMPLE color.rgb *= 0.8; // 简单光照 #endif // 雾效开关 #ifdef FOG_ON color.rgb = lerp(color.rgb, fixed3(0.5,0.5,0.5), 0.3); #endif return color; } ENDCG } } }这个 shader 会生成2(雾开关)× 3(光照模式)= 6 个变体。
九、总结速记
multi_compile 是什么? → 让一份 shader 代码,按关键字取值编译出多个变体,运行时切换 核心语法: #pragma multi_compile _ KEYWORD_A KEYWORD_B #ifdef KEYWORD_A ... #endif 关键要点: ① 多组关键字是【相乘】关系 → 小心变体爆炸 ② _ 表示"不定义任何关键字"(常用作默认/关) ③ multi_compile = 全部保留(运行时动态切换用) shader_feature = 剔除未用(材质面板勾选、打包固定用) ④ _local 变体不占全局关键字槽位,优先使用 避免变体爆炸: 优先 shader_feature → 用 _local → 打包剔除 → 精简关键字组一句话:
multi_compile是"用编译期分身术换取运行期灵活性"——牺牲编译时间和包体,换来一份代码应对多种运行时状态的能力。用好它的关键,是时刻警惕变体数量的乘法爆炸。