URP 渲染实战 / 第 29 章 性能优化 / 案例 29.2
案例 29.2自定义变体剥离脚本进阶
目标:用 IPreprocessShaders 在构建时剥掉项目确定用不到的变体,并在构建结束时输出统计。
Assets/Cookbook/Ch29/Editor/CookbookVariantStripper.cs(节选:rules)
class CookbookVariantStripper : IPreprocessShaders, IPreprocessBuildWithReport, IPostprocessBuildWithReport { // ---- 项目约定(示例):根据你的项目改 ---- const bool k_ProjectUsesLightmaps = false; // 全实时光照,没有烘焙光照贴图 const bool k_ProjectUsesScreenSpaceShadows = false; // 没有加 Screen Space Shadows Feature const bool k_ProjectUsesVertexAdditionalLights = false; // 附加光一律逐像素 const string k_OnlyShadersWithPrefix = ""; // 只处理某些 Shader(空 = 全部) static readonly ShaderKeyword s_LightmapOn = new ShaderKeyword("LIGHTMAP_ON"); static readonly ShaderKeyword s_DynLightmapOn = new ShaderKeyword("DYNAMICLIGHTMAP_ON"); static readonly ShaderKeyword s_DirLightmap = new ShaderKeyword("DIRLIGHTMAP_COMBINED"); static readonly ShaderKeyword s_ShadowsShadowmask = new ShaderKeyword("SHADOWS_SHADOWMASK"); static readonly ShaderKeyword s_LightmapShadowMixing = new ShaderKeyword("LIGHTMAP_SHADOW_MIXING"); static readonly ShaderKeyword s_ScreenShadows = new ShaderKeyword("_MAIN_LIGHT_SHADOWS_SCREEN"); static readonly ShaderKeyword s_VertexLights = new ShaderKeyword("_ADDITIONAL_LIGHTS_VERTEX"); static readonly ShaderKeyword s_DebugDisplay = new ShaderKeyword("DEBUG_DISPLAY"); static int s_Before, s_After; static readonly Dictionary<string, int> s_RemovedPerShader = new Dictionary<string, int>(); // 数字越小越早执行。URP 自己的剥离器也实现了这个接口,顺序不影响结果(都是从同一个列表里删) public int callbackOrder => 10; bool ShouldStrip(ShaderSnippetData snippet, ShaderCompilerData data) { var keys = data.shaderKeywordSet; if (!k_ProjectUsesLightmaps && (keys.IsEnabled(s_LightmapOn) || keys.IsEnabled(s_DynLightmapOn) || keys.IsEnabled(s_DirLightmap) || keys.IsEnabled(s_ShadowsShadowmask) || keys.IsEnabled(s_LightmapShadowMixing))) return true; if (!k_ProjectUsesScreenSpaceShadows && keys.IsEnabled(s_ScreenShadows)) return true; if (!k_ProjectUsesVertexAdditionalLights && keys.IsEnabled(s_VertexLights)) return true; // 发布版不需要 Rendering Debugger 的调试变体 if (!EditorUserBuildSettings.development && keys.IsEnabled(s_DebugDisplay)) return true; // Meta Pass 给光照烘焙/实时 GI 提取反照率和自发光用;项目完全不用 GI 时才剥 if (!k_ProjectUsesLightmaps && snippet.passType == PassType.Meta) return true; return false; }
Assets/Cookbook/Ch29/Editor/CookbookVariantStripper.cs(节选:process)
public void OnProcessShader(Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> data) { if (k_OnlyShadersWithPrefix.Length > 0 && !shader.name.StartsWith(k_OnlyShadersWithPrefix)) return; int before = data.Count; // 倒序删除,避免下标错位 for (int i = data.Count - 1; i >= 0; --i) if (ShouldStrip(snippet, data[i])) data.RemoveAt(i); s_Before += before; s_After += data.Count; int removed = before - data.Count; if (removed > 0) { s_RemovedPerShader.TryGetValue(shader.name, out int n); s_RemovedPerShader[shader.name] = n + removed; } }
工作方式:构建时,Unity 对每个 Shader 的每个 Pass、每个着色阶段(顶点、片元……)调用一次 OnProcessShader,data 里是这一阶段即将编译的全部变体,每个元素的 shaderKeywordSet 记录了它启用了哪些关键字。从列表里删掉的变体不会被编译,也不会进包。snippet 提供了 Pass 的类型(passType)、名字和着色阶段,可以按 Pass 剥离。
编写剥离规则的原则:
- 只剥确定用不到的。被剥掉的变体如果运行时被请求,Unity 会退回到“最接近”的已有变体,画面表现为光照、阴影莫名其妙地不对,而且只在真机上出现——这是最难查的一类 bug(第 30 章的问题手册会再提到)。
- 规则要和项目约定绑定。上面的示例把约定写成了常量,实际项目中更好的做法是做成一个 ScriptableObject 配置,或者从 URP Asset、场景设置里自动推断。
- 剥离本身就有坑:全局关键字用
new ShaderKeyword(name),局部关键字要用new ShaderKeyword(shader, name),否则IsEnabled永远返回 false,剥离不起作用。 - 每次修改规则后,对比构建日志里的变体数量,并在目标设备上把所有场景、所有画质档位都跑一遍。