1什么是 Shader 变体?
在 GPU 着色器世界里,预处理器宏(Preprocessor Macro) 是代码复用的核心手段。Unity 会在 构建期(Build Time) 对每一组宏组合分别编译出一份独立的 Shader 程序,这每一份都叫做一个 Shader 变体(Shader Variant)。
在运行时,Unity 根据当前渲染状态(光照模式、关键字是否开启等)选择对应变体加载并执行。这种机制既保证了 GPU 代码的高度特化(无动态分支开销),又带来了一个长期困扰开发者的问题------变体数量的组合爆炸。

2multi_compile:全量编译关键字
#pragma multi_compile 告诉 Unity:"为这行声明的每一组关键字组合,都编译一份变体,构建时一个都不漏。"
语法
cs
// 布尔关键字(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
运行时控制
cs
// ── 材质级(仅影响该 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_local
Unity 2019.1 起提供 multi_compile_local 变体,关键字作用域从 全局 降为 材质本地,避免全局关键字槽位(最多 384 个)被占满:
cs
// 全局关键字 --- 会消耗全局槽位,跨所有 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 开启某个关键字,它对应的变体就不会出现在包体里。
cs
/ 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_SCALED
shader_feature 的关键限制
🚨
运行时动态切换风险: 若在运行时通过脚本开启一个 shader_feature 关键字,而构建时没有任何 Material 使用它,该变体将 不存在,Unity 会静默回退到最近可用变体------这可能引发渲染异常而不报错。
解决方法:将需要运行时动态切换的关键字改用 multi_compile,或显式将对应变体加入 Shader Variant Collection。
4两者核心差异对比

| 维度 | multi_compile | shader_feature |
|---|---|---|
| 编译策略 | 所有组合全量编译 | 仅编译被实际引用的变体 |
| 包体大小影响 | ⬆ 较大 | ⬇ 较小 |
| 运行时动态切换 | ✓ 安全 | ⚠ 需提前打包变体 |
| 适用场景 | URP 内置特性(阴影、雾效、光照模式) | 材质属性开关(Normal Map、Emission 等) |
| 本地变体版本 | multi_compile_local |
shader_feature_local |
| 全局关键字槽位 | 消耗全局槽(若非 _local) | 消耗全局槽(若非 _local) |
| 构建分析可见性 | Shader Variant Collection 中可见 | 仅材质引用变体可见 |
5变体爆炸:成因与量化
假设一个 Shader 声明了以下关键字组:
cs
#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 × ... → 轻易超过 500+

6减少变体爆炸的七大策略
策略 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 剥离脚本
cs
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,
IList<ShaderCompilerData> 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变体调试工具箱
工具 1:Shader Variant Log
在 URP Asset → Advanced → Shader Variant Log Level 设置为 All,构建结束后 Console 中会打印每个 Shader 编译了多少变体:
cs
// 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)
工具 2:Editor 脚本统计变体
cs
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}");
}
}
工具 3:Build Report 分析
通过 UnityEditor.Build.Reporting.BuildReport 可在构建后拿到每个 Asset 的大小贡献,配合 BuildReportInspector(Package Manager 中搜索)可可视化查看 Shader 占比。
9完整示例:一个零爆炸自定义 URP Shader
下面是一个遵循所有最佳实践的 URP 自定义 Lit Shader 骨架:仅用 shader_feature_local 处理材质开关,仅引入项目实际需要的 URP multi_compile,控制变体总数在 16 以内。
cs
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
}
}
}
总结:决策清单
