ModelStructureMatcher.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. using UnityEngine;
  2. using UnityEditor;
  3. using System.Collections.Generic;
  4. public class ModelStructureMatcher : EditorWindow
  5. {
  6. public GameObject modelA; // 已命名旧模型
  7. public GameObject modelB; // 未命名新模型
  8. [Header("匹配选项")]
  9. public bool rebuildHierarchy = false; // 是否重建层级
  10. public bool keepWorldPosition = true; // 移动时保持世界坐标
  11. public bool showDetailedLog = true; // 打印详细日志
  12. public bool cleanupEmptyNodes = true; // 清理空节点
  13. private Dictionary<string, Transform> meshLookupB = new Dictionary<string, Transform>();
  14. private Transform newPartsGroup;
  15. private int totalMatches = 0;
  16. private int totalMeshesA = 0;
  17. private int totalUnmatched = 0;
  18. [MenuItem("Tools/模型匹配")]
  19. public static void ShowWindow()
  20. {
  21. GetWindow<ModelStructureMatcher>("模型匹配");
  22. }
  23. void OnGUI()
  24. {
  25. GUILayout.Label("模型匹配", EditorStyles.boldLabel);
  26. modelA = (GameObject)EditorGUILayout.ObjectField("旧模型(A)", modelA, typeof(GameObject), true);
  27. modelB = (GameObject)EditorGUILayout.ObjectField("新模型(B)", modelB, typeof(GameObject), true);
  28. rebuildHierarchy = EditorGUILayout.ToggleLeft("同步层级", rebuildHierarchy);
  29. if (rebuildHierarchy)
  30. keepWorldPosition = EditorGUILayout.ToggleLeft("保持坐标", keepWorldPosition);
  31. showDetailedLog = EditorGUILayout.ToggleLeft("详细日志", showDetailedLog);
  32. cleanupEmptyNodes = EditorGUILayout.ToggleLeft("清理空节点", cleanupEmptyNodes);
  33. if (GUILayout.Button("执行"))
  34. {
  35. if (modelA == null || modelB == null)
  36. {
  37. Debug.LogError("指定模型A和B");
  38. return;
  39. }
  40. Undo.RegisterFullObjectHierarchyUndo(modelB, "Model");
  41. RunSync();
  42. Debug.Log($"匹配完成:共 {totalMeshesA} 个 Mesh,匹配成功 {totalMatches} 个,未匹配 {totalUnmatched} 个。");
  43. if (cleanupEmptyNodes)
  44. {
  45. int count = CleanupEmptyNodesDeep(modelB.transform);
  46. Debug.Log($"清理完成:共移除 {count} 个空节点。");
  47. }
  48. }
  49. }
  50. // 主逻辑
  51. void RunSync()
  52. {
  53. totalMatches = 0;
  54. totalMeshesA = 0;
  55. totalUnmatched = 0;
  56. meshLookupB.Clear();
  57. //建立 B 模型几何签名表
  58. foreach (var renderer in modelB.GetComponentsInChildren<MeshRenderer>(true))
  59. {
  60. var mf = renderer.GetComponent<MeshFilter>();
  61. if (mf && mf.sharedMesh)
  62. {
  63. string key = GetMeshKey(mf.sharedMesh);
  64. if (!meshLookupB.ContainsKey(key))
  65. meshLookupB[key] = renderer.transform;
  66. }
  67. }
  68. foreach (var smr in modelB.GetComponentsInChildren<SkinnedMeshRenderer>(true))
  69. {
  70. if (smr.sharedMesh)
  71. {
  72. string key = GetMeshKey(smr.sharedMesh);
  73. if (!meshLookupB.ContainsKey(key))
  74. meshLookupB[key] = smr.transform;
  75. }
  76. }
  77. // --- 创建 NewParts 组 ---
  78. var existingGroup = modelB.transform.Find("NewParts");
  79. if (existingGroup == null)
  80. {
  81. GameObject newGroup = new GameObject("NewParts");
  82. newPartsGroup = newGroup.transform;
  83. newPartsGroup.SetParent(modelB.transform);
  84. }
  85. else
  86. {
  87. newPartsGroup = existingGroup;
  88. }
  89. //遍历A的结构
  90. TraverseAndSync(modelA.transform, modelB.transform);
  91. //递归移动所有未匹配 Mesh
  92. totalUnmatched = MoveAllUnmatchedToNewParts(modelB.transform);
  93. if (showDetailedLog)
  94. Debug.Log($"未匹配节点已移动到 NewParts,共 {totalUnmatched} 个");
  95. //递归移动所有未匹配Mesh
  96. totalUnmatched = MoveAllUnmatchedToNewParts(modelB.transform);
  97. // 最后兜底扫描:把遗漏的Mesh全部移入NewParts
  98. int extraMoved = ForceCollectAllUnmatchedMeshes(modelB.transform, newPartsGroup);
  99. if (extraMoved > 0)
  100. {
  101. totalUnmatched += extraMoved;
  102. Debug.Log($"额外收集未匹配Mesh{extraMoved}个,已归入NewParts");
  103. }
  104. if (showDetailedLog)
  105. Debug.Log($"未匹配节点已移动到NewParts,共{totalUnmatched}个");
  106. }
  107. // 递归遍历 A 树结构
  108. void TraverseAndSync(Transform aNode, Transform bParent)
  109. {
  110. if (!HasRenderer(aNode))
  111. {
  112. Transform bGroup = bParent.Find(aNode.name);
  113. if (bGroup == null)
  114. {
  115. GameObject newGroup = new GameObject(aNode.name);
  116. newGroup.transform.SetParent(bParent, false);
  117. bGroup = newGroup.transform;
  118. }
  119. foreach (Transform aChild in aNode)
  120. TraverseAndSync(aChild, bGroup);
  121. return;
  122. }
  123. Mesh meshA = GetMesh(aNode);
  124. if (meshA == null) return;
  125. totalMeshesA++;
  126. string key = GetMeshKey(meshA);
  127. if (meshLookupB.TryGetValue(key, out Transform bNode))
  128. {
  129. // 命名
  130. bNode.name = aNode.name;
  131. // 层级
  132. if (rebuildHierarchy)
  133. {
  134. Transform targetParent = EnsurePathInB(aNode, modelA.transform, modelB.transform);
  135. bNode.SetParent(targetParent, keepWorldPosition);
  136. }
  137. meshLookupB.Remove(key);
  138. totalMatches++;
  139. if (showDetailedLog)
  140. Debug.Log($"匹配成功: {aNode.name}");
  141. }
  142. else
  143. {
  144. if (showDetailedLog)
  145. Debug.LogWarning($"未找到匹配Mesh: {aNode.name}");
  146. }
  147. }
  148. // 递归移动所有未匹配的 Mesh 到 NewParts
  149. int MoveAllUnmatchedToNewParts(Transform root)
  150. {
  151. int moved = 0;
  152. var allMeshes = root.GetComponentsInChildren<MeshRenderer>(true);
  153. foreach (var mr in allMeshes)
  154. {
  155. if (mr == null) continue;
  156. var mf = mr.GetComponent<MeshFilter>();
  157. if (mf && mf.sharedMesh != null)
  158. {
  159. string key = GetMeshKey(mf.sharedMesh);
  160. if (meshLookupB.ContainsKey(key))
  161. {
  162. mr.transform.SetParent(newPartsGroup, true);
  163. meshLookupB.Remove(key);
  164. moved++;
  165. }
  166. }
  167. }
  168. var skinned = root.GetComponentsInChildren<SkinnedMeshRenderer>(true);
  169. foreach (var smr in skinned)
  170. {
  171. if (smr.sharedMesh != null)
  172. {
  173. string key = GetMeshKey(smr.sharedMesh);
  174. if (meshLookupB.ContainsKey(key))
  175. {
  176. smr.transform.SetParent(newPartsGroup, true);
  177. meshLookupB.Remove(key);
  178. moved++;
  179. }
  180. }
  181. }
  182. return moved;
  183. }
  184. // 延迟清理
  185. int CleanupEmptyNodesDeep(Transform root)
  186. {
  187. int count = 0;
  188. List<GameObject> toDelete = new List<GameObject>();
  189. // 收集所有空节点
  190. foreach (Transform t in root.GetComponentsInChildren<Transform>(true))
  191. {
  192. if (t == null || t == root) continue;
  193. if (IsDeepEmpty(t))
  194. toDelete.Add(t.gameObject);
  195. }
  196. // 逐个销毁
  197. foreach (var go in toDelete)
  198. {
  199. if (go == null) continue;
  200. #if UNITY_EDITOR
  201. // 用 Undo 系统保证可撤销,同时防止立即触发重绘异常
  202. Undo.DestroyObjectImmediate(go);
  203. #else
  204. Object.Destroy(go);
  205. #endif
  206. count++;
  207. }
  208. // 强制刷新编辑器层级树
  209. EditorApplication.delayCall += () => { EditorApplication.RepaintHierarchyWindow(); };
  210. return count;
  211. }
  212. bool IsDeepEmpty(Transform t)
  213. {
  214. if (t == null) return true;
  215. if (HasRenderer(t)) return false;
  216. // 如果有任何子物体非空,则此节点不空
  217. foreach (Transform child in t)
  218. {
  219. if (child == null) continue;
  220. if (!IsDeepEmpty(child))
  221. return false;
  222. }
  223. return true;
  224. }
  225. // 工具函数
  226. bool HasRenderer(Transform t)
  227. {
  228. return t.GetComponent<MeshRenderer>() != null || t.GetComponent<SkinnedMeshRenderer>() != null;
  229. }
  230. Mesh GetMesh(Transform t)
  231. {
  232. var mr = t.GetComponent<MeshRenderer>();
  233. if (mr)
  234. {
  235. var mf = t.GetComponent<MeshFilter>();
  236. return mf ? mf.sharedMesh : null;
  237. }
  238. var smr = t.GetComponent<SkinnedMeshRenderer>();
  239. return smr ? smr.sharedMesh : null;
  240. }
  241. string GetMeshKey(Mesh mesh)
  242. {
  243. if (mesh == null) return "null";
  244. Vector3 s = mesh.bounds.size;
  245. return $"{mesh.vertexCount}_{mesh.triangles.Length}_{s.x:F3}_{s.y:F3}_{s.z:F3}";
  246. }
  247. Transform EnsurePathInB(Transform aNode, Transform aRoot, Transform bRoot)
  248. {
  249. List<string> path = new List<string>();
  250. Transform cur = aNode.parent;
  251. while (cur != null && cur != aRoot)
  252. {
  253. path.Add(cur.name);
  254. cur = cur.parent;
  255. }
  256. path.Reverse();
  257. Transform current = bRoot;
  258. foreach (string name in path)
  259. {
  260. Transform next = current.Find(name);
  261. if (next == null)
  262. {
  263. GameObject g = new GameObject(name);
  264. g.transform.SetParent(current, false);
  265. next = g.transform;
  266. }
  267. current = next;
  268. }
  269. return current;
  270. }
  271. // 强制收集
  272. int ForceCollectAllUnmatchedMeshes(Transform root, Transform newParts)
  273. {
  274. int moved = 0;
  275. foreach (Transform t in root.GetComponentsInChildren<Transform>(true))
  276. {
  277. if (t == root) continue;
  278. // 跳过已经在 NewParts 下的
  279. if (t.IsChildOf(newParts)) continue;
  280. // 只处理有 Mesh 的节点
  281. var mr = t.GetComponent<MeshRenderer>();
  282. var smr = t.GetComponent<SkinnedMeshRenderer>();
  283. if (mr != null || smr != null)
  284. {
  285. // 过滤掉匹配成功的:如果名字已在 A 结构中出现过则认为命中
  286. if (IsInModelANameList(t.name)) continue;
  287. // 否则强制放入 NewParts
  288. t.SetParent(newParts, true);
  289. moved++;
  290. }
  291. }
  292. return moved;
  293. }
  294. // 缓存A模型所有命名列表
  295. HashSet<string> modelANamesCache = null;
  296. bool IsInModelANameList(string name)
  297. {
  298. if (modelANamesCache == null)
  299. {
  300. modelANamesCache = new HashSet<string>();
  301. foreach (var tr in modelA.GetComponentsInChildren<Transform>(true))
  302. modelANamesCache.Add(tr.name);
  303. }
  304. return modelANamesCache.Contains(name);
  305. }
  306. }