GameTech/URP 渲染实战

案例 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 剥离。

编写剥离规则的原则: