Unity URP 下 Shader 变体 (Variants):multi_compile 与 shader_feature的关键字管理及变体爆炸防控策略

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_localshader_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
        }
    }
}

总结:决策清单

相关推荐
天人合一peng4 小时前
unity 生成标记根据背景色标记变色
unity·游戏引擎
天人合一peng8 小时前
unity 生成标记根据背景色变色为明显的颜色
unity·游戏引擎
魔士于安8 小时前
Unity 超市总动员 超市收银台 超市货架 超市购物手推车 超市常见商品
游戏·unity·游戏引擎·贴图·模型
CandyU29 小时前
Unity —— 数据持久化
unity·游戏引擎
zh路西法9 小时前
【Unity实现Oneshot胶卷显形】游戏窗口化与Win32API的使用
游戏·unity·游戏引擎
迪捷软件10 小时前
显控系统虚拟仿真的工程化路径
游戏引擎·cocos2d
凡情13 小时前
android隐私合规检测
android·unity
小贺儿开发14 小时前
Unity3D 本地 Stable Diffusion 文生图效果演示
人工智能·unity·stable diffusion·文生图·ai绘画·本地化
Swift社区14 小时前
传统游戏引擎 vs 鸿蒙 System 架构
架构·游戏引擎·harmonyos
mxwin1 天前
Unity Shader 半透明物体为什么不能写入深度缓冲?
unity·游戏引擎·shader