using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using TriLibCore;
using UltimateGameTools.MeshSimplifier;
namespace ModelLODTool
{
// 尺寸类型枚举
public enum ObjectSizeType
{
Tiny,
Small,
MediumSmall,
SmallMedium,
Medium,
MediumLarge,
Large,
Giant
}
// LOD层级配置
[Serializable]
public class LODLevelConfig
{
public int lodIndex;
public float distance;
public float trianglePercentage;
public GameObject meshObject;
public string assetPath;
}
// 模型配置
[Serializable]
public class ModelConfig : ScriptableObject
{
public string objectName;
public string chineseName;
public string englishName;
public ObjectSizeType sizeType;
public string modelPath;
public List<LODLevelConfig> lodLevels = new List<LODLevelConfig>();
}
// 配置数据库
public class ModelConfigDatabase : ScriptableObject
{
public List<ModelConfig> modelConfigs = new List<ModelConfig>();
public ModelConfig GetConfig(string name)
{
return modelConfigs.FirstOrDefault(c => c.objectName == name);
}
public void AddConfig(ModelConfig config)
{
if (!modelConfigs.Contains(config))
modelConfigs.Add(config);
}
public void UpdateConfig(ModelConfig config)
{
int index = modelConfigs.FindIndex(c => c.objectName == config.objectName);
if (index != -1)
modelConfigs[index] = config;
}
public bool ContainsConfig(string name)
{
return modelConfigs.Any(c => c.objectName == name);
}
}
// LOD管理器(场景中使用)
public class ModelLODManager : MonoBehaviour
{
public string modelName;
public LODGroup lodGroup;
public ModelConfig config;
public void ApplyLODConfig()
{
if (lodGroup == null || config == null || config.lodLevels == null) return;
var lods = new LOD[config.lodLevels.Count];
for (int i = 0; i < config.lodLevels.Count; i++)
{
var lodConfig = config.lodLevels[i];
if (lodConfig.meshObject)
{
var renderers = lodConfig.meshObject.GetComponentsInChildren<Renderer>();
lods[i] = new LOD(1 - (i / (float)config.lodLevels.Count), renderers);
}
}
lodGroup.SetLODs(lods);
lodGroup.RecalculateBounds();
}
}
// 主工具类
public class ModelLODConfigTool : EditorWindow
{
// 配置与UI变量
private ModelConfig currentConfig;
private ModelConfigDatabase configDatabase;
private readonly string dbPath = "Assets/ModelLODConfig/ModelConfigDatabase.asset";
private Vector2 scrollPos;
private GameObject previewRoot;
private GameObject mainModel;
private string[] configNames;
private int selectedConfigIndex = -1;
private bool showPreview = true;
// LOD距离预设(按尺寸类型)
private readonly Dictionary<ObjectSizeType, List<float>> sizeLodDistances = new Dictionary<ObjectSizeType, List<float>>
{
{ ObjectSizeType.Tiny, new() { 0.5f, 1f, 2f } },
{ ObjectSizeType.Small, new() { 1f, 2f, 3f } },
{ ObjectSizeType.MediumSmall, new() { 1f, 3f, 5f } },
{ ObjectSizeType.SmallMedium, new() { 1f, 5f, 10f } },
{ ObjectSizeType.Medium, new() { 2f, 7f, 15f } },
{ ObjectSizeType.MediumLarge, new() { 4f, 10f, 20f } },
{ ObjectSizeType.Large, new() { 8f, 20f, 50f } },
{ ObjectSizeType.Giant, new() { 10f, 50f, 100f } },
};
// 面数百分比预设
private readonly List<float> lodTrianglePercent = new() { 100f, 50f, 10f };
[MenuItem("Tools/模型LOD配置工具")]
public static void ShowWindow() => GetWindow<ModelLODConfigTool>("模型LOD配置工具");
private void OnEnable()
{
LoadDatabase();
CreatePreviewRoot();
CreateNewConfig();
}
private void OnDisable()
{
if (previewRoot) DestroyImmediate(previewRoot);
}
private void OnGUI()
{
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
DrawConfigSelection();
DrawModelInfo();
DrawMainModelSection();
DrawLODConfig();
DrawPreview();
DrawActionButtons();
EditorGUILayout.EndScrollView();
}
#region UI绘制
private void DrawConfigSelection()
{
EditorGUILayout.LabelField("配置管理", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
if (configNames != null && configNames.Length > 0)
{
selectedConfigIndex = EditorGUILayout.Popup("选择配置", selectedConfigIndex, configNames);
if (GUILayout.Button("加载", GUILayout.Width(60)) && selectedConfigIndex >= 0)
LoadSelectedConfig();
}
if (GUILayout.Button("新建", GUILayout.Width(60)))
CreateNewConfig();
EditorGUILayout.EndHorizontal();
}
private void DrawModelInfo()
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("模型信息", EditorStyles.boldLabel);
currentConfig.objectName = EditorGUILayout.TextField("物体名称", currentConfig.objectName);
currentConfig.chineseName = EditorGUILayout.TextField("中文名称", currentConfig.chineseName);
currentConfig.englishName = EditorGUILayout.TextField("英文名称", currentConfig.englishName);
currentConfig.sizeType = (ObjectSizeType)EditorGUILayout.EnumPopup("尺寸类型", currentConfig.sizeType);
if (GUILayout.Button("应用尺寸预设LOD"))
ApplySizeLodPreset();
}
private void DrawMainModelSection()
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("主模型", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(mainModel ? $"已加载: {mainModel.name}" : "未加载模型");
if (GUILayout.Button("加载主模型", GUILayout.Width(100)))
LoadMainModel();
EditorGUILayout.EndHorizontal();
}
private void DrawLODLevel(int index)
{
var lod = currentConfig.lodLevels[index];
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField($"LOD {index}", EditorStyles.boldLabel);
lod.distance = EditorGUILayout.FloatField("距离阈值(m)", lod.distance);
lod.trianglePercentage = EditorGUILayout.FloatField("面数百分比(%)", lod.trianglePercentage);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(lod.meshObject ? lod.meshObject.name : "未加载模型");
if (GUILayout.Button("加载模型", GUILayout.Width(80)))
LoadLODModel(index);
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
private void DrawLODConfig()
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("LOD配置", EditorStyles.boldLabel);
for (int i = 0; i < currentConfig.lodLevels.Count; i++)
{
DrawLODLevel(i);
EditorGUILayout.Space();
}
if (GUILayout.Button("自动生成LOD"))
GenerateAutoLOD();
}
private void DrawPreview()
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("预览", EditorStyles.boldLabel);
showPreview = EditorGUILayout.Toggle("显示预览", showPreview);
if (showPreview && previewRoot)
{
Rect rect = GUILayoutUtility.GetRect(position.width - 40, 200);
EditorGUI.DrawPreviewTexture(rect, GeneratePreviewTex());
}
}
private void DrawActionButtons()
{
EditorGUILayout.Space();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("保存配置")) SaveConfig();
if (GUILayout.Button("应用到场景")) ApplyToScene();
if (GUILayout.Button("导出LOD")) ExportLODs();
EditorGUILayout.EndHorizontal();
}
#endregion
#region 核心逻辑
private void LoadDatabase()
{
configDatabase = AssetDatabase.LoadAssetAtPath<ModelConfigDatabase>(dbPath);
if (!configDatabase)
{
Directory.CreateDirectory(Path.GetDirectoryName(dbPath));
configDatabase = CreateInstance<ModelConfigDatabase>();
AssetDatabase.CreateAsset(configDatabase, dbPath);
AssetDatabase.SaveAssets();
}
UpdateConfigNames();
}
private void UpdateConfigNames() =>
configNames = configDatabase.modelConfigs.Select(c => c.objectName).ToArray();
private void CreateNewConfig()
{
currentConfig = CreateInstance<ModelConfig>();
currentConfig.objectName = "新模型";
currentConfig.sizeType = ObjectSizeType.Medium;
InitLODLevels();
ClearPreview();
selectedConfigIndex = -1;
}
private void InitLODLevels()
{
currentConfig.lodLevels.Clear();
var distances = sizeLodDistances[currentConfig.sizeType];
for (int i = 0; i < distances.Count; i++)
{
currentConfig.lodLevels.Add(new LODLevelConfig
{
lodIndex = i,
distance = distances[i],
trianglePercentage = lodTrianglePercent[i]
});
}
}
private void LoadSelectedConfig()
{
currentConfig = configDatabase.GetConfig(configNames[selectedConfigIndex]);
if (currentConfig == null) return;
if (!string.IsNullOrEmpty(currentConfig.modelPath) && File.Exists(currentConfig.modelPath))
LoadModel(currentConfig.modelPath, go => mainModel = go);
for (int i = 0; i < currentConfig.lodLevels.Count; i++)
{
var lod = currentConfig.lodLevels[i];
if (!string.IsNullOrEmpty(lod.assetPath) && File.Exists(lod.assetPath))
LoadModel(lod.assetPath, go => currentConfig.lodLevels[i].meshObject = go);
}
}
private void ApplySizeLodPreset()
{
var distances = sizeLodDistances[currentConfig.sizeType];
for (int i = 0; i < distances.Count; i++)
currentConfig.lodLevels[i].distance = distances[i];
}
private void LoadMainModel()
{
string path = EditorUtility.OpenFilePanel("选择主模型", "", "fbx,obj,glb,gltf");
if (string.IsNullOrEmpty(path)) return;
LoadModel(path, go =>
{
mainModel = go;
mainModel.transform.SetParent(previewRoot.transform);
mainModel.transform.localPosition = Vector3.zero;
currentConfig.modelPath = path;
});
}
private void LoadLODModel(int index)
{
string path = EditorUtility.OpenFilePanel($"选择LOD {index}模型", "", "fbx,obj,glb,gltf");
if (string.IsNullOrEmpty(path)) return;
LoadModel(path, go =>
{
currentConfig.lodLevels[index].meshObject = go;
currentConfig.lodLevels[index].assetPath = path;
go.transform.SetParent(previewRoot.transform);
go.transform.localPosition = new Vector3(index * 2, 0, 0);
});
}
// 模型加载(完全适配TriLib 2.2.0)
private void LoadModel(string path, Action<GameObject> onLoaded)
{
if (!File.Exists(path))
{
EditorUtility.DisplayDialog("错误", "文件不存在!", "确定");
return;
}
try
{
var options = AssetLoader.CreateDefaultLoaderOptions();
options.ImportMaterials = true;
// 使用TriLib 2.2.0的异步加载方法
AssetLoader.LoadModelFromFile(
path,
// 成功回调 - 只接受AssetLoaderContext参数
context =>
{
GameObject rootGameObject = context.RootGameObject;
if (rootGameObject != null)
{
onLoaded?.Invoke(rootGameObject);
Repaint();
}
else
{
EditorUtility.DisplayDialog("加载失败", "无法获取加载的游戏对象", "确定");
}
},
// 进度回调 - 接受AssetLoaderContext和float两个参数
(context, progress) =>
{
EditorUtility.DisplayProgressBar("加载中", $"进度: {progress:P0}", progress);
if (progress >= 1f)
EditorUtility.ClearProgressBar();
},
// 错误回调
error =>
{
EditorUtility.ClearProgressBar();
string errorMsg = error != null ? error.GetErrorMessage() : "未知错误";
EditorUtility.DisplayDialog("加载失败", errorMsg, "确定");
},
null, // 取消回调
null, // 自定义资源创建器
options // 加载选项
);
}
catch (NotImplementedException niex)
{
EditorUtility.ClearProgressBar();
Debug.LogError($"未实现的方法: {niex.Message}");
EditorUtility.DisplayDialog("错误", $"遇到未实现的方法: {niex.Message}\n\n请检查TriLib插件版本是否兼容。", "确定");
}
catch (Exception ex)
{
EditorUtility.ClearProgressBar();
Debug.LogError($"加载失败: {ex.Message}");
EditorUtility.DisplayDialog("错误", ex.Message, "确定");
}
}
// 自动生成LOD(适配Mesh Simplifier v1.11)
private void GenerateAutoLOD()
{
if (!mainModel)
{
EditorUtility.DisplayDialog("错误", "请先加载主模型!", "确定");
return;
}
// 清空现有LOD
foreach (var lod in currentConfig.lodLevels)
if (lod.meshObject) DestroyImmediate(lod.meshObject);
// 获取主模型网格和材质
var mainMeshFilter = mainModel.GetComponentInChildren<MeshFilter>();
if (mainMeshFilter == null)
{
EditorUtility.DisplayDialog("错误", "主模型无网格数据!", "确定");
return;
}
Mesh mainMesh = mainMeshFilter.sharedMesh;
var mainMaterials = mainModel.GetComponentInChildren<Renderer>().sharedMaterials;
try
{
// 初始化网格简化器 - 适配Mesh Simplifier v1.11
var simplifier = new Simplifier();
simplifier.ProtectionMode = 1; // 保护UV接缝和边界
simplifier.MaximumError = 0.01f; // 设置最大误差
simplifier.KeepSymmetry = true; // 保持对称性
simplifier.KeepBorderEdges = true; // 保持边界边
simplifier.KeepUVSeamEdges = true; // 保持UV接缝边
// 生成各层级LOD
for (int i = 0; i < currentConfig.lodLevels.Count; i++)
{
var lod = currentConfig.lodLevels[i];
float ratio = lod.trianglePercentage / 100f;
EditorUtility.DisplayProgressBar("生成LOD", $"正在生成LOD {i} ({ratio:P0})", (float)i / currentConfig.lodLevels.Count);
try
{
// 执行网格简化 - 使用v1.11的正确API
Mesh simplifiedMesh = new Mesh();
// 配置简化参数
simplifier.VertexCountReduction = ratio;
// 执行简化
simplifier.ReduceMesh(mainMesh, ref simplifiedMesh);
// 确保简化后的网格有效
if (simplifiedMesh.vertexCount > 0 && simplifiedMesh.triangles.Length > 0)
{
// 创建LOD模型
GameObject lodObj = new GameObject($"LOD{i}_({ratio:P0})");
lodObj.transform.SetParent(previewRoot.transform);
lodObj.transform.localPosition = new Vector3(i * 2, 0, 0);
// 配置网格和材质
var filter = lodObj.AddComponent<MeshFilter>();
filter.sharedMesh = simplifiedMesh;
var renderer = lodObj.AddComponent<MeshRenderer>();
renderer.sharedMaterials = mainMaterials;
lod.meshObject = lodObj;
}
else
{
throw new Exception("简化后的网格无效,请调整简化参数");
}
}
catch (NotImplementedException niex)
{
EditorUtility.ClearProgressBar();
Debug.LogError($"Mesh Simplifier中未实现的方法: {niex.Message}");
EditorUtility.DisplayDialog("错误", $"生成LOD {i}时遇到未实现的方法: {niex.Message}\n\n请检查Mesh Simplifier插件版本是否兼容。", "确定");
return;
}
catch (Exception ex)
{
EditorUtility.ClearProgressBar();
Debug.LogError($"生成LOD {i}失败: {ex.Message}");
EditorUtility.DisplayDialog("错误", $"生成LOD {i}失败: {ex.Message}", "确定");
return;
}
}
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("成功", "LOD生成完成!", "确定");
}
catch (NotImplementedException niex)
{
EditorUtility.ClearProgressBar();
Debug.LogError($"Mesh Simplifier中未实现的方法: {niex.Message}");
EditorUtility.DisplayDialog("错误", $"初始化网格简化器时遇到未实现的方法: {niex.Message}\n\n请检查Mesh Simplifier插件版本是否兼容。", "确定");
}
catch (Exception ex)
{
EditorUtility.ClearProgressBar();
Debug.LogError($"生成LOD失败: {ex.Message}");
EditorUtility.DisplayDialog("错误", $"生成LOD时出错: {ex.Message}", "确定");
}
}
private void SaveConfig()
{
if (string.IsNullOrEmpty(currentConfig.objectName))
{
EditorUtility.DisplayDialog("错误", "请输入物体名称!", "确定");
return;
}
if (configDatabase.ContainsConfig(currentConfig.objectName))
{
if (!EditorUtility.DisplayDialog("提示", "覆盖现有配置?", "是", "否"))
return;
configDatabase.UpdateConfig(currentConfig);
}
else
{
configDatabase.AddConfig(currentConfig);
AssetDatabase.AddObjectToAsset(currentConfig, dbPath);
}
EditorUtility.SetDirty(configDatabase);
AssetDatabase.SaveAssets();
UpdateConfigNames();
EditorUtility.DisplayDialog("成功", "配置已保存!", "确定");
}
private void ApplyToScene()
{
GameObject obj = new GameObject(currentConfig.objectName);
LODGroup lodGroup = obj.AddComponent<LODGroup>();
ModelLODManager manager = obj.AddComponent<ModelLODManager>();
manager.modelName = currentConfig.objectName;
manager.lodGroup = lodGroup;
manager.config = currentConfig;
for (int i = 0; i < currentConfig.lodLevels.Count; i++)
{
if (currentConfig.lodLevels[i].meshObject)
{
GameObject lodObj = Instantiate(currentConfig.lodLevels[i].meshObject, obj.transform);
lodObj.transform.localPosition = Vector3.zero;
}
}
manager.ApplyLODConfig();
Selection.activeObject = obj;
}
private void ExportLODs()
{
string path = EditorUtility.OpenFolderPanel("选择导出目录", "", "");
if (string.IsNullOrEmpty(path)) return;
for (int i = 0; i < currentConfig.lodLevels.Count; i++)
{
EditorUtility.DisplayProgressBar("导出LOD", $"正在导出LOD {i}", (float)i / currentConfig.lodLevels.Count);
var lod = currentConfig.lodLevels[i];
if (lod.meshObject)
{
try
{
string exportPath = $"{path}/{currentConfig.objectName}_LOD{i}.fbx";
ExportMesh(lod.meshObject.GetComponent<MeshFilter>().sharedMesh, exportPath);
lod.assetPath = exportPath;
}
catch (Exception ex)
{
EditorUtility.ClearProgressBar();
Debug.LogError($"导出LOD {i}失败: {ex.Message}");
EditorUtility.DisplayDialog("错误", $"导出LOD {i}失败: {ex.Message}", "确定");
return;
}
}
}
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("成功", "导出完成!", "确定");
}
private void ExportMesh(Mesh mesh, string path)
{
try
{
// 使用Unity的FBX导出功能
GameObject temp = new GameObject("TempExport");
var filter = temp.AddComponent<MeshFilter>();
filter.sharedMesh = mesh;
// 使用反射调用UnityEditor.FBXExporter(如果可用)
var exporterType = Type.GetType("UnityEditor.FBXExporter,UnityEditor");
if (exporterType != null)
{
var exportMethod = exporterType.GetMethod("ExportObject", BindingFlags.Static | BindingFlags.Public);
if (exportMethod != null)
{
exportMethod.Invoke(null, new object[] { path, temp });
}
else
{
// 备用方法:使用AssetDatabase
if (AssetDatabase.Contains(mesh))
{
string assetPath = AssetDatabase.GetAssetPath(mesh);
AssetDatabase.CopyAsset(assetPath, path);
}
else
{
AssetDatabase.CreateAsset(mesh, path);
}
}
}
else
{
// 备用方法:使用AssetDatabase
if (AssetDatabase.Contains(mesh))
{
string assetPath = AssetDatabase.GetAssetPath(mesh);
AssetDatabase.CopyAsset(assetPath, path);
}
else
{
AssetDatabase.CreateAsset(mesh, path);
}
}
DestroyImmediate(temp);
}
catch (Exception ex)
{
Debug.LogError($"导出网格失败: {ex.Message}");
throw; // 重新抛出异常,由调用者处理
}
}
#endregion
#region 辅助方法
private void CreatePreviewRoot()
{
if (!previewRoot)
{
previewRoot = new GameObject("LOD预览根节点");
previewRoot.hideFlags = HideFlags.HideAndDontSave;
}
else ClearPreview();
}
private void ClearPreview()
{
if (previewRoot)
foreach (Transform child in previewRoot.transform)
DestroyImmediate(child.gameObject);
mainModel = null;
}
private Texture2D GeneratePreviewTex()
{
Texture2D tex = new Texture2D(256, 256);
Color[] pixels = new Color[256 * 256];
Array.Fill(pixels, new Color(0.1f, 0.1f, 0.1f));
tex.SetPixels(pixels);
tex.Apply();
return tex;
}
#endregion
}
}
严重性 代码 说明 项目 文件 行 禁止显示状态
错误(活动) CS1593 委托“Action<AssetLoaderContext>”未采用 2 个参数 Assembly-CSharp-Editor H:\YaoMo\MeshLod\Assets\Editor\ModelLODConfigTool.cs 393
错误(活动) CS1593 委托“Action<AssetLoaderContext, float>”未采用 1 个参数 Assembly-CSharp-Editor H:\YaoMo\MeshLod\Assets\Editor\ModelLODConfigTool.cs 400
错误(活动) CS1061 “Simplifier”未包含“VertexCountReduction”的定义,并且找不到可接受第一个“Simplifier”类型参数的可访问扩展方法“VertexCountReduction”(是否缺少 using 指令或程序集引用?) Assembly-CSharp-Editor H:\YaoMo\MeshLod\Assets\Editor\ModelLODConfigTool.cs 472
错误(活动) CS1061 “Simplifier”未包含“ReduceMesh”的定义,并且找不到可接受第一个“Simplifier”类型参数的可访问扩展方法“ReduceMesh”(是否缺少 using 指令或程序集引用?) Assembly-CSharp-Editor H:\YaoMo\MeshLod\Assets\Editor\ModelLODConfigTool.cs 475
错误(活动) CS1061 “Simplifier”未包含“ProtectionMode”的定义,并且找不到可接受第一个“Simplifier”类型参数的可访问扩展方法“ProtectionMode”(是否缺少 using 指令或程序集引用?) Assembly-CSharp-Editor H:\YaoMo\MeshLod\Assets\Editor\ModelLODConfigTool.cs 452
错误(活动) CS1061 “Simplifier”未包含“MaximumError”的定义,并且找不到可接受第一个“Simplifier”类型参数的可访问扩展方法“MaximumError”(是否缺少 using 指令或程序集引用?) Assembly-CSharp-Editor H:\YaoMo\MeshLod\Assets\Editor\ModelLODConfigTool.cs 453
错误(活动) CS1061 “Simplifier”未包含“KeepUVSeamEdges”的定义,并且找不到可接受第一个“Simplifier”类型参数的可访问扩展方法“KeepUVSeamEdges”(是否缺少 using 指令或程序集引用?) Assembly-CSharp-Editor H:\YaoMo\MeshLod\Assets\Editor\ModelLODConfigTool.cs 456
错误(活动) CS1061 “Simplifier”未包含“KeepSymmetry”的定义,并且找不到可接受第一个“Simplifier”类型参数的可访问扩展方法“KeepSymmetry”(是否缺少 using 指令或程序集引用?) Assembly-CSharp-Editor H:\YaoMo\MeshLod\Assets\Editor\ModelLODConfigTool.cs 454
错误(活动) CS1061 “Simplifier”未包含“KeepBorderEdges”的定义,并且找不到可接受第一个“Simplifier”类型参数的可访问扩展方法“KeepBorderEdges”(是否缺少 using 指令或程序集引用?) Assembly-CSharp-Editor H:\YaoMo\MeshLod\Assets\Editor\ModelLODConfigTool.cs 455
最新发布