Unity Shader - Built-in管线下优化变体


变体过多的缺点

为了让大家了解为何要减少变体,这里列出变体过多的缺点

  • 打包时编译变体的时间增加(如果你的变体使用 unity built-in standard shader,那么可能会有几千个变体,编译单单这个 shader 也许会需要 10 分钟)
  • 增加 运行时 shaderlab 内存,游戏与运行时使用的变体多,那么意味着 shader 的实例很多,每个 shader 实例都是需要占用内存的
  • 增加包体大小,因为 unity shader 变体多的话 编译出来 的样本就会很多,每个样本就是一个 shader 文件(如果我们自己写引擎的话,就知道这个变体是怎么回事)

项目情况

由于项目没使用 SRP,还是 built-in 管线,那么要使用 built-in 的阴影就需要使用到 #pragma multi_compile_fwdbase


#pragma multi_compile_fwdbase 和 multi_compile_fog 生存的变体(keyword)

该内置的 pragma 会生存很多不需要的 multi_compilekeyword,为了定制效果,让 shader 尽可能的小,那么我们可以这么整:#pragma multi_compile_fwdbasemulti_compile_fog 编译生存的 keywrod手动来定义需要的


如果我们只要

  • fog linear
  • lighting
  • shadow

的功能


生存的变体

如下图,会生存一堆不需要的 keyword
在这里插入图片描述

变体的数量

如下图:98 个
在这里插入图片描述

查看编译生存的各个变体的代码,并搜索 Global Keywords - 源码级别

选中 shader 文件,点击 Inspector 视图中的按钮:Compile and show code,如下图的:
在这里插入图片描述
生存的代码中搜索 Global Keywords,就可以看到各个变体的代码

查看编译生存的各个变体 - keyword 级别

在这里插入图片描述

以上两种方式都可以查看变体情况


如何优化

根据上面 搜索 Global Keywords 的方式,我们可以知道生存了很多不必要的变体代码

变体多的缺点 上面有提到

为了优化,可以这么做,上面也有提到,这里再次重新强调一下:#pragma multi_compile_fwdbasemulti_compile_fog 编译生存的 keywrod手动来定义需要的


将 #pragma multi_compile_fwdbase 和 multi_compile_fog 预生成项拆解为单个的手动定义项

如:

//#pragma multi_compile_fog
//#pragma multi_compile_fwdbase
#define FOG_LINEAR
#define DIRECTIONAL
#define SHADOWS_SCREEN

优化之后

