
Shader 变体搜集与剔除方案概述Shader 变体Shader Variant是 Unity 渲染体系中最重要的性能杠杆之一。每一个变体对应一组特定的#pragma关键字组合如_MAIN_LIGHT_SHADOWS、_ADDITIONAL_LIGHTS_VERTEX等未经控制的变体会使打包后的着色器数据暴涨轻则增大包体、加重运行时内存重则导致构建超时甚至失败。本教程围绕搜集Collection→ 分析Analysis→ 剔除Stripping三条主线展开结合 URP 和 DOTS 项目的实际场景提供一套可落地的方案。1. 理解 Shader 变体的来源1.1 变体是如何产生的着色器源码中的#pragma multi_compile和#pragma shader_feature会在构建时展开为变体列表#pragma multi_compile _ _MAIN_LIGHT_SHADOWS #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS_PIXEL #pragma shader_feature _ALPHATEST_ONmulti_compile所有关键字组合都会被构建无论项目是否使用。适合引擎级全局开关。shader_feature构建时只保留材质实际用到的关键字组合。适合材质级特性开关。1.2 URP 的内置变体URP 的Lit.shader、Unlit.shader等内置着色器自带大量multi_compile关键字。以 URP 14 为例// 光照系列 #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS_PIXEL #pragma multi_compile _ _FORWARD_PLUS // 阴影系列 #pragma multi_compile _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH // 雾效系列 #pragma multi_compile _ _FOG_FRAGMENT // DBuffer #pragma multi_compile _ _WRITE_RENDERING_LAYERS // 混合 lighting用于混合渲染 #pragma multi_compile _ _LIGHT_LAYERS #pragma multi_compile _ _RENDER_PASS_ENABLED // 屏幕空间 #pragma multi_compile _ SCREEN_COORD_OVERRIDE // LOD 过渡 #pragma multi_compile _ LOD_FADE_CROSSFADE即使不用这些组合也会被全量构建是变体膨胀的主要源头。2. 变体搜集2.1 自动化搜集管线变体搜集的核心目标是只保留运行时实际用到的变体。常用的方案是在构建前通过IPreprocessShaders回调进行追踪或者通过 ShaderVariantCollection 配合预设的材质列表进行采集。方案 AShaderVariantCollection Warmup通过ShaderVariantCollection声明项目中使用的变体并调用WarmUp()提前加载// 在启动或加载场景时触发publicclassShaderVariantWarmup:MonoBehaviour{publicShaderVariantCollectionvariantCollection;privatevoidStart(){variantCollection?.WarmUp();}}但手工维护 ShaderVariantCollection 很难。更实际的方案是通过构建时的采集管线自动生成。方案 BIPreprocessShaders 采集推荐利用 Unity 的IPreprocessShaders回调在 Build 编译着色器时拦截并记录所有出现的变体本项目的 DOTS 环境已包含此能力usingUnityEditor;usingUnityEditor.Build;usingUnityEditor.Rendering;usingUnityEngine;usingUnityEngine.Rendering;usingSystem.Collections.Generic;usingSystem.IO;usingSystem.Linq;publicclassShaderVariantCollector:IPreprocessShaders{publicintcallbackOrder0;// 收集到的变体privatestaticHashSetstringcollectedVariantsnewHashSetstring();privatestaticboolenabledtrue;publicvoidOnProcessShader(Shadershader,ShaderSnippetDatasnippet,IListShaderCompilerDatadata){if(!enabled)return;for(intidata.Count-1;i0;i--){varvariantdata[i];varkeywordsvariant.shaderKeywordSet.GetShaderKeywords().Where(kshader.IsKeywordEnabled(k)).Select(kk.name).OrderBy(kk).ToList();stringkey${shader.name}:{snippet.passType}/{snippet.shaderType}({string.Join(,,keywords)});collectedVariants.Add(key);}}[MenuItem(Tools/Dump Shader Variants)]publicstaticvoidDumpVariants(){enabledfalse;// 先停止采集避免递归stringpathShaderVariants.txt;varlinescollectedVariants.OrderBy(vv).ToList();File.WriteAllLines(path,lines);Debug.Log($Dumped{lines.Count}variants to{path});enabledtrue;}}运行一次 Full Build不一定要打包完成编译到中途即可后调用DumpVariants()就能拿到完整的变体清单。注意针对 DOTS 项目ECS 的渲染不经过传统 Material而是通过RenderMeshArrayMaterialMeshInfo下发所以采集时务必确保所有使用到的 Mesh/Material 组合都在场景或预制体中被引用。3. 变体分析3.1 分析收集结果收集到的变体列表应分为四类类别示例处理方式必用_MAIN_LIGHT_SHADOWS保留无用_ADDITIONAL_LIGHTS_VERTEXForward 不使用剔除冗余LOD_FADE_CROSSFADE全场景不用剔除可合并_SHADOWS_SOFT_LOW/MEDIUM/HIGH固定用 Medium保留一个3.2 常见冗余关键字DOTS 项目场景在 DOTS URP 项目中以下关键字通常是冗余的关键字原因LOD_FADE_CROSSFADEDOTS 渲染不使用 LOD Crossfade_ADDITIONAL_LIGHTS_VERTEXForward 只走 Pixel_FORWARD_PLUS与 Forward 绑定的关键字_WRITE_RENDERING_LAYERS项目不需要 Decal Layer / Rendering Layer_LIGHT_LAYERS项目中未使用 Light Layer_RENDER_PASS_ENABLEDNative RenderPass 默认关闭_SHADOWS_SOFT_LOW / _SHADOWS_SOFT_MEDIUM / _SHADOWS_SOFT_HIGH只需保留_SHADOWS_SOFT_FOG_FRAGMENT项目不开启逐像素雾4. 变体剔除4.1 URP Global Settings 自带的剔除开关在Project Settings Graphics URP Global Settings中本项目已开启选项作用Strip Debug Variants移除调试可视化关键字Strip Unused Post Processing Variants仅保留已启用的后处理特效变体Strip Unused Variants根据 Pipeline Asset 的 Quality 等级剔除变体Strip Unused LOD Cross Fade Variants移除 LOD Crossfade 变体这些是最基础的减负手段但仅靠它们不足以精细控制。4.2 自定义 Shader Preprocessor更精准的手段是通过IPreprocessShaders直接剔除不需要的变体本项目已接入此机制usingUnityEditor;usingUnityEditor.Build;usingUnityEditor.Rendering;usingUnityEngine;usingUnityEngine.Rendering;usingSystem.Collections.Generic;usingSystem.Linq;publicclassShaderVariantStripper:IPreprocessShaders{publicintcallbackOrder-100;// 优先执行privatestaticreadonlyDictionarystring,HashSetstringStripRulesnew(){[string.Empty]new()// 针对所有 Shader 的全局规则{LOD_FADE_CROSSFADE,_WRITE_RENDERING_LAYERS,_LIGHT_LAYERS,_RENDER_PASS_ENABLED,SCREEN_COORD_OVERRIDE,},[Universal Render Pipeline/Lit]new(){_ADDITIONAL_LIGHTS_VERTEX,_FORWARD_PLUS,},[Universal Render Pipeline/Unlit]new(){// Unlit 不需要光照相关变体},};publicvoidOnProcessShader(Shadershader,ShaderSnippetDatasnippet,IListShaderCompilerDatadata){varshaderNameshader.name;// 找出该 Shader 适用的剔除规则varrulesnewHashSetstring();if(StripRules.TryGetValue(string.Empty,outvarglobalRules))rules.UnionWith(globalRules);if(StripRules.TryGetValue(shaderName,outvarshaderRules))rules.UnionWith(shaderRules);if(rules.Count0)return;// 从 data 中移除匹配的变体for(intidata.Count-1;i0;i--){varkeywordsdata[i].shaderKeywordSet;if(rules.Any(rShaderKeyword.IsKeywordEnabled(keywords,r))){data.RemoveAt(i);}}}}注意事项callbackOrder控制执行顺序多个 Preprocessor 按 order 升序执行。剔除逻辑必须是确定性的——同一个版本的同一个 Build 应产生相同结果。如果某些材质确实用到了被剔除的变体Unity 会在运行时在 Log 中打印警告需要开启Shader Variant Logging可通过反复迭代完善规则。4.3 Conditional Stripping按 Quality Level / 平台不同平台PC vs Mobile和不同 Quality Level 可应用不同的剔除策略publicvoidOnProcessShader(Shadershader,ShaderSnippetDatasnippet,IListShaderCompilerDatadata){vartargetEditorUserBuildSettings.activeBuildTarget;for(intidata.Count-1;i0;i--){varkeywordsdata[i].shaderKeywordSet;// PC 保留级联阴影移动端只保留单级if(targetBuildTarget.Android||targetBuildTarget.iOS){if(ShaderKeyword.IsKeywordEnabled(keywords,_MAIN_LIGHT_SHADOWS_CASCADE))data.RemoveAt(i);}}}4.4 辅助函数检测 ShaderKeyword 是否启用ShaderKeywordSet没有直接的Contains(string)方法需要通过以下方式检测publicstaticclassShaderKeywordExtensions{publicstaticboolIsKeywordEnabled(thisShaderKeywordSetset,stringkeywordName){varlocalKeywordShaderKeyword.GetBuiltinKeyword(keywordName);// URP 内建关键字returnset.IsEnabled(localKeyword);}}注意Unity 2022.3 中ShaderKeyword的构造方式因关键字来源Builtin / Custom而异。建议统一使用ShaderKeyword(string)构造。5. Build 时自动触发将收集和剔除整合进 Build Pipeline确保每次构建都会执行usingUnityEditor.Build;// 自动在 Build 前开启采集构建后输出报告publicclassShaderVariantBuildProcessor:IPreprocessBuildWithReport,IPostprocessBuildWithReport{publicintcallbackOrder0;publicvoidOnPreprocessBuild(BuildReportreport){ShaderVariantCollector.Reset();// 清空上次记录ShaderVariantCollector.Enabledtrue;// 开启采集}publicvoidOnPostprocessBuild(BuildReportreport){ShaderVariantCollector.Enabledfalse;ShaderVariantCollector.DumpVariants();// 输出变体清单}}6. 验证流程6.1 构建前验证在编辑器中对每个 Quality Level 触发一次着色器编译查看编译的变体数量Window Analysis Shader Variants Viewer6.2 构建日志验证构建后检查 Editor.log搜索关键字Compiled shader ... with N variants对比优化前后的变体数量。6.3 运行时验证通过 Profiler 观察ShaderLab的内存占用以及着色器加载耗时Profiler Memory ShaderInfo Total Variants6.4 运行时缺失警告开启ShaderVariantLogLevel 2Full运行游戏覆盖所有场景检查 Console 中是否有变体缺失警告。确认无问题后在发布版本中关闭。7. 实践经验总结7.1 推荐的工作流1. 建立采集管线IPreprocessShaders 收集 2. 做一次完整的 Build中途即可导出变体清单 3. 分析清单标记冗余关键字 4. 实现剔除规则IPreprocessShaders 剔除 5. 再次 Build对比变体数量 6. 运行时全量覆盖测试确认无渲染异常 7. 将剔除规则纳入 CI每次构建自动应用7.2 常见陷阱过度剔除剔除的变体如果确实被某些 Material 用到运行时会出现紫色错误材质。跑一遍 QA 全流程就能暴露。ShaderVariantCollection 的热加载WarmUp()只影响加载速度不影响变体数量。打包时最终决定变体数量的是Stripping不是 Collection。DOTS 的渲染路径ECS 的RenderMeshArray不受传统 Material 引用保护先用一个 Editor 脚本收集所有 ECS 渲染所用的 Material 列表再反推变体需求。shader_featurevsmulti_compile自定义 Shader 中优先使用shader_feature构建系统会自动剔除无用的关键字组合。7.3 本项目的优化目标指标优化前预估优化后目标URP Lit Shader 变体数3000 500总着色器内存200 MB 60 MBAndroid 包体着色器部分80 MB 25 MB7.4效果8. 参考资源Unity Shader Variant Stripping 官方文档URP Shader Stripping 文档Unity IPreprocessShaders API