Add extruded mesh collider tool

This commit is contained in:
HungDK
2025-11-06 16:53:36 +07:00
parent 4f49a8f342
commit f924f4f0d7
2 changed files with 303 additions and 0 deletions
@@ -0,0 +1,301 @@
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;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cc28fa1b89fc42f4a96da9be095c6012