using Unity.Collections; using UnityEditor; using UnityEngine; using UnityEngine.Rendering; using NBShader; namespace NBShaderEditor { [CustomEditor(typeof(MeshUI))] [CanEditMultipleObjects] public class MeshUIInspector : Editor { private SerializedProperty _meshProp; private SerializedProperty _subMeshMaterialsProp; private SerializedProperty _colorProp; private SerializedProperty _raycastTargetProp; private SerializedProperty _maskableProp; private void OnEnable() { _meshProp = serializedObject.FindProperty("mesh"); _subMeshMaterialsProp = serializedObject.FindProperty("subMeshMaterials"); _colorProp = serializedObject.FindProperty("m_Color"); _raycastTargetProp = serializedObject.FindProperty("m_RaycastTarget"); _maskableProp = serializedObject.FindProperty("m_Maskable"); } public override void OnInspectorGUI() { serializedObject.Update(); DrawMeshSection(); EditorGUILayout.Space(); DrawSubMeshMaterials(); EditorGUILayout.Space(); DrawUGUISection(); EditorGUILayout.Space(); DrawMeshInfo(); serializedObject.ApplyModifiedProperties(); } private void DrawMeshSection() { EditorGUILayout.LabelField("Mesh", EditorStyles.boldLabel); EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(_meshProp, new GUIContent("模型")); if (EditorGUI.EndChangeCheck()) { serializedObject.ApplyModifiedProperties(); SyncSlotsToMesh(); serializedObject.Update(); } DrawBakeMeshSection(); } private void DrawSubMeshMaterials() { Mesh mesh = _meshProp.objectReferenceValue as Mesh; int targetSize = mesh != null ? mesh.subMeshCount : _subMeshMaterialsProp.arraySize; EditorGUILayout.LabelField("Mesh Materials", EditorStyles.boldLabel); if (mesh != null && _subMeshMaterialsProp.arraySize != targetSize) { EditorGUILayout.HelpBox($"当前材质槽位数量为 {_subMeshMaterialsProp.arraySize},Mesh 的 SubMesh 数量为 {targetSize}。更换 Mesh 后会自动同步槽位。", MessageType.Warning); } for (int i = 0; i < _subMeshMaterialsProp.arraySize; i++) { SerializedProperty element = _subMeshMaterialsProp.GetArrayElementAtIndex(i); string label = $"Sub Mesh {i}"; if (mesh != null && i < mesh.subMeshCount) { label += $" ({mesh.GetTopology(i)})"; } EditorGUILayout.PropertyField(element, new GUIContent(label)); } } private void DrawUGUISection() { EditorGUILayout.LabelField("UGUI", EditorStyles.boldLabel); EditorGUILayout.PropertyField(_colorProp, new GUIContent("颜色")); EditorGUILayout.PropertyField(_raycastTargetProp, new GUIContent("射线检测")); EditorGUILayout.PropertyField(_maskableProp, new GUIContent("支持 Mask")); } private void DrawMeshInfo() { Mesh mesh = _meshProp.objectReferenceValue as Mesh; EditorGUILayout.LabelField("信息", EditorStyles.boldLabel); if (mesh == null) { EditorGUILayout.HelpBox("未指定 Mesh。请先指定一个包含三角形拓扑的模型。", MessageType.Warning); return; } int subMeshCount = mesh.subMeshCount; int triangleSubMeshCount = GetTriangleSubMeshCount(mesh); int vertexCount = mesh.vertexCount; int indexCount = 0; for (int i = 0; i < subMeshCount; i++) { if (mesh.GetTopology(i) == MeshTopology.Triangles) { indexCount += (int)mesh.GetIndexCount(i); } } string renderMode = GetRenderModeDescription(mesh); string info = $"可读性: {(mesh.isReadable ? "是" : "否")}\n" + $"顶点数: {vertexCount}\n" + $"SubMesh 数量: {subMeshCount}\n" + $"三角形 SubMesh 数量: {triangleSubMeshCount}\n" + $"三角形索引数: {indexCount}\n" + $"运行时模式: {renderMode}"; EditorGUILayout.HelpBox(info, MessageType.Info); if (triangleSubMeshCount == 0) { EditorGUILayout.HelpBox("当前 Mesh 不包含可渲染的三角形 SubMesh。", MessageType.Error); } else if (triangleSubMeshCount != subMeshCount) { EditorGUILayout.HelpBox("当前 Mesh 中存在非三角形拓扑的 SubMesh,运行时会生成 Mesh,并且只渲染三角形 SubMesh。", MessageType.Warning); } } private string GetRenderModeDescription(Mesh mesh) { if (!mesh.isReadable) { return "当前 Mesh 不可读,无法渲染"; } if (GetTriangleSubMeshCount(mesh) != mesh.subMeshCount) { return "将生成运行时 Mesh(原因:存在非三角形 SubMesh)"; } Color color = _colorProp.colorValue; if (color != Color.white) { return "将生成运行时 Mesh(原因:需要应用 UGUI 颜色)"; } return "将直接使用原始 Mesh"; } private void DrawBakeMeshSection() { Mesh mesh = _meshProp.objectReferenceValue as Mesh; if (mesh == null || mesh.isReadable) { return; } EditorGUILayout.HelpBox("当前 Mesh 没有开启 Read/Write,MeshUI 无法直接通过 CanvasRenderer 渲染。请先烘焙出一个可读的 Mesh。", MessageType.Warning); if (GUILayout.Button("烘焙只读Mesh")) { BakeMeshForTargets(mesh); } } private int GetTriangleSubMeshCount(Mesh mesh) { int count = 0; for (int i = 0; i < mesh.subMeshCount; i++) { if (mesh.GetTopology(i) == MeshTopology.Triangles) { count++; } } return count; } private void BakeMeshForTargets(Mesh sourceMesh) { string defaultDirectory = GetDefaultSaveDirectory(sourceMesh); string defaultName = $"{sourceMesh.name}_MeshUI"; string assetPath = EditorUtility.SaveFilePanelInProject( "烘焙 MeshUI Mesh", defaultName, "asset", "请选择烘焙后 Mesh 的保存位置。", defaultDirectory); if (string.IsNullOrEmpty(assetPath)) { return; } Mesh bakedMesh = CreateReadableMeshCopy(sourceMesh, System.IO.Path.GetFileNameWithoutExtension(assetPath)); if (bakedMesh == null) { return; } string uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(assetPath); AssetDatabase.CreateAsset(bakedMesh, uniqueAssetPath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Mesh bakedAsset = AssetDatabase.LoadAssetAtPath(uniqueAssetPath); if (bakedAsset == null) { Debug.LogError($"无法加载已烘焙的 Mesh 资源: '{uniqueAssetPath}'"); return; } Undo.RecordObjects(targets, "烘焙 MeshUI Mesh"); foreach (Object editorTarget in targets) { MeshUI meshUI = editorTarget as MeshUI; if (meshUI == null) { continue; } meshUI.Mesh = bakedAsset; EditorUtility.SetDirty(meshUI); PrefabUtility.RecordPrefabInstancePropertyModifications(meshUI); } serializedObject.Update(); SyncSlotsToMesh(); } private string GetDefaultSaveDirectory(Mesh sourceMesh) { string sourcePath = AssetDatabase.GetAssetPath(sourceMesh); if (string.IsNullOrEmpty(sourcePath)) { return "Assets"; } string directory = System.IO.Path.GetDirectoryName(sourcePath); if (string.IsNullOrEmpty(directory)) { return "Assets"; } return directory.Replace('\\', '/'); } private static Mesh CreateReadableMeshCopy(Mesh sourceMesh, string newMeshName) { using (Mesh.MeshDataArray meshDataArray = MeshUtility.AcquireReadOnlyMeshData(sourceMesh)) { if (meshDataArray.Length == 0) { Debug.LogError($"无法读取 Mesh 数据: {sourceMesh.name}"); return null; } Mesh.MeshData meshData = meshDataArray[0]; Mesh bakedMesh = new Mesh { name = newMeshName, indexFormat = meshData.indexFormat }; int vertexCount = meshData.vertexCount; if (vertexCount <= 0) { Debug.LogError($"Mesh '{sourceMesh.name}' 不包含任何顶点。"); Object.DestroyImmediate(bakedMesh); return null; } using (NativeArray vertices = new NativeArray(vertexCount, Allocator.Temp)) { meshData.GetVertices(vertices); bakedMesh.SetVertices(vertices); } if (meshData.HasVertexAttribute(VertexAttribute.Normal)) { using (NativeArray normals = new NativeArray(vertexCount, Allocator.Temp)) { meshData.GetNormals(normals); bakedMesh.SetNormals(normals); } } if (meshData.HasVertexAttribute(VertexAttribute.Tangent)) { using (NativeArray tangents = new NativeArray(vertexCount, Allocator.Temp)) { meshData.GetTangents(tangents); bakedMesh.SetTangents(tangents); } } if (meshData.HasVertexAttribute(VertexAttribute.TexCoord0)) { using (NativeArray uv0 = new NativeArray(vertexCount, Allocator.Temp)) { meshData.GetUVs(0, uv0); bakedMesh.SetUVs(0, uv0); } } int subMeshCount = sourceMesh.subMeshCount; bakedMesh.subMeshCount = subMeshCount; bool hasTriangleSubMesh = false; for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++) { SubMeshDescriptor subMesh = meshData.GetSubMesh(subMeshIndex); if (subMesh.topology == MeshTopology.Triangles && subMesh.indexCount > 0) { hasTriangleSubMesh = true; if (meshData.indexFormat == IndexFormat.UInt16) { using (NativeArray indices = new NativeArray((int)subMesh.indexCount, Allocator.Temp)) { meshData.GetIndices(indices, subMeshIndex, true); bakedMesh.SetIndices(indices, MeshTopology.Triangles, subMeshIndex, false); } } else { using (NativeArray indices = new NativeArray((int)subMesh.indexCount, Allocator.Temp)) { meshData.GetIndices(indices, subMeshIndex, true); bakedMesh.SetIndices(indices, MeshTopology.Triangles, subMeshIndex, false); } } } else { bakedMesh.SetIndices(System.Array.Empty(), MeshTopology.Triangles, subMeshIndex, false); } } if (!hasTriangleSubMesh) { Debug.LogError($"Mesh '{sourceMesh.name}' 不包含可烘焙的三角形 SubMesh。"); Object.DestroyImmediate(bakedMesh); return null; } bakedMesh.RecalculateBounds(); return bakedMesh; } } private void SyncSlotsToMesh() { foreach (Object editorTarget in targets) { MeshUI meshUI = editorTarget as MeshUI; if (meshUI == null) { continue; } SerializedObject targetObject = new SerializedObject(meshUI); SerializedProperty meshProp = targetObject.FindProperty("mesh"); SerializedProperty materialsProp = targetObject.FindProperty("subMeshMaterials"); Mesh mesh = meshProp.objectReferenceValue as Mesh; int targetSize = mesh != null ? mesh.subMeshCount : 0; materialsProp.arraySize = Mathf.Max(0, targetSize); targetObject.ApplyModifiedProperties(); EditorUtility.SetDirty(meshUI); PrefabUtility.RecordPrefabInstancePropertyModifications(meshUI); } } } }