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;
}
}
}