using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; public class MeshColliderGeneratorWindow : EditorWindow { private const string GeneratedMeshFolder = "Assets/GeneratedColliders"; [SerializeField] private List targetRoots = new List(); [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(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(includeInactive); foreach (var meshFilter in meshFilters) { if (meshFilter == null) { continue; } var meshRenderer = meshFilter.GetComponent(); if (meshRenderer == null) { continue; } var sourceMesh = meshFilter.sharedMesh; if (sourceMesh == null) { continue; } var parent = meshFilter.transform; var targetGo = parent.gameObject; if (skipIfExists && targetGo.GetComponent() != null) { continue; } var meshCollider = Undo.AddComponent(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; } }