302 lines
8.7 KiB
C#
302 lines
8.7 KiB
C#
using System.Collections.Generic;
|
|
using System.IO;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
public class MeshColliderGeneratorWindow : EditorWindow
|
|
{
|
|
private const string GeneratedMeshFolder = "Assets/GeneratedColliders";
|
|
[SerializeField]
|
|
private List<GameObject> targetRoots = new List<GameObject>();
|
|
|
|
[SerializeField]
|
|
private bool includeInactive = true;
|
|
|
|
[SerializeField]
|
|
private float xScale = 1f;
|
|
|
|
[SerializeField]
|
|
private bool skipIfExists = true;
|
|
|
|
[SerializeField]
|
|
private bool setConvex = false;
|
|
|
|
[SerializeField]
|
|
private bool extrudeAlongNormals = false;
|
|
|
|
[SerializeField]
|
|
private float extrusionDistance = 0f;
|
|
|
|
private SerializedObject serializedObjectRef;
|
|
private SerializedProperty targetsProp;
|
|
|
|
[MenuItem("Tools/Mesh Collider Generator")]
|
|
public static void ShowWindow()
|
|
{
|
|
var window = GetWindow<MeshColliderGeneratorWindow>(true, "Mesh Collider Generator");
|
|
window.minSize = new Vector2(420, 320);
|
|
window.Show();
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
serializedObjectRef = new SerializedObject(this);
|
|
targetsProp = serializedObjectRef.FindProperty("targetRoots");
|
|
}
|
|
|
|
private void OnGUI()
|
|
{
|
|
serializedObjectRef.Update();
|
|
|
|
EditorGUILayout.LabelField("Target GameObjects", EditorStyles.boldLabel);
|
|
EditorGUILayout.HelpBox("Assign one or more root GameObjects. All children will be scanned.", MessageType.Info);
|
|
EditorGUILayout.PropertyField(targetsProp, true);
|
|
|
|
EditorGUILayout.Space();
|
|
EditorGUILayout.LabelField("Options", EditorStyles.boldLabel);
|
|
includeInactive = EditorGUILayout.Toggle(new GUIContent("Include Inactive", "Scan inactive children too"), includeInactive);
|
|
xScale = EditorGUILayout.FloatField(new GUIContent("X Scale", "Local X scale for the generated collider GameObject"), Mathf.Max(0f, xScale));
|
|
skipIfExists = EditorGUILayout.Toggle(new GUIContent("Skip If MeshCollider Exists", "Skip if the target object already has a MeshCollider"), skipIfExists);
|
|
setConvex = EditorGUILayout.Toggle(new GUIContent("Set Convex", "Set MeshCollider.convex on generated colliders"), setConvex);
|
|
|
|
EditorGUILayout.Space();
|
|
EditorGUILayout.LabelField("Extrusion", EditorStyles.boldLabel);
|
|
extrudeAlongNormals = EditorGUILayout.Toggle(new GUIContent("Extrude Along Normals", "If enabled, duplicates the mesh and offsets vertices along their normals for the collider"), extrudeAlongNormals);
|
|
using (new EditorGUI.DisabledScope(!extrudeAlongNormals))
|
|
{
|
|
extrusionDistance = EditorGUILayout.FloatField(new GUIContent("Extrusion Distance", "Distance to offset vertices along normals (units). Positive = expand, negative = shrink."), extrusionDistance);
|
|
}
|
|
|
|
EditorGUILayout.Space();
|
|
using (new EditorGUI.DisabledScope(targetsProp.arraySize == 0))
|
|
{
|
|
if (GUILayout.Button("Scan And Generate"))
|
|
{
|
|
GenerateForTargets();
|
|
}
|
|
}
|
|
|
|
serializedObjectRef.ApplyModifiedProperties();
|
|
}
|
|
|
|
private void GenerateForTargets()
|
|
{
|
|
var totalCreated = 0;
|
|
try
|
|
{
|
|
Undo.IncrementCurrentGroup();
|
|
var undoGroup = Undo.GetCurrentGroup();
|
|
|
|
for (int i = 0; i < targetRoots.Count; i++)
|
|
{
|
|
var root = targetRoots[i];
|
|
if (root == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var meshFilters = root.GetComponentsInChildren<MeshFilter>(includeInactive);
|
|
foreach (var meshFilter in meshFilters)
|
|
{
|
|
if (meshFilter == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var meshRenderer = meshFilter.GetComponent<MeshRenderer>();
|
|
if (meshRenderer == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var sourceMesh = meshFilter.sharedMesh;
|
|
if (sourceMesh == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var parent = meshFilter.transform;
|
|
|
|
var targetGo = parent.gameObject;
|
|
if (skipIfExists && targetGo.GetComponent<MeshCollider>() != null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var meshCollider = Undo.AddComponent<MeshCollider>(targetGo);
|
|
|
|
var workingMesh = sourceMesh;
|
|
if (extrudeAlongNormals && !Mathf.Approximately(extrusionDistance, 0f))
|
|
{
|
|
var extruded = CreateExtrudedMesh(workingMesh, extrusionDistance);
|
|
if (extruded != null)
|
|
{
|
|
workingMesh = extruded;
|
|
}
|
|
}
|
|
|
|
var uniformScale = Mathf.Approximately(xScale, 0f) ? 0.0001f : xScale;
|
|
if (!Mathf.Approximately(uniformScale, 1f))
|
|
{
|
|
var scaled = CreateScaledMesh(workingMesh, uniformScale);
|
|
if (scaled != null)
|
|
{
|
|
workingMesh = scaled;
|
|
}
|
|
}
|
|
|
|
meshCollider.sharedMesh = workingMesh;
|
|
meshCollider.convex = setConvex;
|
|
|
|
totalCreated++;
|
|
}
|
|
}
|
|
|
|
Undo.CollapseUndoOperations(Undo.GetCurrentGroup());
|
|
EditorUtility.DisplayDialog("Mesh Collider Generator", $"Created {totalCreated} collider object(s).", "OK");
|
|
}
|
|
catch (System.SystemException e)
|
|
{
|
|
Debug.LogError($"Mesh Collider generation failed: {e.Message}\n{e}");
|
|
}
|
|
}
|
|
|
|
private static Mesh CreateExtrudedMesh(Mesh source, float distance)
|
|
{
|
|
if (source == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (!source.isReadable)
|
|
{
|
|
Debug.LogWarning($"Source mesh '{source.name}' is not readable. Using original mesh for collider.");
|
|
return null;
|
|
}
|
|
|
|
var vertices = source.vertices;
|
|
var normals = source.normals;
|
|
var triangles = source.triangles;
|
|
|
|
if (normals == null || normals.Length != vertices.Length)
|
|
{
|
|
var temp = new Mesh();
|
|
temp.vertices = vertices;
|
|
temp.triangles = triangles;
|
|
temp.RecalculateNormals();
|
|
normals = temp.normals;
|
|
}
|
|
|
|
var newVertices = new Vector3[vertices.Length];
|
|
for (int i = 0; i < vertices.Length; i++)
|
|
{
|
|
newVertices[i] = vertices[i] + normals[i] * distance;
|
|
}
|
|
|
|
var extruded = new Mesh();
|
|
extruded.name = string.IsNullOrEmpty(source.name) ? "ExtrudedMesh" : source.name + "_Extruded";
|
|
extruded.vertices = newVertices;
|
|
extruded.triangles = triangles;
|
|
extruded.RecalculateBounds();
|
|
|
|
SaveMeshAsset(extruded, source);
|
|
|
|
return extruded;
|
|
}
|
|
|
|
private static Mesh CreateScaledMesh(Mesh source, float uniformScale)
|
|
{
|
|
if (source == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (Mathf.Approximately(uniformScale, 1f))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (!source.isReadable)
|
|
{
|
|
Debug.LogWarning($"Source mesh '{source.name}' is not readable. Using original mesh for collider.");
|
|
return null;
|
|
}
|
|
|
|
var vertices = source.vertices;
|
|
var triangles = source.triangles;
|
|
|
|
var newVertices = new Vector3[vertices.Length];
|
|
for (int i = 0; i < vertices.Length; i++)
|
|
{
|
|
newVertices[i] = vertices[i] * uniformScale;
|
|
}
|
|
|
|
var scaled = new Mesh();
|
|
scaled.name = string.IsNullOrEmpty(source.name) ? "ScaledMesh" : source.name + "_Scaled_Collider";
|
|
scaled.vertices = newVertices;
|
|
scaled.triangles = triangles;
|
|
scaled.RecalculateBounds();
|
|
SaveMeshAsset(scaled, source);
|
|
return scaled;
|
|
}
|
|
|
|
private static void SaveMeshAsset(Mesh mesh, Mesh sourceForPath)
|
|
{
|
|
if (mesh == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string baseFolder = GeneratedMeshFolder;
|
|
if (sourceForPath != null)
|
|
{
|
|
var srcPath = AssetDatabase.GetAssetPath(sourceForPath);
|
|
if (!string.IsNullOrEmpty(srcPath))
|
|
{
|
|
var dir = Path.GetDirectoryName(srcPath);
|
|
if (!string.IsNullOrEmpty(dir) && AssetDatabase.IsValidFolder(dir.Replace('\\','/')))
|
|
{
|
|
baseFolder = dir.Replace('\\','/');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!AssetDatabase.IsValidFolder(baseFolder))
|
|
{
|
|
// Fallback: ensure default folder exists
|
|
if (!AssetDatabase.IsValidFolder(GeneratedMeshFolder))
|
|
{
|
|
var parts = GeneratedMeshFolder.Split('/');
|
|
for (int i = 1; i < parts.Length; i++)
|
|
{
|
|
var parentPath = string.Join("/", parts, 0, i);
|
|
var thisFolder = string.Join("/", parts, 0, i + 1);
|
|
if (!AssetDatabase.IsValidFolder(thisFolder))
|
|
{
|
|
AssetDatabase.CreateFolder(parentPath, parts[i]);
|
|
}
|
|
}
|
|
}
|
|
baseFolder = GeneratedMeshFolder;
|
|
}
|
|
|
|
var sanitizedName = SanitizeFileName(string.IsNullOrEmpty(mesh.name) ? "GeneratedMesh" : mesh.name);
|
|
var combined = baseFolder.EndsWith("/") ? baseFolder + sanitizedName + ".asset" : baseFolder + "/" + sanitizedName + ".asset";
|
|
var path = AssetDatabase.GenerateUniqueAssetPath(combined.Replace('\\','/'));
|
|
AssetDatabase.CreateAsset(mesh, path);
|
|
AssetDatabase.SaveAssets();
|
|
}
|
|
|
|
private static string SanitizeFileName(string name)
|
|
{
|
|
foreach (var c in Path.GetInvalidFileNameChars())
|
|
{
|
|
name = name.Replace(c, '_');
|
|
}
|
|
return name;
|
|
}
|
|
}
|
|
|
|
|
|
|