407 lines
14 KiB
C#
407 lines
14 KiB
C#
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<AdvancedMeshMerger>("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<Material, List<CombineInstance>> materialToCombine = new();
|
|
|
|
foreach (GameObject root in selected)
|
|
{
|
|
foreach (MeshFilter mf in root.GetComponentsInChildren<MeshFilter>())
|
|
{
|
|
MeshRenderer mr = mf.GetComponent<MeshRenderer>();
|
|
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<Material> finalMaterials = new();
|
|
List<Mesh> 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<MeshFilter>();
|
|
MeshRenderer mrFinal = newObj.AddComponent<MeshRenderer>();
|
|
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<MeshFilter>();
|
|
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<MeshFilter>();
|
|
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<MeshFilter>() == 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<MeshFilter>();
|
|
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<MeshFilter>();
|
|
MeshRenderer mr = obj.GetComponent<MeshRenderer>();
|
|
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}");
|
|
}
|
|
}
|