
1. 项目概述为什么我们需要Shader Stripping如果你是一个Unity开发者尤其是负责过项目上线前打包的TA或者主程肯定对动辄几十分钟甚至几个小时的构建时间深恶痛绝。每次修改一点代码想打个包测试一下结果光是等待构建完成就能泡杯咖啡、刷会儿手机开发节奏被严重拖慢。这背后一个巨大的“元凶”就是Shader变体Shader Variants的爆炸式增长。Unity的Shader系统非常强大它允许你通过关键字Keywords来定义不同的渲染路径、功能开关比如是否接收阴影、是否有法线贴图从而生成一个Shader的多个“变体”。一个看似简单的Standard Shader在不同的渲染管线Built-in, URP, HDRP、不同的平台PC, Android, iOS、不同的质量等级高、中、低下会组合出成千上万个变体。Unity在构建时为了确保你的游戏在任何可能的配置下都能正常运行默认会把这些它认为“可能用到”的变体全部编译并打包进去。结果就是最终的构建包APK/IPA/EXE里塞满了大量你项目中根本用不到的Shader代码不仅让包体臃肿不堪更严重的是编译这些海量变体消耗了构建过程中绝大部分的时间。“UnityShaderStripper”这个项目就是专门为了解决这个问题而生的利器。它不是一个官方的Unity功能而是一个由社区开发者“SixWays”创建的开源工具。它的核心目标非常明确让你能够精确地控制在最终的游戏包中只包含那些真正被用到的Shader变体从而大幅削减构建时间和最终包体大小。你可以把它想象成一个构建时的“Shader代码清洁工”它会仔细检查你的项目然后无情地剔除所有冗余的、未被引用的Shader变体。根据项目复杂度和原有Shader设置的不同使用它通常能为构建时间带来30%到70%的惊人提升同时让包体缩小几十甚至上百MB。这对于追求敏捷开发和优化包体大小的团队来说价值不言而喻。2. 核心原理与工作流程拆解在深入解决具体问题之前我们必须先搞清楚UnityShaderStripper是如何工作的。理解了这个你才能明白后续所有配置和问题的根源。2.1 Shader变体编译的“黑盒”与痛点Unity默认的Shader编译行为我们可以称之为“黑盒式”或“防御式”编译。它的逻辑大致是Unity会遍历项目中所有的Shader文件以及所有Material材质球上设置的关键字然后根据当前项目的Graphics Settings图像设置和Player Settings播放器设置中定义的“变体集合”去预计算所有可能的组合。比如你的Shader定义了_NORMALMAP法线贴图这个关键字Unity就会为“启用法线贴图”和“不启用法线贴图”这两种状态各生成一个变体即使你的项目中没有一个材质球真正开启了_NORMALMAP。这种机制保证了兼容性但付出了巨大的代价。一个常见的URP Lit Shader配合几个渲染特性关键字和不同的渲染管线设置轻松就能产生数千个变体。编译它们是一个CPU和内存密集型任务是构建流程中最耗时的部分之一。2.2 UnityShaderStripper的“外科手术式”剔除方案UnityShaderStripper的核心思路是介入Unity的构建管线在Shader变体被最终编译和打包之前进行一轮精确的“外科手术式”剔除。它主要依赖两个Unity提供的底层回调接口IPreprocessShaders.OnProcessShader: 这是最核心的接口。Unity在编译每个Shader的每个变体时都会调用这个方法。在这个回调里你可以拿到当前正在处理的Shader变体的所有信息包括其使用的Shader对象、Pass类型、以及所有启用的关键字。UnityShaderStripper在这里判断根据用户定义的规则这个变体是否应该被保留如果规则判定为剔除则直接“丢弃”这个变体Unity就不会编译它。IPreprocessComputeShaders.OnProcessComputeShader: 作用类似但是针对Compute Shader的。那么判断的“规则”从哪里来这就是UnityShaderStripper设计的巧妙之处。它允许你在Unity编辑器中创建一种特殊的资产文件——ShaderStripper资产。在这个资产的Inspector面板里你可以可视化的方式组合多种规则Ruleset例如路径规则Path: 只处理特定路径下的Shader如Assets/Art/Shaders/。平台规则Platform: 只针对特定平台如Android、iOS生效。层级规则Tier: 根据Graphics Tier图形层级设置来区分规则。关键字规则Keyword: 强制保留或剔除包含特定关键字的变体。变体集合规则Variant Collection: 引用一个Unity官方的ShaderVariantCollection资产只保留这个集合里记录的变体其他全部剔除。这是最常用、最精准的规则。工作流程可以总结为创建规则资产 - 规则在构建时被自动加载 - 每个Shader变体经过规则过滤器 - 被判定保留或剔除 - 最终只有保留的变体进入编译和打包流程。3. 安装、配置与基础使用指南3.1 项目安装与导入UnityShaderStripper的安装非常直接因为它就是一个纯粹的Editor工具不包含任何运行时代码。获取工具访问GitHub上SixWays的UnityShaderStripper仓库。你可以直接下载最新的Release压缩包或者克隆仓库到本地。导入Unity项目将下载的UnityShaderStripper文件夹通常包含Editor目录和一系列.cs脚本文件直接拖入你项目的Assets目录下的任意位置例如Assets/Plugins/或Assets/EditorTools/。我个人的习惯是放在Assets/Editor/UnityShaderStripper下方便管理。验证安装导入后Unity会重新编译Editor脚本。如果安装成功你在Project窗口右键菜单Create中应该能找到一个新的类别Shader Stripper里面可以创建各种类型的Stripper资产。注意确保你的Unity版本与该工具兼容。一般来说支持2019.4 LTS及以上的版本。如果导入后报错请检查Unity版本并查看GitHub仓库的Issues页面。3.2 创建你的第一个ShaderStripper规则理论说再多不如动手操作。我们来创建一个最常用、也最有效的规则——基于ShaderVariantCollection的精准剔除。生成Shader变体集合这是最关键的一步。你需要在编辑器模式下以某种方式“收集”到你游戏中所有实际被用到的Shader变体。有两种主流方法手动/半自动收集确保所有场景都被打开并加载过包括通过Addressables动态加载的然后使用Unity菜单栏的Window - Analysis - Shader Variant Collection工具。点击Save all shader variants它会将当前已加载场景中所有材质用到的变体保存到一个.shadervariants资产中。但这个方法有缺陷它只能收集当前打开场景的对于动态加载的资源可能覆盖不全。构建管线收集推荐更可靠的方法是在开发阶段先不打剔除正常构建一次游戏比如Development Build。在构建日志中Unity会输出所有实际被编译的Shader变体信息。有一些工具或脚本可以解析这个日志并生成ShaderVariantCollection。UnityShaderStripper的Wiki也提到了这种方法。这是确保“收集全”的最佳实践。创建Stripper资产在Project窗口右键Create - Shader Stripper - Shader Stripper。给它起个名字比如MainShaderStripper。配置规则选中刚创建的资产在Inspector面板中点击Add Ruleset。在新建的Ruleset中Path: 可以留空表示所有Shader或指定你的Shader目录。Platform: 选择对应的目标平台如Android。点击Add Stripper按钮选择Variant Collection。将上一步生成的ShaderVariantCollection资产拖入对应的Slot中。确保Mode设置为Use Collection默认。这个模式的意思是只保留出现在这个Collection中的变体其他全部剔除。应用到构建你不需要做任何额外的操作。UnityShaderStripper会自动扫描项目中所有的ShaderStripper资产并在构建时应用它们。只需像往常一样点击Build即可。3.3 配置策略与规则组合实战单一规则往往不够用我们需要组合策略来应对复杂情况。场景一针对不同平台使用不同的优化策略你的项目要发布到PCStandalone和移动端Android/iOS。PC平台包体大小不敏感但需要高画质移动端则极度敏感。解决方案创建两个Ruleset放在同一个或不同的Stripper资产里。Ruleset A:Platform Standalone, 使用一个包含所有PC端高画质变体的ShaderVariantCollection或者甚至可以不使用Variant Collection规则而使用Keyword规则只剔除一些完全用不到的关键字如_MOBILE相关的简化关键字。Ruleset B:Platform Android, iOS, 使用一个为移动端精心收集的、只包含必需变体的ShaderVariantCollection并可以额外添加Keyword规则强制剔除_REFLECTION_PROBE_BLENDING等高性能消耗的关键字。这样在打Android包时只有Ruleset B生效执行严格的剔除打PC包时Ruleset A生效保留更多变体。场景二为不同图形质量等级准备不同变体你的游戏有“低、中、高”三档画质设置在Graphics Settings里设置了对应的Graphics Tier。解决方案利用Tier规则。Ruleset 1:Tier 1 (Low) 使用为低画质收集的变体集合并强制剔除_PARALLAXMAP视差贴图、_DETAIL_MULX2细节贴图等关键字。Ruleset 2:Tier 2 (Medium) 使用中画质变体集合。Ruleset 3:Tier 3 (High) 使用高画质变体集合。这样构建时所有变体都会被收集但根据目标Tier只有对应Ruleset的剔除规则会生效。最终包体里会包含所有三个Tier的变体但每个Tier下的变体都是最精简的。一个综合的Inspector配置示例看起来可能是这样的// 这不是代码是规则描述的示意 Shader Stripper Asset: “MyGame_Stripper” | ├── Ruleset 1: “Mobile_Strict” │ ├── Platforms: Android, iOS │ ├── Tier: Any │ └── Stripper: Variant Collection (引用 “Mobile_CollectedVariants.shadervariants”) │ └── Mode: Use Collection (只保留集合内的) | ├── Ruleset 2: “PC_High” │ ├── Platforms: StandaloneWindows64 │ ├── Tier: 3 │ └── Stripper: Keyword │ ├── Mode: Remove │ └── Keywords: _MOBILE, _SIMPLE_LIGHTING (剔除移动端和简易光照关键字) | └── Ruleset 3: “PC_Low” ├── Platforms: StandaloneWindows64 ├── Tier: 1, 2 └── Stripper: Variant Collection (引用 “PC_LowTier_Variants.shadervariants”)4. 常见问题、陷阱与解决方案实录在实际项目中使用UnityShaderStripper你几乎一定会遇到下面这些问题。我把它们和解决方案整理出来希望能帮你省下大量排查时间。4.1 问题构建后游戏运行时出现“粉红/洋红色”错误材质现象构建出来的游戏在运行时某些物体变成了标志性的粉红色Shader错误颜色。原因这是最经典的问题根本原因是过度剔除。你定义的剔除规则过于激进把某个运行时实际需要的Shader变体给删除了。比如你的ShaderVariantCollection没有收集全。动态加载的资源、通过代码Material.EnableKeyword启用的特效、或者某些特定条件下才出现的材质其变体可能没有被收集到。规则配置错误。例如为Android平台配置的规则意外地应用到了PC平台的构建上剔除了PC所需的高配变体。使用了Keyword规则强制剔除某个关键字但游戏中确有材质在特定情况下需要它。解决方案与排查步骤定位罪魁祸首当出现粉红材质时首先在编辑器中运行游戏Development Build模式查看Console窗口。Unity通常会输出具体的错误信息告诉你哪个Shader的哪个Pass缺少了哪个变体Missing shader variant。记下Shader名字和缺失的关键字组合。检查变体集合打开你用于剔除的ShaderVariantCollection资产搜索那个出错的Shader。看看里面是否包含了报错信息中指明的那个具体的关键字组合。如果没有说明收集不全。完善收集流程确保全场景覆盖在收集变体前通过脚本或手动方式确保所有游戏场景包括启动器、关卡、UI场景都被加载一遍。触发所有代码路径检查所有通过代码EnableKeyword或global shader keyword启用关键字的逻辑确保在收集变体时这些代码被执行过。使用构建日志分析最可靠进行一次不启用Stripper的Development Build保存完整的构建日志。使用脚本工具社区有开源工具或自己写一个日志解析器分析日志中“Compiling shader...”部分提取出所有实际编译的变体列表并以此生成或补充你的ShaderVariantCollection。这是保证收集范围与最终构建需求完全匹配的金标准。调整规则策略如果只是个别变体缺失不建议放宽整个规则。更好的方法是找到缺失变体对应的材质和使用场景。如果该变体确实必要确保它被包含在ShaderVariantCollection中。或者为该Shader单独创建一个更宽松的规则。例如可以添加一个额外的Ruleset使用Path规则定位到那个特定的Shader文件然后使用Keyword规则显式地Keep保留那个缺失的关键字这样即使主规则剔除了它这个特殊规则也会把它保下来。4.2 问题构建时间没有减少甚至反而增加了现象配置了UnityShaderStripper但打包速度感觉没什么变化或者有时在构建初期似乎卡顿更久了。原因分析规则配置过于复杂或低效如果你创建了大量复杂的规则特别是很多正则表达式路径匹配在构建初期Stripper需要解析和应用这些规则这会增加一些开销。如果规则最终没有剔除多少变体那么这点开销就成了净损失。变体集合ShaderVariantCollection本身巨大Unity在构建时需要加载和处理你指定的ShaderVariantCollection文件。如果这个文件里记录了数万个变体处理它本身也需要时间。你的规则是“只保留集合内的”那么Unity仍然需要将这个集合与所有可能的变体进行比对这个过程可能比想象中耗时。没有真正剔除核心的冗余变体构建时间的大头是编译那些数量巨大的、未被引用的变体。如果你的ShaderVariantCollection是通过“全场景加载”方式收集的而你的场景又加载了几乎所有资源那么这个集合本身可能就很大接近Unity默认的“防御式”集合。剔除掉的变体数量有限节省的时间自然不明显。解决方案量化分析不要凭感觉。在Unity Editor Log中搜索关键字“Shader stripping”或“Variants stripped”。UnityShaderStripper和Unity自身都会输出日志告诉你初始有多少变体最终剔除了多少。例如“Stripped X shader variants out of Y total (Z%)”。如果Z%很低比如低于20%那节省时间不多是正常的。优化变体集合审查你的ShaderVariantCollection。使用Unity的Shader Variant Collection预览窗口可以看到每个Shader的变体数量。尝试找出变体数量异常多的Shader思考这些变体是否都是必需的。也许你的Shader本身写了太多不必要的#pragma multi_compile。优化Shader代码减少不必要的关键字组合是从源头上解决问题。采用分平台精细化配置不要用一个巨大的变体集合通用于所有平台。严格按照3.3节的策略为移动端配置一个极其精简的集合。确保移动端集合里只包含移动端渲染路径如URP的Forward渲染器和移动端质量关键字下的变体。这样针对移动端构建时剔除率才能上去。平衡规则复杂度与收益如果项目Shader不多可以尝试不使用庞大的Variant Collection而是改用针对性的Keyword剔除规则。例如明确知道项目用不到_PARALLAXMAP和_EMISSION就直接写规则剔除它们。这样规则简单开销小对于特定冗余变体的剔除效率高。4.3 问题动态加载的材质或特效显示异常现象通过AssetBundle或Addressables动态加载的模型、特效其材质显示不正确但编辑器下正常。原因这是“收集不全”问题的典型动态加载版本。构建Player时Unity只会处理包含在构建场景中的资源以及明确标记在“Resources”文件夹或构建设置中的资源。动态加载的资源除非在构建时以某种方式被“引用”到否则它们使用的Shader变体不会被Unity默认包含在构建的Shader变体集合中。如果你用了一个只包含静态场景变体的集合去做剔除那么动态资源所需的变体就被剔除了。解决方案确保动态资源被“引用”Unity判断一个Shader变体是否需要被编译是基于是否有任何被构建进包体的资源引用了它。因此你需要确保至少有一个“桩”资源被直接构建进包体并且这个“桩”资源引用了动态资源所需的所有Shader变体。常见做法是创建一个隐藏的、永不渲染的场景里面放置使用所有动态材质变体的“预览”物体。或者创建一个专门的ShaderVariantCollection资产手动将动态资源用到的Shader和关键字添加进去并将这个资产放在Resources文件夹或直接拖入项目的Preloaded Assets中。在收集阶段模拟动态加载在编辑器模式下运行一个脚本在按下Play后模拟游戏完整的资源加载流程确保所有通过Addressables或AssetBundle加载的材质都被实例化并应用到渲染物体上。然后在这个状态下使用ShaderVariantCollection工具的“Save all”功能。这样收集到的集合就包含了动态资源的变体。使用AssetBundle变体收集工具有一些第三方工具或自定义脚本可以扫描指定AssetBundle中包含的所有材质并提取出它们使用的Shader和关键字自动生成或补充ShaderVariantCollection。这对于大型项目管理动态资源至关重要。4.4 问题与SRP Batcher、GPU Instancing等优化机制的冲突现象启用Shader Stripping后GPU性能统计中SRP Batcher的合批效率下降或者GPU Instancing失效。原因SRP Batcher和GPU Instancing对Shader的编译方式有特定要求。为了合批Unity需要Shader变体在常量缓冲区CBUFFER的布局上保持一致。如果两个材质使用同一个Shader的不同变体例如一个启用了_EMISSION一个没有但只要这些变体来自于同一个“Shader变体集合”并且编译时#pragma指令兼容它们通常还是可以合批的。风险点在于激进的Shader Stripping可能会把某些“中间状态”的变体剔除掉。比如Shader代码中有一组multi_compileA B C。你的场景中只有使用A和C变体的材质。如果你用Variant Collection精确地只保留A和C那么变体B就被剔除了。这本身没问题。但是如果Shader的某些#pragma指令与SRP Batcher相关的排列组合依赖于这些关键字剔除B可能会影响Unity内部为SRP Batcher生成的“变体集合”的完整性理论上可能导致一些预期外的合批失败。解决方案与建议优先保证功能正确首先确保游戏画面渲染正确没有粉红错误性能优化是第二步。性能对比测试在启用Stripping前后使用Unity的Frame Debugger和Profiler在相同的游戏场景下对比Draw Calls、SRP Batches和SetPass Calls的数量。如果数据没有显著劣化则无需担心。谨慎使用“精确剔除”模式对于非常核心、使用频繁的Shader如URP Lit、Simple Lit如果你观察到合批问题可以尝试对这个Shader采用更保守的剔除策略。例如不使用“Use Collection”只保留集合内的而是使用“Use Collection as Base”以集合为基础但允许其他变体或者只使用Keyword规则剔除一些确定无用的、不影响合批的关键字如一些全局的、与渲染管线设置绑定的关键字。理解Shader代码检查你的核心Shader看看#pragma multi_compile和#pragma shader_feature的使用。shader_feature的变体只有在被材质使用时才会被编译它本身就更“安全”。而multi_compile则会编译所有组合。在自定义Shader时合理选择这两者可以从源头减少冗余变体。5. 高级技巧与最佳实践心得经过多个项目的实战我总结出一些能让Shader Stripping工作更顺畅、更安全的心得。5.1 建立可迭代的变体收集与验证流程不要指望一次收集就能搞定整个项目。建立一个流程首次收集在项目主体内容完成后进行一次全面的“收集构建”。打一个Development Build用日志分析工具生成初始的ShaderVariantCollection。创建基础规则用这个集合配置Stripper进行第一次剔除构建。自动化测试构建后运行所有场景的自动化测试或手动遍历检查是否有粉红错误。将发现的缺失变体补充到Collection中。版本化与增量更新将ShaderVariantCollection资产和ShaderStripper配置纳入版本控制。每当有新的Shader、新的材质或新的渲染特性加入时重复步骤1-3更新这些资产。可以编写一个Editor脚本在每次打包前自动运行一次简化的变体收集和规则验证。5.2 分模块、分阶段管理Stripper配置对于大型项目不要使用一个巨大的、全局的Stripper配置。按渲染管线/渲染特性分组为URP核心Shader、UI Shader、粒子Shader、后处理Shader等分别创建不同的ShaderStripper资产和ShaderVariantCollection。这样管理起来更清晰也便于针对不同模块调整策略比如对UI Shader可以极度激进对角色Shader则相对保守。开发阶段与发布阶段在开发阶段可以禁用或使用非常宽松的Stripper规则以保证快速迭代时不出错。在准备发布版本Alpha, Beta, Release时再启用严格的全套剔除规则。可以通过Unity的Custom Define Symbol来条件编译不同的Stripper配置。5.3 监控与日志分析养成查看构建日志的习惯。搜索“Stripping shaders”相关的日志行。关注两个关键数字总变体数和剔除百分比。将这个数据记录下来作为项目优化的一项KPI。如果某次构建后剔除率突然大幅下降可能意味着引入了新的、未经验证的Shader或材质需要及时检查。5.4 与Addressables的协同如果你的项目大量使用AddressablesShader Stripping会变得更复杂但也更重要。Addressables的“分离构建”特性意味着主包和资源包可能在不同的时间构建。你必须确保主包包含引擎和核心Shader构建时其Stripper规则已经考虑到了所有可能从远端加载的Addressable资源所需的变体。这通常要求你有一个“全局”的变体集合。或者采用更现代的方案Unity的Shader Stripping Variant Collection功能与这个工具配合。你可以将变体集合本身也标记为Addressable并确保它在主包中。这样无论资源包如何构建主包中的Shader变体都是全的。但这需要更精细的配置。最后一个最朴素的建议备份你的项目尤其是在第一次应用激进剔除规则之前。Shader Stripping一旦出错可能导致构建出的游戏无法运行。确保你有快速回滚到“未剔除”状态的能力。将Shader Stripping视为构建流水线上的一个关键且需要谨慎对待的优化环节而非一劳永逸的设置这样才能在享受它带来的构建加速红利的同时最大限度地规避风险。