当前位置: 首页 > 工具软件 > Baker > 使用案例 >

MeshBaker一键合并网格编辑器实现

袁炳
2023-12-01

简介

MeshBaker是一款性能优化的插件。它可以做到材质、网格合并,从而降低渲染消耗。
MeshBaker

功能

本次编写的编辑器对场景特定目录下的网格进行合并,隐藏原零散网格,并将合并后的网格挂在CombinedMeshNode节点,由于移动端打包后在手机上没有GI效果,故合批后删除了放置光源重新烘焙的过程,有需要的话自行补充编写。

代码

实现流程

// ********************************************************
// 描述:自动烘焙器 自动分析当前场景 对网格和材质进行合批
// 作者:ShadowRabbit 
// 创建时间:2020年7月30日 20:42:14
// ********************************************************

using UnityEditor;

namespace EditorTools.MeshBakerEditor
{
    public static class AutoBaker
    {
        private static readonly AutoBakerInternal Abi = new AutoBakerInternal();

        /// <summary>
        /// 自动烘焙
        /// </summary>
        [MenuItem("Tools/Mesh Baker/AutoBake &-", false, 100)]
        public static void AutoBake() {
            //数据储存目录检测
            Abi.CheckGenerateAssetsFolder();
            //组合节点检测
            Abi.CheckCombinedNode();
            //组合节点
            var combinedNode = Abi.GetCombinedNode();

            //场景自动解析
            var sceneAnalysisResults = Abi.AutoAnalysis();

            //合并网格和材质
            var generateAssetsFolder = Abi.GetGenerateAssetsFolder();
            Abi.CombineMeshAndMaterials(sceneAnalysisResults, generateAssetsFolder, combinedNode);
            //重新生成光照
            //因打包后移动端GI不生效 故省略此过程
        }
    }
}

具体实现过程

