using UnityEngine; using UnityEditor; using System.Collections.Generic; public class SmartColliderGeneratorMerged : EditorWindow { private float manualVoxelSize = 0.0f; private int mergeClusterSize = 2; private bool overwriteExisting = true; private bool showPreview = true; private bool forceSimpleBox = false; private static List<(Vector3 center, Vector3 size, Transform transform)> previewBoxes = new(); [MenuItem("Tools/Smart Collider Generator (Merged)")] public static void ShowWindow() { var window = GetWindow("Smart Collider Generator"); SceneView.duringSceneGui -= DrawPreview; SceneView.duringSceneGui += DrawPreview; window.Show(); } void OnGUI() { GUILayout.Label("Collider Optimization Tool", EditorStyles.boldLabel); manualVoxelSize = EditorGUILayout.FloatField("Voxel Size (0 = auto)", manualVoxelSize); mergeClusterSize = EditorGUILayout.IntSlider("Merge Cluster Size", mergeClusterSize, 1, 5); overwriteExisting = EditorGUILayout.Toggle("Overwrite Existing Colliders", overwriteExisting); forceSimpleBox = EditorGUILayout.Toggle("Force Simple BoxCollider", forceSimpleBox); showPreview = EditorGUILayout.Toggle("Show Collider Preview", showPreview); if (GUILayout.Button("Generate Colliders for Selection")) { GenerateCollidersForSelection(); } if (GUILayout.Button("Refresh Preview")) { RefreshPreview(); } } void GenerateCollidersForSelection() { foreach (var obj in Selection.objects) { if (obj is GameObject go) { string path = AssetDatabase.GetAssetPath(go); if (!string.IsNullOrEmpty(path) && PrefabUtility.GetPrefabAssetType(go) != PrefabAssetType.NotAPrefab) { ProcessPrefabAsset(go, path); } else { // Scene object var filters = go.GetComponentsInChildren(); foreach (var filter in filters) { if (filter.sharedMesh == null) continue; if (overwriteExisting) { foreach (var col in filter.GetComponents()) { if (col != null) DestroyImmediate(col); } } else if (filter.GetComponent() != null) { continue; } ProcessMesh(filter, applyToGameObject: true); } } } } previewBoxes.Clear(); Debug.Log(" Collider generation complete."); } void ProcessPrefabAsset(GameObject prefabAsset, string assetPath) { GameObject prefabInstance = PrefabUtility.LoadPrefabContents(assetPath); var filters = prefabInstance.GetComponentsInChildren(); foreach (var filter in filters) { if (filter.sharedMesh == null) continue; if (overwriteExisting) { foreach (var col in filter.GetComponents()) { if (col != null) DestroyImmediate(col); } } else if (filter.GetComponent() != null) { continue; } ProcessMesh(filter, applyToGameObject: true); } PrefabUtility.SaveAsPrefabAsset(prefabInstance, assetPath); PrefabUtility.UnloadPrefabContents(prefabInstance); Debug.Log($" Updated prefab: {assetPath}"); } void RefreshPreview() { previewBoxes.Clear(); foreach (GameObject go in Selection.gameObjects) { var filters = go.GetComponentsInChildren(); foreach (var filter in filters) { if (filter.sharedMesh == null) continue; ProcessMesh(filter, applyToGameObject: false); } } SceneView.RepaintAll(); } void ProcessMesh(MeshFilter filter, bool applyToGameObject) { var mesh = filter.sharedMesh; var bounds = mesh.bounds; var size = bounds.size; float ratio = Mathf.Max(size.x, size.y, size.z) / Mathf.Max(0.0001f, Mathf.Min(size.x, size.y, size.z)); if (forceSimpleBox || (mesh.vertexCount < 100 && ratio < 3f)) { if (applyToGameObject) { var box = filter.gameObject.AddComponent(); box.center = bounds.center; box.size = size; } else { previewBoxes.Add((bounds.center, size, filter.transform)); } return; } float voxelSize = manualVoxelSize > 0 ? manualVoxelSize : Mathf.Max(size.x, size.y, size.z) / 10f; RunVoxelMerge(filter, mesh, voxelSize, applyToGameObject); } void RunVoxelMerge(MeshFilter filter, Mesh mesh, float voxelSize, bool applyToGameObject) { Vector3[] vertices = mesh.vertices; Transform t = filter.transform; HashSet filledVoxels = new(); foreach (var v in vertices) { Vector3 world = t.TransformPoint(v); Vector3Int voxel = Vector3Int.FloorToInt(world / voxelSize); filledVoxels.Add(voxel); } HashSet visited = new(); foreach (var voxel in filledVoxels) { if (visited.Contains(voxel)) continue; List groupPoints = new(); for (int x = 0; x < mergeClusterSize; x++) { for (int y = 0; y < mergeClusterSize; y++) { for (int z = 0; z < mergeClusterSize; z++) { Vector3Int offset = voxel + new Vector3Int(x, y, z); if (filledVoxels.Contains(offset) && !visited.Contains(offset)) { visited.Add(offset); groupPoints.Add((Vector3)offset * voxelSize); } } } } if (groupPoints.Count == 0) continue; Bounds mergedBounds = new Bounds(groupPoints[0], Vector3.zero); foreach (var p in groupPoints) mergedBounds.Encapsulate(p); if (applyToGameObject) { var box = filter.gameObject.AddComponent(); box.center = t.InverseTransformPoint(mergedBounds.center); box.size = Vector3.one * voxelSize * mergeClusterSize; } else { previewBoxes.Add((mergedBounds.center, Vector3.one * voxelSize * mergeClusterSize, filter.transform)); } } } static void DrawPreview(SceneView sceneView) { if (!EditorWindow.HasOpenInstances()) return; var window = (SmartColliderGeneratorMerged)EditorWindow.GetWindow(typeof(SmartColliderGeneratorMerged)); if (!window.showPreview) return; Handles.color = new Color(0f, 1f, 0f, 0.3f); foreach (var (center, size, transform) in previewBoxes) { var matrix = Matrix4x4.TRS(transform.position, transform.rotation, transform.lossyScale); using (new Handles.DrawingScope(matrix)) { Handles.DrawWireCube(transform.InverseTransformPoint(center), size); } } } }