您当前的位置: 首页 >  Jave.Lin unity

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

Jave.Lin 发布时间:2021-08-30 12:36:18 ,浏览量:3

文章目录
  • 变体过多的缺点
  • 项目情况
  • #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 在这里插入图片描述

变体的数量

如下图: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 shaderInfoList = new List();
    private List 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();
        win.titleContent = new GUIContent("打包构建前、后 shader 的处理工具");
        win.Show();
    }

    private void OnGUI()
    {
#if __DATAFORM_TXT__
        if (shaderList == null)
        {
            shaderList = new List();
            BeforeAndAfterBuildShadersHandler.LoadDataFromTxt(BeforeAndAfterBuildShadersHandler.dataPath, shaderList);
        }
#else
        if (shaderList == null)
        {
            shaderList = new List();
            var data = AssetDatabase.LoadAssetAtPath(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  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  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 ret)
    {
#if __DATAFORM_TXT__
        LoadDataFromTxt(assetPath, ret);
#else
        LoadDataFromAsset(assetPath, ret);
#endif
    }

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

    public static void LoadDataFromTxt(string txtPath, List 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(path);
            if (shader == null) continue;
            ret.Add(shader);
        }
    }

    public static void SaveAsset(List 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(dataPath);
        if (data == null)
        {
            data = ScriptableObject.CreateInstance();
            isNew = true;
        }
        var saveList = new List();
        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();
            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 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(" ", 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   ...             
关注
打赏
1688896170
查看更多评论
0.1231s