1什么是 Shader 变体在 GPU 着色器世界里预处理器宏Preprocessor Macro是代码复用的核心手段。Unity 会在构建期Build Time对每一组宏组合分别编译出一份独立的 Shader 程序这每一份都叫做一个Shader 变体Shader Variant。在运行时Unity 根据当前渲染状态光照模式、关键字是否开启等选择对应变体加载并执行。这种机制既保证了 GPU 代码的高度特化无动态分支开销又带来了一个长期困扰开发者的问题——变体数量的组合爆炸。2multi_compile全量编译关键字#pragma multi_compile告诉 Unity为这行声明的每一组关键字组合都编译一份变体构建时一个都不漏。语法// 布尔关键字off 变体 on 变体 #pragma multi_compile _ MY_FEATURE_ON // 枚举关键字互斥三选一 #pragma multi_compile QUALITY_LOW QUALITY_MEDIUM QUALITY_HIGH // 顶点着色器专用仅在 vertex pass 生效减少片元变体 #pragma multi_compile_vertex _ VERT_WIND_ON // 片元着色器专用 #pragma multi_compile_fragment _ FRAG_FOG_ON运行时控制// ── 材质级仅影响该 Material────────────────────────── material.EnableKeyword(MY_FEATURE_ON); material.DisableKeyword(MY_FEATURE_ON); // ── 全局级影响所有使用该 Shader 的 Material────────── Shader.EnableKeyword(QUALITY_HIGH); Shader.DisableKeyword(QUALITY_LOW); // ── CommandBuffer 级推荐渲染管线内精准控制───────── using UnityEngine.Rendering; CoreUtils.SetKeyword(cmd, MY_FEATURE_ON, isEnabled);⚠️multi_compile声明的变体无论场景中是否用到都会在构建时全部编译进包体。这是它与shader_feature最本质的区别。multi_compile_localUnity 2019.1 起提供multi_compile_local变体关键字作用域从全局降为材质本地避免全局关键字槽位最多 384 个被占满// 全局关键字 — 会消耗全局槽位跨所有 Shader 共享 #pragma multi_compile _ GLOBAL_FEATURE // 本地关键字 — 每个 Shader 独立最多 64 个本地关键字 #pragma multi_compile_local _ LOCAL_FEATURE // C# 配套材质本地关键字用 SetKeyword / GetLocalKeywords material.SetKeyword(new LocalKeyword(shader, LOCAL_FEATURE), true);3shader_feature按需编译关键字#pragma shader_feature的哲学截然不同只编译场景或构建中实际被材质使用的变体。如果没有任何 Material 开启某个关键字它对应的变体就不会出现在包体里。/ ShaderLab shader_feature 基本语法 // 布尔 shader_feature #pragma shader_feature _ _NORMALMAP // 枚举 shader_feature材质检查器 enum drawer 常用 #pragma shader_feature _SURFACE_TYPE_OPAQUE _SURFACE_TYPE_TRANSPARENT // 本地版本推荐避免占用全局关键字 #pragma shader_feature_local _ _EMISSION // 片元专用Unity 2020 支持 #pragma shader_feature_local_fragment _ _DETAIL_MULX2 _DETAIL_SCALEDshader_feature 的关键限制运行时动态切换风险若在运行时通过脚本开启一个shader_feature关键字而构建时没有任何 Material 使用它该变体将不存在Unity 会静默回退到最近可用变体——这可能引发渲染异常而不报错。解决方法将需要运行时动态切换的关键字改用multi_compile或显式将对应变体加入Shader Variant Collection。4两者核心差异对比维度multi_compileshader_feature编译策略所有组合全量编译仅编译被实际引用的变体包体大小影响⬆ 较大⬇ 较小运行时动态切换✓ 安全⚠ 需提前打包变体适用场景URP 内置特性阴影、雾效、光照模式材质属性开关Normal Map、Emission 等本地变体版本multi_compile_localshader_feature_local全局关键字槽位消耗全局槽若非 _local消耗全局槽若非 _local构建分析可见性Shader Variant Collection 中可见仅材质引用变体可见5变体爆炸成因与量化假设一个 Shader 声明了以下关键字组#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS #pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS #pragma multi_compile_fragment _ _REFLECTION_PROBE_BLENDING #pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH #pragma multi_compile _ LIGHTMAP_ON #pragma multi_compile _ _MIXED_LIGHTING_SUBTRACTIVE // ... // 理论变体数 3 × 3 × 2 × 2 × 4 × 2 × 2 × ... → 轻易超过 5006减少变体爆炸的七大策略策略 1 — shader_feature 优先凡是与材质属性绑定的开关Normal Map、Emission、Metallic 等一律用shader_feature_local而非multi_compile让 Unity 按需裁剪。策略 2 — 使用 _local 变体所有不需要全局切换的关键字都改用multi_compile_local或shader_feature_local节省宝贵的全局关键字槽位上限 384。策略 3 — 精简 URP Pipeline Asset在 URP Asset 中关闭项目不使用的特性Soft Shadows、Additional Lights、Reflection Probe Blending 等每关闭一项可消除数个 multi_compile 分支。策略 4 — strip_unused_variants在 URP Asset → Advanced → Shader Variant Log Level 设置为All配合IPreprocessShaders接口编写构建期剥离脚本主动删除不需要的变体。策略 5 — 枚举替代多布尔将多个布尔关键字A/B/C/D合并为一个枚举关键字MODE_A / MODE_B / MODE_C / MODE_D将 2⁴16 变体压缩到 4 变体。策略 6 — Shader Variant Collection使用 Window → Shader Variant Collection 工具将实际运行中遇到的变体录制为集合并在 Graphics Settings 预加载既减少卡顿也避免编译冗余变体。策略 7 — 动态分支 fallback对于高端平台部分简单特性可用uniform bool[branch]动态分支替代牺牲极少 GPU 性能换取大幅减少变体数——在移动端慎用。策略 4 深入IPreprocessShaders 剥离脚本using System.Collections.Generic; using UnityEditor.Build; using UnityEditor.Rendering; using UnityEngine; using UnityEngine.Rendering; /// summary /// 构建期 Shader 变体剥离器移除移动端用不到的高质量阴影变体 /// /summary public class MobileShaderVariantStripper : IPreprocessShaders { // callbackOrder 越小越先执行 public int callbackOrder 0; // 移动端不使用的高质量软阴影关键字 static readonly string[] kStripKeywords { _SHADOWS_SOFT_HIGH, _SHADOWS_SOFT_MEDIUM, _REFLECTION_PROBE_BOX_PROJECTION, }; public void OnProcessShader( Shader shader, ShaderSnippetData snippet, IListShaderCompilerData data) { // 仅在 Android / iOS 构建时剥离 if (!BuildHelper.IsMobileBuild()) return; for (int i data.Count - 1; i 0; i--) { ShaderKeywordSet keywords data[i].shaderKeywordSet; foreach (var kw in kStripKeywords) { if (keywords.IsEnabled(new ShaderKeyword(shader, kw))) { data.RemoveAt(i); break; } } } } }URP Asset 提供了丰富的选项每个选项背后对应若干multi_compile分支的存在与否URP Asset 设置项 → Shader 关键字映射✅对于移动端项目建议创建独立的 Mobile URP Asset在其中关闭所有高端特性通过 Quality Settings 在不同平台使用不同 Asset可大幅缩减移动包体的变体数量。8变体调试工具箱工具 1Shader Variant Log在URP Asset → Advanced → Shader Variant Log Level设置为All构建结束后 Console 中会打印每个 Shader 编译了多少变体// Unity 构建日志示例输出 Compiled shader Universal Render Pipeline/Lit in 12.34s d3d11 (total internal programs: 624, unique: 612) vulkan (total internal programs: 518, unique: 502) gles3 (total internal programs: 384, unique: 361)工具 2Editor 脚本统计变体using UnityEditor; using UnityEngine; public static class ShaderVariantCounter { [MenuItem(Tools/Count Shader Variants)] static void Count() { var shader Selection.activeObject as Shader; if (shader null) { Debug.LogError(请先选中一个 Shader); return; } int count ShaderUtil.GetVariantCount(shader, true); Debug.Log($[{shader.name}] 变体数量: {count}); } }工具 3Build Report 分析通过UnityEditor.Build.Reporting.BuildReport可在构建后拿到每个 Asset 的大小贡献配合BuildReportInspectorPackage Manager 中搜索可可视化查看 Shader 占比。9完整示例一个零爆炸自定义 URP Shader下面是一个遵循所有最佳实践的 URP 自定义 Lit Shader 骨架仅用shader_feature_local处理材质开关仅引入项目实际需要的 URP multi_compile控制变体总数在16 以内。Shader Custom/MyURPLit { Properties { // [Toggle] 属性对应 shader_feature_local 关键字 [Toggle(_NORMALMAP)] _UseNormalMap (Use Normal Map, Float) 0 [Toggle(_EMISSION)] _UseEmission (Use Emission, Float) 0 [Toggle(_ALPHATEST_ON)] _UseAlphaClip (Alpha Clip, Float) 0 // ... 其他贴图、颜色属性 ... } SubShader { Tags { RenderType Opaque RenderPipeline UniversalPipeline } Pass { Name ForwardLit Tags { LightMode UniversalForward } HLSLPROGRAM #pragma vertex MyVertexShader #pragma fragment MyFragmentShader // ── 材质属性开关用 shader_feature_local ────────── #pragma shader_feature_local _ _NORMALMAP #pragma shader_feature_local_fragment _ _EMISSION #pragma shader_feature_local_fragment _ _ALPHATEST_ON // → 以上 3 组最多产生 2×2×2 8 个材质变体 // ── URP 管线特性仅保留项目需要的 ─────────────── #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE #pragma multi_compile_fragment _ _SHADOWS_SOFT // ↑ 不支持软阴影高质量 → 不添加 _SHADOWS_SOFT_HIGH/_MED // → 以上 2 组3×2 6 个管线变体 // ── 光照贴图 ────────────────────────────────────── #pragma multi_compile _ LIGHTMAP_ON DIRLIGHTMAP_COMBINED // → 3 个光照贴图变体 // ── 总变体数上界8 × 6 × 3 144 ─────────────── // ── 实际使用 shader_feature只编译需要的 → 远小于上界 ─ #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl // ── 顶点着色器 ──────────────────────────────────── Varyings MyVertexShader(Attributes IN) { // ... 标准 URP 顶点变换 ... return OUT; } // ── 片元着色器 ──────────────────────────────────── half4 MyFragmentShader(Varyings IN) : SV_Target { #if defined(_NORMALMAP) // 法线贴图采样分支此分支在编译期消除无运行时开销 half3 normalTS UnpackNormal(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, uv)); #else half3 normalTS half3(0, 0, 1); #endif #if defined(_EMISSION) emission SAMPLE_TEXTURE2D(_EmissionMap, sampler_EmissionMap, uv).rgb * _EmissionColor; #endif // ... 其余 PBR 计算 ... return color; } ENDHLSL } } }总结决策清单