// ********************************************************
// AutoBakerInternal.cs
// 描述:
// 作者:ShadowRabbit 
// 创建时间:2020年8月3日 14:22:08
// ********************************************************

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using DigitalOpus.MB.Core;
using EditorTools.MeshBakerEditor.Extension;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace EditorTools.MeshBakerEditor
{
    public class AutoBakerInternal
    {
        private const int MaxVertex = 65535;
        private const int AtlasSize = 2048;
        private const string CombinedMeshNodeName = "CombinedMeshNode"; //所有生成的组合网格将挂载于场景中的这个节点下
        private Transform combinedNode;

        /// <summary>
        /// 目录检测
        /// </summary>
        public void CheckGenerateAssetsFolder() {
            //资源生成路径
            var folder = GetGenerateAssetsFolder();
            //创建数据储存目录
            if (!Directory.Exists(folder)) {
                Directory.CreateDirectory(folder);
            }

            //删除目录下旧数据
            var directionInfo = new DirectoryInfo(folder);
            var fileInfos = directionInfo.GetFiles("*", SearchOption.AllDirectories);
            foreach (var t in fileInfos) {
                var filePath = folder + "/" + t.Name;
                File.Delete(filePath);
            }
        }

        /// <summary>
        /// 组合节点检测
        /// </summary>
        public void CheckCombinedNode() {
            var objCombined = GameObject.Find(CombinedMeshNodeName);
            //销毁挂载节点
            if (objCombined != null) {
                Undo.DestroyObjectImmediate(objCombined);
            }

            combinedNode = new GameObject {name = CombinedMeshNodeName}.transform;
        }

        /// <summary>
        /// 获取组合节点
        /// </summary>
        public Transform GetCombinedNode() {
            if (combinedNode == null) {
                Debug.LogWarning("组合节点不存在");
            }

            return combinedNode;
        }

        /// <summary>
        /// 获取烘焙资源保存路径
        /// </summary>
        /// <returns></returns>
        public string GetGenerateAssetsFolder() {
            var sb = new StringBuilder();
            var scenePath = FindSceneAssetPath();
            sb.Append(scenePath);
            sb.Append("/MeshBakerData");
            sb.Append("/");
            Debug.Log("资源文件生成目录:" + sb);
            //储存当前烘焙数据的路径
            return sb.ToString();
        }

        /// <summary>
        /// 合并网格和材质
        /// </summary>
        /// <param name="sceneAnalysisResults">分析结果</param>
        /// <param name="generateAssetsFolder">烘焙数据保存路径</param>
        /// <param name="generateNode">生成后需要挂载的节点</param>
        public void CombineMeshAndMaterials(IEnumerable<List<GameObjectFilterInfo>> sceneAnalysisResults,
            string generateAssetsFolder, Transform generateNode) {
            var buildingNode = FindBuildingNode();
            if (buildingNode == null) {
                Debug.LogError("建筑节点不存在");
                return;
            }

            //激活建筑节点
            buildingNode.gameObject.SetActive(true);
            //根据解析结果创建meshBaker
            foreach (var objFilterInfoList in sceneAnalysisResults) {
                //分析结果过滤
                var activeObjFilterInfoList = objFilterInfoList.Where(objFilterInfo => objFilterInfo.go.IsValid())
                    .Where(objFilterInfo => objFilterInfo.go.IsInBuildingNode());
                var gameObjectFilterInfos = activeObjFilterInfoList.ToList();
                //没有有效数据
                if (!gameObjectFilterInfos.Any()) {
                    Debug.LogWarning("没有有效数据");
                    continue;
                }

                var obj = CreateAndSetupBaker(gameObjectFilterInfos, generateAssetsFolder);
                var texBaker = obj.GetComponent<MB3_TextureBaker>();
                texBaker.CreateAtlases(UpdateProgressBar, true, new MB3_EditorMethods());
                EditorUtility.ClearProgressBar();
                if (texBaker.textureBakeResults != null) EditorUtility.SetDirty(texBaker.textureBakeResults);
                var meshBaker = obj.GetComponentInChildren<MB3_MeshBaker>();
                //保留原有光照贴图 注意光照贴图索引不同的物体组合在一起,光照贴图将无法生效
                meshBaker.meshCombiner.lightmapOption = MB2_LightmapOptions.preserve_current_lightmapping;
                MB3_MeshBakerEditorInternal.bake(meshBaker);
                meshBaker.meshCombiner.resultSceneObject.transform.SetParent(generateNode);
                Undo.DestroyObjectImmediate(obj);
            }

            //建筑节点失活
            buildingNode.gameObject.SetActive(false);
        }

        /// <summary>
        /// 自动分析场景
        /// </summary>
        /// <returns></returns>
        public IEnumerable<List<GameObjectFilterInfo>> AutoAnalysis() {
            try {
                EditorUtility.DisplayProgressBar("自动合并烘焙", "合并开始", .99f);
                //过滤组
                var filters = GetFilters();
                //已经包含在baker中的objs
                EditorUtility.DisplayProgressBar("自动合并烘焙", "开始计算baker中的objs", .7f);
                GetObjectsAlreadyIncludedInBakers(out var objectsAlreadyIncludedInBakers);
                //获取场景中obj过滤信息列表
                EditorUtility.DisplayProgressBar("自动合并烘焙", "获取物体过滤信息列表", .5f);
                GetObjectFilterInfos(objectsAlreadyIncludedInBakers, filters, out var gameObjectFilterInfoList);
                //网格分析
                EditorUtility.DisplayProgressBar("自动合并烘焙", "正在分析网格数据", .4f);
                MeshAnalysis(gameObjectFilterInfoList);
                //不会被添加到烘焙器中的obj集合
                var objsNotAddedToBaker = new List<GameObjectFilterInfo>();
                //分析结果 
                EditorUtility.DisplayProgressBar("自动合并烘焙", "正在分析结果", .3f);
                var mapAnalysisResults =
                    MB3_MeshBakerEditorWindow.sortIntoBakeGroups3(gameObjectFilterInfoList, objsNotAddedToBaker,
                        filters.ToArray(),
                        true, AtlasSize);

                //三元集合转成2元
                EditorUtility.DisplayProgressBar("自动合并烘焙", "正在转换结果", .2f);
                AnalysisResultsMapToList(mapAnalysisResults, out var sceneAnalysisResults);
                return sceneAnalysisResults;
            }
            catch (Exception ex) {
                Debug.LogError(ex.StackTrace);
            }
            finally {
                EditorUtility.ClearProgressBar();
            }

            return null;
        }

        private void UpdateProgressBar(string msg, float progress) {
            EditorUtility.DisplayProgressBar("合并网格中", msg, progress);
        }

        /// <summary>
        /// 创建baker
        /// </summary>
        /// <param name="objFilterInfoList">过滤信息集合</param>
        /// <param name="dataSavePath">数据储存路径</param>
        private GameObject CreateAndSetupBaker(IList<GameObjectFilterInfo> objFilterInfoList,
            string dataSavePath) {
            //清除无效数据
            for (var i = objFilterInfoList.Count - 1; i >= 0; i--) {
                if (objFilterInfoList[i].go == null) objFilterInfoList.RemoveAt(i);
            }

            if (objFilterInfoList.Count < 1) {
                Debug.LogError("空的过滤器信息");
                return null;
            }

            if (string.IsNullOrEmpty(dataSavePath)) {
                Debug.LogError("空的储存目录");
                return null;
            }

            //顶点数累加
            var numVerts = objFilterInfoList.Sum(t => t.numVerts);

            //顶点数超过整型最大了 使用multiMeshBaker
            var newMeshBaker = numVerts >= MaxVertex
                ? MB3_MultiMeshBakerEditor.CreateNewMeshBaker()
                : MB3_MeshBakerEditor.CreateNewMeshBaker();

            newMeshBaker.name =
                ("MeshBaker-" + objFilterInfoList[0].shaderName + "-LM" + objFilterInfoList[0].lightmapIndex)
                .Replace("/", "-");

            var tb = newMeshBaker.GetComponent<MB3_TextureBaker>();
            var mb = tb.GetComponentInChildren<MB3_MeshBakerCommon>();

            tb.GetObjectsToCombine().Clear();
            //去重复 添加到待组合列表中
            foreach (var t in objFilterInfoList) {
                if (t.go != null && !tb.GetObjectsToCombine().Contains(t.go)) {
                    tb.GetObjectsToCombine().Add(t.go);
                }
            }

            tb.maxAtlasSize = AtlasSize;
            if (objFilterInfoList[0].numMaterials > 1) {
                // 多材质有bug 暂时不调用
                // //材质储存路径
                // var pthMat = AssetDatabase.GenerateUniqueAssetPath(dataSavePath + newMeshBaker.name + ".asset");
                // //创建图集
                // MB3_TextureBakerEditorInternal.CreateCombinedMaterialAssets(tb, pthMat);
                // tb.doMultiMaterial = true;
                // var tbr = new SerializedObject(tb);
                // var resultMaterials = tbr.FindProperty("resultMaterials");
                // MB3_TextureBakerEditorInternal.ConfigureMutiMaterialsFromObjsToCombine2(tb, resultMaterials, tbr);
                Debug.LogWarning(objFilterInfoList[0].go.name + "使用了多个材质,由于MeshBaker同一网格使用多材质有bug,故本次没有调用");
                var pthMat = AssetDatabase.GenerateUniqueAssetPath(dataSavePath + newMeshBaker.name + ".asset");
                MB3_TextureBakerEditorInternal.CreateCombinedMaterialAssets(tb, pthMat);
            }
            else {
                var pthMat = AssetDatabase.GenerateUniqueAssetPath(dataSavePath + newMeshBaker.name + ".asset");
                MB3_TextureBakerEditorInternal.CreateCombinedMaterialAssets(tb, pthMat);
            }

            mb.meshCombiner.renderType = objFilterInfoList[0].isMeshRenderer
                ? MB_RenderType.meshRenderer
                : MB_RenderType.skinnedMeshRenderer;
            return newMeshBaker;
        }

        /// <summary>
        /// 数据结构转换
        /// </summary>
        /// <param name="mapAnalysisResults"></param>
        /// <param name="sceneAnalysisResults"></param>
        private void AnalysisResultsMapToList(
            Dictionary<GameObjectFilterInfo, List<List<GameObjectFilterInfo>>> mapAnalysisResults,
            out List<List<GameObjectFilterInfo>> sceneAnalysisResults) {
            //三元集合转成2元
            sceneAnalysisResults = new List<List<GameObjectFilterInfo>>();
            foreach (var gow in mapAnalysisResults.Keys) {
                var gows = mapAnalysisResults[gow];
                sceneAnalysisResults.AddRange(gows);
            }
        }

        /// <summary>
        /// 获取过滤组
        /// </summary>
        /// <returns></returns>
        private List<IGroupByFilter> GetFilters() {
            var filters = new List<IGroupByFilter>();
            var filterByShader = new GroupByShader();
            var filterByRenderType = new GroupByRenderType();
            var filterByStatic = new GroupByStatic();
            var filterByOutOfBoundsUVs = new GroupByOutOfBoundsUVs();
            var filterByLightMapIndex = new GroupByLightmapIndex();
            filters.Add(filterByShader);
            filters.Add(filterByRenderType);
            filters.Add(filterByStatic);
            filters.Add(filterByOutOfBoundsUVs);
            filters.Add(filterByLightMapIndex);
            return filters;
        }

        /// <summary>
        /// 获取已经包含在baker中的objs
        /// </summary>
        /// <param name="objectsAlreadyIncludedInBakers"></param>
        private void GetObjectsAlreadyIncludedInBakers(out HashSet<GameObject> objectsAlreadyIncludedInBakers) {
            //寻找场景中的baker
            var allBakers = Resources.FindObjectsOfTypeAll<MB3_MeshBakerRoot>();
            objectsAlreadyIncludedInBakers = new HashSet<GameObject>();
            //遍历baker
            foreach (var t in allBakers) {
                //某个baker中等待组合的obj集合
                var objsToCombine = t.GetObjectsToCombine();
                //将等待组合的objs存入objectsAlreadyIncludedInBakers
                foreach (var t1 in objsToCombine.Where(t1 => t1 != null)) {
                    objectsAlreadyIncludedInBakers.Add(t1);
                }
            }
        }

        /// <summary>
        /// 获取场景中obj过滤信息列表
        /// </summary>
        /// <param name="objectsAlreadyIncludedInBakers">已经包含在baker中的objs</param>
        /// <param name="filters">过滤器组</param>
        /// <param name="gameObjectFilterInfoList">obj过滤信息列表</param>
        /// <returns>obj过滤信息列表</returns>
        private void GetObjectFilterInfos(HashSet<GameObject> objectsAlreadyIncludedInBakers,
            List<IGroupByFilter> filters, out List<GameObjectFilterInfo> gameObjectFilterInfoList) {
            gameObjectFilterInfoList = new List<GameObjectFilterInfo>();
            //获取场景中所有renderer组件
            var renderers = (Renderer[]) Resources.FindObjectsOfTypeAll(typeof(Renderer));
            gameObjectFilterInfoList.AddRange(from renderer in renderers
                where renderer is MeshRenderer || renderer is SkinnedMeshRenderer
                where renderer.GetComponent<TextMesh>() == null
                select new GameObjectFilterInfo(renderer.gameObject, objectsAlreadyIncludedInBakers, filters.ToArray())
                into objFilterInfo
                where objFilterInfo.materials.Length > 0
                select objFilterInfo);
            // foreach (var renderer in renderers) {
            //     if (!(renderer is MeshRenderer) && !(renderer is SkinnedMeshRenderer)) continue;
            //     //不管textMesh
            //     if (renderer.GetComponent<TextMesh>() != null) {
            //         continue;
            //     }
            //     //实例化obj过滤器信息
            //     var objFilterInfo = new GameObjectFilterInfo(renderer.gameObject, objectsAlreadyIncludedInBakers,
            //         filters.ToArray());
            //     //没有材质的不考虑
            //     if (objFilterInfo.materials.Length <= 0) continue;
            //     gameObjectFilterInfoList.Add(objFilterInfo);
            // }
        }

        /// <summary>
        /// mesh相关分析
        /// </summary>
        private void MeshAnalysis(IReadOnlyList<GameObjectFilterInfo> gameObjects) {
            //网格分析 mesh
            var meshAnalysisResultCache = new Dictionary<int, MB_Utility.MeshAnalysisResult>();
            //遍历obj过滤器信息集合
            for (var i = 0; i < gameObjects.Count; i++) {
                var rpt = $"Processing {gameObjects[i].go.name} [{i} of {gameObjects.Count}]";
                var mm = MB_Utility.GetMesh(gameObjects[i].go); //当前物体的网格
                if (mm != null) {
                    if (!meshAnalysisResultCache.TryGetValue(mm.GetInstanceID(), out var mar)) {
                        EditorUtility.DisplayProgressBar("Analysing Scene", rpt + " Check Out Of Bounds UVs", .6f);
                        MB_Utility.hasOutOfBoundsUVs(mm, ref mar);
                        MB_Utility.doSubmeshesShareVertsOrTris(mm, ref mar);
                        meshAnalysisResultCache.Add(mm.GetInstanceID(), mar);
                    }

                    //uv越界检测
                    if (mar.hasOutOfBoundsUVs) {
                        var w = (int) mar.uvRect.width;
                        var h = (int) mar.uvRect.height;
                        gameObjects[i].outOfBoundsUVs = true;
                        gameObjects[i].warning +=
                            " [WARNING: has uvs outside the range (0,1) tex is tiled " + w + "x" + h +
                            " times]";
                    }

                    //顶点重合
                    if (mar.hasOverlappingSubmeshVerts) {
                        gameObjects[i].submeshesOverlap = true;
                        gameObjects[i].warning +=
                            " [WARNING: Submeshes share verts or triangles. 'Multiple Combined Materials' feature may not work.]";
                    }
                }

                var mr = gameObjects[i].go.GetComponent<Renderer>();
                //多个子网格使用相同材质
                if (!MB_Utility.AreAllSharedMaterialsDistinct(mr.sharedMaterials)) {
                    gameObjects[i].warning +=
                        " [WARNING: Object uses same material on multiple submeshes. This may produce poor results when used with multiple materials or fix out of bounds uvs.]";
                }
            }
        }

        /// <summary>
        /// 查找建筑节点
        /// </summary>
        /// <returns></returns>
        private Transform FindBuildingNode() {
            var transforms = Resources.FindObjectsOfTypeAll<Transform>();
            return transforms.FirstOrDefault(transform => transform.name.Equals("Building"));
        }

        /// <summary>
        /// 寻找场景资源路径
        /// </summary>
        private string FindSceneAssetPath() {
            var scene = SceneManager.GetActiveScene();
            var scenePath = scene.path;
            var directoryInfo = Directory.GetParent(scenePath);
            var fullName = directoryInfo.FullName.Replace("\\", "/");
            var relativePath = fullName.Substring(fullName.IndexOf("Assets", StringComparison.Ordinal));
            return relativePath;
        }

        //递归压栈搜索目录,因为有api暂时废弃了,如果有需要再粘回来

        #region Abandoned

        /// <summary>
        /// 目录搜索
        /// </summary>
        /// <param name="path">要搜索的目录</param>
        /// <param name="targetPath">关键词</param>
        private string SearchPath(string path, string targetPath) {
            var pathStack = new Stack<string>();
            PushPaths(ref pathStack, path);
            while (pathStack.Count > 0) {
                var currentPath = pathStack.Pop();
                if (!currentPath.EndsWith(targetPath)) continue;
                return currentPath;
            }

            Debug.Log(path + "中不存在目录" + targetPath);
            return string.Empty;
        }

        /// <summary>
        /// 将待检测的path入栈
        /// </summary>
        /// <param name="pathStack"></param>
        /// <param name="path"></param>
        private void PushPaths(ref Stack<string> pathStack, string path) {
            if (pathStack == null) {
                return;
            }

            if (string.IsNullOrEmpty(path)) {
                return;
            }

            var fixPath = path.Replace("\\", "/");
            pathStack.Push(fixPath);
            //目录修正
            var subPaths = Directory.GetDirectories(fixPath);
            //存在子目录
            if (subPaths.Length <= 0) return;
            foreach (var subPath in subPaths) {
                var fixSubPath = subPath.Replace("\\", "/");
                PushPaths(ref pathStack, fixSubPath);
            }
        }

        #endregion
    }
}

