|
/* |
|
Build colliders in DCC application like maya and blender |
|
Export and import it as fbx, |
|
Run the tool to convert |
|
************ |
|
Only supports cuboids shape, |
|
so build your colliders with cubes. |
|
*********** |
|
Full support of transform, rotation and scale in object space |
|
Does not support shear |
|
*/ |
|
|
|
|
|
using UnityEngine; |
|
using UnityEditor; |
|
using System.Collections.Generic; |
|
|
|
public class FBXToPrimitiveCubes : EditorWindow |
|
{ |
|
private GameObject sourceObject; |
|
private bool removeMeshRenderer = true; |
|
private bool removeMeshFilter = true; |
|
private bool useSelection = true; |
|
|
|
[MenuItem("Tools/Colliders/FBX To Primitive Cubes")] |
|
public static void ShowWindow() |
|
{ |
|
GetWindow<FBXToPrimitiveCubes>("FBX To Cubes"); |
|
} |
|
|
|
private void OnGUI() |
|
{ |
|
EditorGUILayout.LabelField("FBX to Primitive Cubes", EditorStyles.boldLabel); |
|
EditorGUILayout.Space(); |
|
|
|
useSelection = EditorGUILayout.Toggle("Use Selection", useSelection); |
|
if (useSelection) |
|
sourceObject = Selection.activeGameObject; |
|
else |
|
sourceObject = (GameObject)EditorGUILayout.ObjectField("Source FBX", sourceObject, typeof(GameObject), true); |
|
|
|
removeMeshRenderer = EditorGUILayout.Toggle("Remove Mesh Renderer", removeMeshRenderer); |
|
removeMeshFilter = EditorGUILayout.Toggle("Remove Mesh Filter", removeMeshFilter); |
|
|
|
EditorGUILayout.Space(); |
|
|
|
GUI.enabled = sourceObject != null; |
|
if (GUILayout.Button("Generate Primitive Cubes")) |
|
GenerateCubes(); |
|
GUI.enabled = true; |
|
|
|
EditorGUILayout.Space(); |
|
EditorGUILayout.HelpBox( |
|
"Instructions:\n" + |
|
"1. Build colliders as cubes in DCC (Maya/Blender)\n" + |
|
"2. Group them (Maya/3ds Max) or \n Put under an empty (Blender) named \n 'Collider' or 'Colliders'\n" + |
|
"3. Export as FBX and import into Unity\n" + |
|
"4. Bring the FBX into the scene, select it, and run this tool\n\n" + |
|
"Note: Only cuboid shapes supported. No shear.", |
|
MessageType.Info); |
|
} |
|
|
|
private void GenerateCubes() |
|
{ |
|
// Find child named "collider" or "colliders" (case insensitive, whole word) |
|
Transform colliderContainer = FindColliderContainer(sourceObject.transform); |
|
if (colliderContainer == null) |
|
{ |
|
Debug.LogWarning("No child named 'Collider' or 'Colliders' found."); |
|
return; |
|
} |
|
|
|
MeshFilter[] meshFilters = colliderContainer.GetComponentsInChildren<MeshFilter>(true); |
|
if (meshFilters.Length == 0) |
|
{ |
|
Debug.LogWarning("No MeshFilters found under collider container."); |
|
return; |
|
} |
|
|
|
GameObject container = new GameObject("Colliders"); |
|
container.transform.position = sourceObject.transform.position; |
|
Undo.RegisterCreatedObjectUndo(container, "Create Cube Container"); |
|
|
|
int count = 0; |
|
foreach (MeshFilter mf in meshFilters) |
|
{ |
|
Mesh mesh = mf.sharedMesh; |
|
if (mesh == null) continue; |
|
|
|
if (!ExtractCubeTransform(mesh, out Vector3 localPos, out Quaternion localRot, out Vector3 localScale)) |
|
{ |
|
Debug.LogWarning($"Failed to extract transform from {mf.name}"); |
|
continue; |
|
} |
|
|
|
Transform src = mf.transform; |
|
Vector3 worldPos = src.TransformPoint(localPos); |
|
Quaternion worldRot = src.rotation * localRot; |
|
Vector3 worldScale = Vector3.Scale(localScale, src.lossyScale); |
|
|
|
count++; |
|
GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube); |
|
cube.name = $"Box_Collider_{count}"; |
|
cube.transform.SetParent(container.transform, false); |
|
cube.transform.position = worldPos; |
|
cube.transform.rotation = worldRot; |
|
cube.transform.localScale = worldScale; |
|
|
|
if (removeMeshRenderer) |
|
DestroyImmediate(cube.GetComponent<MeshRenderer>()); |
|
|
|
if (removeMeshFilter) |
|
DestroyImmediate(cube.GetComponent<MeshFilter>()); |
|
|
|
Undo.RegisterCreatedObjectUndo(cube, "Create Primitive Cube"); |
|
} |
|
|
|
// Delete the collider container |
|
Undo.DestroyObjectImmediate(colliderContainer.gameObject); |
|
|
|
Debug.Log($"Generated {count} cubes."); |
|
} |
|
|
|
private Transform FindColliderContainer(Transform parent) |
|
{ |
|
foreach (Transform child in parent) |
|
{ |
|
string name = child.name.ToLowerInvariant(); |
|
if (name == "collider" || name == "colliders") |
|
return child; |
|
} |
|
return null; |
|
} |
|
|
|
private bool ExtractCubeTransform(Mesh mesh, out Vector3 position, out Quaternion rotation, out Vector3 scale) |
|
{ |
|
position = Vector3.zero; |
|
rotation = Quaternion.identity; |
|
scale = Vector3.one; |
|
|
|
Vector3[] verts = mesh.vertices; |
|
if (verts.Length == 0) return false; |
|
|
|
// Cube mesh has 24 verts (4 per face), reduce to 8 unique corners |
|
List<Vector3> unique = GetUniqueVertices(verts, 0.0001f); |
|
|
|
if (unique.Count != 8) |
|
{ |
|
Debug.LogWarning($"Expected 8 vertices, got {unique.Count}. Using bounds fallback."); |
|
position = mesh.bounds.center; |
|
scale = mesh.bounds.size; |
|
return true; |
|
} |
|
|
|
// Center is average of all corners |
|
foreach (var v in unique) |
|
position += v; |
|
position /= 8f; |
|
|
|
Vector3 corner = unique[0]; |
|
List<Vector3> others = new List<Vector3>(); |
|
foreach (var v in unique) |
|
{ |
|
if (v != corner) |
|
others.Add(v); |
|
} |
|
others.Sort((a, b) => Vector3.Distance(corner, a).CompareTo(Vector3.Distance(corner, b))); |
|
|
|
// Find 3 edges from corner using orthogonality (dot product ~0), not just distance |
|
// Avoids picking face diagonal when it's shorter than longest edge |
|
Vector3 e1 = others[0] - corner; |
|
|
|
Vector3 e2 = Vector3.zero; |
|
foreach (var v in others) |
|
{ |
|
Vector3 candidate = v - corner; |
|
if (candidate == e1) continue; |
|
if (Mathf.Abs(Vector3.Dot(e1.normalized, candidate.normalized)) < 0.1f) |
|
{ |
|
e2 = candidate; |
|
break; |
|
} |
|
} |
|
|
|
Vector3 e3 = Vector3.zero; |
|
Vector3 expectedDir = Vector3.Cross(e1, e2).normalized; |
|
foreach (var v in others) |
|
{ |
|
Vector3 candidate = v - corner; |
|
if (Mathf.Abs(Mathf.Abs(Vector3.Dot(expectedDir, candidate.normalized)) - 1f) < 0.1f) |
|
{ |
|
e3 = candidate; |
|
break; |
|
} |
|
} |
|
|
|
if (e2 == Vector3.zero || e3 == Vector3.zero) |
|
{ |
|
Debug.LogWarning("Could not find orthogonal edges. Using bounds fallback."); |
|
position = mesh.bounds.center; |
|
scale = mesh.bounds.size; |
|
return true; |
|
} |
|
|
|
// Build rotation matrix from edge directions |
|
float s1 = e1.magnitude; |
|
float s2 = e2.magnitude; |
|
float s3 = e3.magnitude; |
|
|
|
Vector3 ax1 = e1 / s1; |
|
Vector3 ax2 = e2 / s2; |
|
Vector3 ax3 = e3 / s3; |
|
|
|
if (Vector3.Dot(Vector3.Cross(ax1, ax2), ax3) < 0) |
|
ax3 = -ax3; |
|
|
|
Matrix4x4 rotMat = new Matrix4x4(); |
|
rotMat.SetColumn(0, new Vector4(ax1.x, ax1.y, ax1.z, 0)); |
|
rotMat.SetColumn(1, new Vector4(ax2.x, ax2.y, ax2.z, 0)); |
|
rotMat.SetColumn(2, new Vector4(ax3.x, ax3.y, ax3.z, 0)); |
|
rotMat.SetColumn(3, new Vector4(0, 0, 0, 1)); |
|
|
|
rotation = rotMat.rotation; |
|
scale = new Vector3(s1, s2, s3); |
|
|
|
return true; |
|
} |
|
|
|
private List<Vector3> GetUniqueVertices(Vector3[] verts, float tolerance) |
|
{ |
|
List<Vector3> unique = new List<Vector3>(); |
|
foreach (var v in verts) |
|
{ |
|
bool found = false; |
|
foreach (var u in unique) |
|
{ |
|
if (Vector3.Distance(v, u) < tolerance) |
|
{ |
|
found = true; |
|
break; |
|
} |
|
} |
|
if (!found) |
|
unique.Add(v); |
|
} |
|
return unique; |
|
} |
|
} |