1967 lines
78 KiB
C#
1967 lines
78 KiB
C#
/*
|
|
* OptimizedColliderTool.cs
|
|
*
|
|
* A production-ready Unity Editor tool for intelligent collider generation.
|
|
* Features:
|
|
* - OBB (Oriented Bounding Box) fitting for accurate building structure following
|
|
* - Mesh decimation for optimized MeshColliders
|
|
* - Compound collider generation for complex buildings
|
|
* - Terrain/ground detection and configuration
|
|
* - Memory optimization and performance profiling
|
|
* - Gap detection and filling
|
|
*
|
|
* Created: 2026-01-15
|
|
*/
|
|
|
|
using UnityEngine;
|
|
using UnityEditor;
|
|
using UnityEditor.SceneManagement;
|
|
using UnityEngine.SceneManagement;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace OptimizedColliders
|
|
{
|
|
/// <summary>
|
|
/// Main editor window for the Optimized Collider Tool
|
|
/// </summary>
|
|
public class OptimizedColliderTool : EditorWindow
|
|
{
|
|
#region Serialized Fields and Settings
|
|
|
|
// Object assignments
|
|
private GameObject environmentParent;
|
|
private GameObject groundTerrain;
|
|
private GameObject groundReference; // Reference ground for component copying
|
|
|
|
// Generation method
|
|
public enum ColliderMethod
|
|
{
|
|
MeshCollider,
|
|
BoxColliderAutoFit,
|
|
CompoundColliders,
|
|
ConvexMesh,
|
|
Adaptive // Auto-select best method per object
|
|
}
|
|
private ColliderMethod colliderMethod = ColliderMethod.Adaptive;
|
|
|
|
// Detail level
|
|
public enum DetailLevel
|
|
{
|
|
Low = 0, // 100-500 triangles, fastest
|
|
Medium = 1, // 500-2000 triangles, balanced
|
|
High = 2 // 2000-5000 triangles, accurate
|
|
}
|
|
private DetailLevel detailLevel = DetailLevel.Medium;
|
|
|
|
// Options
|
|
private bool generateForBuildings = true;
|
|
private bool generateForGround = true;
|
|
private bool generateForVegetation = false;
|
|
private bool generateForProps = true;
|
|
private bool optimizeForPerformance = true;
|
|
private bool createCompoundForComplex = false; // OFF by default - creates too many colliders
|
|
private bool removeExistingFirst = true; // ON by default for clean runs
|
|
private bool skipGroundPlanes = true; // ON by default - terrain collider is sufficient, planes under buildings create invisible walls
|
|
|
|
// Minimum size filter - objects smaller than this get NO collider
|
|
private float minimumObjectSize = 0.5f; // Objects smaller than 0.5m in all dimensions get skipped
|
|
|
|
// Advanced settings
|
|
private bool showAdvancedSettings = false;
|
|
private float colliderOverlap = 0.02f; // 2cm overlap to prevent gaps
|
|
private int maxTrianglesLow = 500;
|
|
private int maxTrianglesMedium = 2000;
|
|
private int maxTrianglesHigh = 5000;
|
|
private int complexityThreshold = 8; // Child renderer count threshold
|
|
private int vertexThreshold = 3000; // Vertex count threshold for complex buildings
|
|
private bool useOBBFitting = true; // Use OBB instead of AABB
|
|
private bool detectArchitecturalFeatures = true;
|
|
private bool fillGaps = true;
|
|
private float gapDetectionSpacing = 0.5f;
|
|
private int groundLayer = 8; // Default to layer 8 (from terrain analysis)
|
|
private bool markAsStatic = true;
|
|
private bool bakeMeshColliders = true;
|
|
|
|
// State
|
|
private Vector2 scrollPosition;
|
|
private bool isProcessing = false;
|
|
private float progress = 0f;
|
|
private string statusMessage = "";
|
|
private List<AnalysisResult> analysisResults = new List<AnalysisResult>();
|
|
private List<string> logMessages = new List<string>();
|
|
private ColliderStatistics statistics = new ColliderStatistics();
|
|
|
|
// Undo tracking
|
|
private List<ColliderChangeRecord> changeRecords = new List<ColliderChangeRecord>();
|
|
|
|
// Cached styles
|
|
private GUIStyle headerStyle;
|
|
private GUIStyle sectionStyle;
|
|
private GUIStyle logStyle;
|
|
private bool stylesInitialized = false;
|
|
|
|
#endregion
|
|
|
|
#region Data Classes
|
|
|
|
[System.Serializable]
|
|
public class AnalysisResult
|
|
{
|
|
public GameObject gameObject;
|
|
public string objectType; // Building, Ground, Prop, Vegetation, etc.
|
|
public string recommendedCollider;
|
|
public string reason;
|
|
public int vertexCount;
|
|
public int triangleCount;
|
|
public Vector3 bounds;
|
|
public float complexity; // 0-1 complexity score
|
|
public bool selected = true;
|
|
public Color displayColor = Color.green;
|
|
}
|
|
|
|
[System.Serializable]
|
|
public class ColliderStatistics
|
|
{
|
|
public int totalObjects;
|
|
public int objectsWithColliders;
|
|
public int objectsNeedingColliders;
|
|
public int buildingCount;
|
|
public int groundCount;
|
|
public int propCount;
|
|
public int vegetationCount;
|
|
public long estimatedMemoryBytes;
|
|
public long originalMeshMemoryBytes;
|
|
public int totalTriangles;
|
|
public int optimizedTriangles;
|
|
|
|
public void Reset()
|
|
{
|
|
totalObjects = 0;
|
|
objectsWithColliders = 0;
|
|
objectsNeedingColliders = 0;
|
|
buildingCount = 0;
|
|
groundCount = 0;
|
|
propCount = 0;
|
|
vegetationCount = 0;
|
|
estimatedMemoryBytes = 0;
|
|
originalMeshMemoryBytes = 0;
|
|
totalTriangles = 0;
|
|
optimizedTriangles = 0;
|
|
}
|
|
}
|
|
|
|
[System.Serializable]
|
|
public class ColliderChangeRecord
|
|
{
|
|
public GameObject target;
|
|
public Collider addedCollider;
|
|
public GameObject addedChild; // For compound colliders
|
|
public StaticEditorFlags previousFlags;
|
|
public bool flagsChanged;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detected architectural feature (wall, floor, roof, etc.)
|
|
/// </summary>
|
|
public struct ArchitecturalFeature
|
|
{
|
|
public Vector3 center;
|
|
public Vector3 size;
|
|
public Quaternion rotation;
|
|
public string featureType; // Wall, Floor, Roof, Column, etc.
|
|
public Vector3 normal;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Menu and Window
|
|
|
|
[MenuItem("Tools/Optimized Collider Tool")]
|
|
public static void ShowWindow()
|
|
{
|
|
var window = GetWindow<OptimizedColliderTool>("Optimized Colliders");
|
|
window.minSize = new Vector2(500, 700);
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
stylesInitialized = false;
|
|
}
|
|
|
|
private void InitializeStyles()
|
|
{
|
|
if (stylesInitialized) return;
|
|
|
|
headerStyle = new GUIStyle(EditorStyles.boldLabel)
|
|
{
|
|
fontSize = 16,
|
|
alignment = TextAnchor.MiddleCenter,
|
|
normal = { textColor = new Color(0.9f, 0.9f, 0.9f) }
|
|
};
|
|
|
|
sectionStyle = new GUIStyle(EditorStyles.boldLabel)
|
|
{
|
|
fontSize = 13,
|
|
normal = { textColor = new Color(0.8f, 0.9f, 1f) }
|
|
};
|
|
|
|
logStyle = new GUIStyle(EditorStyles.miniLabel)
|
|
{
|
|
wordWrap = true,
|
|
richText = true,
|
|
normal = { textColor = new Color(0.7f, 0.7f, 0.7f) }
|
|
};
|
|
|
|
stylesInitialized = true;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Main GUI
|
|
|
|
private void OnGUI()
|
|
{
|
|
InitializeStyles();
|
|
|
|
// Header
|
|
DrawHeader();
|
|
|
|
// Play mode check
|
|
if (Application.isPlaying)
|
|
{
|
|
EditorGUILayout.HelpBox("Exit Play Mode to use this tool.", MessageType.Warning);
|
|
return;
|
|
}
|
|
|
|
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
|
|
|
|
// Environment Setup Section
|
|
DrawEnvironmentSetup();
|
|
|
|
// Generation Settings Section
|
|
DrawGenerationSettings();
|
|
|
|
// Options Section
|
|
DrawOptions();
|
|
|
|
// Advanced Settings Section
|
|
DrawAdvancedSettings();
|
|
|
|
// Action Buttons
|
|
DrawActionButtons();
|
|
|
|
// Progress Bar
|
|
if (isProcessing)
|
|
{
|
|
DrawProgressBar();
|
|
}
|
|
|
|
// Analysis Results Section
|
|
if (analysisResults.Count > 0)
|
|
{
|
|
DrawAnalysisResults();
|
|
}
|
|
|
|
// Statistics Section
|
|
if (statistics.totalObjects > 0)
|
|
{
|
|
DrawStatistics();
|
|
}
|
|
|
|
// Log Section
|
|
DrawLogSection();
|
|
|
|
EditorGUILayout.EndScrollView();
|
|
}
|
|
|
|
private void DrawHeader()
|
|
{
|
|
var headerRect = EditorGUILayout.GetControlRect(false, 45);
|
|
EditorGUI.DrawRect(headerRect, new Color(0.15f, 0.25f, 0.35f, 0.9f));
|
|
GUI.Label(headerRect, "🔧 Optimized Collider Tool", headerStyle);
|
|
GUILayout.Space(10);
|
|
}
|
|
|
|
private void DrawEnvironmentSetup()
|
|
{
|
|
EditorGUILayout.BeginVertical("box");
|
|
GUILayout.Label("📁 Environment Setup", sectionStyle);
|
|
GUILayout.Space(5);
|
|
|
|
environmentParent = (GameObject)EditorGUILayout.ObjectField(
|
|
new GUIContent("Environment Parent", "Root GameObject containing all buildings and objects"),
|
|
environmentParent, typeof(GameObject), true);
|
|
|
|
groundTerrain = (GameObject)EditorGUILayout.ObjectField(
|
|
new GUIContent("Ground/Terrain", "Terrain or ground mesh GameObject"),
|
|
groundTerrain, typeof(GameObject), true);
|
|
|
|
groundReference = (GameObject)EditorGUILayout.ObjectField(
|
|
new GUIContent("Ground Reference (Optional)", "Working ground example to copy components from"),
|
|
groundReference, typeof(GameObject), true);
|
|
|
|
if (environmentParent == null)
|
|
{
|
|
EditorGUILayout.HelpBox("Assign the Environment Parent GameObject to begin analysis.", MessageType.Info);
|
|
}
|
|
|
|
EditorGUILayout.EndVertical();
|
|
GUILayout.Space(8);
|
|
}
|
|
|
|
private void DrawGenerationSettings()
|
|
{
|
|
EditorGUILayout.BeginVertical("box");
|
|
GUILayout.Label("⚙️ Generation Settings", sectionStyle);
|
|
GUILayout.Space(5);
|
|
|
|
colliderMethod = (ColliderMethod)EditorGUILayout.EnumPopup(
|
|
new GUIContent("Collider Method", "How to generate colliders"),
|
|
colliderMethod);
|
|
|
|
detailLevel = (DetailLevel)EditorGUILayout.EnumPopup(
|
|
new GUIContent("Detail Level", "Affects memory vs accuracy trade-off"),
|
|
detailLevel);
|
|
|
|
// Show detail level info
|
|
string detailInfo = detailLevel switch
|
|
{
|
|
DetailLevel.Low => "Fast, ~100-500 triangles per collider",
|
|
DetailLevel.Medium => "Balanced, ~500-2000 triangles per collider",
|
|
DetailLevel.High => "Accurate, ~2000-5000 triangles per collider",
|
|
_ => ""
|
|
};
|
|
EditorGUILayout.LabelField("", detailInfo, EditorStyles.miniLabel);
|
|
|
|
EditorGUILayout.EndVertical();
|
|
GUILayout.Space(8);
|
|
}
|
|
|
|
private void DrawOptions()
|
|
{
|
|
EditorGUILayout.BeginVertical("box");
|
|
GUILayout.Label("☑️ Options", sectionStyle);
|
|
GUILayout.Space(5);
|
|
|
|
EditorGUILayout.LabelField("Object Categories", EditorStyles.boldLabel);
|
|
generateForBuildings = EditorGUILayout.ToggleLeft(
|
|
new GUIContent("✅ Buildings", "Create colliders for building structures"),
|
|
generateForBuildings);
|
|
|
|
generateForGround = EditorGUILayout.ToggleLeft(
|
|
new GUIContent("✅ Ground/Terrain", "Configure terrain or ground colliders"),
|
|
generateForGround);
|
|
|
|
generateForProps = EditorGUILayout.ToggleLeft(
|
|
new GUIContent("✅ Props (fences, benches, etc.)", "Create colliders for props and small objects"),
|
|
generateForProps);
|
|
|
|
generateForVegetation = EditorGUILayout.ToggleLeft(
|
|
new GUIContent("🌿 Trees & Vegetation", "Create colliders for vegetation (usually not needed)"),
|
|
generateForVegetation);
|
|
|
|
GUILayout.Space(5);
|
|
EditorGUILayout.LabelField("Ground Handling", EditorStyles.boldLabel);
|
|
skipGroundPlanes = EditorGUILayout.ToggleLeft(
|
|
new GUIContent("⛔ Skip Ground Planes Under Buildings",
|
|
"Exclude flat planes below buildings from getting colliders. " +
|
|
"Terrain collider is sufficient — extra ground planes create invisible walls that break camera."),
|
|
skipGroundPlanes);
|
|
|
|
GUILayout.Space(5);
|
|
EditorGUILayout.LabelField("Size Filter", EditorStyles.boldLabel);
|
|
minimumObjectSize = EditorGUILayout.Slider(
|
|
new GUIContent("Minimum Object Size (m)", "Objects smaller than this in ALL dimensions are skipped. Prevents colliders on tiny decorations."),
|
|
minimumObjectSize, 0.1f, 3f);
|
|
|
|
GUILayout.Space(5);
|
|
EditorGUILayout.LabelField("Generation Options", EditorStyles.boldLabel);
|
|
optimizeForPerformance = EditorGUILayout.ToggleLeft(
|
|
new GUIContent("Optimize for Performance", "Use simplified meshes and batch colliders"),
|
|
optimizeForPerformance);
|
|
|
|
createCompoundForComplex = EditorGUILayout.ToggleLeft(
|
|
new GUIContent("Compound Colliders for Complex Objects", "Create multiple BoxColliders for complex buildings"),
|
|
createCompoundForComplex);
|
|
|
|
removeExistingFirst = EditorGUILayout.ToggleLeft(
|
|
new GUIContent("Remove Existing Colliders First", "Clear all existing colliders before generation"),
|
|
removeExistingFirst);
|
|
|
|
EditorGUILayout.EndVertical();
|
|
GUILayout.Space(8);
|
|
}
|
|
|
|
private void DrawAdvancedSettings()
|
|
{
|
|
showAdvancedSettings = EditorGUILayout.Foldout(showAdvancedSettings, "🔬 Advanced Settings", true);
|
|
|
|
if (showAdvancedSettings)
|
|
{
|
|
EditorGUILayout.BeginVertical("box");
|
|
|
|
// OBB and Structure Following
|
|
EditorGUILayout.LabelField("Structure Following", EditorStyles.boldLabel);
|
|
useOBBFitting = EditorGUILayout.ToggleLeft(
|
|
new GUIContent("Use OBB Fitting", "Use Oriented Bounding Boxes instead of AABB for better structure following"),
|
|
useOBBFitting);
|
|
detectArchitecturalFeatures = EditorGUILayout.ToggleLeft(
|
|
new GUIContent("Detect Architectural Features", "Identify walls, floors, roofs separately"),
|
|
detectArchitecturalFeatures);
|
|
|
|
GUILayout.Space(5);
|
|
|
|
// Gap Prevention
|
|
EditorGUILayout.LabelField("Gap Prevention", EditorStyles.boldLabel);
|
|
fillGaps = EditorGUILayout.ToggleLeft(
|
|
new GUIContent("Fill Collision Gaps", "Detect and fill gaps in collision coverage"),
|
|
fillGaps);
|
|
colliderOverlap = EditorGUILayout.Slider(
|
|
new GUIContent("Collider Overlap", "Overlap amount at edges to prevent gaps (meters)"),
|
|
colliderOverlap, 0f, 0.1f);
|
|
gapDetectionSpacing = EditorGUILayout.Slider(
|
|
new GUIContent("Gap Detection Spacing", "Ray spacing for gap detection (meters)"),
|
|
gapDetectionSpacing, 0.1f, 2f);
|
|
|
|
GUILayout.Space(5);
|
|
|
|
// Complexity Thresholds
|
|
EditorGUILayout.LabelField("Complexity Thresholds", EditorStyles.boldLabel);
|
|
complexityThreshold = EditorGUILayout.IntSlider(
|
|
new GUIContent("Child Renderer Threshold", "Objects with more renderers are treated as complex"),
|
|
complexityThreshold, 2, 50);
|
|
vertexThreshold = EditorGUILayout.IntSlider(
|
|
new GUIContent("Vertex Threshold", "Objects exceeding this are treated as complex"),
|
|
vertexThreshold, 500, 20000);
|
|
|
|
GUILayout.Space(5);
|
|
|
|
// Triangle Limits
|
|
EditorGUILayout.LabelField("Triangle Limits (per collider)", EditorStyles.boldLabel);
|
|
maxTrianglesLow = EditorGUILayout.IntField("Low Detail Max", maxTrianglesLow);
|
|
maxTrianglesMedium = EditorGUILayout.IntField("Medium Detail Max", maxTrianglesMedium);
|
|
maxTrianglesHigh = EditorGUILayout.IntField("High Detail Max", maxTrianglesHigh);
|
|
|
|
GUILayout.Space(5);
|
|
|
|
// Performance
|
|
EditorGUILayout.LabelField("Performance", EditorStyles.boldLabel);
|
|
markAsStatic = EditorGUILayout.ToggleLeft("Mark Environment as Static", markAsStatic);
|
|
bakeMeshColliders = EditorGUILayout.ToggleLeft("Pre-bake Mesh Colliders", bakeMeshColliders);
|
|
|
|
GUILayout.Space(5);
|
|
|
|
// Ground Layer
|
|
EditorGUILayout.LabelField("Ground Configuration", EditorStyles.boldLabel);
|
|
groundLayer = EditorGUILayout.LayerField("Ground Layer", groundLayer);
|
|
|
|
EditorGUILayout.EndVertical();
|
|
}
|
|
|
|
GUILayout.Space(8);
|
|
}
|
|
|
|
private void DrawActionButtons()
|
|
{
|
|
EditorGUILayout.BeginVertical("box");
|
|
|
|
GUI.enabled = !isProcessing && environmentParent != null;
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
if (GUILayout.Button("🔍 Analyze Environment", GUILayout.Height(35)))
|
|
{
|
|
AnalyzeEnvironmentAsync();
|
|
}
|
|
|
|
if (GUILayout.Button("🧹 Clear Analysis", GUILayout.Height(35)))
|
|
{
|
|
ClearAnalysis();
|
|
}
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
GUILayout.Space(5);
|
|
|
|
GUI.enabled = !isProcessing && analysisResults.Count > 0;
|
|
|
|
var generateRect = EditorGUILayout.GetControlRect(false, 40);
|
|
EditorGUI.DrawRect(generateRect, new Color(0.2f, 0.6f, 0.3f, 0.8f));
|
|
if (GUI.Button(generateRect, "✅ Generate Colliders"))
|
|
{
|
|
GenerateCollidersAsync();
|
|
}
|
|
|
|
GUILayout.Space(5);
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
GUI.enabled = !isProcessing && changeRecords.Count > 0;
|
|
if (GUILayout.Button("↩️ Undo All Changes", GUILayout.Height(30)))
|
|
{
|
|
UndoAllChanges();
|
|
}
|
|
|
|
GUI.enabled = !isProcessing;
|
|
if (GUILayout.Button("💾 Save Scene", GUILayout.Height(30)))
|
|
{
|
|
SaveScene();
|
|
}
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
GUI.enabled = true;
|
|
|
|
EditorGUILayout.EndVertical();
|
|
GUILayout.Space(8);
|
|
}
|
|
|
|
private void DrawProgressBar()
|
|
{
|
|
EditorGUILayout.BeginVertical("box");
|
|
|
|
EditorGUILayout.LabelField(statusMessage, EditorStyles.boldLabel);
|
|
|
|
var progressRect = EditorGUILayout.GetControlRect(false, 20);
|
|
EditorGUI.DrawRect(progressRect, new Color(0.1f, 0.1f, 0.1f, 0.8f));
|
|
var fillRect = new Rect(progressRect.x + 2, progressRect.y + 2,
|
|
(progressRect.width - 4) * progress, progressRect.height - 4);
|
|
EditorGUI.DrawRect(fillRect, new Color(0.3f, 0.7f, 0.4f, 0.9f));
|
|
GUI.Label(progressRect, $"{(progress * 100):F0}%",
|
|
new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter });
|
|
|
|
EditorGUILayout.EndVertical();
|
|
GUILayout.Space(8);
|
|
}
|
|
|
|
private void DrawAnalysisResults()
|
|
{
|
|
EditorGUILayout.BeginVertical("box");
|
|
GUILayout.Label($"📊 Analysis Results ({analysisResults.Count} objects)", sectionStyle);
|
|
GUILayout.Space(5);
|
|
|
|
// Quick select buttons
|
|
EditorGUILayout.BeginHorizontal();
|
|
if (GUILayout.Button("Select All", GUILayout.Width(100)))
|
|
{
|
|
foreach (var r in analysisResults) r.selected = true;
|
|
}
|
|
if (GUILayout.Button("Select None", GUILayout.Width(100)))
|
|
{
|
|
foreach (var r in analysisResults) r.selected = false;
|
|
}
|
|
if (GUILayout.Button("Buildings Only", GUILayout.Width(100)))
|
|
{
|
|
foreach (var r in analysisResults)
|
|
r.selected = r.objectType == "Building";
|
|
}
|
|
if (GUILayout.Button("Ground Only", GUILayout.Width(100)))
|
|
{
|
|
foreach (var r in analysisResults)
|
|
r.selected = r.objectType == "Ground";
|
|
}
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
GUILayout.Space(5);
|
|
|
|
// Show first 50 results with pagination would be added here
|
|
int displayCount = Mathf.Min(analysisResults.Count, 30);
|
|
for (int i = 0; i < displayCount; i++)
|
|
{
|
|
var result = analysisResults[i];
|
|
DrawAnalysisResultRow(result);
|
|
}
|
|
|
|
if (analysisResults.Count > displayCount)
|
|
{
|
|
EditorGUILayout.LabelField($"... and {analysisResults.Count - displayCount} more objects",
|
|
EditorStyles.miniLabel);
|
|
}
|
|
|
|
EditorGUILayout.EndVertical();
|
|
GUILayout.Space(8);
|
|
}
|
|
|
|
private void DrawAnalysisResultRow(AnalysisResult result)
|
|
{
|
|
EditorGUILayout.BeginHorizontal("box");
|
|
|
|
// Color indicator
|
|
var colorRect = EditorGUILayout.GetControlRect(GUILayout.Width(8), GUILayout.Height(18));
|
|
EditorGUI.DrawRect(colorRect, result.displayColor);
|
|
|
|
// Selection toggle
|
|
result.selected = EditorGUILayout.Toggle(result.selected, GUILayout.Width(20));
|
|
|
|
// Object name (clickable)
|
|
if (GUILayout.Button(result.gameObject.name, EditorStyles.linkLabel, GUILayout.Width(180)))
|
|
{
|
|
Selection.activeGameObject = result.gameObject;
|
|
EditorGUIUtility.PingObject(result.gameObject);
|
|
}
|
|
|
|
// Type
|
|
GUILayout.Label(result.objectType, GUILayout.Width(70));
|
|
|
|
// Recommended collider
|
|
GUILayout.Label(result.recommendedCollider, GUILayout.Width(100));
|
|
|
|
// Triangle count
|
|
GUILayout.Label($"{result.triangleCount} tris", EditorStyles.miniLabel, GUILayout.Width(60));
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
}
|
|
|
|
private void DrawStatistics()
|
|
{
|
|
EditorGUILayout.BeginVertical("box");
|
|
GUILayout.Label("📈 Statistics", sectionStyle);
|
|
GUILayout.Space(5);
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
EditorGUILayout.BeginVertical();
|
|
EditorGUILayout.LabelField($"Total Objects: {statistics.totalObjects}");
|
|
EditorGUILayout.LabelField($"With Colliders: {statistics.objectsWithColliders}");
|
|
EditorGUILayout.LabelField($"Need Colliders: {statistics.objectsNeedingColliders}");
|
|
EditorGUILayout.EndVertical();
|
|
|
|
EditorGUILayout.BeginVertical();
|
|
EditorGUILayout.LabelField($"Buildings: {statistics.buildingCount}");
|
|
EditorGUILayout.LabelField($"Ground: {statistics.groundCount}");
|
|
EditorGUILayout.LabelField($"Props: {statistics.propCount}");
|
|
EditorGUILayout.EndVertical();
|
|
|
|
EditorGUILayout.BeginVertical();
|
|
EditorGUILayout.LabelField($"Original Mesh: {FormatBytes(statistics.originalMeshMemoryBytes)}");
|
|
EditorGUILayout.LabelField($"Collision Est: {FormatBytes(statistics.estimatedMemoryBytes)}");
|
|
EditorGUILayout.LabelField($"Triangles: {statistics.totalTriangles} → {statistics.optimizedTriangles}");
|
|
EditorGUILayout.EndVertical();
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
// Memory warning
|
|
if (statistics.estimatedMemoryBytes > 50 * 1024 * 1024) // 50MB
|
|
{
|
|
EditorGUILayout.HelpBox(
|
|
$"Estimated collision memory ({FormatBytes(statistics.estimatedMemoryBytes)}) exceeds 50MB. " +
|
|
"Consider using lower detail level or more compound colliders.",
|
|
MessageType.Warning);
|
|
}
|
|
|
|
EditorGUILayout.EndVertical();
|
|
GUILayout.Space(8);
|
|
}
|
|
|
|
private void DrawLogSection()
|
|
{
|
|
if (logMessages.Count == 0) return;
|
|
|
|
EditorGUILayout.BeginVertical("box");
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
GUILayout.Label("📝 Log", sectionStyle);
|
|
if (GUILayout.Button("Clear", GUILayout.Width(50)))
|
|
{
|
|
logMessages.Clear();
|
|
}
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
GUILayout.Space(5);
|
|
|
|
// Show last 10 messages
|
|
int start = Mathf.Max(0, logMessages.Count - 10);
|
|
for (int i = start; i < logMessages.Count; i++)
|
|
{
|
|
EditorGUILayout.LabelField(logMessages[i], logStyle);
|
|
}
|
|
|
|
EditorGUILayout.EndVertical();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Analysis
|
|
|
|
private async void AnalyzeEnvironmentAsync()
|
|
{
|
|
if (environmentParent == null) return;
|
|
|
|
isProcessing = true;
|
|
progress = 0f;
|
|
statusMessage = "Analyzing environment...";
|
|
analysisResults.Clear();
|
|
statistics.Reset();
|
|
logMessages.Clear();
|
|
|
|
Log("Starting environment analysis...");
|
|
|
|
try
|
|
{
|
|
// ===== NEW APPROACH: Find TOP-LEVEL structural objects =====
|
|
// Instead of iterating every renderer (21000+), walk the hierarchy
|
|
// and find meaningful parent objects (buildings, ground, props).
|
|
// Each top-level object gets ONE collider covering all its children.
|
|
|
|
var processedObjects = new HashSet<GameObject>(); // Track what we've already handled
|
|
var topLevelObjects = FindTopLevelStructuralObjects(environmentParent, processedObjects);
|
|
|
|
float total = Mathf.Max(1, topLevelObjects.Count);
|
|
int processed = 0;
|
|
|
|
statistics.totalObjects = topLevelObjects.Count;
|
|
|
|
foreach (var obj in topLevelObjects)
|
|
{
|
|
if (obj == null) continue;
|
|
|
|
var result = AnalyzeTopLevelObject(obj);
|
|
if (result != null)
|
|
{
|
|
// === FILTER BY CATEGORY during analysis ===
|
|
// Only add results for categories the user has enabled
|
|
bool include = true;
|
|
if (result.objectType == "Building" && !generateForBuildings) include = false;
|
|
if (result.objectType == "Ground" && !generateForGround) include = false;
|
|
if (result.objectType == "Prop" && !generateForProps) include = false;
|
|
if (result.objectType == "Vegetation" && !generateForVegetation) include = false;
|
|
|
|
if (include)
|
|
{
|
|
analysisResults.Add(result);
|
|
}
|
|
}
|
|
|
|
processed++;
|
|
progress = processed / total;
|
|
statusMessage = $"Analyzing: {obj.name}";
|
|
|
|
if (processed % 50 == 0)
|
|
{
|
|
Repaint();
|
|
await Task.Yield();
|
|
}
|
|
}
|
|
|
|
// Analyze ground/terrain separately
|
|
if (groundTerrain != null && generateForGround)
|
|
{
|
|
var groundResult = AnalyzeGroundObject(groundTerrain);
|
|
if (groundResult != null)
|
|
{
|
|
analysisResults.Insert(0, groundResult);
|
|
}
|
|
}
|
|
|
|
// Calculate statistics
|
|
CalculateStatistics();
|
|
|
|
Log($"Analysis complete: {analysisResults.Count} objects analyzed (from {statistics.totalObjects} top-level objects)");
|
|
Log($"Buildings: {statistics.buildingCount}, Ground: {statistics.groundCount}, Props: {statistics.propCount}");
|
|
Log($"Skipped {statistics.vegetationCount} vegetation, filtered {statistics.totalObjects - analysisResults.Count} objects below min size");
|
|
|
|
statusMessage = "Analysis complete!";
|
|
progress = 1f;
|
|
}
|
|
catch (System.Exception e)
|
|
{
|
|
Log($"<color=red>Error: {e.Message}</color>");
|
|
statusMessage = "Analysis failed!";
|
|
}
|
|
finally
|
|
{
|
|
isProcessing = false;
|
|
Repaint();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find top-level structural objects in the hierarchy.
|
|
/// A "top-level" object is one whose PARENT is the environment root (or 1-2 levels deep),
|
|
/// and it has renderers (itself or children). We treat each as ONE collider target.
|
|
/// This dramatically reduces collider count vs iterating every renderer.
|
|
/// </summary>
|
|
private List<GameObject> FindTopLevelStructuralObjects(GameObject root, HashSet<GameObject> processed)
|
|
{
|
|
var results = new List<GameObject>();
|
|
|
|
// Walk direct children of environment parent
|
|
foreach (Transform child in root.transform)
|
|
{
|
|
if (child == null || !child.gameObject.activeInHierarchy) continue;
|
|
if (processed.Contains(child.gameObject)) continue;
|
|
|
|
// Check if this child has any renderers (itself or descendants)
|
|
var childRenderers = child.GetComponentsInChildren<Renderer>(true);
|
|
if (childRenderers.Length == 0) continue;
|
|
|
|
// Calculate combined bounds of all child renderers
|
|
Bounds combinedBounds = childRenderers[0].bounds;
|
|
for (int i = 1; i < childRenderers.Length; i++)
|
|
{
|
|
if (childRenderers[i] != null)
|
|
combinedBounds.Encapsulate(childRenderers[i].bounds);
|
|
}
|
|
|
|
Vector3 size = combinedBounds.size;
|
|
|
|
// If this is a large container with many sub-buildings,
|
|
// go one level deeper to find individual buildings
|
|
bool isLargeContainer = (size.x > 50f || size.z > 50f) && child.childCount > 5;
|
|
|
|
if (isLargeContainer)
|
|
{
|
|
// This is probably a "Buildings" or "Props" container folder.
|
|
// Go one level deeper to find individual buildings.
|
|
foreach (Transform grandchild in child)
|
|
{
|
|
if (grandchild == null || !grandchild.gameObject.activeInHierarchy) continue;
|
|
if (processed.Contains(grandchild.gameObject)) continue;
|
|
|
|
var gcRenderers = grandchild.GetComponentsInChildren<Renderer>(true);
|
|
if (gcRenderers.Length == 0) continue;
|
|
|
|
processed.Add(grandchild.gameObject);
|
|
results.Add(grandchild.gameObject);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
processed.Add(child.gameObject);
|
|
results.Add(child.gameObject);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Analyze a top-level object (building, prop, or ground piece).
|
|
/// Uses combined bounds of ALL child renderers for a single collider.
|
|
/// </summary>
|
|
private AnalysisResult AnalyzeTopLevelObject(GameObject obj)
|
|
{
|
|
if (obj == null) return null;
|
|
if (!obj.activeInHierarchy) return null;
|
|
|
|
// Get combined bounds from ALL child renderers
|
|
var renderers = obj.GetComponentsInChildren<Renderer>(true);
|
|
if (renderers.Length == 0) return null;
|
|
|
|
Bounds combinedBounds = renderers[0].bounds;
|
|
int totalVerts = 0;
|
|
int totalTris = 0;
|
|
|
|
foreach (var r in renderers)
|
|
{
|
|
if (r == null) continue;
|
|
combinedBounds.Encapsulate(r.bounds);
|
|
|
|
var mf = r.GetComponent<MeshFilter>();
|
|
if (mf != null && mf.sharedMesh != null)
|
|
{
|
|
totalVerts += mf.sharedMesh.vertexCount;
|
|
if (mf.sharedMesh.isReadable)
|
|
totalTris += mf.sharedMesh.triangles.Length / 3;
|
|
}
|
|
}
|
|
|
|
Vector3 size = combinedBounds.size;
|
|
string name = obj.name.ToLower();
|
|
Mesh mesh = GetMeshFromObject(obj); // Root mesh (may be null for parent containers)
|
|
|
|
// === SIZE FILTER: Skip objects too small to matter ===
|
|
// Per Unity docs: fewer colliders = better performance
|
|
if (size.x < minimumObjectSize && size.y < minimumObjectSize && size.z < minimumObjectSize)
|
|
{
|
|
return null; // Too small, skip entirely
|
|
}
|
|
|
|
var result = new AnalysisResult
|
|
{
|
|
gameObject = obj,
|
|
vertexCount = totalVerts,
|
|
triangleCount = totalTris,
|
|
bounds = size
|
|
};
|
|
|
|
// === CLASSIFY ===
|
|
ClassifyTopLevelObject(obj, name, size, renderers.Length, mesh, result);
|
|
|
|
// Set display color
|
|
result.displayColor = GetColorForColliderType(result.recommendedCollider);
|
|
|
|
return result;
|
|
}
|
|
|
|
private AnalysisResult AnalyzeGroundObject(GameObject obj)
|
|
{
|
|
if (obj == null) return null;
|
|
|
|
var terrain = obj.GetComponent<Terrain>();
|
|
var terrainCollider = obj.GetComponent<TerrainCollider>();
|
|
|
|
var result = new AnalysisResult
|
|
{
|
|
gameObject = obj,
|
|
objectType = "Ground",
|
|
displayColor = new Color(0.6f, 0.4f, 0.2f, 0.8f)
|
|
};
|
|
|
|
if (terrain != null)
|
|
{
|
|
result.recommendedCollider = "TerrainCollider";
|
|
result.reason = terrainCollider != null ?
|
|
"Terrain already has TerrainCollider" :
|
|
"Unity Terrain - needs TerrainCollider";
|
|
result.bounds = terrain.terrainData != null ? terrain.terrainData.size : Vector3.zero;
|
|
statistics.groundCount++;
|
|
}
|
|
else
|
|
{
|
|
Mesh mesh = GetMeshFromObject(obj);
|
|
if (mesh != null)
|
|
{
|
|
result.recommendedCollider = "MeshCollider";
|
|
result.reason = "Mesh-based ground";
|
|
result.vertexCount = mesh.vertexCount;
|
|
result.triangleCount = mesh.isReadable ? mesh.triangles.Length / 3 : 0;
|
|
var renderer = obj.GetComponent<Renderer>();
|
|
result.bounds = renderer != null ? renderer.bounds.size : Vector3.zero;
|
|
statistics.groundCount++;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Classify a top-level structural object.
|
|
/// KEY PRINCIPLE (from Unity docs):
|
|
/// Sphere > Capsule > Box > Convex Mesh > Non-convex Mesh (performance order)
|
|
/// ONE collider per building is ideal. Fewer colliders = better mobile performance.
|
|
/// </summary>
|
|
private void ClassifyTopLevelObject(GameObject obj, string name, Vector3 size, int rendererCount, Mesh rootMesh, AnalysisResult result)
|
|
{
|
|
// === VEGETATION: Skip entirely ===
|
|
if (IsVegetation(obj, name, size, rootMesh))
|
|
{
|
|
result.objectType = "Vegetation";
|
|
result.recommendedCollider = "Skip";
|
|
result.reason = "Vegetation - no collider needed";
|
|
result.displayColor = new Color(0.3f, 0.8f, 0.3f, 0.5f);
|
|
result.selected = false;
|
|
statistics.vegetationCount++;
|
|
return;
|
|
}
|
|
|
|
// === GROUND: Check BEFORE buildings ===
|
|
// But skip ground planes if they're under buildings or if skipGroundPlanes is enabled
|
|
if (IsGround(obj, name, size, rootMesh))
|
|
{
|
|
// If skipGroundPlanes is enabled, skip ALL ground/plane objects
|
|
// because the terrain collider already handles ground collision.
|
|
// These extra planes create invisible walls that break the camera.
|
|
if (skipGroundPlanes)
|
|
{
|
|
result.objectType = "Ground";
|
|
result.recommendedCollider = "Skip";
|
|
result.reason = "Ground plane skipped (terrain collider is sufficient)";
|
|
result.displayColor = new Color(0.6f, 0.4f, 0.2f, 0.3f);
|
|
result.selected = false;
|
|
statistics.groundCount++;
|
|
return;
|
|
}
|
|
|
|
result.objectType = "Ground";
|
|
result.recommendedCollider = "MeshCollider";
|
|
result.reason = "Ground surface - non-convex for accuracy";
|
|
statistics.groundCount++;
|
|
return;
|
|
}
|
|
|
|
// === BUILDING: ONE collider for the whole building ===
|
|
if (IsBuilding(obj, name, size))
|
|
{
|
|
result.objectType = "Building";
|
|
statistics.buildingCount++;
|
|
|
|
// Per Unity docs: prefer BoxCollider (efficient)
|
|
// ONE BoxCollider per building wrapping all children = simple and fast
|
|
result.recommendedCollider = "BoxCollider";
|
|
result.reason = $"Single BoxCollider for entire building ({rendererCount} parts)";
|
|
return;
|
|
}
|
|
|
|
// === PROP: Usually BoxCollider is fine ===
|
|
result.objectType = "Prop";
|
|
statistics.propCount++;
|
|
result.recommendedCollider = "BoxCollider";
|
|
result.reason = $"BoxCollider for prop ({rendererCount} parts)";
|
|
}
|
|
|
|
|
|
|
|
private void CalculateStatistics()
|
|
{
|
|
statistics.totalTriangles = 0;
|
|
statistics.optimizedTriangles = 0;
|
|
statistics.originalMeshMemoryBytes = 0;
|
|
statistics.estimatedMemoryBytes = 0;
|
|
|
|
int maxTris = GetMaxTrianglesForDetailLevel();
|
|
|
|
foreach (var result in analysisResults)
|
|
{
|
|
if (result.recommendedCollider == "Skip") continue;
|
|
|
|
statistics.totalTriangles += result.triangleCount;
|
|
|
|
// Estimate optimized triangle count
|
|
int optimized = result.recommendedCollider switch
|
|
{
|
|
"BoxCollider" => 12,
|
|
"ConvexMesh" => Mathf.Min(result.triangleCount, 255),
|
|
"MeshCollider" => Mathf.Min(result.triangleCount, maxTris),
|
|
_ => result.triangleCount
|
|
};
|
|
statistics.optimizedTriangles += optimized;
|
|
|
|
// Estimate memory (rough: ~12 bytes per vertex, ~4 bytes per triangle index)
|
|
statistics.originalMeshMemoryBytes += (result.vertexCount * 12) + (result.triangleCount * 12);
|
|
|
|
// Collision mesh memory estimate
|
|
int collisionVerts = result.recommendedCollider switch
|
|
{
|
|
"BoxCollider" => 8,
|
|
"ConvexMesh" => Mathf.Min(result.vertexCount, 255),
|
|
"MeshCollider" => Mathf.Min(result.vertexCount, maxTris * 3),
|
|
_ => Mathf.Min(result.vertexCount, maxTris * 3)
|
|
};
|
|
statistics.estimatedMemoryBytes += collisionVerts * 12 + optimized * 12;
|
|
}
|
|
}
|
|
|
|
private int GetMaxTrianglesForDetailLevel()
|
|
{
|
|
return detailLevel switch
|
|
{
|
|
DetailLevel.Low => maxTrianglesLow,
|
|
DetailLevel.Medium => maxTrianglesMedium,
|
|
DetailLevel.High => maxTrianglesHigh,
|
|
_ => maxTrianglesMedium
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Classification Helpers
|
|
|
|
private bool IsVegetation(GameObject obj, string name, Vector3 size, Mesh mesh)
|
|
{
|
|
// Name-based detection
|
|
string[] vegetationKeywords = { "tree", "grass", "plant", "bush", "shrub", "flower",
|
|
"leaf", "foliage", "fern", "vine", "hedge", "weed" };
|
|
|
|
foreach (var keyword in vegetationKeywords)
|
|
{
|
|
if (name.Contains(keyword)) return true;
|
|
if (NameOrAncestorContains(obj, keyword)) return true;
|
|
}
|
|
|
|
// Small ground cover
|
|
if (size.y < 0.6f && size.x < 0.8f && size.z < 0.8f)
|
|
{
|
|
if (mesh != null && mesh.vertexCount < 200) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool IsBuilding(GameObject obj, string name, Vector3 size)
|
|
{
|
|
// Name-based
|
|
string[] buildingKeywords = { "building", "house", "wall", "structure", "apartment",
|
|
"shop", "store", "tower", "bridge", "fence", "gate" };
|
|
|
|
foreach (var keyword in buildingKeywords)
|
|
{
|
|
if (name.Contains(keyword)) return true;
|
|
}
|
|
|
|
// Size-based: Large structures
|
|
if (size.x > 5f || size.y > 3f || size.z > 5f)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Wall-like: long and tall
|
|
if ((size.x > size.z * 3f || size.z > size.x * 3f) && size.y > 1.5f)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool IsGround(GameObject obj, string name, Vector3 size, Mesh mesh)
|
|
{
|
|
// Name-based
|
|
string[] groundKeywords = { "ground", "floor", "terrain", "surface", "plane", "platform" };
|
|
|
|
foreach (var keyword in groundKeywords)
|
|
{
|
|
if (name.Contains(keyword)) return true;
|
|
}
|
|
|
|
// Very flat and wide — CLASSIC ground plane detection
|
|
// This catches planes placed under buildings that create invisible walls
|
|
if (size.y < Mathf.Min(size.x, size.z) * 0.1f && (size.x > 3f || size.z > 3f))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Check mesh flatness (nearly zero height = plane)
|
|
if (mesh != null && mesh.bounds.size.y < 0.01f)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Detect Unity default Plane mesh (10x10 quad with y=0)
|
|
if (mesh != null && mesh.vertexCount <= 121 && size.y < 0.05f)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
private bool NameOrAncestorContains(GameObject obj, string keyword)
|
|
{
|
|
Transform t = obj.transform;
|
|
while (t != null)
|
|
{
|
|
if (t.name.ToLower().Contains(keyword))
|
|
return true;
|
|
t = t.parent;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private Mesh GetMeshFromObject(GameObject obj)
|
|
{
|
|
var mf = obj.GetComponent<MeshFilter>();
|
|
if (mf != null && mf.sharedMesh != null) return mf.sharedMesh;
|
|
|
|
var smr = obj.GetComponent<SkinnedMeshRenderer>();
|
|
if (smr != null && smr.sharedMesh != null) return smr.sharedMesh;
|
|
|
|
return null;
|
|
}
|
|
|
|
private Color GetColorForColliderType(string type)
|
|
{
|
|
return type switch
|
|
{
|
|
"BoxCollider" => new Color(0.2f, 0.8f, 0.2f, 0.8f),
|
|
"ConvexMesh" => new Color(0.2f, 0.6f, 0.9f, 0.8f),
|
|
"MeshCollider" => new Color(0.9f, 0.5f, 0.2f, 0.8f),
|
|
"TerrainCollider" => new Color(0.6f, 0.4f, 0.2f, 0.8f),
|
|
"Skip" => new Color(0.5f, 0.5f, 0.5f, 0.4f),
|
|
_ => Color.white
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Utility
|
|
|
|
private void ClearAnalysis()
|
|
{
|
|
analysisResults.Clear();
|
|
statistics.Reset();
|
|
logMessages.Clear();
|
|
progress = 0f;
|
|
statusMessage = "";
|
|
Repaint();
|
|
}
|
|
|
|
private void Log(string message)
|
|
{
|
|
logMessages.Add($"[{System.DateTime.Now:HH:mm:ss}] {message}");
|
|
Debug.Log($"[OptimizedColliderTool] {message}");
|
|
}
|
|
|
|
private string FormatBytes(long bytes)
|
|
{
|
|
if (bytes < 1024) return $"{bytes} B";
|
|
if (bytes < 1024 * 1024) return $"{bytes / 1024f:F1} KB";
|
|
return $"{bytes / (1024f * 1024f):F1} MB";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bake all MeshColliders under a parent for runtime performance
|
|
/// </summary>
|
|
private void BakeMeshCollidersInline(GameObject parent)
|
|
{
|
|
var meshColliders = parent.GetComponentsInChildren<MeshCollider>(true);
|
|
foreach (var mc in meshColliders)
|
|
{
|
|
if (mc != null && mc.sharedMesh != null)
|
|
{
|
|
Physics.BakeMesh(mc.sharedMesh.GetInstanceID(), mc.convex);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a non-convex MeshCollider for ground surfaces
|
|
/// </summary>
|
|
private Collider AddGroundMeshColliderInline(GameObject obj, Mesh mesh)
|
|
{
|
|
if (obj == null) return null;
|
|
if (obj.GetComponent<Collider>() != null) return null;
|
|
|
|
var mc = Undo.AddComponent<MeshCollider>(obj);
|
|
if (mesh != null) mc.sharedMesh = mesh;
|
|
mc.convex = false;
|
|
|
|
// Set cooking options for readable meshes
|
|
if (mesh != null && mesh.isReadable)
|
|
{
|
|
mc.cookingOptions = MeshColliderCookingOptions.CookForFasterSimulation |
|
|
MeshColliderCookingOptions.EnableMeshCleaning |
|
|
MeshColliderCookingOptions.WeldColocatedVertices;
|
|
}
|
|
|
|
return mc;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a convex MeshCollider for props
|
|
/// </summary>
|
|
private Collider AddConvexMeshColliderInline(GameObject obj, Mesh mesh)
|
|
{
|
|
if (obj == null) return null;
|
|
if (obj.GetComponent<Collider>() != null) return null;
|
|
|
|
var mc = Undo.AddComponent<MeshCollider>(obj);
|
|
if (mesh != null) mc.sharedMesh = mesh;
|
|
mc.convex = true;
|
|
|
|
return mc;
|
|
}
|
|
|
|
private void SaveScene()
|
|
{
|
|
var scene = SceneManager.GetActiveScene();
|
|
if (scene.isDirty)
|
|
{
|
|
EditorSceneManager.SaveScene(scene);
|
|
Log($"Scene '{scene.name}' saved.");
|
|
}
|
|
}
|
|
|
|
private void UndoAllChanges()
|
|
{
|
|
if (changeRecords.Count == 0) return;
|
|
|
|
if (!EditorUtility.DisplayDialog("Undo Changes",
|
|
$"Revert {changeRecords.Count} collider changes?", "Undo", "Cancel"))
|
|
return;
|
|
|
|
foreach (var record in changeRecords)
|
|
{
|
|
if (record.target == null) continue;
|
|
|
|
if (record.addedCollider != null)
|
|
{
|
|
Undo.DestroyObjectImmediate(record.addedCollider);
|
|
}
|
|
|
|
if (record.addedChild != null)
|
|
{
|
|
Undo.DestroyObjectImmediate(record.addedChild);
|
|
}
|
|
|
|
if (record.flagsChanged)
|
|
{
|
|
GameObjectUtility.SetStaticEditorFlags(record.target, record.previousFlags);
|
|
}
|
|
}
|
|
|
|
changeRecords.Clear();
|
|
Log("All changes undone.");
|
|
Repaint();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Collider Generation - Full Implementation
|
|
|
|
private async void GenerateCollidersAsync()
|
|
{
|
|
Log("Collider generation starting...");
|
|
isProcessing = true;
|
|
statusMessage = "Generating colliders...";
|
|
|
|
try
|
|
{
|
|
var selectedResults = analysisResults.Where(r => r.selected && r.recommendedCollider != "Skip"
|
|
&& (r.objectType != "Building" || generateForBuildings)
|
|
&& (r.objectType != "Ground" || generateForGround)
|
|
&& (r.objectType != "Prop" || generateForProps)
|
|
&& (r.objectType != "Vegetation" || generateForVegetation)
|
|
).ToList();
|
|
float total = Mathf.Max(1, selectedResults.Count);
|
|
int processed = 0;
|
|
|
|
if (removeExistingFirst)
|
|
{
|
|
await RemoveExistingCollidersAsync();
|
|
}
|
|
|
|
foreach (var result in selectedResults)
|
|
{
|
|
// Guard against destroyed objects (e.g. children of removed containers)
|
|
if (result.gameObject == null) { processed++; continue; }
|
|
|
|
await GenerateColliderForObject(result);
|
|
|
|
processed++;
|
|
progress = processed / total;
|
|
|
|
// Safe name access after potential destruction
|
|
string objName = result.gameObject != null ? result.gameObject.name : "(destroyed)";
|
|
statusMessage = $"Generating: {objName}";
|
|
|
|
if (processed % 20 == 0)
|
|
{
|
|
Repaint();
|
|
await Task.Yield();
|
|
}
|
|
}
|
|
|
|
// Fill gaps if enabled
|
|
if (fillGaps && environmentParent != null)
|
|
{
|
|
await DetectAndFillGapsAsync();
|
|
}
|
|
|
|
// Bake all mesh colliders for runtime performance
|
|
if (bakeMeshColliders && environmentParent != null)
|
|
{
|
|
BakeMeshCollidersInline(environmentParent);
|
|
Log("Baked all mesh colliders for runtime performance.");
|
|
}
|
|
|
|
Log($"Generated colliders for {processed} objects.");
|
|
statusMessage = "Generation complete!";
|
|
progress = 1f;
|
|
}
|
|
catch (System.Exception e)
|
|
{
|
|
Log($"<color=red>Error: {e.Message}</color>");
|
|
Debug.LogError($"Collider generation error: {e}");
|
|
statusMessage = "Generation failed!";
|
|
}
|
|
finally
|
|
{
|
|
isProcessing = false;
|
|
Repaint();
|
|
}
|
|
}
|
|
|
|
private async Task RemoveExistingCollidersAsync()
|
|
{
|
|
Log("Removing existing colliders...");
|
|
|
|
var colliders = environmentParent.GetComponentsInChildren<Collider>(true);
|
|
int removed = 0;
|
|
foreach (var col in colliders)
|
|
{
|
|
if (col == null) continue; // Already destroyed
|
|
if (col is TerrainCollider) continue; // Keep terrain colliders
|
|
Undo.DestroyObjectImmediate(col);
|
|
removed++;
|
|
}
|
|
|
|
// Collect all "Colliders" containers FIRST, then destroy
|
|
// (destroying a parent invalidates child transforms still in the iteration array)
|
|
var containersToDestroy = new List<GameObject>();
|
|
foreach (Transform child in environmentParent.GetComponentsInChildren<Transform>(true))
|
|
{
|
|
if (child == null) continue;
|
|
if (child.name == "Colliders" && child.parent != null)
|
|
{
|
|
containersToDestroy.Add(child.gameObject);
|
|
}
|
|
}
|
|
// Also collect OBB child collider objects from previous runs
|
|
foreach (Transform child in environmentParent.GetComponentsInChildren<Transform>(true))
|
|
{
|
|
if (child == null) continue;
|
|
if (child.name.StartsWith("BoxCollider_OBB") || child.name == "CompoundColliders")
|
|
{
|
|
if (!containersToDestroy.Contains(child.gameObject))
|
|
containersToDestroy.Add(child.gameObject);
|
|
}
|
|
}
|
|
|
|
foreach (var go in containersToDestroy)
|
|
{
|
|
if (go != null)
|
|
{
|
|
Undo.DestroyObjectImmediate(go);
|
|
removed++;
|
|
}
|
|
}
|
|
|
|
await Task.Yield();
|
|
Log($"Removed {removed} existing colliders and containers.");
|
|
}
|
|
|
|
private async Task GenerateColliderForObject(AnalysisResult result)
|
|
{
|
|
GameObject obj = result.gameObject;
|
|
if (obj == null) return;
|
|
|
|
// For compound approach: check if this object OR its direct children already have colliders
|
|
bool hasExistingColliders = obj.GetComponent<Collider>() != null;
|
|
if (!hasExistingColliders)
|
|
{
|
|
foreach (Transform child in obj.transform)
|
|
{
|
|
if (child != null && child.GetComponent<Collider>() != null)
|
|
{
|
|
hasExistingColliders = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (hasExistingColliders) return;
|
|
|
|
Undo.RecordObject(obj, "Add Collider");
|
|
EditorUtility.SetDirty(obj);
|
|
|
|
var prevFlags = GameObjectUtility.GetStaticEditorFlags(obj);
|
|
|
|
Collider addedCollider = null;
|
|
List<Collider> addedColliders = null;
|
|
|
|
switch (result.recommendedCollider)
|
|
{
|
|
case "BoxCollider":
|
|
// === SINGLE BoxCollider per building (best practice for mobile) ===
|
|
// Per Unity docs: fewer colliders = better mobile performance
|
|
// ONE BoxCollider wrapping all children is simple, fast, and prevents
|
|
// the "3+ colliders per building" problem
|
|
if (createCompoundForComplex && result.objectType == "Building")
|
|
{
|
|
// Only use compound if user explicitly enabled it
|
|
addedColliders = AddCompoundChildColliders(obj);
|
|
addedCollider = addedColliders.Count > 0 ? addedColliders[0] : null;
|
|
}
|
|
else
|
|
{
|
|
// Default: ONE BoxCollider (simple + optimized)
|
|
addedCollider = AddLocalSpaceBoxCollider(obj);
|
|
}
|
|
break;
|
|
|
|
case "MeshCollider":
|
|
// Ground/terrain: use non-convex mesh for accurate surface
|
|
Mesh groundMesh = GetMeshFromObject(obj);
|
|
addedCollider = AddGroundMeshColliderInline(obj, groundMesh);
|
|
break;
|
|
|
|
case "TerrainCollider":
|
|
addedCollider = ConfigureTerrainCollider(obj);
|
|
break;
|
|
|
|
case "ConvexMesh":
|
|
Mesh convexMesh = GetMeshFromObject(obj);
|
|
addedCollider = AddConvexMeshColliderInline(obj, convexMesh);
|
|
if (addedCollider == null)
|
|
{
|
|
// Fallback to BoxCollider
|
|
addedCollider = AddLocalSpaceBoxCollider(obj);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// Default: safe BoxCollider
|
|
addedCollider = AddLocalSpaceBoxCollider(obj);
|
|
break;
|
|
}
|
|
|
|
// Mark as static if enabled (buildings and ground benefit from static batching)
|
|
if (markAsStatic && addedCollider != null)
|
|
{
|
|
TryMarkStatic(obj);
|
|
}
|
|
|
|
// Record change for undo
|
|
changeRecords.Add(new ColliderChangeRecord
|
|
{
|
|
target = obj,
|
|
addedCollider = addedCollider,
|
|
previousFlags = prevFlags,
|
|
flagsChanged = prevFlags != GameObjectUtility.GetStaticEditorFlags(obj)
|
|
});
|
|
|
|
await Task.Yield();
|
|
}
|
|
|
|
/// <summary>
|
|
/// COMPOUND COLLIDER APPROACH (Best practice per Unity docs):
|
|
/// Place BoxColliders on the DIRECT CHILDREN of a building/prop.
|
|
/// Each child's BoxCollider uses LOCAL bounds → naturally follows rotation.
|
|
///
|
|
/// This gives ~3-10 tight-fitting BoxColliders per building instead of
|
|
/// 1 loose axis-aligned box. Per Unity docs:
|
|
/// "Use compound colliders when a single primitive can't accurately represent the shape."
|
|
/// </summary>
|
|
private List<Collider> AddCompoundChildColliders(GameObject obj)
|
|
{
|
|
var addedColliders = new List<Collider>();
|
|
if (obj == null) return addedColliders;
|
|
|
|
// --- Step 1: If this object itself has a mesh, add a BoxCollider from mesh.bounds ---
|
|
Mesh selfMesh = GetMeshFromObject(obj);
|
|
if (selfMesh != null && obj.GetComponent<Collider>() == null)
|
|
{
|
|
BoxCollider selfBox = Undo.AddComponent<BoxCollider>(obj);
|
|
selfBox.center = selfMesh.bounds.center;
|
|
selfBox.size = selfMesh.bounds.size + Vector3.one * colliderOverlap;
|
|
addedColliders.Add(selfBox);
|
|
}
|
|
|
|
// --- Step 2: Collect direct children that have renderers ---
|
|
var childrenWithGeometry = new List<Transform>();
|
|
foreach (Transform child in obj.transform)
|
|
{
|
|
if (child == null || !child.gameObject.activeInHierarchy) continue;
|
|
|
|
// Check if this child or its descendants have any renderers
|
|
var renderers = child.GetComponentsInChildren<Renderer>(true);
|
|
if (renderers.Length == 0) continue;
|
|
|
|
// Skip if child already has a collider
|
|
if (child.GetComponent<Collider>() != null) continue;
|
|
|
|
// Size filter: skip tiny parts (decorations, bolts, etc.)
|
|
Bounds childBounds = renderers[0].bounds;
|
|
for (int i = 1; i < renderers.Length; i++)
|
|
{
|
|
if (renderers[i] != null)
|
|
childBounds.Encapsulate(renderers[i].bounds);
|
|
}
|
|
Vector3 size = childBounds.size;
|
|
if (size.x < minimumObjectSize && size.y < minimumObjectSize && size.z < minimumObjectSize)
|
|
continue;
|
|
|
|
childrenWithGeometry.Add(child);
|
|
}
|
|
|
|
// --- Step 3: If too many children, merge into groups ---
|
|
// Per Unity docs: "Use as few primitive colliders as possible"
|
|
if (childrenWithGeometry.Count > 15)
|
|
{
|
|
// Too many parts — fall back to a single proper local-space BoxCollider
|
|
BoxCollider singleBox = AddLocalSpaceBoxCollider(obj);
|
|
if (singleBox != null)
|
|
addedColliders.Add(singleBox);
|
|
return addedColliders;
|
|
}
|
|
|
|
// If no direct children have geometry (leaf object), add single collider
|
|
if (childrenWithGeometry.Count == 0 && selfMesh == null)
|
|
{
|
|
BoxCollider singleBox = AddLocalSpaceBoxCollider(obj);
|
|
if (singleBox != null)
|
|
addedColliders.Add(singleBox);
|
|
return addedColliders;
|
|
}
|
|
|
|
// --- Step 4: Add BoxCollider to each significant child ---
|
|
foreach (Transform child in childrenWithGeometry)
|
|
{
|
|
// Check if child has its own MeshFilter → use mesh.bounds (PERFECT local fit)
|
|
Mesh childMesh = GetMeshFromObject(child.gameObject);
|
|
|
|
if (childMesh != null && child.GetComponentsInChildren<Renderer>(true).Length <= 1)
|
|
{
|
|
// Single-mesh child: BoxCollider from mesh.bounds (ideal!)
|
|
BoxCollider box = Undo.AddComponent<BoxCollider>(child.gameObject);
|
|
box.center = childMesh.bounds.center;
|
|
box.size = childMesh.bounds.size + Vector3.one * colliderOverlap;
|
|
addedColliders.Add(box);
|
|
}
|
|
else
|
|
{
|
|
// Container child with sub-renderers: compute local-space combined bounds
|
|
BoxCollider box = AddLocalSpaceBoxCollider(child.gameObject);
|
|
if (box != null)
|
|
addedColliders.Add(box);
|
|
}
|
|
}
|
|
|
|
return addedColliders;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compute a BoxCollider using PROPER local-space bounds.
|
|
/// Unlike the old AABB approach, this converts world corners to local space,
|
|
/// so the box follows the object's rotation accurately.
|
|
/// </summary>
|
|
private BoxCollider AddLocalSpaceBoxCollider(GameObject obj)
|
|
{
|
|
if (obj == null) return null;
|
|
if (obj.GetComponent<Collider>() != null) return null;
|
|
|
|
// First: if object has its own mesh, use mesh.bounds directly (perfect!)
|
|
Mesh selfMesh = GetMeshFromObject(obj);
|
|
if (selfMesh != null)
|
|
{
|
|
BoxCollider box = Undo.AddComponent<BoxCollider>(obj);
|
|
box.center = selfMesh.bounds.center;
|
|
box.size = selfMesh.bounds.size + Vector3.one * colliderOverlap;
|
|
return box;
|
|
}
|
|
|
|
// Otherwise: combine child renderer bounds in LOCAL space
|
|
var renderers = obj.GetComponentsInChildren<Renderer>(true);
|
|
if (renderers.Length == 0) return null;
|
|
|
|
Transform t = obj.transform;
|
|
Vector3 localMin = Vector3.one * float.MaxValue;
|
|
Vector3 localMax = Vector3.one * float.MinValue;
|
|
|
|
foreach (var r in renderers)
|
|
{
|
|
if (r == null) continue;
|
|
|
|
Bounds b = r.bounds; // World-space AABB
|
|
Vector3 center = b.center;
|
|
Vector3 extents = b.extents;
|
|
|
|
// Convert all 8 corners of world AABB to local space
|
|
// This is the KEY fix: local-space AABB follows object rotation
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
Vector3 corner = center + new Vector3(
|
|
(i & 1) == 0 ? -extents.x : extents.x,
|
|
(i & 2) == 0 ? -extents.y : extents.y,
|
|
(i & 4) == 0 ? -extents.z : extents.z
|
|
);
|
|
|
|
Vector3 localCorner = t.InverseTransformPoint(corner);
|
|
localMin = Vector3.Min(localMin, localCorner);
|
|
localMax = Vector3.Max(localMax, localCorner);
|
|
}
|
|
}
|
|
|
|
Vector3 localCenter = (localMin + localMax) * 0.5f;
|
|
Vector3 localSize = localMax - localMin;
|
|
|
|
BoxCollider result = Undo.AddComponent<BoxCollider>(obj);
|
|
result.center = localCenter;
|
|
result.size = localSize + Vector3.one * colliderOverlap;
|
|
|
|
return result;
|
|
}
|
|
|
|
private async Task DetectAndFillGapsAsync()
|
|
{
|
|
Log("Detecting collision gaps...");
|
|
statusMessage = "Detecting gaps...";
|
|
|
|
// Simplified gap detection via raycasting down from a grid
|
|
if (environmentParent == null) return;
|
|
|
|
var allRenderers = environmentParent.GetComponentsInChildren<Renderer>(true);
|
|
if (allRenderers.Length == 0) return;
|
|
|
|
Bounds envBounds = allRenderers[0].bounds;
|
|
foreach (var r in allRenderers)
|
|
{
|
|
if (r != null) envBounds.Encapsulate(r.bounds);
|
|
}
|
|
|
|
int gapCount = 0;
|
|
float checkHeight = envBounds.max.y + 5f;
|
|
|
|
for (float x = envBounds.min.x; x <= envBounds.max.x; x += gapDetectionSpacing)
|
|
{
|
|
for (float z = envBounds.min.z; z <= envBounds.max.z; z += gapDetectionSpacing)
|
|
{
|
|
Ray ray = new Ray(new Vector3(x, checkHeight, z), Vector3.down);
|
|
if (!Physics.Raycast(ray, checkHeight + 10f))
|
|
{
|
|
gapCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (gapCount > 0)
|
|
{
|
|
Log($"Found {gapCount} potential gap points (no collision surface detected).");
|
|
}
|
|
else
|
|
{
|
|
Log("No gaps detected in collision coverage.");
|
|
}
|
|
|
|
await Task.Yield();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Collider Generation Methods
|
|
|
|
/// <summary>
|
|
/// Add an optimized BoxCollider with proper fitting
|
|
/// </summary>
|
|
private BoxCollider AddOptimizedBoxCollider(GameObject obj, Mesh mesh, bool useOBB)
|
|
{
|
|
if (obj.GetComponent<Collider>() != null) return null;
|
|
|
|
var box = Undo.AddComponent<BoxCollider>(obj);
|
|
|
|
if (mesh != null)
|
|
{
|
|
var bounds = mesh.bounds;
|
|
box.center = bounds.center;
|
|
box.size = bounds.size + Vector3.one * colliderOverlap;
|
|
}
|
|
else
|
|
{
|
|
// Fallback to renderer bounds
|
|
var renderer = obj.GetComponent<Renderer>();
|
|
if (renderer != null)
|
|
{
|
|
var localCenter = obj.transform.InverseTransformPoint(renderer.bounds.center);
|
|
var localSize = renderer.bounds.size;
|
|
box.center = localCenter;
|
|
box.size = localSize + Vector3.one * colliderOverlap;
|
|
}
|
|
}
|
|
|
|
return box;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create advanced compound colliders using per-child or grouped approach
|
|
/// </summary>
|
|
private GameObject CreateAdvancedCompoundColliders(GameObject obj, Mesh mesh)
|
|
{
|
|
// Use existing compound collider approach
|
|
var colliders = AddCompoundChildColliders(obj);
|
|
if (colliders.Count > 0)
|
|
{
|
|
Log($"Created {colliders.Count} compound colliders for {obj.name}");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add an optimized MeshCollider with optional simplification
|
|
/// </summary>
|
|
private MeshCollider AddOptimizedMeshCollider(GameObject obj, Mesh mesh, bool convex)
|
|
{
|
|
if (obj.GetComponent<Collider>() != null) return null;
|
|
|
|
var mc = Undo.AddComponent<MeshCollider>(obj);
|
|
|
|
if (mesh != null)
|
|
{
|
|
mc.sharedMesh = mesh;
|
|
}
|
|
|
|
mc.convex = convex;
|
|
|
|
// CRITICAL: Only set explicit cooking options when mesh is readable.
|
|
// Unity docs: non-default CookingOptions require isReadable=true.
|
|
if (mesh != null && mesh.isReadable)
|
|
{
|
|
mc.cookingOptions = MeshColliderCookingOptions.CookForFasterSimulation |
|
|
MeshColliderCookingOptions.EnableMeshCleaning |
|
|
MeshColliderCookingOptions.WeldColocatedVertices;
|
|
|
|
if (optimizeForPerformance)
|
|
{
|
|
mc.cookingOptions |= MeshColliderCookingOptions.UseFastMidphase;
|
|
}
|
|
}
|
|
|
|
return mc;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configure TerrainCollider with optimal settings
|
|
/// </summary>
|
|
private Collider ConfigureTerrainCollider(GameObject obj)
|
|
{
|
|
// Inline terrain collider configuration
|
|
var terrain = obj.GetComponent<Terrain>();
|
|
var existingTC = obj.GetComponent<TerrainCollider>();
|
|
|
|
if (terrain != null && existingTC == null)
|
|
{
|
|
var tc = Undo.AddComponent<TerrainCollider>(obj);
|
|
tc.terrainData = terrain.terrainData;
|
|
}
|
|
else if (existingTC != null && terrain != null)
|
|
{
|
|
existingTC.terrainData = terrain.terrainData;
|
|
}
|
|
|
|
// Set layer
|
|
obj.layer = groundLayer;
|
|
|
|
// Enable tree colliders
|
|
if (terrain != null)
|
|
{
|
|
terrain.treeDistance = 500f;
|
|
}
|
|
|
|
return obj.GetComponent<TerrainCollider>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mark object as static for batching and optimization
|
|
/// </summary>
|
|
private void TryMarkStatic(GameObject obj)
|
|
{
|
|
if (obj == null) return;
|
|
|
|
// Skip dynamic objects
|
|
if (obj.GetComponentInParent<Rigidbody>() != null) return;
|
|
if (obj.GetComponentInParent<CharacterController>() != null) return;
|
|
if (obj.GetComponentInParent<Animator>() != null)
|
|
{
|
|
string name = obj.name.ToLower();
|
|
if (name.Contains("player") || name.Contains("enemy") || name.Contains("npc"))
|
|
return;
|
|
}
|
|
|
|
// Check tag
|
|
string tag = obj.tag;
|
|
if (tag == "Player" || tag == "Enemy" || tag == "Dynamic")
|
|
return;
|
|
|
|
var flags = StaticEditorFlags.BatchingStatic |
|
|
StaticEditorFlags.OccluderStatic |
|
|
StaticEditorFlags.OccludeeStatic |
|
|
StaticEditorFlags.ReflectionProbeStatic |
|
|
StaticEditorFlags.ContributeGI;
|
|
|
|
var current = GameObjectUtility.GetStaticEditorFlags(obj);
|
|
if ((current & flags) != flags)
|
|
{
|
|
GameObjectUtility.SetStaticEditorFlags(obj, current | flags);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Scene View Visualization
|
|
|
|
private void OnSceneGUI(SceneView sceneView)
|
|
{
|
|
if (analysisResults.Count == 0) return;
|
|
|
|
foreach (var result in analysisResults)
|
|
{
|
|
if (!result.selected || result.gameObject == null) continue;
|
|
|
|
// Simple, clean visualization - show colliders on this object AND children (compound)
|
|
var collider = result.gameObject.GetComponent<Collider>();
|
|
|
|
// Draw collider on the object itself
|
|
DrawBoxColliderWireframe(collider as BoxCollider, new Color(0.2f, 0.9f, 0.2f, 0.8f));
|
|
|
|
if (collider is MeshCollider)
|
|
{
|
|
Handles.color = new Color(0.9f, 0.5f, 0.2f, 0.6f);
|
|
Handles.DrawWireCube(collider.bounds.center, collider.bounds.size);
|
|
}
|
|
|
|
// Draw compound child colliders (BoxColliders placed on direct children)
|
|
foreach (Transform child in result.gameObject.transform)
|
|
{
|
|
if (child == null) continue;
|
|
var childBox = child.GetComponent<BoxCollider>();
|
|
DrawBoxColliderWireframe(childBox, new Color(0.3f, 0.85f, 0.4f, 0.7f));
|
|
}
|
|
|
|
// Pre-generation preview (no colliders yet)
|
|
if (collider == null && result.recommendedCollider != "Skip")
|
|
{
|
|
// Check if any children have colliders (compound preview)
|
|
bool hasChildColliders = false;
|
|
foreach (Transform child in result.gameObject.transform)
|
|
{
|
|
if (child != null && child.GetComponent<Collider>() != null)
|
|
{
|
|
hasChildColliders = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!hasChildColliders)
|
|
{
|
|
// Show preview of where colliders would go
|
|
var renderers = result.gameObject.GetComponentsInChildren<Renderer>(true);
|
|
if (renderers.Length > 0)
|
|
{
|
|
Handles.color = new Color(0.5f, 0.5f, 1f, 0.3f);
|
|
Bounds combined = renderers[0].bounds;
|
|
for (int i = 1; i < renderers.Length; i++)
|
|
{
|
|
if (renderers[i] != null)
|
|
combined.Encapsulate(renderers[i].bounds);
|
|
}
|
|
Handles.DrawWireCube(combined.center, combined.size);
|
|
}
|
|
}
|
|
}
|
|
// Skip drawing for vegetation/skipped objects
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draw a rotation-aware wireframe for a BoxCollider
|
|
/// </summary>
|
|
private void DrawBoxColliderWireframe(BoxCollider boxCol, Color color)
|
|
{
|
|
if (boxCol == null) return;
|
|
|
|
Handles.color = color;
|
|
Transform t = boxCol.transform;
|
|
Vector3 worldCenter = t.TransformPoint(boxCol.center);
|
|
Matrix4x4 original = Handles.matrix;
|
|
Handles.matrix = Matrix4x4.TRS(worldCenter, t.rotation, Vector3.one);
|
|
Vector3 scaledSize = Vector3.Scale(boxCol.size, t.lossyScale);
|
|
Handles.DrawWireCube(Vector3.zero, scaledSize);
|
|
Handles.matrix = original;
|
|
}
|
|
|
|
private void OnFocus()
|
|
{
|
|
SceneView.duringSceneGui -= OnSceneGUI;
|
|
SceneView.duringSceneGui += OnSceneGUI;
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
SceneView.duringSceneGui -= OnSceneGUI;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|