using UnityEngine; using UnityEditor; using System.Collections.Generic; public class ModelStructureMatcher : EditorWindow { public GameObject modelA; // 已命名旧模型 public GameObject modelB; // 未命名新模型 [Header("匹配选项")] public bool rebuildHierarchy = false; // 是否重建层级 public bool keepWorldPosition = true; // 移动时保持世界坐标 public bool showDetailedLog = true; // 打印详细日志 public bool cleanupEmptyNodes = true; // 清理空节点 private Dictionary meshLookupB = new Dictionary(); private Transform newPartsGroup; private int totalMatches = 0; private int totalMeshesA = 0; private int totalUnmatched = 0; [MenuItem("Tools/模型匹配")] public static void ShowWindow() { GetWindow("模型匹配"); } void OnGUI() { GUILayout.Label("模型匹配", EditorStyles.boldLabel); modelA = (GameObject)EditorGUILayout.ObjectField("旧模型(A)", modelA, typeof(GameObject), true); modelB = (GameObject)EditorGUILayout.ObjectField("新模型(B)", modelB, typeof(GameObject), true); rebuildHierarchy = EditorGUILayout.ToggleLeft("同步层级", rebuildHierarchy); if (rebuildHierarchy) keepWorldPosition = EditorGUILayout.ToggleLeft("保持坐标", keepWorldPosition); showDetailedLog = EditorGUILayout.ToggleLeft("详细日志", showDetailedLog); cleanupEmptyNodes = EditorGUILayout.ToggleLeft("清理空节点", cleanupEmptyNodes); if (GUILayout.Button("执行")) { if (modelA == null || modelB == null) { Debug.LogError("指定模型A和B"); return; } Undo.RegisterFullObjectHierarchyUndo(modelB, "Model"); RunSync(); Debug.Log($"匹配完成:共 {totalMeshesA} 个 Mesh,匹配成功 {totalMatches} 个,未匹配 {totalUnmatched} 个。"); if (cleanupEmptyNodes) { int count = CleanupEmptyNodesDeep(modelB.transform); Debug.Log($"清理完成:共移除 {count} 个空节点。"); } } } // 主逻辑 void RunSync() { totalMatches = 0; totalMeshesA = 0; totalUnmatched = 0; meshLookupB.Clear(); //建立 B 模型几何签名表 foreach (var renderer in modelB.GetComponentsInChildren(true)) { var mf = renderer.GetComponent(); if (mf && mf.sharedMesh) { string key = GetMeshKey(mf.sharedMesh); if (!meshLookupB.ContainsKey(key)) meshLookupB[key] = renderer.transform; } } foreach (var smr in modelB.GetComponentsInChildren(true)) { if (smr.sharedMesh) { string key = GetMeshKey(smr.sharedMesh); if (!meshLookupB.ContainsKey(key)) meshLookupB[key] = smr.transform; } } // --- 创建 NewParts 组 --- var existingGroup = modelB.transform.Find("NewParts"); if (existingGroup == null) { GameObject newGroup = new GameObject("NewParts"); newPartsGroup = newGroup.transform; newPartsGroup.SetParent(modelB.transform); } else { newPartsGroup = existingGroup; } //遍历A的结构 TraverseAndSync(modelA.transform, modelB.transform); //递归移动所有未匹配 Mesh totalUnmatched = MoveAllUnmatchedToNewParts(modelB.transform); if (showDetailedLog) Debug.Log($"未匹配节点已移动到 NewParts,共 {totalUnmatched} 个"); //递归移动所有未匹配Mesh totalUnmatched = MoveAllUnmatchedToNewParts(modelB.transform); // 最后兜底扫描:把遗漏的Mesh全部移入NewParts int extraMoved = ForceCollectAllUnmatchedMeshes(modelB.transform, newPartsGroup); if (extraMoved > 0) { totalUnmatched += extraMoved; Debug.Log($"额外收集未匹配Mesh{extraMoved}个,已归入NewParts"); } if (showDetailedLog) Debug.Log($"未匹配节点已移动到NewParts,共{totalUnmatched}个"); } // 递归遍历 A 树结构 void TraverseAndSync(Transform aNode, Transform bParent) { if (!HasRenderer(aNode)) { Transform bGroup = bParent.Find(aNode.name); if (bGroup == null) { GameObject newGroup = new GameObject(aNode.name); newGroup.transform.SetParent(bParent, false); bGroup = newGroup.transform; } foreach (Transform aChild in aNode) TraverseAndSync(aChild, bGroup); return; } Mesh meshA = GetMesh(aNode); if (meshA == null) return; totalMeshesA++; string key = GetMeshKey(meshA); if (meshLookupB.TryGetValue(key, out Transform bNode)) { // 命名 bNode.name = aNode.name; // 层级 if (rebuildHierarchy) { Transform targetParent = EnsurePathInB(aNode, modelA.transform, modelB.transform); bNode.SetParent(targetParent, keepWorldPosition); } meshLookupB.Remove(key); totalMatches++; if (showDetailedLog) Debug.Log($"匹配成功: {aNode.name}"); } else { if (showDetailedLog) Debug.LogWarning($"未找到匹配Mesh: {aNode.name}"); } } // 递归移动所有未匹配的 Mesh 到 NewParts int MoveAllUnmatchedToNewParts(Transform root) { int moved = 0; var allMeshes = root.GetComponentsInChildren(true); foreach (var mr in allMeshes) { if (mr == null) continue; var mf = mr.GetComponent(); if (mf && mf.sharedMesh != null) { string key = GetMeshKey(mf.sharedMesh); if (meshLookupB.ContainsKey(key)) { mr.transform.SetParent(newPartsGroup, true); meshLookupB.Remove(key); moved++; } } } var skinned = root.GetComponentsInChildren(true); foreach (var smr in skinned) { if (smr.sharedMesh != null) { string key = GetMeshKey(smr.sharedMesh); if (meshLookupB.ContainsKey(key)) { smr.transform.SetParent(newPartsGroup, true); meshLookupB.Remove(key); moved++; } } } return moved; } // 延迟清理 int CleanupEmptyNodesDeep(Transform root) { int count = 0; List toDelete = new List(); // 收集所有空节点 foreach (Transform t in root.GetComponentsInChildren(true)) { if (t == null || t == root) continue; if (IsDeepEmpty(t)) toDelete.Add(t.gameObject); } // 逐个销毁 foreach (var go in toDelete) { if (go == null) continue; #if UNITY_EDITOR // 用 Undo 系统保证可撤销,同时防止立即触发重绘异常 Undo.DestroyObjectImmediate(go); #else Object.Destroy(go); #endif count++; } // 强制刷新编辑器层级树 EditorApplication.delayCall += () => { EditorApplication.RepaintHierarchyWindow(); }; return count; } bool IsDeepEmpty(Transform t) { if (t == null) return true; if (HasRenderer(t)) return false; // 如果有任何子物体非空,则此节点不空 foreach (Transform child in t) { if (child == null) continue; if (!IsDeepEmpty(child)) return false; } return true; } // 工具函数 bool HasRenderer(Transform t) { return t.GetComponent() != null || t.GetComponent() != null; } Mesh GetMesh(Transform t) { var mr = t.GetComponent(); if (mr) { var mf = t.GetComponent(); return mf ? mf.sharedMesh : null; } var smr = t.GetComponent(); return smr ? smr.sharedMesh : null; } string GetMeshKey(Mesh mesh) { if (mesh == null) return "null"; Vector3 s = mesh.bounds.size; return $"{mesh.vertexCount}_{mesh.triangles.Length}_{s.x:F3}_{s.y:F3}_{s.z:F3}"; } Transform EnsurePathInB(Transform aNode, Transform aRoot, Transform bRoot) { List path = new List(); Transform cur = aNode.parent; while (cur != null && cur != aRoot) { path.Add(cur.name); cur = cur.parent; } path.Reverse(); Transform current = bRoot; foreach (string name in path) { Transform next = current.Find(name); if (next == null) { GameObject g = new GameObject(name); g.transform.SetParent(current, false); next = g.transform; } current = next; } return current; } // 强制收集 int ForceCollectAllUnmatchedMeshes(Transform root, Transform newParts) { int moved = 0; foreach (Transform t in root.GetComponentsInChildren(true)) { if (t == root) continue; // 跳过已经在 NewParts 下的 if (t.IsChildOf(newParts)) continue; // 只处理有 Mesh 的节点 var mr = t.GetComponent(); var smr = t.GetComponent(); if (mr != null || smr != null) { // 过滤掉匹配成功的:如果名字已在 A 结构中出现过则认为命中 if (IsInModelANameList(t.name)) continue; // 否则强制放入 NewParts t.SetParent(newParts, true); moved++; } } return moved; } // 缓存A模型所有命名列表 HashSet modelANamesCache = null; bool IsInModelANameList(string name) { if (modelANamesCache == null) { modelANamesCache = new HashSet(); foreach (var tr in modelA.GetComponentsInChildren(true)) modelANamesCache.Add(tr.name); } return modelANamesCache.Contains(name); } }