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

总结:决策清单

相关推荐
RReality3 小时前
【Unity Shader URP】全息扫描线(Hologram Scanline)源码+脚本控制
ui·unity·游戏引擎·图形渲染
渔民小镇5 小时前
一次编写到处对接 —— 为 Godot/Unity/React 生成统一交互接口
java·分布式·游戏·unity·godot
RReality18 小时前
【Unity Shader URP】序列帧动画(Sprite Sheet)实战教程
unity·游戏引擎
mxwin18 小时前
Unity URP 多线程渲染:理解 Shader 变体对加载时间的影响
unity·游戏引擎·shader
呆呆敲代码的小Y19 小时前
【Unity工具篇】| 游戏完整资源热更新流程,YooAsset官方示例项目
人工智能·游戏·unity·游戏引擎·热更新·yooasset·免费游戏
nainaire20 小时前
自学虚幻引擎记录1
游戏引擎·虚幻
想你依然心痛1 天前
HarmonyOS 5.0游戏开发实战:构建高性能2D休闲游戏引擎与 monetization 系统
华为·游戏引擎·harmonyos
黄思搏2 天前
基于标注平台数据的 Unity UI 自动化构建工作流设计与工程实践
ui·unity·蓝湖·vectoui
羊羊20352 天前
开发手札:Unity6000与Android交互
android·unity·android-studio