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

文章目录

  • 变体过多的缺点
  • 项目情况
  • #pragma multi_compile_fwdbase 和 multi_compile_fog 生存的变体(keyword)
    • 生存的变体
    • 变体的数量
    • 查看编译生存的各个变体的代码,并搜索 Global Keywords - 源码级别
    • 查看编译生存的各个变体 - keyword 级别
  • 如何优化
    • 将 #pragma multi_compile_fwdbase 和 multi_compile_fog 预生成项拆解为单个的手动定义项
    • 优化之后
  • 优化后的小问题、如何解决
    • 编辑器 源码
    • 操作界面
  • 示例
  • multi_compile 的一些 built-in 快件方式包含的 keyword
  • ShaderFinderTool.cs
  • 其他优化方式 shader_feature_local 加上 shader + ref material 打包的方式
  • IPreprocessShaders - 在shader AB 构建处理时,删除对应的变体
  • UBer Shader 拆分为 #define + 少量 #multi_compile 的多份 shader 优化实践结果
    • 优化前 - Shader - 298.5 MB
    • 优化后 - Shader - 20.2 MB
    • 优化思路
    • 优点
    • 缺点
      • 阴影兼容性缺点要慎重处理
  • References


变体过多的缺点

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

  • 打包时编译变体的时间增加(如果你的变体使用 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
Unity Shader - Built-in管线下优化变体_第1张图片

变体的数量

如下图:98 个
Unity Shader - Built-in管线下优化变体_第2张图片

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

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

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

Unity Shader - Built-in管线下优化变体_第3张图片

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


如何优化

根据上面 搜索 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 Shader - Built-in管线下优化变体_第4张图片


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

但是这样优化后的了另一个问题: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   ... 
            // 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 脚本化调用
Unity Shader - Built-in管线下优化变体_第5张图片


另外,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 变体的统计都可以使用下面自己写的工具来辅助加速分析

Unity Shader - Built-in管线下优化变体_第6张图片

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

Unity Shader - Built-in管线下优化变体_第7张图片

// 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//..."
         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
    Unity Shader - Built-in管线下优化变体_第8张图片

优化后 - Shader - 20.2 MB

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

  • Shader - 20.2 MB
  • RenderTexture - 50.7 MB
  • Texture2D - 111.1 MB
    Unity Shader - Built-in管线下优化变体_第9张图片

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

优化前
Unity Shader - Built-in管线下优化变体_第10张图片

优化后
Unity Shader - Built-in管线下优化变体_第11张图片
Unity Shader - Built-in管线下优化变体_第12张图片

优化思路

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

优化前
Unity Shader - Built-in管线下优化变体_第13张图片

优化后
Unity Shader - Built-in管线下优化变体_第14张图片

如果在 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.

Unity Shader - Built-in管线下优化变体_第15张图片

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

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


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

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


References

  • Optimizing shader variants
  • IPreprocessShaders.OnProcessShader
  • Nicholas10128/AAAResearch
  • Optimizing shader runtime performance - 优化变体固然重要,shader性能同样重要,双剑合璧之下,才能,加载快,运行也快
  • 图形引擎实战:Unity Shader变体管理流程
  • 一种Shader变体收集打包以及编译优化的思路
  • ShaderVariantCollector - github

你可能感兴趣的:(unity-shader,unity,unity,unity,shader,变体,减少shader变体)