Files
HauntedBloodlines/Assets/Editor/AdvancedMeshMerger.cs
2025-05-29 22:31:40 +03:00

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}");
}
}