变体的数量剧减,有原来的 98 个变成了 6 个(还有一些自定义的 multi_compile,不然只有3个,而这三个都是内置生成的 tier:1,2,3的,这个 tier: 1,2,3暂时不知道如何删除(注:后续发现可以再 IPreprocessShaders 中删除),不然的话,只有1个变体,那么这个 shader 文件就会小的可怜,打包速度、占用内存都会极小

在这里插入图片描述


优化后的小问题、如何解决

但是这样优化后的了另一个问题:Unity Editor 下的 Prefab、Material 都无法预览正确的效果

所以我们可以写个工具,在:打包前,批量修改 shader 的 fwdbase, fog 的变体,在打包后,在恢复过来,这样,Unity Editor 下既可以正常预览 Prefab、Material 的渲染效果,也可以在打包后变体减少

public void BuildPackage()
{
	// 打包资源前,先处理一波 shader 的 fwdbase, fog 变体替换
	BeforeAndAfterBuildShadersHandler.BeforeBuildHandle();
	// 正常 building 逻辑在此
	...
	// 打包资源后,恢复 shader 文件
	BeforeAndAfterBuildShadersHandler.AfterBuldHandle();
}

编辑器 源码

#define __DATAFORM_TXT__ // 使用 文本来存 shader 数据

// jave.lin 2021/08/30
// 打包构建前、后 shader 的处理工具
// 因为使用的是 built-in 管线,基本很多光影都需要 fwdbase, fog 等 built-in 的变体
// 而这个工具是不使用 built-in 变体,改用手动的方式来定义需要的变体
// 所以会导致 unity 编辑器是对 material 的预览效果出问题
// 因为在发布程序前,可以使用会 fwdbase, fog 等 built-in 变体
// 但是在发布程序时,必须使用自己手动的方式来定义变体(可以减少很多变体的数量)
// 如果使用 SRP 的话,可以变体的把控会更容易

using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;

// jave.lin shader 文件信息
public class ShaderFileInfo
{
    public string path;
    public UnityEngine.Shader obj;

    public string lowPath;
    public string lowShaderName;
}

// jave.lin shader 在 building 处理的工具类
public class BeforeAndAfterBuildShadersEditorWindow : EditorWindow
{
    private string defaultShadersPath = "Assets/GameAssets/shaders";
    private List<ShaderFileInfo> shaderInfoList = new List<ShaderFileInfo>();
    private List<Shader> shaderList = null;

    // 根据 shader name 过滤
    private bool shadeNameFilter = true;
    private string shaderNameFilterContent = "";
    // 根据 file name 过滤
    private bool fileNameFilter = true;
    private string fileNameFilterContent = "";
    private Vector2 dragDropFileScrollViewPos;

    [MenuItem("实用工具/资源工具/打包构建前、后 shader 的处理工具")]
    public static void _Show()
    {
        var win = EditorWindow.GetWindow<BeforeAndAfterBuildShadersEditorWindow>();
        win.titleContent = new GUIContent("打包构建前、后 shader 的处理工具");
        win.Show();
    }

    private void OnGUI()
    {
#if __DATAFORM_TXT__
        if (shaderList == null)
        {
            shaderList = new List<Shader>();
            BeforeAndAfterBuildShadersHandler.LoadDataFromTxt(BeforeAndAfterBuildShadersHandler.dataPath, shaderList);
        }
#else
        if (shaderList == null)
        {
            shaderList = new List<Shader>();
            var data = AssetDatabase.LoadAssetAtPath<BeforeAndAfterBuildShadersHandleData>(BeforeAndAfterBuildShadersHandler.dataPath);
            shaderList.AddRange(data.shaders);
        }
#endif

        var src_endabled = GUI.enabled;
        if (BeforeAndAfterBuildShadersHandler.ShaderBackupCount() > 0) GUI.enabled = false;
        if (GUILayout.Button("Building前 手动 处理"))
        {
            BeforeAndAfterBuildShadersHandler.BeforeBuildHandle();
        }
        GUI.enabled = src_endabled;

        if (BeforeAndAfterBuildShadersHandler.ShaderBackupCount() == 0) GUI.enabled = false;
        if (GUILayout.Button("Building后 手动 处理"))
        {
            BeforeAndAfterBuildShadersHandler.AfterBuldHandle();
        }
        GUI.enabled = src_endabled;

        if (BeforeAndAfterBuildShadersHandler.IsMemoryBK() && GUILayout.Button("清空备份数据"))
        {
            BeforeAndAfterBuildShadersHandler.ClearBackupInfo();
        }

        if (GUILayout.Button("清理配置中不存在的shaders文件"))
        {
            BeforeAndAfterBuildShadersHandler.ClearNotExistsShaders();
        }

        DisplayCombineFileInfoList();
    }

    private void DisplayCombineFileInfoList()
    {
        EditorGUILayout.Space();

        var srcCol = GUI.contentColor;
        GUI.contentColor = Color.gray;
        EditorGUILayout.LabelField("================================================ Data List ================================================");
        GUI.contentColor = srcCol;

        EditorGUILayout.BeginHorizontal();
        if (GUILayout.Button("Clear"))
        {
            shaderInfoList.Clear();
        }
        if (GUILayout.Button("Reload"))
        {
            RefreshFileListInfo();
        }
        if (GUILayout.Button("Save"))
        {
            SaveDataAsset();
        }
        if (GUILayout.Button("Select"))
        {
            SelectAssetList();
        }
        if (GUILayout.Button("LoadShadersFolder"))
        {
            LoadShadersFolder();
        }
        EditorGUILayout.EndHorizontal();

        GUI.contentColor = Color.green;
        EditorGUILayout.BeginHorizontal();
        EditorGUILayout.LabelField($"Shader Name Filter : ", GUILayout.Width(150));
        shadeNameFilter = EditorGUILayout.Toggle(shadeNameFilter, GUILayout.Width(20));
        shaderNameFilterContent = EditorGUILayout.TextField(shaderNameFilterContent);
        EditorGUILayout.EndHorizontal();
        EditorGUILayout.BeginHorizontal();
        EditorGUILayout.LabelField($"File Name Filter : ", GUILayout.Width(150));
        fileNameFilter = EditorGUILayout.Toggle(fileNameFilter, GUILayout.Width(20));
        fileNameFilterContent = EditorGUILayout.TextField(fileNameFilterContent);
        EditorGUILayout.EndHorizontal();
        GUI.contentColor = srcCol;

        var lowShaderNameFilterContent = shaderNameFilterContent.ToLower();
        var lowFileNameFilterContent = fileNameFilterContent.ToLower();

        dragDropFileScrollViewPos = EditorGUILayout.BeginScrollView(dragDropFileScrollViewPos/*, GUILayout.Width(300), GUILayout.Width(300)*/);

        for (int i = 0; i < shaderInfoList.Count; i++)
        {
            var remove = false;
            var info = shaderInfoList[i];

            if (shadeNameFilter && !string.IsNullOrEmpty(lowShaderNameFilterContent))
            {
                if (!info.lowShaderName.Contains(lowShaderNameFilterContent))
                {
                    continue;
                }
            }

            if (fileNameFilter && !string.IsNullOrEmpty(lowFileNameFilterContent))
            {
                if (!info.lowPath.Contains(lowFileNameFilterContent))
                {
                    continue;
                }
            }

            EditorGUILayout.BeginHorizontal(GUIStyle.none);
            remove = GUILayout.Button("X", GUILayout.Width(20));
            EditorGUILayout.ObjectField(info.obj, typeof(UnityEngine.Object), false, GUILayout.Width(200));
            EditorGUILayout.TextField(info.path);
            EditorGUILayout.EndHorizontal();
            if (remove)
            {
                shaderInfoList.RemoveAt(i);
                --i;
            }
        }
        EditorGUILayout.EndScrollView();
    }

    private void RefreshFileListInfo(List<string> addList = null)
    {
        shaderInfoList.Clear();
        foreach (var shader in shaderList)
        {
            var path = AssetDatabase.GetAssetPath(shader);
            shaderInfoList.Add(new ShaderFileInfo
            {
                path = path,
                obj = shader,
                lowPath = path.ToLower(),
                lowShaderName = shader.name.ToLower(),
            });
        }
        if (shaderInfoList.Count > 1)
        {
            SortDataList();
        }
    }

    private void SortDataList()
    {
        shaderInfoList.Sort((a, b) => { return string.Compare(a.path, b.path); });
    }

    private void SaveDataAsset()
    {
        if (shaderList != null)
        {
            shaderList.Clear();
            foreach (var info in shaderInfoList)
            {
                if (info.obj == null)
                {
                    // 中途被删除
                    continue;
                }
                shaderList.Add(info.obj);
            }
            BeforeAndAfterBuildShadersHandler.SaveAsset(shaderList);
        }
    }

    private void SelectAssetList()
    {
        // jave.lin : method0, Selection.gameObjects Read Only
        //var arr = new GameObject[combineFileInfoList.Count];
        //for (int i = 0; i < arr.Length; i++)
        //{
        //    arr[i] = combineFileInfoList[i].obj as GameObject;
        //}
        //Selection.gameObjects = arr; // read only

        // jave.lin : method1 : reflection, 但是获取不了 add 方法
        //var addFunc = typeof(Selection).GetMethod(
        //    "Add", 
        //    new Type[]
        //    { 
        //        typeof(UnityEngine.Object) 
        //    },
        //    new ParameterModifier[] { new ParameterModifier(1) }
        //    );
        //Debug.Log($"PrefabCombinerEditorWindow.SelectPrefabList addFunc : {addFunc}");

        // jave.lin : method2 : Selection.objects, 后来发现此 API 可以 setter
        var arr = new UnityEngine.Object[shaderInfoList.Count];
        for (int i = 0; i < arr.Length; i++)
        {
            arr[i] = shaderInfoList[i].obj;
        }
        Selection.objects = arr;
    }

    private void LoadShadersFolder()
    {
        shaderInfoList.Clear();
        var shaderGUIDs = AssetDatabase.FindAssets("t:Shader", new string[] { defaultShadersPath });
        foreach (var guid in shaderGUIDs)
        {
            var path = AssetDatabase.GUIDToAssetPath(guid);
            var shader = AssetDatabase.LoadAssetAtPath<Shader>(path);
            shaderInfoList.Add(new ShaderFileInfo
            {
                path = path,
                obj = shader,
                lowPath = path.ToLower(),
                lowShaderName = shader.name.ToLower(),
            });
        }
    }
}

// jave.lin 文件恢复方式
public enum eFileRevertType
{
    MEMORY,
    SVN,
    GIT,
}

// jave.lin shader 的内存备份数据
public struct ShaderBackupInfo
{
    public string path;
    public string source;
}

// jave.lin building 前后的处理器
public class BeforeAndAfterBuildShadersHandler
{
    // 数据路径
#if __DATAFORM_TXT__
    public static string dataPath = "Assets/Editor/Shaders/BeforeAndAfterBuildShadersHandleTextData.txt";
#else
    public static string dataPath = "Assets/Editor/Shaders/BeforeAndAfterBuildShadersHandleData.asset";
#endif
    // shader 内存的备份数据
    private static List<ShaderBackupInfo> shaderBackInfoList = new List<ShaderBackupInfo>();
    // jave.lin : 使用 svn 还是 git
    private static eFileRevertType eFileRevertType = eFileRevertType.MEMORY;
    // jave.lin : 替换并获取缩进组
    private static Regex fogReplaceRegex = new Regex("\\n(\\s+)#pragma multi_compile_fog", RegexOptions.Compiled);
    private static Regex fwdbaseReplaceRegex = new Regex("\\n(\\s+)#pragma multi_compile_fwdbase", RegexOptions.Compiled);
    // jave.lin : 读写文件时 encoding 是否带 BOM
    private static bool readAndWriteWithBOM = false;
    // jave.lin : 换行是否 CRLF
    private static bool endLineFlagWithCRLF = true;

    private static List<Shader> shaderList = new List<Shader>();

    public static int ShaderBackupCount() => shaderBackInfoList != null ? shaderBackInfoList.Count : 0;

    // statics
    public static string GetFullPath(string assetPath)
    {
        return $"{Application.dataPath.Replace("Assets", "")}/{assetPath}".Replace("//", "/");
    }

    public static bool IsMemoryBK()=> eFileRevertType == eFileRevertType.MEMORY;

    public static void ClearBackupInfo()
    {
        shaderBackInfoList.Clear();
    }

    public static void LoadData(string assetPath, List<Shader> ret)
    {
#if __DATAFORM_TXT__
        LoadDataFromTxt(assetPath, ret);
#else
        LoadDataFromAsset(assetPath, ret);
#endif
    }

    public static void LoadDataFromAsset(string assetPath, List<Shader> ret)
    {
        var data = AssetDatabase.LoadAssetAtPath<BeforeAndAfterBuildShadersHandleData>(assetPath);
        if (data == null) return;
        ret.AddRange(data.shaders);
    }

    public static void LoadDataFromTxt(string txtPath, List<Shader> ret)
    {
        string txtContent;
        if (File.Exists(txtPath))
        {
            txtContent = File.ReadAllText(txtPath);
        }
        else
        {
            var fullPath = GetFullPath(txtPath);
            if (!File.Exists(fullPath))
            {
                return;
            }
            txtContent = File.ReadAllText(fullPath);
        }
        if (string.IsNullOrEmpty(txtContent))
        {
            return;
        }
        var paths = txtContent.Split(new string[] { "\n" }, System.StringSplitOptions.RemoveEmptyEntries);
        foreach (var path in paths)
        {
            var shader = AssetDatabase.LoadAssetAtPath<Shader>(path);
            if (shader == null) continue;
            ret.Add(shader);
        }
    }

    public static void SaveAsset(List<Shader> shaders)
    {
#if __DATAFORM_TXT__
        var content = "";
        foreach (var shader in shaders)
        {
            var shaderPath = AssetDatabase.GetAssetPath(shader);
            content += $"{shaderPath}\n";
        }
        var fullPath = GetFullPath(dataPath);
        File.WriteAllText(fullPath, content);
        AssetDatabase.Refresh();
#else
        var isNew = false;
        var data = AssetDatabase.LoadAssetAtPath<BeforeAndAfterBuildShadersHandleData>(dataPath);
        if (data == null)
        {
            data = ScriptableObject.CreateInstance<BeforeAndAfterBuildShadersHandleData>();
            isNew = true;
        }
        var saveList = new List<Shader>();
        foreach (var shader in shaders)
        {
            if (shader == null) continue; // 中途被删除
            saveList.Add(shader);
        }

        data.shaders.Clear();
        data.shaders.AddRange(saveList);

        if (!isNew) data = data.Clone();

        AssetDatabase.CreateAsset(data, dataPath);
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
#endif
    }

    // 读取 *.asset 配置的数据
    public static void BeforeBuildHandle()
    {
        // jave.lin : 遍历里面的 *.shader 逐一处理
        /*
            - 如果是shader代码使用内存恢复先备份到内存
            - 如果是shader代码使用文件恢复先备份到临时文件
            - 如果是shader代码使用svn/git的版本控制,不需要我们手动备份,恢复时直接使用svn/git来恢复即可
            - #pragma multi_compile_fog 替换为:
                - #define FOG_LINEAR
            - #pragma multi_compile_fwdbase 替换为:
                - #define DIRECTIONAL
                - #define SHADOWS_SCREEN
            - 将对应的 *.shader 文件标记为:EditorUtility.SetDirty(shaderObj);
            - 然后 AssetsDatabase.SaveAssets(); AssetsDatabase.Refresh();
         */
        shaderBackInfoList.Clear();

        shaderList.Clear();
        LoadData(dataPath, shaderList);

        if (shaderList.Count > 0)
        {
            var encoding = new System.Text.UTF8Encoding(readAndWriteWithBOM);
            var endLineStr = endLineFlagWithCRLF ? "\r\n" : "\n"; // Environment.NewLine;
            var fogReplacmentStr = $"{endLineStr}$1#define FOG_LINEAR";
            var fwdbaseReplacmentStr = $"{endLineStr}$1#define DIRECTIONAL{endLineStr}$1#define SHADOWS_SCREEN";
            foreach (var shader in shaderList)
            {
                if (shader == null) continue;
                var path = AssetDatabase.GetAssetPath(shader);
                var fullPath = GetFullPath(path);
                if (!File.Exists(fullPath)) continue;
                try
                {
                    var txt = File.ReadAllText(fullPath, encoding);
                    shaderBackInfoList.Add(new ShaderBackupInfo { path = fullPath, source = txt });
                    // jave.lin : 普通替换,缩进很难看
                    //txt = txt.Replace("#pragma multi_compile_fog", "#define FOG_LINEAR");
                    //txt = txt.Replace("#pragma multi_compile_fwdbase", "#define DIRECTIONAL\n#define SHADOWS_SCREEN");
                    // jave.lin : 使用正则来替换,缩进就可以还原来的
                    txt = fogReplaceRegex.Replace(txt, fogReplacmentStr);
                    txt = fwdbaseReplaceRegex.Replace(txt, fwdbaseReplacmentStr);
                    File.WriteAllText(fullPath, txt);
                }
                catch(System.Exception er)
                {
                    Debug.LogError(er);
                }
            }
        }
    }

    public static void AfterBuldHandle()
    {
        // jave.lin : 遍历里面的 *.shader 逐一处理
        /*
         // method 0:
            - 使用从内存中恢复shader代码(如果shader代码过多,可以考虑先backup 到 disk,然后再从 disk 恢复

         // method 1:
            - #define FOG_LINEAR 替换为:
                - #pragma multi_compile_fog
            - #define DIRECTIONAL、#define SHADOWS_SCREEN 替换为:
                - #pragma multi_compile_fwdbase
            - 将对应的 *.shader 文件标记为:EditorUtility.SetDirty(shaderObj);
            - 然后 AssetsDatabase.SaveAssets(); AssetsDatabase.Refresh();

         // method 2:(更方便的方式)
            - 使用 svn/git 命令行来还原打包后的 shaders 文件
         */
        if (shaderBackInfoList.Count == 0)
        {
            return;
        }
        if (eFileRevertType == eFileRevertType.MEMORY)
        {
            var encoding = new System.Text.UTF8Encoding(readAndWriteWithBOM);
            foreach (var info in shaderBackInfoList)
            {
                try
                {
                    File.WriteAllText(info.path, info.source, encoding);
                }
                catch (System.Exception er)
                {
                    Debug.LogError(er);
                }
            }
        }
        else
        {
            var revertFiles = new List<string>();
            foreach (var info in shaderBackInfoList)
            {
                revertFiles.Add(info.path);
            }
            try
            {
                RevertFiles(revertFiles);
            }
            catch(System.Exception er)
            {
                Debug.LogError(er);
            }
        }
        shaderBackInfoList.Clear();
    }

    public static void ClearNotExistsShaders()
    {
        shaderList.Clear();
        LoadData(dataPath, shaderList);
        if (shaderList.Count > 0)
        {
            for (int i = shaderList.Count - 1; i > -1; i--)
            {
                var shader = shaderList[i];
                if (shader == null) continue;
                var path = AssetDatabase.GetAssetPath(shader);
                var fullPath = GetFullPath(path);
                if (!File.Exists(fullPath))
                {
                    shaderList.RemoveAt(i);
                }
            }
        }
        SaveAsset(shaderList);
    }

    private static void RevertFiles(List<string> files)
    {
        // TortoiseProc.exe /command:log /path:"H:\WorkFiles\ProjectArt\Assets\Art\(Temporary).meta" /closeonend:1
        var revision = eFileRevertType.ToString().ToLower();
        System.Diagnostics.ProcessStartInfo info = new System.Diagnostics.ProcessStartInfo(revision);
        var revertFiles = string.Join<string>(" ", files);

        if (eFileRevertType == eFileRevertType.SVN)
        {
            // 需要确保安装了 小乌龟 svn 命令行工具,可参考:https://blog.csdn.net/linjf520/article/details/119617076
            // e.g.: svn revert "C:\\test1.txt" "C:\\test2.txt"
            Debug.Log($"svn revert {revertFiles}");
            info.Arguments = $" revert {revertFiles}";
        }
        else if (eFileRevertType == eFileRevertType.GIT)
        {
            // 需要确保安装了 git,使用 git checkout <file1> <file2> ... <fileN
            // e.g.: git checkout "C:\\test1.txt" "C:\\test2.txt"
            Debug.Log($"git checkout {revertFiles}");
            info.Arguments = $" checkout {revertFiles}";
        }
        else if (eFileRevertType == eFileRevertType.MEMORY)
        {
            // nops
        }
        else
        {
            Debug.LogError($"unimplements eRevesionApp : {eFileRevertType} revert handle.");
            return;
        }
        info.CreateNoWindow = false;
        info.ErrorDialog = true;
        info.UseShellExecute = true;

        if (info.UseShellExecute)
        {
            info.RedirectStandardOutput = false;
            info.RedirectStandardError = false;
            info.RedirectStandardInput = false;
        }
        else
        {
            info.RedirectStandardOutput = true;
            info.RedirectStandardError = true;
            info.RedirectStandardInput = true;
            info.StandardOutputEncoding = System.Text.UTF8Encoding.UTF8;
            info.StandardErrorEncoding = System.Text.UTF8Encoding.UTF8;
        }

        System.Diagnostics.Process process = System.Diagnostics.Process.Start(info);

        if (!info.UseShellExecute)
        {
            Debug.Log(process.StandardOutput);
            Debug.Log(process.StandardError);
        }

        process.WaitForExit();
        process.Close();
    }
}

// jave.lin building 前后需要特殊处理的 shader
[CreateAssetMenu(menuName = "Tools/Create BeforeAndAfterBuildShadersHandleData ")]
public class BeforeAndAfterBuildShadersHandleData : ScriptableObject
{
    public List<Shader> shaders;

    public BeforeAndAfterBuildShadersHandleData Clone()
    {
        var ret = ScriptableObject.CreateInstance<BeforeAndAfterBuildShadersHandleData>();
        ret.shaders = new List<Shader>(this.shaders);
        return ret;
    }
}

操作界面

可以 GUI,也可以 Script 脚本化调用
在这里插入图片描述


另外,PlayerSettings/Other Settings/Rendering/Graphics API 去掉勾,只保留要发布的单个目标平台(切换到切他平台,再切到对应平台即可)

这个 shader 变体也可以减少


示例

另外,如果是 surface shader 的话,很多的变体,可以通过 一些 surface shader 指令来删除,参考文档:Writing Surface Shaders - 搜索:Code generation options 部分

下面是 surface shader 中的 各种 noxxx 的删除变体、skip_variants 删除变体的示例:

//jave.lin - 测试删除 surface shader 的变体
#pragma surface surf Lambert fullforwardshadows addshadow alphatest:_Cutoff nodynlightmap nodirlightmap noforwardadd nolppv nofog noambient novertexlights
#pragma skip_variants SHADOWS_SCREEN LIGHTPROBE_SH VERTEXLIGHT_ON FOG_LINEAR DIRECTIONAL_COOKIE
#pragma skip_variants SPOT POINT POINT_COOKIE SHADOWS_CUBE SHADOWS_DEPTH FOG_EXP FOG_EXP2
#pragma skip_variants INSTANCING_ON DIRLIGHTMAP_COMBINED DYNAMICLIGHTMAP_ON
#pragma skip_variants UNITY_HDR_ON EDITOR_VISUALIZATION
#pragma skip_variants SHADOWS_CUBE 
#pragma fragmentoption ARB_precision_hint_fastest

multi_compile 的一些 built-in 快件方式包含的 keyword

Declaring and using shader keywords in HLSL - 查看:multi_compile shortcuts 部分

直接抄过来吧:

Unity provides several “shortcut” notations for declaring shader keywords.

The following shortcuts relate to light, shadow and lightmapping in the Built-in Render Pipeline:

  • multi_compile_fwdbase adds this set of keywords: DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_COMBINED DYNAMICLIGHTMAP_ON SHADOWS_SCREEN SHADOWS_SHADOWMASK LIGHTMAP_SHADOW_MIXING LIGHTPROBE_SH. These variants are needed by PassType.ForwardBase.
  • multi_compile_fwdbasealpha adds this set of keywords: DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_COMBINED DYNAMICLIGHTMAP_ON LIGHTMAP_SHADOW_MIXING VERTEXLIGHT_ON LIGHTPROBE_SH. These variants are needed by PassType.ForwardBase.
  • multi_compile_fwdadd adds this set of keywords: POINT DIRECTIONAL SPOT POINT_COOKIE DIRECTIONAL_COOKIE. These variants are needed by PassType.ForwardAdd.
  • multi_compile_fwdadd_fullshadows adds this set of keywords: POINT DIRECTIONAL SPOT POINT_COOKIE DIRECTIONAL_COOKIE SHADOWS_DEPTH SHADOWS_SCREEN SHADOWS_CUBE SHADOWS_SOFT SHADOWS_SHADOWMASK LIGHTMAP_SHADOW_MIXING. This is the same as multi_compile_fwdadd, but this adds the ability for the lights to have real-time shadows.
  • multi_compile_lightpass adds this set of keywords: POINT DIRECTIONAL SPOT POINT_COOKIE DIRECTIONAL_COOKIE SHADOWS_DEPTH SHADOWS_SCREEN SHADOWS_CUBE SHADOWS_SOFT SHADOWS_SHADOWMASK LIGHTMAP_SHADOW_MIXING. This is effectively a catch-all shortcut for all functionality related to real-time light and shadows, other than Light Probes.
  • multi_compile_shadowcaster adds this set of keywords: SHADOWS_DEPTH SHADOWS_CUBE. These variants are needed by PassType.ShadowCaster.
  • multi_compile_shadowcollector adds this set of keywords: SHADOWS_SPLIT_SPHERES SHADOWS_SINGLE_CASCADE. It also compiles variants without any of these keywords. These variants are needed for screen-space shadows.
  • multi_compile_prepassfinal adds this set of keywords: LIGHTMAP_ON DIRLIGHTMAP_COMBINED DYNAMICLIGHTMAP_ON UNITY_HDR_ON SHADOWS_SHADOWMASK LIGHTPROBE_SH. It also compiles variants without any of these keywords. These variants are needed by PassType.LightPrePassFinal and PassType.Deferred.

The following shortcuts relate to other settings:

  • multi_compile_particles adds this keyword relating to the Built-in particle system : SOFTPARTICLES_ON. It also compiles variants without this keyword. For more information, see Built-in Particle System.
  • multi_compile_fog adds this set of keywords relating to fog: FOG_LINEAR, FOG_EXP, FOG_EXP2. It also compiles variants without any of these keywords. You can control this behavior in the Graphics settings window.
  • multi_compile_instancing adds keywords relating to instancing. If the shader uses procedural instancing, it adds this set of keywords: INSTANCING_ON PROCEDURAL_ON. Otherwise, it adds this keyword: INSTANCING_ON. It also compiles variants without any of these keywords. You can control this behavior in the Graphics settings window.

ShaderFinderTool.cs

便于对需要的 shader 查找
或是 shader 变体的统计都可以使用下面自己写的工具来辅助加速分析

在这里插入图片描述

比如,我们可以 Write All Shader Variant Info Logs 后,会生成一份 csv ,然后使用 excel 打开,对 变体数量那列降序排序即可发现那个 shader 变体过多,但是这个 shader variant count 的统计感觉不太对,和 unity build asset bundle 后的变体数量差异有点大,应该是 unity API 的 BUG

在这里插入图片描述

// jave.lin 2022/12/19 shader 查找器

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEditor;
using UnityEngine;

public class ShaderFinderTools : EditorWindow
{
    enum eConditionType
    {
        Single,
        MultiAnd,
        MultiOr,
    }
    enum eShaderFindType
    {
        ShaderName,
        ShaderAssetName,
        ShaderVariantCount,
    }
    enum eShaderVariantCountCompare
    {
        Equal,
        Less,
        LessEqual,
        Greater,
        GreaterEqual,
        NotEqual,
    }

    [MenuItem("实用工具/Shader/Shader Finder")]
    private static void _Show()
    {
        var win = EditorWindow.GetWindow<ShaderFinderTools>();
        win.titleContent = new GUIContent("Shader Finder");
        win.Init();
        win.Show();
    }
    private void Init()
    {

    }

    private eConditionType conditionType;
    private eShaderFindType findType;
    private string shader_name;
    private string shader_asset_name;
    private UInt64 shader_variant_count;
    private eShaderVariantCountCompare variantCountCompare;
    private bool usedBySceneOnly;
    private string logsPath = "ShaderVariantInfoLogs.csv";
    private Vector2 shaderVariantRectPos;
    private bool showAllRet;
    private int pageSize;
    private int pageIndex;

    private List<Shader> shader_filter_list_helper1 = new List<Shader>();
    private List<Shader> shader_filter_list_helper2 = new List<Shader>();

    private void OnGUI()
    {
        if (shader_filter_list_helper1 == null) shader_filter_list_helper1 = new List<Shader>();
        if (shader_filter_list_helper2 == null) shader_filter_list_helper2 = new List<Shader>();

        conditionType = (eConditionType)EditorGUILayout.EnumPopup("Condition Type", conditionType);

        if (conditionType == eConditionType.Single)
        {
            findType = (eShaderFindType)EditorGUILayout.EnumPopup("Find Type", findType);

            switch (findType)
            {
                case eShaderFindType.ShaderName:
                    shader_name = EditorGUILayout.TextField("Shader Name", shader_name);
                    break;
                case eShaderFindType.ShaderAssetName:
                    shader_asset_name = EditorGUILayout.TextField("Shader Asset Name", shader_asset_name);
                    break;
                case eShaderFindType.ShaderVariantCount:
                    shader_variant_count = (UInt64)EditorGUILayout.IntField("Shader Variant Count", (int)shader_variant_count);
                    variantCountCompare = (eShaderVariantCountCompare)EditorGUILayout.EnumPopup("Variant Count Compare", variantCountCompare);
                    usedBySceneOnly = EditorGUILayout.Toggle("Used By Scene Only", usedBySceneOnly);
                    break;
                default:
                    break;
            }
        }
        else
        {
            shader_name = EditorGUILayout.TextField("Shader Name", shader_name);
            shader_asset_name = EditorGUILayout.TextField("Shader Asset Name", shader_asset_name);
            shader_variant_count = (UInt64)EditorGUILayout.IntField("Shader Variant Count", (int)shader_variant_count);
            variantCountCompare = (eShaderVariantCountCompare)EditorGUILayout.EnumPopup("Variant Count Compare", variantCountCompare);
            usedBySceneOnly = EditorGUILayout.Toggle("Used By Scene Only", usedBySceneOnly);
        }

        if (GUILayout.Button("Find"))
        {
            shader_filter_list_helper1.Clear();
            if (conditionType == eConditionType.Single)
            {
                switch (findType)
                {
                    case eShaderFindType.ShaderName:
                        FilterByShaderName(shader_name, shader_filter_list_helper1);
                        break;
                    case eShaderFindType.ShaderAssetName:
                        FilterByShaderAssetName(shader_asset_name, shader_filter_list_helper1);
                        break;
                    case eShaderFindType.ShaderVariantCount:
                        FilterByShaderVariantCount(shader_variant_count, usedBySceneOnly, variantCountCompare, shader_filter_list_helper1);
                        break;
                    default:
                        break;
                }
            }
            else if (conditionType == eConditionType.MultiOr)
            {
                FilterByShaderName(shader_name, shader_filter_list_helper1);
                FilterByShaderAssetName(shader_asset_name, shader_filter_list_helper1);
                FilterByShaderVariantCount(shader_variant_count, usedBySceneOnly, variantCountCompare, shader_filter_list_helper1);
            }
            else
            {
                FilterByMultiAnd(
                    shader_name,
                    shader_asset_name,
                    shader_variant_count, usedBySceneOnly, variantCountCompare,
                    shader_filter_list_helper1
                    );
            }

            shader_filter_list_helper2.Clear();
            shader_filter_list_helper2.AddRange(shader_filter_list_helper1.Distinct());
        }
        EditorGUILayout.LabelField(new string('-', 200));
        if (GUILayout.Button("Write All Shader Variant Info Logs"))
        {
            WriteAllShaderVariantInfoLogs();
        }
        if (GUILayout.Button("Open Shader Variant Info Logs Path"))
        {
            OpenAllShaderVariantInfoLogsPath();
        }
        EditorGUILayout.LabelField(new string('-', 200));

        {
            var srcEnabled = GUI.enabled;
            GUI.enabled = false;
            EditorGUILayout.LabelField($"Result Count : {shader_filter_list_helper2.Count}");
            GUI.enabled = srcEnabled;

            showAllRet = EditorGUILayout.Toggle("Show All", showAllRet);

            if (!showAllRet)
            {
                pageSize = EditorGUILayout.IntSlider("Page Size", pageSize, 2, 100);

                var pageTotal = (int)Mathf.Ceil((float)shader_filter_list_helper2.Count / pageSize);
                if (pageTotal == 1)
                {
                    GUI.enabled = false;
                }
                EditorGUILayout.BeginHorizontal();
                if (GUILayout.Button("<", GUILayout.Width(30))) pageIndex--;
                var textField_style = EditorStyles.textField;
                var textField_style_srcAlign = textField_style.alignment;
                textField_style.alignment = TextAnchor.MiddleCenter;
                EditorGUILayout.TextField(pageIndex.ToString(), EditorStyles.textField, GUILayout.Width(40));
                textField_style.alignment = textField_style_srcAlign;
                if (GUILayout.Button(">", GUILayout.Width(30))) pageIndex++;
                EditorGUILayout.EndHorizontal();
                pageIndex = (int)Mathf.Clamp(pageIndex, 0, pageTotal);

                pageIndex = EditorGUILayout.IntSlider("Page Index", pageIndex, 0, pageTotal);
                GUI.enabled = srcEnabled;
            }
        }

        {
            EditorGUILayout.LabelField(new string('-', 200));
            shaderVariantRectPos = EditorGUILayout.BeginScrollView(shaderVariantRectPos);

            if (showAllRet)
            {
                for (int i = 0; i < shader_filter_list_helper2.Count; i++)
                {
                    EditorGUILayout.ObjectField("Shader:", shader_filter_list_helper2[i], typeof(Shader), false);
                }
            }
            else
            {
                var start = pageIndex * pageSize;
                var end = (int)(Mathf.Min(start + pageSize, shader_filter_list_helper2.Count));
                for (int i = start; i < end; i++)
                {
                    EditorGUILayout.ObjectField("Shader:", shader_filter_list_helper2[i], typeof(Shader), false);
                }
            }
            EditorGUILayout.EndScrollView();
        }
    }

    private List<string> FilterAllShaderGUIDs()
    {
        var ret = new List<string>();
        var guids = AssetDatabase.FindAssets("t:shader");
        //var guids1 = AssetDatabase.FindAssets("t:shader", new string[] { "Assets" });
         jave.lin : Referring to package paths : "Packages/<package-name>/..."
         jave.lin : ref : https://docs.unity3d.com/Manual/upm-assets.html
        //var guids2 = AssetDatabase.FindAssets("t:shader", new string[] { "Packges/com.unity.postprocessing" });
        //ret.AddRange(guids1);
        //ret.AddRange(guids2);
        ret.AddRange(guids);
        return ret;
    }

    private void FilterByMultiAnd(
        string target_shader_name, 
        string target_shader_asset_name,
        UInt64 count, bool usedBySceneOnly, eShaderVariantCountCompare variantCountCompare,
        List<Shader> shader_filter_list)
    {
        var method = typeof(ShaderUtil).GetMethod("GetVariantCount", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);

        target_shader_name = target_shader_name.ToLower();
        target_shader_asset_name = target_shader_asset_name.ToLower();
        var guidlist = FilterAllShaderGUIDs();
        foreach (var guid in guidlist)
        {
            var shaderPath = AssetDatabase.GUIDToAssetPath(guid);
            var shader = AssetDatabase.LoadAssetAtPath<Shader>(shaderPath);
            if (shader.name.ToLower().Contains(target_shader_name) &&
                shaderPath.ToLower().Contains(target_shader_asset_name)
                )
            {
                var variantCount = (UInt64)method.Invoke(null, new System.Object[] { shader, true });

                var add = false;

                switch (variantCountCompare)
                {
                    case eShaderVariantCountCompare.Equal:
                        add = variantCount == count;
                        break;
                    case eShaderVariantCountCompare.Less:
                        add = variantCount < count;
                        break;
                    case eShaderVariantCountCompare.LessEqual:
                        add = variantCount <= count;
                        break;
                    case eShaderVariantCountCompare.Greater:
                        add = variantCount > count;
                        break;
                    case eShaderVariantCountCompare.GreaterEqual:
                        add = variantCount >= count;
                        break;
                    case eShaderVariantCountCompare.NotEqual:
                        add = variantCount != count;
                        break;
                    default:
                        break;
                }
                if (add)
                {
                    shader_filter_list.Add(shader);
                }
            }
        }
    }

    private void FilterByShaderName(string target_shader_name, List<Shader> shader_filter_list)
    {
        target_shader_name = target_shader_name.ToLower();
        var guidlist = FilterAllShaderGUIDs();
        foreach (var guid in guidlist)
        {
            var shaderPath = AssetDatabase.GUIDToAssetPath(guid);
            var shader = AssetDatabase.LoadAssetAtPath<Shader>(shaderPath);
            if (shader.name.ToLower().Contains(target_shader_name))
            {
                shader_filter_list.Add(shader);
            }
        }
    }

    private void FilterByShaderAssetName(string target_shader_asset_name, List<Shader> shader_filter_list)
    {
        target_shader_asset_name = target_shader_asset_name.ToLower();
        var guidlist = FilterAllShaderGUIDs();
        foreach (var guid in guidlist)
        {
            var shaderPath = AssetDatabase.GUIDToAssetPath(guid);
            if (shaderPath.ToLower().Contains(target_shader_asset_name))
            {
                shader_filter_list.Add(AssetDatabase.LoadAssetAtPath<Shader>(shaderPath));
            }
        }
    }

    private void FilterByShaderVariantCount(UInt64 count, bool usedBySceneOnly, eShaderVariantCountCompare variantCountCompare, List<Shader> shader_filter_list)
    {
        var method = typeof(ShaderUtil).GetMethod("GetVariantCount", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);

        var guidlist = FilterAllShaderGUIDs();
        foreach (var guid in guidlist)
        {
            var shaderPath = AssetDatabase.GUIDToAssetPath(guid);
            var shader = AssetDatabase.LoadAssetAtPath<Shader>(shaderPath);
            var variantCount = (UInt64)method.Invoke(null, new System.Object[] { shader, true });

            var add = false;

            switch (variantCountCompare)
            {
                case eShaderVariantCountCompare.Equal:
                    add = variantCount == count;
                    break;
                case eShaderVariantCountCompare.Less:
                    add = variantCount < count;
                    break;
                case eShaderVariantCountCompare.LessEqual:
                    add = variantCount <= count;
                    break;
                case eShaderVariantCountCompare.Greater:
                    add = variantCount > count;
                    break;
                case eShaderVariantCountCompare.GreaterEqual:
                    add = variantCount >= count;
                    break;
                case eShaderVariantCountCompare.NotEqual:
                    add = variantCount != count;
                    break;
                default:
                    break;
            }
            if (add)
            {
                shader_filter_list.Add(shader);
            }
        }
    }

    private void WriteAllShaderVariantInfoLogs()
    {
        //logsPath
        var method = typeof(ShaderUtil).GetMethod("GetVariantCount", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);

        var sb = new StringBuilder();

        var guidlist = FilterAllShaderGUIDs();
        foreach (var guid in guidlist)
        {
            var shaderPath = AssetDatabase.GUIDToAssetPath(guid);
            var shader = AssetDatabase.LoadAssetAtPath<Shader>(shaderPath);
            var variantCount = (UInt64)method.Invoke(null, new System.Object[] { shader, true });

            sb.AppendLine($"shaderPath:{shaderPath},shaderName:{shader.name},variantCount:{variantCount}");
        }

        System.IO.File.WriteAllText(logsPath, sb.ToString());
    }

    private void OpenAllShaderVariantInfoLogsPath()
    {
		var selectPath = logsPath.Replace("/", "\\");
        System.Diagnostics.Process.Start("explorer.exe", $"/select,{selectPath}");
    }
}


其他优化方式 shader_feature_local 加上 shader + ref material 打包的方式

这种方式不用 shader variant collection 的方式来收集,因为 shader variant collection 不容易维护
然后多增加变体或是漏了变体
所以我在项目中使用的这种方式:

  • 遍历项目中所有使用到对应 shader 的材质
  • 将这些 材质 copy 到 ref_mat 目录
  • 将 shader 和 ref_mat 的 ab 名字设置同一个(即:shader 和 ref_mat 打进同一个AB包,这样变体不多也不少)

具体工具代码如下:

// jave.lni 2022/12/28 T2M shader 变体识别包打包工具

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public class T2M_Shader_Variant_PackerTools : EditorWindow
{
    [MenuItem("实用工具/Shader/T2M shader 变体识别包打包工具")]
    private static void _Show()
    {
        var win = EditorWindow.GetWindow<T2M_Shader_Variant_PackerTools>();
        win.titleContent = new GUIContent("T2M shader 变体识别包打包工具");
        win.Init();
        win.Show();
    }

    private void Init()
    {

    }

    private void OnGUI()
    {
        if (GUILayout.Button("打包T2M识别包"))
        {
            Shader_Variant_Analysic_Pack_Handle();
        }
        if (GUILayout.Button("清理Ref_Mats纹理"))
        {
            ClearRefMatsTexs();
        }
    }

    private void TexIfExsitPushToList(Material mat, string tex_name, HashSet<string> ret)
    {
        var tex = mat.HasProperty(tex_name) ? mat.GetTexture(tex_name) : null;
        if (tex != null)
        {
            var tex_asset_path = AssetDatabase.GetAssetPath(tex);
            if (!ret.Contains(tex_asset_path)) ret.Add(tex_asset_path);
        }
    }

    private void TexIfExsitClear(Material mat, string tex_name)
    {
        if (mat.HasProperty(tex_name))
        {
            mat.SetTexture(tex_name, null);
        }
    }

    private void Shader_Variant_Analysic_Pack_Handle()
    {
        var shader_name_hashset = new HashSet<string>();
        var shader_asset_path_list = new List<string>();
        var shader_tex_name_list = new List<string>();

        // 参考生成变体的 material 路径
        const string ref_mat_path = "Assets/Art/Shaders/T2M_Ref_Mats";

        // variant 包名
        const string variant_ab_name = "t2m_variant";

        // shader 名字集合
        shader_name_hashset.Add("Amazing Assets/Terrain To Mesh/Splatmap");

        // shader 文件集合
        shader_asset_path_list.Add("Assets/Amazing Assets/Terrain To Mesh/Shaders/Splatmap/Splatmap.shader");

        // shader texture name 集合
        shader_tex_name_list.Add("_T2M_SplatMap_0");
        shader_tex_name_list.Add("_T2M_Layer_0_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_0_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_0_Mask");
        shader_tex_name_list.Add("_T2M_Layer_1_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_1_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_1_Mask");
        shader_tex_name_list.Add("_T2M_Layer_2_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_2_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_2_Mask");
        shader_tex_name_list.Add("_T2M_Layer_3_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_3_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_3_Mask");
        shader_tex_name_list.Add("_T2M_SplatMap_1");
        shader_tex_name_list.Add("_T2M_Layer_4_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_4_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_4_Mask");
        shader_tex_name_list.Add("_T2M_Layer_5_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_5_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_5_Mask");
        shader_tex_name_list.Add("_T2M_Layer_6_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_6_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_6_Mask");
        shader_tex_name_list.Add("_T2M_Layer_7_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_7_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_7_Mask");
        shader_tex_name_list.Add("_T2M_SplatMap_2");
        shader_tex_name_list.Add("_T2M_Layer_8_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_8_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_8_Mask");
        shader_tex_name_list.Add("_T2M_Layer_9_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_9_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_9_Mask");
        shader_tex_name_list.Add("_T2M_Layer_10_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_10_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_10_Mask");
        shader_tex_name_list.Add("_T2M_Layer_11_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_11_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_11_Mask");
        shader_tex_name_list.Add("_T2M_SplatMap_3");
        shader_tex_name_list.Add("_T2M_Layer_12_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_12_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_12_Mask");
        shader_tex_name_list.Add("_T2M_Layer_13_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_13_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_13_Mask");
        shader_tex_name_list.Add("_T2M_Layer_14_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_14_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_14_Mask");
        shader_tex_name_list.Add("_T2M_Layer_15_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_15_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_15_Mask");
        shader_tex_name_list.Add("_T2M_SplatMaps2DArray");
        shader_tex_name_list.Add("_T2M_DiffuseMaps2DArray");
        shader_tex_name_list.Add("_T2M_NormalMaps2DArray");
        shader_tex_name_list.Add("_T2M_MaskMaps2DArray");
        shader_tex_name_list.Add("_T2M_HolesMap");
        shader_tex_name_list.Add("_MainTex");
        shader_tex_name_list.Add("_BumpMap");

        var tex_asset_path_hashset = new HashSet<string>();

        var mat_counter = 0;
        var all_mat_guids = AssetDatabase.FindAssets("t:material", new string[] { "Assets" });
        var sn = 0;

        if (!System.IO.Directory.Exists(ref_mat_path))
        {
            System.IO.Directory.CreateDirectory(ref_mat_path);
        }

        try
        {
            foreach (var mat_guid in all_mat_guids)
            {
                var mat_asset_path = AssetDatabase.GUIDToAssetPath(mat_guid);
                if (mat_asset_path.Contains("Editor") ||
                    mat_asset_path.StartsWith("Assets/Test/") ||
                    mat_asset_path.Contains("Ref_Mats")
                    )
                {
                    continue;
                }
                var mat = AssetDatabase.LoadAssetAtPath<Material>(mat_asset_path);

                // 过滤 shader
                if (mat != null && shader_name_hashset.Contains(mat.shader.name))
                {
                    var new_mat_asset_path = $"{ref_mat_path}/ref_mat_{sn++}.mat";
                    var new_mat = AssetDatabase.LoadAssetAtPath<Material>(new_mat_asset_path);

                    if (new_mat != null)
                    {
                        System.IO.File.Copy(mat_asset_path, new_mat_asset_path, true);
                    }
                    else
                    {
                        if (!AssetDatabase.CopyAsset(mat_asset_path, new_mat_asset_path))
                        {
                            System.IO.File.Copy(mat_asset_path, new_mat_asset_path, true);
                        }
                        new_mat = AssetDatabase.LoadAssetAtPath<Material>(new_mat_asset_path);
                    }

                    var src_ai = AssetImporter.GetAtPath(mat_asset_path);
                    var new_ai = AssetImporter.GetAtPath(new_mat_asset_path);
                    // 设置 src, new 材质 pbr_variant ab
                    //src_ai.assetBundleName = string.Empty;
                    new_ai.assetBundleName = variant_ab_name;
                    mat_counter++;
                }
            }
        }
        catch
        {

        }
        Debug.Log($"variant sn : {sn}");

        // 清理 材质中的纹理
        all_mat_guids = AssetDatabase.FindAssets("t:material", new string[] { ref_mat_path });
        foreach (var mat_guid in all_mat_guids)
        {
            var mat_asset_path = AssetDatabase.GUIDToAssetPath(mat_guid);
            var mat = AssetDatabase.LoadAssetAtPath<Material>(mat_asset_path);
            foreach (var tex_name in shader_tex_name_list)
            {
                TexIfExsitClear(mat, tex_name);
                //TexIfExsitPushToList(new_mat, tex_name, tex_asset_path_hashset);
            }
        }

        // 设置 shader pbr_variant ab
        {
            foreach (var item in shader_asset_path_list)
            {
                var ai = AssetImporter.GetAtPath(item);
                if (ai != null) ai.assetBundleName = variant_ab_name;
            }
        }

        //ScalableBufferManager
        //QualitySettings.resolutionScalingFixedDPIFactor
        //GraphicsSettings.reso


        // jave.lin : 先取消掉之前的纹理的 ab 信息
         设置 每个 pbr 材质使用到的纹理的 ab
        //foreach (var tex_asset_path in tex_asset_path_hashset)
        //{
        //    var ai = AssetImporter.GetAtPath(tex_asset_path);
        //    if (ai != null)
        //    {
        //        //ai.assetBundleName = tex_asset_path.ToLower().Replace("Assets/", "");
        //        //ai.assetBundleName = tex_asset_path.Substring(tex_asset_path.IndexOf("/") + 1);
        //        ai.assetBundleName = string.Empty;
        //        //ai.assetBundleName = tex_asset_path;
        //    }
        //}

        AssetDatabase.Refresh();
        AssetDatabase.SaveAssets();

        Debug.Log($"Variant AB识别包,已处理, shader : {shader_asset_path_list.Count}个, mat : {mat_counter}个, tex : {tex_asset_path_hashset.Count}个");
    }

    private void ClearRefMatsTexs()
    {

        var shader_asset_path_list = new List<string>();
        var shader_tex_name_list = new List<string>();

        // 参考生成变体的 material 路径
        const string ref_mat_path = "Assets/Art/Shaders/T2M_Ref_Mats";

        // shader texture name 集合
        shader_tex_name_list.Add("_T2M_SplatMap_0");
        shader_tex_name_list.Add("_T2M_Layer_0_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_0_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_0_Mask");
        shader_tex_name_list.Add("_T2M_Layer_1_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_1_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_1_Mask");
        shader_tex_name_list.Add("_T2M_Layer_2_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_2_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_2_Mask");
        shader_tex_name_list.Add("_T2M_Layer_3_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_3_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_3_Mask");
        shader_tex_name_list.Add("_T2M_SplatMap_1");
        shader_tex_name_list.Add("_T2M_Layer_4_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_4_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_4_Mask");
        shader_tex_name_list.Add("_T2M_Layer_5_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_5_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_5_Mask");
        shader_tex_name_list.Add("_T2M_Layer_6_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_6_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_6_Mask");
        shader_tex_name_list.Add("_T2M_Layer_7_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_7_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_7_Mask");
        shader_tex_name_list.Add("_T2M_SplatMap_2");
        shader_tex_name_list.Add("_T2M_Layer_8_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_8_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_8_Mask");
        shader_tex_name_list.Add("_T2M_Layer_9_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_9_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_9_Mask");
        shader_tex_name_list.Add("_T2M_Layer_10_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_10_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_10_Mask");
        shader_tex_name_list.Add("_T2M_Layer_11_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_11_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_11_Mask");
        shader_tex_name_list.Add("_T2M_SplatMap_3");
        shader_tex_name_list.Add("_T2M_Layer_12_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_12_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_12_Mask");
        shader_tex_name_list.Add("_T2M_Layer_13_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_13_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_13_Mask");
        shader_tex_name_list.Add("_T2M_Layer_14_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_14_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_14_Mask");
        shader_tex_name_list.Add("_T2M_Layer_15_Diffuse");
        shader_tex_name_list.Add("_T2M_Layer_15_NormalMap");
        shader_tex_name_list.Add("_T2M_Layer_15_Mask");
        shader_tex_name_list.Add("_T2M_SplatMaps2DArray");
        shader_tex_name_list.Add("_T2M_DiffuseMaps2DArray");
        shader_tex_name_list.Add("_T2M_NormalMaps2DArray");
        shader_tex_name_list.Add("_T2M_MaskMaps2DArray");
        shader_tex_name_list.Add("_T2M_HolesMap");
        shader_tex_name_list.Add("_MainTex");
        shader_tex_name_list.Add("_BumpMap");

        var all_mat_guids = AssetDatabase.FindAssets("t:material", new string[] { ref_mat_path });
        foreach (var mat_guid in all_mat_guids)
        {
            var mat_asset_path = AssetDatabase.GUIDToAssetPath(mat_guid);
            var mat = AssetDatabase.LoadAssetAtPath<Material>(mat_asset_path);
            foreach (var tex_name in shader_tex_name_list)
            {
                TexIfExsitClear(mat, tex_name);
                //TexIfExsitPushToList(new_mat, tex_name, tex_asset_path_hashset);
            }
        }

        AssetDatabase.Refresh();
        AssetDatabase.SaveAssets();
    }
}


IPreprocessShaders - 在shader AB 构建处理时,删除对应的变体

// jave.lin ref : https://blog.unity.com/technology/stripping-scriptable-shader-variants

using System.Collections.Generic;
using UnityEditor.Build;
using UnityEditor.Rendering;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
using static UnityEditor.Progress;

//public static class TestBuildAB
//{
//    [MenuItem("Assets/BuildABs")]
//    public static void _TestBuild()
//    {
//        var path = "Assets/StreamingAssets/game_assets";
//        if (!System.IO.Directory.Exists(path))
//        {
//            System.IO.Directory.CreateDirectory(path);
//        }
//        BuildPipeline.BuildAssetBundles(path, BuildAssetBundleOptions.ChunkBasedCompression, BuildTarget.Android);
//    }
//}

// Simple example of stripping of a debug build configuration
class ShaderBuildProcessor : IPreprocessShaders
{
    private ShaderKeyword[] skipArr = new ShaderKeyword[]
    {
        new ShaderKeyword("DYNAMICLIGHTMAP_ON"),
        //new ShaderKeyword("DIRLIGHTMAP_COMBINED"), // javelin : indirect.diffuse 有使用到
        //new ShaderKeyword("LIGHTPROBE_SH"), // jave.lin : reflection sh 需要
        //new ShaderKeyword("LIGHTMAP_SHADOW_MIXING"), // jave.lin : distance shadow mask 会使用到

        new ShaderKeyword("FOG_EXP"),
        new ShaderKeyword("FOG_EXP2"), //new ShaderKeyword("SHADOWS_DEPTH"), 
        new ShaderKeyword("DEBUG"),
        //new ShaderKeyword("SHADOWS_DEPTH"), // jave.lin : 主城水体的效果需要
        new ShaderKeyword("SHADOWS_CUBE"),

        new ShaderKeyword("SPOT"),
        //new ShaderKeyword("POINT"), // jave.lin : 角色实时补光需要使用到
        new ShaderKeyword("DIRECTIONAL_COOKIE"),
        new ShaderKeyword("POINT_COOKIE"),
    };

    public ShaderBuildProcessor()
    {
    }

    // Multiple callback may be implemented.
    // The first one executed is the one where callbackOrder is returning the smallest number.
    public int callbackOrder { get { return 0; } }

    private bool ShouldSkipVariant(Shader shader, ShaderCompilerData data)
    {
        foreach (var item in skipArr)
        {
            if (data.shaderKeywordSet.IsEnabled(item))
            {
                //Debug.Log($"shader.name : {shader.name} skip variant:{ShaderKeyword.GetGlobalKeywordName(item)}");
                return true;
            }
        }

        if (data.graphicsTier == GraphicsTier.Tier2)
        {
            //Debug.Log($"shader.name : {shader.name} skip tier 2");
            return true;
        }

        if (data.graphicsTier == GraphicsTier.Tier3)
        {
            //Debug.Log($"shader.name : {shader.name} skip tier 3");
            return true;
        }

        return false;
    }

    public void OnProcessShader(
        Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> shaderCompilerData)
    {
         In development, don't strip debug variants
        //if (EditorUserBuildSettings.development)
        //    return;

        for (int i = 0; i < shaderCompilerData.Count; ++i)
        {
            if (ShouldSkipVariant(shader, shaderCompilerData[i]))
            {
                shaderCompilerData.RemoveAt(i);
                --i;
            }
        }
    }
}


UBer Shader 拆分为 #define + 少量 #multi_compile 的多份 shader 优化实践结果

优化前 - Shader - 298.5 MB

角色展示界面内存:1.3GB
其中比较大块的是:

  • Shader - 298.5 MB
  • RenderTexture - 400 MB
  • Texture2D - 339.7 MB
    在这里插入图片描述

优化后 - Shader - 20.2 MB

角色展示界面内存:398.4 MB
其中比较大块的是:

  • Shader - 20.2 MB
  • RenderTexture - 50.7 MB
  • Texture2D - 111.1 MB
    在这里插入图片描述

特别明显的是 PBR Uber Shader 和 T2M shader 的优化前后

优化前
在这里插入图片描述

优化后
在这里插入图片描述
在这里插入图片描述

优化思路

就是之前说的:UBer Shader 拆分为 #define + 少量 #multi_compile 的多份 shader 优化实践结果
比如 Uber Shader 中的某个 Pass

优化前
在这里插入图片描述

优化后
在这里插入图片描述

如果在 unity editor 下让美术直接使用这个优化过的 shader ,那么会导致美术所见即所得很不方便,比如:要烘焙后,才能显示正常

因此我们的解决方法是: 美术在平时开发的时候,仍然是使用 UBer Shader
然后在 游戏发布 AssetBundle (AB资源)前,我们将所有的使用 UBer Shader 的材质,判断变体开关情况、目录情况,替换为对应的 拆分的变体,这样,让 2^N 的变体指数增长的变体复杂度大大减低

比如:

BeforeBuildMatShaderHandleTools.ReplaceAllMatShader(); // jave.lin : build ab 前材质的替换
ClearUpMatPropKWTools.ClearAllMat(); // jave.lin : 强制执行一下所有材质瘦身
BuildPipeline.BuildAssetBundles(xxxxx); // jave.lin : build ab

优点

shader 内存将:基本上原来 190 MB 的 UBer ,直接降到: 2~4 MB

缺点

容易导致 Unity Editor 下 的渲染和发布后的效果不同,比如:真机上可能需要 Shader.globalMaximumLOD 去切换一下值,才能正常显示 (下面有些解决方法,UNITY 的 BUG 吗?他就不会自动 fallback 到可以低级别的 lod shader 吗?),部分计算会多余浪费掉,比如:强制#define FOG_LINEAR 的shader 比如让 场景的 fog 开启线性雾,否则会有雾效显示异常的情况

比如,出现真机上的这些 BUG:

  • GLSL link error: The number of fragment samplers (17) is greater than the maximum number allowed (16).
  • Note: Creation of internal variant of shader 'Amazing Assets/Terrain To Mesh/Splatmap_layertex_5' failed.

在这里插入图片描述

这类问题解决方法也简单,将 sampler (纹理采样单元) 数量降低即可解决 (修改 shader,或是有一些效果,该删的删)

这也是为何之前说的 Shader.globalMaximumLOD 切换一下就好了,因为低级别一些的 LOD,使用的 sampler 是没有超出 16 个的


阴影变体兼容性缺点要慎重处理

后续补充的踩坑敬仰:Unity - 记一次非正规变体优化带来的兼容性导致部分手机卡死的问题


其他优化方案 - 目前最靠谱的方式


shader

注意尾部我们增加了一些 CUSTOM_SKIP_START(xxx)CUSTOM_SKIP_END 注释来方便剔除的

Shader "xxx"
{
	Properties {}
	XXXPROGRAM
	...
	#pragma multi_compile _ A
	#pragma multi_compile _ B
	#pragma multi_compile _ C
	ENDXXX
}

// jave.lin :
// completely_match_group : 完全吻合的变体,就是不多也不少的匹配规则
// CUSTOM_SKIP_START(completely_match_group)
A C
// CUSTOM_SKIP_END(xxx)

// jave.in :
// any_match_group : 只要变体中 同时存在 A B 的
// CUSTOM_SKIP_START(any_match_group)
A B
// CUSTOM_SKIP_END(xxx)

// jave.lin :
// 其他的规则可以自行添加
// 如果是想要剔除 多个变体,只要包含,就剔除,可以使用unity shaderlab自带的: #pragma skip_variants xxx1 xxx2 xxx3

csharp

IPreprocessShaders 接口实现中,我们要对 shader 的 CUSTOM_SKIP_START(xxx)CUSTOM_SKIP_END 提取
然后这个 shader 变体是否需要剔除

这里的代码我就不写了


References

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值