using UnityEngine; using UnityEditor; using System.Collections.Generic; using System.IO; using System.Text; public class AdvancedMeshMerger : EditorWindow { private string mergedObjectName = "Combined_Mesh_Object"; private bool saveMeshAsAsset = true; private Mesh lastMergedMesh = null; [MenuItem("Tools/Advanced Mesh Merger")] public static void ShowWindow() { GetWindow("Advanced Mesh Merger"); } private void OnGUI() { GUILayout.Label("Merge Settings", EditorStyles.boldLabel); mergedObjectName = EditorGUILayout.TextField("Merged Object Name", mergedObjectName); saveMeshAsAsset = EditorGUILayout.Toggle("Save Mesh As Asset", saveMeshAsAsset); if (GUILayout.Button("📝 Save Mesh As...")) SaveMeshManually(); if (GUILayout.Button("💾 Save Selected Mesh As...")) SaveSelectedMesh(); if (GUILayout.Button("📤 Export Selected Mesh to .OBJ")) ExportSelectedMeshToOBJ(); if (GUILayout.Button("📤 Export Selected Mesh with Materials to .OBJ")) ExportSelectedMeshToOBJWithMaterials(); GUILayout.Space(10); if (GUILayout.Button("🧱 Merge Selected Meshes")) MergeMeshes(); GUILayout.Space(20); GUILayout.Label("Selected Object Tools", EditorStyles.boldLabel); if (GUILayout.Button("🎯 Center Pivot of Selected Mesh")) CenterPivotOfSelected(); if (GUILayout.Button("🧭 Align Pivot Rotation with Selected")) AlignPivotRotationWithSelected(); } private void MergeMeshes() { GameObject[] selected = Selection.gameObjects; if (selected.Length == 0) { Debug.LogWarning("Select objects with MeshFilters."); return; } Dictionary> materialToCombine = new(); foreach (GameObject root in selected) { foreach (MeshFilter mf in root.GetComponentsInChildren()) { MeshRenderer mr = mf.GetComponent(); if (mf == null || mr == null || mf.sharedMesh == null) continue; Mesh mesh = mf.sharedMesh; for (int i = 0; i < mesh.subMeshCount; i++) { if (i >= mr.sharedMaterials.Length) continue; Material mat = mr.sharedMaterials[i]; if (!materialToCombine.ContainsKey(mat)) materialToCombine[mat] = new(); materialToCombine[mat].Add(new CombineInstance { mesh = mesh, subMeshIndex = i, transform = mf.transform.localToWorldMatrix }); } } } List finalMaterials = new(); List subMeshes = new(); foreach (var kvp in materialToCombine) { Mesh subMesh = new(); subMesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; subMesh.CombineMeshes(kvp.Value.ToArray(), true, true); subMeshes.Add(subMesh); finalMaterials.Add(kvp.Key); } CombineInstance[] finalCombine = new CombineInstance[subMeshes.Count]; for (int i = 0; i < subMeshes.Count; i++) { finalCombine[i] = new CombineInstance { mesh = subMeshes[i], subMeshIndex = 0, transform = Matrix4x4.identity }; } Mesh finalMesh = new(); finalMesh.name = mergedObjectName + "_Mesh"; finalMesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; finalMesh.CombineMeshes(finalCombine, false, false); lastMergedMesh = finalMesh; if (saveMeshAsAsset) { string folderPath = "Assets/MergedMeshes"; if (!Directory.Exists(folderPath)) Directory.CreateDirectory(folderPath); string assetPath = $"{folderPath}/{mergedObjectName}_Mesh.asset"; AssetDatabase.CreateAsset(finalMesh, assetPath); AssetDatabase.SaveAssets(); Debug.Log($"💾 Saved mesh asset at: {assetPath}"); } GameObject newObj = new(string.IsNullOrEmpty(mergedObjectName) ? "Combined_Mesh_Object" : mergedObjectName); MeshFilter mfFinal = newObj.AddComponent(); MeshRenderer mrFinal = newObj.AddComponent(); mfFinal.sharedMesh = finalMesh; mrFinal.sharedMaterials = finalMaterials.ToArray(); Undo.RegisterCreatedObjectUndo(newObj, "Create Combined Mesh"); Selection.activeGameObject = newObj; Debug.Log($"✅ Combined mesh created with {finalMaterials.Count} materials."); } private void SaveMeshManually() { if (lastMergedMesh == null) { Debug.LogWarning("No mesh to save. Perform a merge first."); return; } string path = EditorUtility.SaveFilePanelInProject("Save Merged Mesh", lastMergedMesh.name, "asset", "Choose location to save the merged mesh."); if (!string.IsNullOrEmpty(path)) { Mesh meshCopy = Instantiate(lastMergedMesh); AssetDatabase.CreateAsset(meshCopy, path); AssetDatabase.SaveAssets(); Debug.Log($"💾 Mesh manually saved to: {path}"); } } private void SaveSelectedMesh() { GameObject obj = Selection.activeGameObject; MeshFilter mf = obj?.GetComponent(); if (mf?.sharedMesh == null) { Debug.LogWarning("Selected object does not have a valid mesh."); return; } string path = EditorUtility.SaveFilePanelInProject("Save Selected Mesh", mf.sharedMesh.name, "asset", "Choose location to save the selected mesh."); if (!string.IsNullOrEmpty(path)) { Mesh meshCopy = Instantiate(mf.sharedMesh); AssetDatabase.CreateAsset(meshCopy, path); AssetDatabase.SaveAssets(); Debug.Log($"💾 Saved selected mesh to: {path}"); } } private void CenterPivotOfSelected() { GameObject obj = Selection.activeGameObject; MeshFilter mf = obj?.GetComponent(); if (mf?.sharedMesh == null) { Debug.LogWarning("Select an object with a valid mesh."); return; } Mesh meshCopy = Instantiate(mf.sharedMesh); meshCopy.name = mf.sharedMesh.name + "_Centered"; Vector3[] verts = meshCopy.vertices; Vector3 center = meshCopy.bounds.center; for (int i = 0; i < verts.Length; i++) verts[i] -= center; meshCopy.vertices = verts; meshCopy.RecalculateBounds(); mf.sharedMesh = meshCopy; obj.transform.position += obj.transform.rotation * center; Debug.Log("✅ Pivot centered without changing rotation or position."); } private void AlignPivotRotationWithSelected() { GameObject[] selection = Selection.gameObjects; if (selection.Length != 2) { Debug.LogWarning("Select exactly TWO objects:\nFirst: the merged object\nSecond: the reference."); return; } GameObject merged = selection[0]; GameObject reference = selection[1]; if (merged.GetComponent() == null) { Debug.LogWarning("First selected object must have a mesh."); return; } GameObject pivotHolder = new GameObject(merged.name + "_Pivot"); Undo.RegisterCreatedObjectUndo(pivotHolder, "Create Pivot Holder"); pivotHolder.transform.position = reference.transform.position; pivotHolder.transform.rotation = reference.transform.rotation; pivotHolder.transform.localScale = Vector3.one; merged.transform.SetParent(pivotHolder.transform, true); Selection.activeGameObject = pivotHolder; Debug.Log("✅ Created pivot wrapper with reference rotation and position."); } private void ExportSelectedMeshToOBJ() { GameObject obj = Selection.activeGameObject; MeshFilter mf = obj?.GetComponent(); if (mf?.sharedMesh == null) { Debug.LogWarning("Selected object must have a mesh."); return; } string path = EditorUtility.SaveFilePanel("Export .OBJ", "", obj.name + ".obj", "obj"); if (string.IsNullOrEmpty(path)) return; Mesh mesh = mf.sharedMesh; StringBuilder sb = new StringBuilder(); sb.AppendLine($"# Exported from Unity: {obj.name}"); foreach (Vector3 v in mesh.vertices) { Vector3 worldV = obj.transform.TransformPoint(v); sb.AppendLine($"v {worldV.x} {worldV.y} {worldV.z}"); } foreach (Vector2 uv in mesh.uv) sb.AppendLine($"vt {uv.x} {uv.y}"); foreach (Vector3 n in mesh.normals) { Vector3 worldN = obj.transform.TransformDirection(n); sb.AppendLine($"vn {worldN.x} {worldN.y} {worldN.z}"); } int[] triangles = mesh.triangles; for (int i = 0; i < triangles.Length; i += 3) { int i0 = triangles[i] + 1; int i1 = triangles[i + 1] + 1; int i2 = triangles[i + 2] + 1; sb.AppendLine($"f {i0}/{i0}/{i0} {i1}/{i1}/{i1} {i2}/{i2}/{i2}"); } File.WriteAllText(path, sb.ToString()); Debug.Log($"📤 Exported mesh to: {path}"); } private void ExportSelectedMeshToOBJWithMaterials() { GameObject obj = Selection.activeGameObject; if (obj == null) { Debug.LogWarning("No GameObject selected."); return; } MeshFilter mf = obj.GetComponent(); MeshRenderer mr = obj.GetComponent(); if (mf == null || mf.sharedMesh == null || mr == null) { Debug.LogWarning("Selected object must have MeshFilter and MeshRenderer."); return; } string folderPath = EditorUtility.SaveFolderPanel("Export .OBJ with Materials", "", obj.name); if (string.IsNullOrEmpty(folderPath)) return; Mesh mesh = mf.sharedMesh; string objName = obj.name; string objPath = Path.Combine(folderPath, objName + ".obj"); string mtlPath = Path.Combine(folderPath, objName + ".mtl"); Vector3[] vertices = mesh.vertices; Vector3[] normals = mesh.normals; Vector2[] uvs = mesh.uv; using (StreamWriter sw = new StreamWriter(objPath)) { sw.WriteLine($"# Exported from Unity: {objName}"); sw.WriteLine($"mtllib {objName}.mtl"); // Vertices foreach (Vector3 v in vertices) { Vector3 worldV = obj.transform.TransformPoint(v); sw.WriteLine($"v {worldV.x} {worldV.y} {worldV.z}"); } // UVs (if available) if (uvs != null && uvs.Length == vertices.Length) { foreach (Vector2 uv in uvs) sw.WriteLine($"vt {uv.x} {uv.y}"); } else { for (int i = 0; i < vertices.Length; i++) sw.WriteLine("vt 0 0"); } // Normals if (normals != null && normals.Length == vertices.Length) { foreach (Vector3 n in normals) { Vector3 worldN = obj.transform.TransformDirection(n).normalized; sw.WriteLine($"vn {worldN.x} {worldN.y} {worldN.z}"); } } else { for (int i = 0; i < vertices.Length; i++) sw.WriteLine("vn 0 1 0"); } // Faces per submesh int vertexOffset = 1; for (int sub = 0; sub < mesh.subMeshCount; sub++) { Material mat = mr.sharedMaterials.Length > sub ? mr.sharedMaterials[sub] : null; string matName = mat ? mat.name : $"Material_{sub}"; sw.WriteLine($"usemtl {matName}"); int[] tris = mesh.GetTriangles(sub); for (int i = 0; i < tris.Length; i += 3) { int a = tris[i] + vertexOffset; int b = tris[i + 1] + vertexOffset; int c = tris[i + 2] + vertexOffset; sw.WriteLine($"f {a}/{a}/{a} {b}/{b}/{b} {c}/{c}/{c}"); } } } using (StreamWriter sw = new StreamWriter(mtlPath)) { for (int i = 0; i < mr.sharedMaterials.Length; i++) { Material mat = mr.sharedMaterials[i]; string matName = mat ? mat.name : $"Material_{i}"; sw.WriteLine($"newmtl {matName}"); Texture2D tex = mat.mainTexture as Texture2D; if (tex != null) { string texAssetPath = AssetDatabase.GetAssetPath(tex); string texFileName = Path.GetFileName(texAssetPath); string destTexPath = Path.Combine(folderPath, texFileName); try { File.Copy(texAssetPath, destTexPath, true); sw.WriteLine($"map_Kd {texFileName}"); } catch { Debug.LogWarning($"⚠ Could not copy texture: {texFileName}"); } } sw.WriteLine("Kd 1.000 1.000 1.000"); sw.WriteLine("Ka 0.000 0.000 0.000"); sw.WriteLine("Ks 0.000 0.000 0.000"); sw.WriteLine("d 1.0"); sw.WriteLine("illum 1"); sw.WriteLine(); } } Debug.Log($"📤 Exported {objName}.obj with materials and textures to: {folderPath}"); } }