Add extruded mesh collider tool
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user