Unity游戏 Shader 变体搜集与剔除方案
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 配合预设的材质列表进行采集。
方案 A:ShaderVariantCollection + Warmup
通过ShaderVariantCollection声明项目中使用的变体,并调用WarmUp()提前加载:
// 在启动或加载场景时触发publicclassShaderVariantWarmup:MonoBehaviour{publicShaderVariantCollectionvariantCollection;privatevoidStart(){variantCollection?.WarmUp();}}但手工维护 ShaderVariantCollection 很难。更实际的方案是通过构建时的采集管线自动生成。
方案 B:IPreprocessShaders 采集(推荐)
利用 Unity 的IPreprocessShaders回调,在 Build 编译着色器时拦截并记录所有出现的变体(本项目的 DOTS 环境已包含此能力):
usingUnityEditor;usingUnityEditor.Build;usingUnityEditor.Rendering;usingUnityEngine;usingUnityEngine.Rendering;usingSystem.Collections.Generic;usingSystem.IO;usingSystem.Linq;publicclassShaderVariantCollector:IPreprocessShaders{publicintcallbackOrder=>0;// 收集到的变体privatestaticHashSet<string>collectedVariants=newHashSet<string>();privatestaticboolenabled=true;publicvoidOnProcessShader(Shadershader,ShaderSnippetDatasnippet,IList<ShaderCompilerData>data){if(!enabled)return;for(inti=data.Count-1;i>=0;i--){varvariant=data[i];varkeywords=variant.shaderKeywordSet.GetShaderKeywords().Where(k=>shader.IsKeywordEnabled(k)).Select(k=>k.name).OrderBy(k=>k).ToList();stringkey=$"{shader.name}:{snippet.passType}/{snippet.shaderType}({string.Join(",",keywords)})";collectedVariants.Add(key);}}[MenuItem("Tools/Dump Shader Variants")]publicstaticvoidDumpVariants(){enabled=false;// 先停止采集,避免递归stringpath="ShaderVariants.txt";varlines=collectedVariants.OrderBy(v=>v).ToList();File.WriteAllLines(path,lines);Debug.Log($"Dumped{lines.Count}variants to{path}");enabled=true;}}运行一次 Full Build(不一定要打包完成,编译到中途即可)后调用DumpVariants(),就能拿到完整的变体清单。
注意:针对 DOTS 项目,ECS 的渲染不经过传统 Material,而是通过
RenderMeshArray+MaterialMeshInfo下发,所以采集时务必确保所有使用到的 Mesh/Material 组合都在场景或预制体中被引用。
3. 变体分析
3.1 分析收集结果
收集到的变体列表应分为四类:
| 类别 | 示例 | 处理方式 |
|---|---|---|
| 必用 | _MAIN_LIGHT_SHADOWS | 保留 |
| 无用 | _ADDITIONAL_LIGHTS_VERTEX(Forward+ 不使用) | 剔除 |
| 冗余 | LOD_FADE_CROSSFADE(全场景不用) | 剔除 |
| 可合并 | _SHADOWS_SOFT_LOW/MEDIUM/HIGH(固定用 Medium) | 保留一个 |
3.2 常见冗余关键字(DOTS 项目场景)
在 DOTS + URP 项目中,以下关键字通常是冗余的:
| 关键字 | 原因 |
|---|---|
LOD_FADE_CROSSFADE | DOTS 渲染不使用 LOD Crossfade |
_ADDITIONAL_LIGHTS_VERTEX | Forward+ 只走 Pixel |
_FORWARD_PLUS | 与 Forward+ 绑定的关键字 |
_WRITE_RENDERING_LAYERS | 项目不需要 Decal Layer / Rendering Layer |
_LIGHT_LAYERS | 项目中未使用 Light Layer |
_RENDER_PASS_ENABLED | Native 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;// 优先执行privatestaticreadonlyDictionary<string,HashSet<string>>StripRules=new(){[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,IList<ShaderCompilerData>data){varshaderName=shader.name;// 找出该 Shader 适用的剔除规则varrules=newHashSet<string>();if(StripRules.TryGetValue(string.Empty,outvarglobalRules))rules.UnionWith(globalRules);if(StripRules.TryGetValue(shaderName,outvarshaderRules))rules.UnionWith(shaderRules);if(rules.Count==0)return;// 从 data 中移除匹配的变体for(inti=data.Count-1;i>=0;i--){varkeywords=data[i].shaderKeywordSet;if(rules.Any(r=>ShaderKeyword.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,IList<ShaderCompilerData>data){vartarget=EditorUserBuildSettings.activeBuildTarget;for(inti=data.Count-1;i>=0;i--){varkeywords=data[i].shaderKeywordSet;// PC 保留级联阴影,移动端只保留单级if(target==BuildTarget.Android||target==BuildTarget.iOS){if(ShaderKeyword.IsKeywordEnabled(keywords,"_MAIN_LIGHT_SHADOWS_CASCADE"))data.RemoveAt(i);}}}4.4 辅助函数:检测 ShaderKeyword 是否启用
ShaderKeywordSet没有直接的Contains(string)方法,需要通过以下方式检测:
publicstaticclassShaderKeywordExtensions{publicstaticboolIsKeywordEnabled(thisShaderKeywordSetset,stringkeywordName){varlocalKeyword=ShaderKeyword.GetBuiltinKeyword(keywordName);// URP 内建关键字returnset.IsEnabled(localKeyword);}}注意:Unity 2022.3+ 中,
ShaderKeyword的构造方式因关键字来源(Builtin / Custom)而异。建议统一使用ShaderKeyword(string)构造。
5. Build 时自动触发
将收集和剔除整合进 Build Pipeline,确保每次构建都会执行:
usingUnityEditor.Build;// 自动在 Build 前开启采集,构建后输出报告publicclassShaderVariantBuildProcessor:IPreprocessBuildWithReport,IPostprocessBuildWithReport{publicintcallbackOrder=>0;publicvoidOnPreprocessBuild(BuildReportreport){ShaderVariantCollector.Reset();// 清空上次记录ShaderVariantCollector.Enabled=true;// 开启采集}publicvoidOnPostprocessBuild(BuildReportreport){ShaderVariantCollector.Enabled=false;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 = 2(Full),运行游戏覆盖所有场景,检查 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 MB |
| Android 包体着色器部分 | 80+ MB | < 25 MB |
7.4效果
8. 参考资源
- Unity Shader Variant Stripping 官方文档
- URP Shader Stripping 文档
- Unity IPreprocessShaders API