工具类扩展

// ********************************************************
// MBGameObjectExtension.cs
// 描述:GameObject辅助扩展类
// 作者:ShadowRabbit 
// 创建时间:2020年8月1日 10:43:34
// ********************************************************

using UnityEngine;

namespace EditorTools.MeshBakerEditor.Extension
{
    public static class MbGameObjectExtension
    {
        /// <summary>
        /// 判断某个物体是否有效
        /// </summary>
        /// <param name="gameObject"></param>
        /// <returns></returns>
        public static bool IsValid(this GameObject gameObject) {
            //当前节点没有激活
            if (!gameObject.activeSelf) {
                Debug.Log("失效物体:" + gameObject.name + "原因:自身失效");
                return false;
            }

            //父节点
            var currentTransform = gameObject.transform.parent;
            //遍历节点链表 查找父节点中是否有节点未激活
            while (currentTransform != null) {
                if (!currentTransform.gameObject.activeSelf) {
                    Debug.Log("失效物体:" + gameObject.name + "原因:父节点" + currentTransform.gameObject.name + "失效");
                    return false;
                }

                currentTransform = currentTransform.parent;
            }

            return true;
        }

        /// <summary>
        /// 判断某个物体是否在Building节点下
        /// </summary>
        /// <param name="gameObject"></param>
        /// <returns></returns>
        public static bool IsInBuildingNode(this GameObject gameObject) {
            //建筑节点
            Transform buildingNode = null;
            //遍历父节点
            var parent = gameObject.transform.parent;
            while (parent != null) {
                if (parent.name.Equals("Building")) {
                    buildingNode = parent;
                    break;
                }
                parent = parent.parent;
            }

            //建筑节点不存在
            if (buildingNode==null) {
                return false;
            }

            var rootNode = buildingNode.parent;
            //建筑节点没有在根节点上
            if (!rootNode.name.Equals("Root")) {
                return false;
            }

            return rootNode.parent == null;
        }
    }
}
 类似资料: