using System.Collections.Generic; using UnityEngine; #if (UNITY_EDITOR) using UnityEditor; #endif using System.Linq; namespace ECE { /// /// Class to calculate and add colliders /// public class EasyColliderCreator { #if (UNITY_EDITOR) /// /// Used in ECEAutoSkinned when generating previews / depenetration. /// public bool UndoEnabled = true; /// /// Just an easy way to get an instance of preferences to create colliders with. /// /// private EasyColliderPreferences ECEPreferences { get { return EasyColliderPreferences.Preferences; } } #endif /// /// Data struct from calculating a best fit sphere /// private struct BestFitSphere { /// /// Center of the sphere /// public Vector3 Center; /// /// Radius of the sphere /// public float Radius; /// /// Best Fit Sphere /// /// Center of the sphere /// Radius of the sphere public BestFitSphere(Vector3 center, float radius) { this.Center = center; this.Radius = radius; } } // rotate and duplicate are editor-only #if (UNITY_EDITOR) /// /// gets a rotation as a quaternion from a transformation matrix. /// useful for matrix operations in earlier versions of unity /// /// /// private Quaternion RotationFromMatrix(Matrix4x4 matrix) { Vector3 forward = new Vector3(matrix[0, 2], matrix[1, 2], matrix[2, 2]); Vector3 up = new Vector3(matrix[0, 1], matrix[1, 1], matrix[2, 1]); return Quaternion.LookRotation(forward, up); } /// /// gets a scale from a transformation matrix. /// useful for matrix operations in earlier versions of unity /// /// /// private Vector3 ScaleFromMatrix(Matrix4x4 matrix) { Vector3 mScale = Vector3.zero; mScale.x = new Vector4(matrix[0, 0], matrix[1, 0], matrix[2, 0], matrix[3, 0]).magnitude; mScale.y = new Vector4(matrix[0, 1], matrix[1, 1], matrix[2, 1], matrix[3, 1]).magnitude; mScale.z = new Vector4(matrix[0, 2], matrix[1, 2], matrix[2, 2], matrix[3, 2]).magnitude; return mScale; } private Vector3 PositionFromMatrix(Matrix4x4 matrix) { Vector3 mScale = new Vector3(matrix[0, 3], matrix[1, 3], matrix[2, 3]); return mScale; } public List CalculateRotateAndDuplicate(EasyColliderPreferences preferences, EasyColliderEditor ece) { EasyColliderRotateDuplicate ecrd = preferences.rotatedDupeSettings; ecrd.attachTo = ece.AttachToObject; return CalculateRotateAndDuplicate(preferences.PreviewColliderType, ece.GetWorldVertices(true), ecrd); } private List CalculateRotateAndDuplicate(CREATE_COLLIDER_TYPE result, List worldVertices, EasyColliderRotateDuplicate ecrd) { Transform attachTo = ecrd.attachTo.transform; Transform pivot = ecrd.pivot.transform; List data = new List(); // we want to create the normal colliders in the attachto's local space, but rotate them around the pivot point. List localVertices = ToLocalVerts(attachTo, worldVertices); EasyColliderData baseData = null; switch (result) { case CREATE_COLLIDER_TYPE.BOX: // we want the normal colliders calculated in local space of the attach to object. // baseData = CalculateBox(worldVertices, attachTo, false); baseData = CalculateBoxLocal(localVertices); break; case CREATE_COLLIDER_TYPE.ROTATED_BOX: baseData = CalculateBox(worldVertices, pivot, true, true); break; case CREATE_COLLIDER_TYPE.SPHERE: baseData = CalculateSphereMinMaxLocal(localVertices); break; case CREATE_COLLIDER_TYPE.CAPSULE: baseData = CalculateCapsuleMinMaxLocal(localVertices, ECEPreferences.CapsuleColliderMethod); break; case CREATE_COLLIDER_TYPE.ROTATED_CAPSULE: baseData = CalculateCapsuleMinMax(worldVertices, pivot, ECEPreferences.CapsuleColliderMethod, true, true); break; case CREATE_COLLIDER_TYPE.CONVEX_MESH: baseData = CalculateMeshColliderQuickHullLocal(localVertices); break; case CREATE_COLLIDER_TYPE.CYLINDER: baseData = CalculateCylinderColliderLocal(localVertices, ECEPreferences.CylinderNumberOfSides, ECEPreferences.CylinderOrientation, ECEPreferences.CylinderRotationOffset); break; } if (result != CREATE_COLLIDER_TYPE.ROTATED_BOX && result != CREATE_COLLIDER_TYPE.ROTATED_CAPSULE) { // rotated colliders have thier own matrix. baseData.Matrix = attachTo.localToWorldMatrix; } // just calculating the angle and axis to rotate float rotationAngle = (ecrd.EndRotation - ecrd.StartRotation) / (ecrd.NumberOfDuplications); float currentAngle = ecrd.StartRotation; Vector3 axis = Vector3.zero; axis = ecrd.axis == EasyColliderRotateDuplicate.ROTATE_AXIS.X ? pivot.right : axis; axis = ecrd.axis == EasyColliderRotateDuplicate.ROTATE_AXIS.Y ? pivot.up : axis; axis = ecrd.axis == EasyColliderRotateDuplicate.ROTATE_AXIS.Z ? pivot.forward : axis; Quaternion baseRotation = RotationFromMatrix(baseData.Matrix); // will just be the attachTo's position at this point. Vector3 basePosition = PositionFromMatrix(baseData.Matrix); // lossy scale essentially. Vector3 baseScale = ScaleFromMatrix(baseData.Matrix); //Debug.Log("Base Pos:" + basePosition + " Base Rotation:" + baseRotation + " Base Scale:" + baseScale); // used in the longer version, leaving in here for future reference use. // Matrix4x4 pivotTranslation = Matrix4x4.Translate(pivot.position); for (int i = 0; i < ecrd.NumberOfDuplications; i++) { // rotation around pivot's axis. Quaternion q = Quaternion.AngleAxis(currentAngle, axis); // We're gonna leave this here for my future self, as matrix math still confuses me // create rotation matrix // Matrix4x4 rotationMatrix = Matrix4x4.TRS(Vector3.zero, q * baseRotation, mScale); // // // calculate rotated forward from the pivot to the attach to object. //Vector3 forward = q * (attachTo.position - pivot.position); // // create a matrix for the rotated pivot to attach translation // var pushTranslate = Matrix4x4.Translate(forward); // // use the rotation matrix // m = rotationMatrix; // // translated it to the pivot point. // m = pivotTranslation * m; // // push out from pivot // m = pushTranslate * m; // Above: simplified //m = Matrix4x4.TRS(basePosition + forward, q * baseRotation, baseScale); // but the preview matrix isn't correclty factoring in non-uniform scale. //} Vector3 forward = q * (attachTo.position - pivot.position); Matrix4x4 m; m = Matrix4x4.TRS(basePosition + forward, q * baseRotation, baseScale); // preview not correctly using non-uniform scale. switch (result) { case CREATE_COLLIDER_TYPE.CAPSULE: case CREATE_COLLIDER_TYPE.ROTATED_CAPSULE: { CapsuleColliderData d1 = baseData as CapsuleColliderData; CapsuleColliderData d2 = new CapsuleColliderData(); d2.Clone(d1); d2.Matrix = m; data.Add(d2); break; } case CREATE_COLLIDER_TYPE.BOX: case CREATE_COLLIDER_TYPE.ROTATED_BOX: { BoxColliderData d1 = baseData as BoxColliderData; BoxColliderData d2 = new BoxColliderData(); d2.Clone(d1); d2.Matrix = m; data.Add(d2); break; } case CREATE_COLLIDER_TYPE.SPHERE: { SphereColliderData d1 = baseData as SphereColliderData; SphereColliderData d2 = new SphereColliderData(); d2.Clone(d1); d2.Matrix = m; data.Add(d2); break; } case CREATE_COLLIDER_TYPE.CONVEX_MESH: case CREATE_COLLIDER_TYPE.CYLINDER: { MeshColliderData d1 = baseData as MeshColliderData; MeshColliderData d2 = new MeshColliderData(); d2.Clone(d1); d2.Matrix = m; data.Add(d2); break; } } currentAngle += rotationAngle; } return data; } public List CreateRotateAndDuplicateColliders(CREATE_COLLIDER_TYPE collider_type, List worldVertices, EasyColliderProperties properties) { // Debug.Log("ECC: Create Rotated and Duplicate Colliders"); List CreatedColliders = new List(); EasyColliderRotateDuplicate ecrd = ECEPreferences.rotatedDupeSettings; float rotationPerCollider = (ecrd.EndRotation - ecrd.StartRotation) / ecrd.NumberOfDuplications; float currentRotation = 0.0f; Transform parent = properties.AttachTo.transform; List data = CalculateRotateAndDuplicate(collider_type, worldVertices, ecrd); foreach (EasyColliderData d in data) { GameObject obj = new GameObject("Rotated Collider"); #if (UNITY_EDITOR) Undo.RegisterCreatedObjectUndo(obj, " Create Rotated Collider"); #endif obj.transform.rotation = RotationFromMatrix(d.Matrix); obj.transform.SetParent(ecrd.pivot.transform); obj.transform.position = PositionFromMatrix(d.Matrix); obj.transform.localScale = Vector3.one; obj.layer = properties.Layer; properties.AttachTo = obj; properties.Orientation = COLLIDER_ORIENTATION.ROTATED; Collider collider = null; switch (collider_type) { case CREATE_COLLIDER_TYPE.BOX: obj.name = "Rotated Box Collider"; collider = CreateBoxCollider(d as BoxColliderData, properties, false); break; case CREATE_COLLIDER_TYPE.ROTATED_BOX: obj.name = "Rotated Box Collider"; BoxColliderData rbd = d as BoxColliderData; collider = CreateBoxCollider(rbd, properties, false); break; case CREATE_COLLIDER_TYPE.SPHERE: obj.name = "Rotated Sphere Collider"; collider = CreateSphereCollider(d as SphereColliderData, properties, false); break; case CREATE_COLLIDER_TYPE.CAPSULE: obj.name = "Rotated Capsule Collider"; collider = CreateCapsuleCollider(d as CapsuleColliderData, properties, false); break; case CREATE_COLLIDER_TYPE.ROTATED_CAPSULE: obj.name = "Rotated Capsule Collider"; CapsuleColliderData rcd = d as CapsuleColliderData; collider = CreateCapsuleCollider(rcd, properties, false); break; case CREATE_COLLIDER_TYPE.CONVEX_MESH: case CREATE_COLLIDER_TYPE.CYLINDER: MeshColliderData mcd = d as MeshColliderData; if (ECEPreferences.SaveConvexHullAsAsset) { EasyColliderSaving.CreateAndSaveMeshAsset(mcd.ConvexMesh, parent.gameObject); } collider = CreateConvexMeshCollider(mcd.ConvexMesh, properties.AttachTo, properties); break; } CreatedColliders.Add(collider); currentRotation += rotationPerCollider; #if (UNITY_EDITOR) Undo.SetTransformParent(obj.transform, parent, "Change Parent"); #else obj.transform.SetParent(parent); #endif obj.transform.localScale = Vector3.one; PostColliderCreationProcess(collider, properties); } return CreatedColliders; } #endif // merge colliders are editor-only. #if (UNITY_EDITOR) #region MergeColliders /// /// Merges all colliders in the list to a single resultant collider and returns it. /// /// List of colliders to merge /// Type of collider we want the colliders merged into /// Properties to set on the new collider /// Single merged collider. public Collider MergeColliders(List collidersToMerge, CREATE_COLLIDER_TYPE result, EasyColliderProperties properties) { if (properties.Orientation == COLLIDER_ORIENTATION.ROTATED) { properties.AttachTo = GetFirstNonNullTransform(collidersToMerge).gameObject; } EasyColliderData data = MergeCollidersPreview(collidersToMerge, result, properties.AttachTo.transform); if (result == CREATE_COLLIDER_TYPE.BOX || result == CREATE_COLLIDER_TYPE.ROTATED_BOX) { if (result == CREATE_COLLIDER_TYPE.ROTATED_BOX) { properties.AttachTo = GetFirstNonNullTransform(collidersToMerge).gameObject; } return CreateBoxCollider(data as BoxColliderData, properties); } else if (result == CREATE_COLLIDER_TYPE.CAPSULE || result == CREATE_COLLIDER_TYPE.ROTATED_CAPSULE) { if (result == CREATE_COLLIDER_TYPE.ROTATED_CAPSULE) { properties.AttachTo = GetFirstNonNullTransform(collidersToMerge).gameObject; } return CreateCapsuleCollider(data as CapsuleColliderData, properties); } else if (result == CREATE_COLLIDER_TYPE.SPHERE) { return CreateSphereCollider(data as SphereColliderData, properties); } else if (result == CREATE_COLLIDER_TYPE.CONVEX_MESH || result == CREATE_COLLIDER_TYPE.CYLINDER) { MeshColliderData d = data as MeshColliderData; if (ECEPreferences.SaveConvexHullAsAsset) { EasyColliderSaving.CreateAndSaveMeshAsset(d.ConvexMesh, properties.AttachTo); } return CreateConvexMeshCollider(d.ConvexMesh, properties.AttachTo, properties); } return null; } /// /// Returns the first transform of a non-null collider /// /// list of colliders /// first non-null collider's transform, null if no non-null colliders private Transform GetFirstNonNullTransform(List collidersList) { foreach (Collider c in collidersList) { if (c != null) { return c.transform; } } return null; } /// /// Calculates the preview data for merged colliders. /// /// /// /// /// public EasyColliderData MergeCollidersPreview(List collidersToMerge, CREATE_COLLIDER_TYPE result, Transform attachTo) { List worldVertices = GetWorldVertsForColliders(collidersToMerge); EasyColliderData d = new EasyColliderData(); if (result == CREATE_COLLIDER_TYPE.CONVEX_MESH) { return EasyColliderQuickHull.CalculateHullData(worldVertices, attachTo); } else if (result == CREATE_COLLIDER_TYPE.BOX || result == CREATE_COLLIDER_TYPE.ROTATED_BOX) { if (result == CREATE_COLLIDER_TYPE.ROTATED_BOX) { attachTo = GetFirstNonNullTransform(collidersToMerge); } return CalculateBox(worldVertices, attachTo, false); } else if (result == CREATE_COLLIDER_TYPE.SPHERE) { // does it make sense to allow different sphere methods -> or just min-max method. if (ECEPreferences.SphereColliderMethod == SPHERE_COLLIDER_METHOD.MinMax) { return CalculateSphereMinMax(worldVertices, attachTo); } else if (ECEPreferences.SphereColliderMethod == SPHERE_COLLIDER_METHOD.Distance) { return CalculateSphereDistance(worldVertices, attachTo); } else if (ECEPreferences.SphereColliderMethod == SPHERE_COLLIDER_METHOD.BestFit) { return CalculateSphereBestFit(worldVertices, attachTo); } } else if (result == CREATE_COLLIDER_TYPE.CAPSULE || result == CREATE_COLLIDER_TYPE.ROTATED_CAPSULE) { if (result == CREATE_COLLIDER_TYPE.ROTATED_CAPSULE) { attachTo = GetFirstNonNullTransform(collidersToMerge); } // does it make sense to allow capsule methods? -> can use various min max + dia, radius etc. if (ECEPreferences.CapsuleColliderMethod == CAPSULE_COLLIDER_METHOD.BestFit) { return CalculateCapsuleBestFit(worldVertices, attachTo, false); } else { return CalculateCapsuleMinMax(worldVertices, attachTo, ECEPreferences.CapsuleColliderMethod, false); } } else if (result == CREATE_COLLIDER_TYPE.CYLINDER) { return CalculateCylinderCollider(worldVertices, attachTo); } return d; } public List GetWorldVertsForColliders(List colliders) { List worldVertices = new List(); foreach (Collider col in colliders) { // Get world vertices for mesh collider. MeshCollider mc = col as MeshCollider; if (mc != null) { AddWorldVerts(mc, worldVertices); continue; } BoxCollider box = col as BoxCollider; if (box != null) { AddWorldVerts(box, worldVertices); continue; } CapsuleCollider capsule = col as CapsuleCollider; if (capsule != null) { AddWorldVerts(capsule, worldVertices); continue; } SphereCollider sphere = col as SphereCollider; if (sphere != null) { AddWorldVerts(sphere, worldVertices); continue; } } return worldVertices; } /// /// Adds the vertices of a mesh collider to the world vertices list /// /// mesh collider /// world vertices list private void AddWorldVerts(MeshCollider meshCollider, List worldVertices) { if (meshCollider == null || meshCollider.sharedMesh == null) return; Vector3[] vertices = meshCollider.sharedMesh.vertices; Transform t = meshCollider.transform; for (int i = 0; i < vertices.Length; i++) { vertices[i] = t.TransformPoint(vertices[i]); } worldVertices.AddRange(vertices); } /// /// Adds the vertices of a box collider to the world vertices list /// /// box collider /// world vertices list private void AddWorldVerts(BoxCollider boxCollider, List worldVertices) { Vector3 halfSize = boxCollider.size / 2; Vector3 center = boxCollider.center; Vector3[] vertices = new Vector3[8]{ center + halfSize, //0 center + new Vector3(halfSize.x, halfSize.y, -halfSize.z), //1 center + new Vector3(halfSize.x, -halfSize.y, halfSize.z), //2 center + new Vector3(halfSize.x, -halfSize.y, -halfSize.z), //3 center + new Vector3(-halfSize.x, halfSize.y, halfSize.z), //4 center + new Vector3(-halfSize.x, halfSize.y, -halfSize.z), //5 center + new Vector3(-halfSize.x, -halfSize.y, halfSize.z), //6 center - halfSize, //7 }; int triangleOffset = worldVertices.Count; Transform t = boxCollider.transform; for (int i = 0; i < vertices.Length; i++) { vertices[i] = t.TransformPoint(vertices[i]); } // add triangles and verts worldVertices.AddRange(vertices); } /// /// Adds the vertices of a sphere collider to the world vertices list /// /// sphere collider /// world vertices list private void AddWorldVerts(SphereCollider sphereCollider, List worldVertices) { AddWorldVertsSphere(sphereCollider.transform, sphereCollider.center, sphereCollider.radius, worldVertices); } /// /// Adds the vertices of a capsule collider to the world vertices list /// /// capsule collider /// world vertices list private void AddWorldVerts(CapsuleCollider capsuleCollider, List worldVertices) { Vector3 top = Vector3.zero; Vector3 bottom = Vector3.zero; if (capsuleCollider.direction == 0) //x { top = capsuleCollider.center + Vector3.right * ((capsuleCollider.height - capsuleCollider.radius * 2) / 2); bottom = capsuleCollider.center - Vector3.right * ((capsuleCollider.height - capsuleCollider.radius * 2) / 2); } else if (capsuleCollider.direction == 1) //y { top = capsuleCollider.center + Vector3.up * ((capsuleCollider.height - capsuleCollider.radius * 2) / 2); bottom = capsuleCollider.center - Vector3.up * ((capsuleCollider.height - capsuleCollider.radius * 2) / 2); } else if (capsuleCollider.direction == 2) //z { top = capsuleCollider.center + Vector3.forward * ((capsuleCollider.height - capsuleCollider.radius * 2) / 2); bottom = capsuleCollider.center - Vector3.forward * ((capsuleCollider.height - capsuleCollider.radius * 2) / 2); } //manually reselect collider points in an order that maintains rotation for rotated capsule colliders // again the name method is brittle, but seems the easiest way to identify rotated colliders without cluttering tags. if (capsuleCollider.gameObject.name.Contains("Rotated")) { Transform t = capsuleCollider.transform; worldVertices.Add(t.TransformPoint(top)); worldVertices.Add(t.TransformPoint(bottom)); worldVertices.Add(t.TransformPoint(capsuleCollider.center + Vector3.Cross(top, bottom).normalized * capsuleCollider.radius)); } // Easiest to just add the full top and bottom spheres as the result is the same for any collider. // as they contain the min and max values of collider, and the middle section is all on the same plane as the halfsphere's base for convex meshes. // we could write a seperate method to only add the half-spheres, but there's no need. // top sphere AddWorldVertsSphere(capsuleCollider.transform, top, capsuleCollider.radius, worldVertices); // bottom sphere AddWorldVertsSphere(capsuleCollider.transform, bottom, capsuleCollider.radius, worldVertices); } /// /// Adds world space points around a sphere /// /// transform of the collider /// center of the sphere /// radius of the sphere /// private void AddWorldVertsSphere(Transform t, Vector3 center, float radius, List worldVertices) { int accuracy = ECEPreferences.MergeCollidersRoundnessAccuracy; // 360 degrees in radians. float sin, cos = sin = 0.0f; for (int i = 1; i < accuracy; i++) { // center shifted to the z-axis float h = (i / (float)accuracy) * radius * 2; Vector3 centerX = center - (radius - (i / (float)accuracy) * radius * 2) * Vector3.right; Vector3 centerY = center - (radius - (i / (float)accuracy) * radius * 2) * Vector3.up; Vector3 centerZ = center - (radius - (i / (float)accuracy) * radius * 2) * Vector3.forward; float newRadius = Mathf.Sqrt(radius * 2 * h - Mathf.Pow(h, 2)); for (int j = 0; j <= accuracy; j++) { float angleStep = ((j / (float)accuracy) * 360f) * Mathf.Deg2Rad; sin = Mathf.Sin(angleStep); cos = Mathf.Cos(angleStep); // constant z. float xZ = centerZ.x + newRadius * sin; float yZ = centerZ.y + newRadius * cos; // constant x. float yX = centerX.y + newRadius * sin; float zX = centerX.z + newRadius * cos; // constant y. float zY = centerY.z + (newRadius * sin); float xY = centerY.x + (newRadius * cos); // worldVertices.Add(t.TransformPoint(new Vector3(centerX.x, yX, zX))); worldVertices.Add(t.TransformPoint(new Vector3(xY, centerY.y, zY))); worldVertices.Add(t.TransformPoint(new Vector3(xZ, yZ, centerZ.z))); } } } #endregion #endif #region ColliderDataCalculation /// /// Calculates the best fit sphere for a series of points. Providing a larger list of points increases accuracy. /// /// Local space vertices /// The best fit sphere private BestFitSphere CalculateBestFitSphere(List localVertices) { // # of points. int n = localVertices.Count; // Calculate average x, y, and z value of vertices. float xAvg, yAvg, zAvg = xAvg = yAvg = 0.0f; foreach (Vector3 vertex in localVertices) { xAvg += vertex.x; yAvg += vertex.y; zAvg += vertex.z; } xAvg = xAvg * (1.0f / n); yAvg = yAvg * (1.0f / n); zAvg = zAvg * (1.0f / n); // Do some fun math with matrices // B Vector. Vector3 B = Vector3.zero; // Can use a 4x4 as a 3x3 with the 4x4 as 0,0,0,1 in the last row/column. Matrix4x4 AM = Matrix4x4.zero; AM.m33 = 1; float x2, y2, z2 = x2 = y2 = 0.0f; foreach (Vector3 vertex in localVertices) { AM[0, 0] += 2 * (vertex.x * (vertex.x - xAvg)) / n; AM[0, 1] += 2 * (vertex.x * (vertex.y - yAvg)) / n; AM[0, 2] += 2 * (vertex.x * (vertex.z - zAvg)) / n; AM[1, 0] += 2 * (vertex.y * (vertex.x - xAvg)) / n; AM[1, 1] += 2 * (vertex.y * (vertex.y - yAvg)) / n; AM[1, 2] += 2 * (vertex.y * (vertex.z - zAvg)) / n; AM[2, 0] += 2 * (vertex.z * (vertex.x - xAvg)) / n; AM[2, 1] += 2 * (vertex.z * (vertex.y - yAvg)) / n; AM[2, 2] += 2 * (vertex.z * (vertex.z - zAvg)) / n; x2 = vertex.x * vertex.x; y2 = vertex.y * vertex.y; z2 = vertex.z * vertex.z; B.x += ((x2 + y2 + z2) * (vertex.x - xAvg)) / n; B.y += ((x2 + y2 + z2) * (vertex.y - yAvg)) / n; B.z += ((x2 + y2 + z2) * (vertex.z - zAvg)) / n; } // Calculate the center of the best-fit sphere. Vector3 center = (AM.transpose * AM).inverse * AM.transpose * B; // Calculate radius. float radius = 0.0f; foreach (Vector3 vertex in localVertices) { radius += Mathf.Pow((vertex.x - center.x), 2) + Mathf.Pow(vertex.y - center.y, 2) + Mathf.Pow(vertex.z - center.z, 2); } radius = Mathf.Sqrt(radius / localVertices.Count); BestFitSphere bfs = new BestFitSphere(center, radius); return bfs; } /// /// Calculates a box's data from a list of world space vertices /// /// list of vertices in world space /// transform the box will be attached to /// are we creating a rotated box? /// true when the attach to transform is not already correctly rotated for a rotated collider /// Data appropriate variables set for a box collider public BoxColliderData CalculateBox(List worldVertices, Transform attachTo, bool isRotated = false, bool requiresMatrixCalc = false) { if (isRotated && worldVertices.Count < 3) { return new BoxColliderData(); } else if (worldVertices.Count < 2) { return new BoxColliderData(); } Quaternion q = Quaternion.identity; Matrix4x4 m; List localVertices = new List(); if (isRotated && worldVertices.Count >= 3 && requiresMatrixCalc) { Vector3 forward = worldVertices[1] - worldVertices[0]; Vector3 up = Vector3.Cross(forward, worldVertices[2] - worldVertices[1]); q = Quaternion.LookRotation(forward, up); // world space matrix, pos doesn't matter here in preview. m = Matrix4x4.TRS(attachTo.position, q, attachTo.lossyScale); Matrix4x4 mi = m.inverse; // dont actually have the object in the preview. for (int i = 0; i < worldVertices.Count; i++) { localVertices.Add(mi.MultiplyPoint3x4(worldVertices[i])); } } else { localVertices = ToLocalVerts(attachTo, worldVertices); m = attachTo.localToWorldMatrix; } BoxColliderData data = CalculateBoxLocal(localVertices); data.ColliderType = isRotated ? CREATE_COLLIDER_TYPE.ROTATED_BOX : CREATE_COLLIDER_TYPE.BOX; data.Matrix = m; return data; } /// /// Calculates box collider data for a list of local space vertices /// /// list of local space vertices /// box collider data with center and size set public BoxColliderData CalculateBoxLocal(List vertices) { float xMin, yMin, zMin = xMin = yMin = Mathf.Infinity; float xMax, yMax, zMax = xMax = yMax = -Mathf.Infinity; foreach (Vector3 vertex in vertices) { //x min & max. xMin = (vertex.x < xMin) ? vertex.x : xMin; xMax = (vertex.x > xMax) ? vertex.x : xMax; //y min & max yMin = (vertex.y < yMin) ? vertex.y : yMin; yMax = (vertex.y > yMax) ? vertex.y : yMax; //z min & max zMin = (vertex.z < zMin) ? vertex.z : zMin; zMax = (vertex.z > zMax) ? vertex.z : zMax; } Vector3 max = new Vector3(xMax, yMax, zMax); Vector3 min = new Vector3(xMin, yMin, zMin); Vector3 size = max - min; Vector3 center = (max + min) / 2; // set data from calculated values BoxColliderData data = new BoxColliderData(); data.Center = center; data.ColliderType = CREATE_COLLIDER_TYPE.BOX; data.IsValid = true; data.Size = size; return data; } /// /// Calculates a capsule's data from the given values using the best fit method and a list of world space vertices /// /// list of vertices in world space /// transform the capsule will be attached to /// are we creating a rotated capsule? /// true when the attach to transform is not already correctly rotated for a rotated collider /// Data with appropriate variables set for a capsule collider public CapsuleColliderData CalculateCapsuleBestFit(List worldVertices, Transform attachTo, bool isRotated, bool requiresMatrixCalc = false) { if (worldVertices.Count >= 3) { Quaternion q = Quaternion.identity; Matrix4x4 m; List localVertices = new List(); if (isRotated && requiresMatrixCalc) { // for rotated colliders we also re-calculate the to local even though the transform is changed. // this better handles scale-shearing. Vector3 forward = worldVertices[1] - worldVertices[0]; Vector3 up = Vector3.Cross(forward, worldVertices[2] - worldVertices[1]); q = Quaternion.LookRotation(forward, up); m = Matrix4x4.TRS(attachTo.position, q, attachTo.lossyScale); for (int i = 0; i < worldVertices.Count; i++) { localVertices.Add(m.inverse.MultiplyPoint3x4(worldVertices[i])); } } else { localVertices = ToLocalVerts(attachTo, worldVertices); m = attachTo.localToWorldMatrix; } CapsuleColliderData data = CalculateCapsuleBestFitLocal(localVertices); data.ColliderType = isRotated ? CREATE_COLLIDER_TYPE.ROTATED_CAPSULE : CREATE_COLLIDER_TYPE.CAPSULE; data.Matrix = m; return data; } return new CapsuleColliderData(); } /// /// Calculates a best-fit capsule collider from a list of local space vertices /// /// local space vertices /// Capsule collider data with center, direction and height public CapsuleColliderData CalculateCapsuleBestFitLocal(List localVertices) { if (localVertices.Count < 3) { Debug.LogWarning("EasyColliderCreator: Too few vertices passed to calculate a best fit capsule collider."); return new CapsuleColliderData(); } // height from first 2 verts selected. Vector3 v0 = localVertices[0]; Vector3 v1 = localVertices[1]; float height = Vector3.Distance(v0, v1); float dX = Mathf.Abs(v1.x - v0.x); float dY = Mathf.Abs(v1.y - v0.y); float dZ = Mathf.Abs(v1.z - v0.z); localVertices.RemoveAt(1); localVertices.RemoveAt(0); BestFitSphere bfs = CalculateBestFitSphere(localVertices); Vector3 center = bfs.Center; int direction = 0; if (dX > dY && dX > dZ) { direction = 0; center.x = (v1.x + v0.x) / 2; } else if (dY > dX && dY > dZ) { direction = 1; center.y = (v1.y + v0.y) / 2; } else { direction = 2; center.z = (v1.z + v0.z) / 2; } CapsuleColliderData data = new CapsuleColliderData(); data.Center = center; data.ColliderType = CREATE_COLLIDER_TYPE.CAPSULE; data.Direction = direction; data.Height = height; data.IsValid = true; data.Radius = bfs.Radius; return data; } /// /// Calculates a capsule's data from the given values using the min max method and a list of world space vertices /// /// list of vertices in world space /// transform the capsule will be attached to /// method we are using to create the capsule (ie MinMaxPlusRadius) /// are we creating a rotated capsule? /// true when the attach to transform is not already correctly rotated for a rotated collider /// Data with appropriate variables set for a capsule collider public CapsuleColliderData CalculateCapsuleMinMax(List worldVertices, Transform attachTo, CAPSULE_COLLIDER_METHOD method, bool isRotated, bool requiresMatrixCalc = false) { if (isRotated && worldVertices.Count < 3) { return new CapsuleColliderData(); } else if (worldVertices.Count < 2) { return new CapsuleColliderData(); } List localVertices = new List(); Matrix4x4 m; Quaternion q; if (isRotated && worldVertices.Count >= 3 && requiresMatrixCalc) { Vector3 forward = worldVertices[1] - worldVertices[0]; Vector3 up = Vector3.Cross(forward, worldVertices[2] - worldVertices[1]); q = Quaternion.LookRotation(forward, up); m = Matrix4x4.TRS(attachTo.position, q, attachTo.lossyScale); for (int i = 0; i < worldVertices.Count; i++) { localVertices.Add(m.inverse.MultiplyPoint3x4(worldVertices[i])); } } else { localVertices = ToLocalVerts(attachTo.transform, worldVertices); m = attachTo.localToWorldMatrix; } CapsuleColliderData data = CalculateCapsuleMinMaxLocal(localVertices, method); data.ColliderType = isRotated ? CREATE_COLLIDER_TYPE.ROTATED_CAPSULE : CREATE_COLLIDER_TYPE.CAPSULE; data.Matrix = m; return data; } /// /// Calculates a capsule collider from a list of local space vertices /// /// List of local space vertices /// method to use when calculating (used to add radius or diameter to height of capsule) /// Capsule collider data with center, direction, and height public CapsuleColliderData CalculateCapsuleMinMaxLocal(List localVertices, CAPSULE_COLLIDER_METHOD method) { // calculate min and max points from vertices. Vector3 min = new Vector3(Mathf.Infinity, Mathf.Infinity, Mathf.Infinity); Vector3 max = new Vector3(-Mathf.Infinity, -Mathf.Infinity, -Mathf.Infinity); foreach (Vector3 vertex in localVertices) { // Calc minimums min.x = vertex.x < min.x ? vertex.x : min.x; min.y = vertex.y < min.y ? vertex.y : min.y; min.z = vertex.z < min.z ? vertex.z : min.z; // Calc maximums max.x = vertex.x > max.x ? vertex.x : max.x; max.y = vertex.y > max.y ? vertex.y : max.y; max.z = vertex.z > max.z ? vertex.z : max.z; } // Deltas for max-min float dX = max.x - min.x; float dY = max.y - min.y; float dZ = max.z - min.z; // center is between min and max values. Vector3 center = (max + min) / 2; int direction = 0; float height = 0; // set direction and height. if (dX > dY && dX > dZ) // direction is x { direction = 0; // height is the max difference in x. height = dX; } else if (dY > dX && dY > dZ) // direction is y { direction = 1; height = dY; } else // direction is z. { direction = 2; height = dZ; } // Calculate radius, makes sure that all vertices are within the radius. // Esentially to points on plane defined by direction axis, and find the furthest distance. float maxRadius = -Mathf.Infinity; Vector3 current = Vector3.zero; foreach (Vector3 vertex in localVertices) { current = vertex; if (direction == 0) { current.x = center.x; } else if (direction == 1) { current.y = center.y; } else if (direction == 2) { current.z = center.z; } float d = Vector3.Distance(current, center); if (d > maxRadius) { maxRadius = d; } } // method add radius / diameter if (method == CAPSULE_COLLIDER_METHOD.MinMaxPlusRadius) { height += maxRadius; } else if (method == CAPSULE_COLLIDER_METHOD.MinMaxPlusDiameter) { height += maxRadius * 2; } CapsuleColliderData data = new CapsuleColliderData(); data.Center = center; data.ColliderType = CREATE_COLLIDER_TYPE.CAPSULE; data.Direction = direction; data.Height = height; data.IsValid = true; data.Radius = maxRadius; return data; } //TODO: Do a local-only method for cylinders. /// /// Calculates the data needed to create a cylinder shaped convex mesh collider using a list of world space vertices /// /// list of selected world space vertices /// transform the collider will be attached to /// number of sides on the cylinder /// Data to create a a cylinder collider with type, convex mesh, validity, and matrix set public MeshColliderData CalculateCylinderCollider(List worldVertices, Transform attachTo, int numberOfSides = 12, CYLINDER_ORIENTATION orientation = CYLINDER_ORIENTATION.Automatic, float cylinderOffset = 0.0f) { MeshColliderData data = new MeshColliderData(); // convert to local List localVerts = ToLocalVerts(attachTo, worldVertices); #if (UNITY_EDITOR) List cylinderLocalPoints = CalculateCylinderPointsLocal(localVerts, attachTo, ECEPreferences.CylinderNumberOfSides, ECEPreferences.CylinderOrientation, ECEPreferences.CylinderRotationOffset); #else List cylinderLocalPoints = CalculateCylinderPointsLocal(localVerts, attachTo, numberOfSides, orientation, cylinderOffset); #endif // build the mesh using quickhull and the cylinder points. EasyColliderQuickHull qh = EasyColliderQuickHull.CalculateHull(cylinderLocalPoints); data.ColliderType = CREATE_COLLIDER_TYPE.CONVEX_MESH; data.ConvexMesh = qh.Result; if (qh.Result != null) { data.IsValid = true; } data.Matrix = attachTo.transform.localToWorldMatrix; return data; } /// /// Calculates the mesh collider data for a cylinder shaped convex mesh collider using a list of local space vertices /// /// list of local space vertices /// number of sides on the cylinder /// Automatic: Height is along largest axis. X,Y,Z orient height along X, Y, or Z respectively. /// angle to offset cylinder rotation in degrees. /// Mesh collider data with convex mesh set> public MeshColliderData CalculateCylinderColliderLocal(List vertices, int numberOfSides = 12, CYLINDER_ORIENTATION orientation = CYLINDER_ORIENTATION.Automatic, float cylinderOffset = 0.0f) { MeshColliderData data = new MeshColliderData(); #if (UNITY_EDITOR) List cylinderLocalPoints = CalculateCylinderPointsLocal(vertices, null, ECEPreferences.CylinderNumberOfSides, ECEPreferences.CylinderOrientation, ECEPreferences.CylinderRotationOffset); #else List cylinderLocalPoints = CalculateCylinderPointsLocal(vertices, null, numberOfSides, orientation, cylinderOffset); #endif EasyColliderQuickHull qh = EasyColliderQuickHull.CalculateHull(cylinderLocalPoints); data.ColliderType = CREATE_COLLIDER_TYPE.CONVEX_MESH; data.ConvexMesh = qh.Result; if (qh.Result != null) { data.IsValid = true; } data.Matrix = new Matrix4x4(); return data; } /// /// Calculates mesh collider data for a list of local space vertices /// /// list of local space vertices /// Mesh collider data with convex mesh set public MeshColliderData CalculateMeshColliderQuickHullLocal(List localVertices) { MeshColliderData data = new MeshColliderData(); EasyColliderQuickHull qh = EasyColliderQuickHull.CalculateHull(localVertices); data.ConvexMesh = qh.Result; if (qh.Result != null) { data.ColliderType = CREATE_COLLIDER_TYPE.CONVEX_MESH; data.IsValid = true; } return data; } /// /// Calculates a sphere using the best fit method and a list of world space vertices /// /// list of vertex positions in world space /// transform sphere would be attached to /// Data with appropriate variables set for a sphere collider public SphereColliderData CalculateSphereBestFit(List worldVertices, Transform attachTo) { if (worldVertices.Count < 2) { return new SphereColliderData(); } List localVertices = ToLocalVerts(attachTo, worldVertices); // set data from values SphereColliderData data = CalculateSphereBestFitLocal(localVertices); data.Matrix = attachTo.localToWorldMatrix; return data; } /// /// Calculates a best fit sphere collider using a list of local space vertices /// /// list of local space vertices /// Sphere collider data with center and radius set public SphereColliderData CalculateSphereBestFitLocal(List localVertices) { BestFitSphere bfs = CalculateBestFitSphere(localVertices); // set data from values SphereColliderData data = new SphereColliderData(); data.Center = bfs.Center; data.ColliderType = CREATE_COLLIDER_TYPE.SPHERE; data.IsValid = true; data.Radius = bfs.Radius; return data; } // distance sphere is editor-only for now /// /// Calculates a sphere using the distance method and a list of world space vertices /// /// list of vertex positions in world space /// transform sphere would be attached to /// Data with appropriate variables set for a sphere collider public SphereColliderData CalculateSphereDistance(List worldVertices, Transform attachTo) { if (worldVertices.Count < 2) { return new SphereColliderData(); } List localVertices = ToLocalVerts(attachTo, worldVertices); // set data from values SphereColliderData data = CalculateSphereDistanceLocal(localVertices); data.Matrix = attachTo.localToWorldMatrix; return data; } /// /// Calculates a sphere collider using a list of local space vertices /// /// list of local space vertices /// Sphere collider data with center and radius public SphereColliderData CalculateSphereDistanceLocal(List localVertices) { // if calculations take to long, it switches to a faster less accurate algorithm using the mean. bool switchToFasterAlgorithm = false; #if (UNITY_EDITOR) double startTime = EditorApplication.timeSinceStartup; #else double startTime = Time.realtimeSinceStartup; #endif double maxTime = 0.1f; Vector3 distanceVert1 = Vector3.zero; Vector3 distanceVert2 = Vector3.zero; float maxDistance = -Mathf.Infinity; float distance = 0; for (int i = 0; i < localVertices.Count; i++) { for (int j = i + 1; j < localVertices.Count; j++) { distance = Vector3.Distance(localVertices[i], localVertices[j]); if (distance > maxDistance) { maxDistance = distance; distanceVert1 = localVertices[i]; distanceVert2 = localVertices[j]; } } #if (UNITY_EDITOR) if (EditorApplication.timeSinceStartup - startTime > maxTime) { switchToFasterAlgorithm = true; break; } #else if (Time.realtimeSinceStartup - startTime > maxTime) { switchToFasterAlgorithm = true; break; } #endif } if (switchToFasterAlgorithm) { // use a significantly faster algorithm that is less accurate for a large # of points. Vector3 mean = Vector3.zero; foreach (Vector3 vertex in localVertices) { mean += vertex; } mean = mean / localVertices.Count; foreach (Vector3 vertex in localVertices) { distance = Vector3.Distance(vertex, mean); if (distance > maxDistance) { distanceVert1 = vertex; maxDistance = distance; } } maxDistance = -Mathf.Infinity; foreach (Vector3 vertex in localVertices) { distance = Vector3.Distance(vertex, distanceVert1); if (distance > maxDistance) { maxDistance = distance; distanceVert2 = vertex; } } } // set data from values SphereColliderData data = new SphereColliderData(); data.Center = (distanceVert1 + distanceVert2) / 2; data.ColliderType = CREATE_COLLIDER_TYPE.SPHERE; data.IsValid = true; data.Radius = maxDistance / 2; return data; } /// /// Calculates a sphere using the min max method and a list of world space vertices /// /// list of vertex positions in world space /// transform sphere would be attached to /// Data with appropriate variables set for a sphere collider public SphereColliderData CalculateSphereMinMax(List worldVertices, Transform attachTo) { if (worldVertices.Count < 2) { return new SphereColliderData(); } // use local space verts. List localVertices = ToLocalVerts(attachTo, worldVertices); SphereColliderData data = CalculateSphereMinMaxLocal(localVertices); data.Matrix = attachTo.localToWorldMatrix; return data; } /// /// Calculates a sphere collider using a list of local space vertices /// /// local space vertices /// Sphere collider data with center and radius set public SphereColliderData CalculateSphereMinMaxLocal(List localVertices) { float xMin, yMin, zMin = xMin = yMin = Mathf.Infinity; float xMax, yMax, zMax = xMax = yMax = -Mathf.Infinity; for (int i = 0; i < localVertices.Count; i++) { //x min & max. xMin = (localVertices[i].x < xMin) ? localVertices[i].x : xMin; xMax = (localVertices[i].x > xMax) ? localVertices[i].x : xMax; //y min & max yMin = (localVertices[i].y < yMin) ? localVertices[i].y : yMin; yMax = (localVertices[i].y > yMax) ? localVertices[i].y : yMax; //z min & max zMin = (localVertices[i].z < zMin) ? localVertices[i].z : zMin; zMax = (localVertices[i].z > zMax) ? localVertices[i].z : zMax; } // calculate center Vector3 center = (new Vector3(xMin, yMin, zMin) + new Vector3(xMax, yMax, zMax)) / 2; // calculate radius to contain all points float maxDistance = 0.0f; float distance = 0.0f; // this is what makes it slightly differnt than just converting a box into a sphere. foreach (Vector3 vertex in localVertices) { distance = Vector3.Distance(vertex, center); if (distance > maxDistance) { maxDistance = distance; } } SphereColliderData data = new SphereColliderData(); data.Center = center; data.ColliderType = CREATE_COLLIDER_TYPE.SPHERE; data.IsValid = true; data.Radius = maxDistance; return data; } #endregion // editor only #region CreateMeshColliders #if (UNITY_EDITOR) /// /// Create and saves a mesh with a minimum number of triangles that includes all selected vertices. Editor only. /// /// Full path to save location including a base object name ie: "C:/UnityProjects/ProjectName/Assets/ConvexHulls/SaveNameBase" /// Vertices to create the mesh with in world space /// Gameobject the mesh will be attached to /// The created mesh public Mesh CreateMesh_Messy(List worldSpaceVertices, GameObject attachTo, GameObject selected) { // use vertices to make a useable mesh that contains all the selected points. // The mesh is only used to generate the convex hull Mesh mesh = new Mesh(); // get all vertices in world space and convert to local space. List localVertices = worldSpaceVertices.Select(vertex => attachTo.transform.InverseTransformPoint(vertex)).ToList(); while (localVertices.Count % 3 != 0) { localVertices.Add(localVertices[localVertices.Count % 3]); } // attempt to deal with degenerate triangles (so if user changes the mesh collider flags manually, no crashes will occur) Vector3 p0, p1, p2 = p1 = p0 = Vector3.zero; Vector3 s1, s2 = s1 = Vector3.zero; List verts = new List(); int index = localVertices.Count - 1; while (index >= 0) // need to make sure we include the last vertex. { p0 = localVertices[index]; p1 = localVertices[(index - 1 >= 0) ? index - 1 : localVertices.Count - 1]; p2 = localVertices[(index - 2 >= 0) ? index - 2 : localVertices.Count - 2]; s1 = (p0 - p1).normalized; s2 = (p0 - p2).normalized; int degenIndex = localVertices.Count; // so we can automatically re-use the last vertices if needed. bool degenFixed = false; while (s1 == s2 || -s1 == s2 || (s2 == Vector3.zero && s1 != Vector3.zero)) { degenFixed = true; degenIndex--; if (degenIndex < 0) { Debug.LogError("Easy Collider Editor: Unable to generate a valid mesh collider from the selected points. This happens when all points are in a straight line."); return null; } p2 = localVertices[degenIndex]; s2 = (p0 - p2).normalized; } // if we fixed a degenerate we still need to do the last vertex, so only move back 2 indexs' in that case. index -= degenFixed ? 2 : 3; verts.Add(p0); verts.Add(p1); verts.Add(p2); } int[] triangles = new int[verts.Count]; for (int i = 0; i < verts.Count; i++) { triangles[i] = i; } // mesh.vertices = vertices; mesh.vertices = verts.ToArray(); mesh.triangles = triangles; // the mesh has to be saved somewhere so it can actually be used (although this is still just optional) try { if (selected == null) { EasyColliderSaving.CreateAndSaveMeshAsset(mesh, attachTo); } else { EasyColliderSaving.CreateAndSaveMeshAsset(mesh, selected); } return mesh; } catch { Debug.LogError("EasyColliderEditor: Error saving mesh at path:" + EasyColliderSaving.GetValidConvexHullPath(attachTo)); return null; } } /// /// Creates and saves (if set in preferences) a convex mesh collider using QuickHull. Editor only. /// /// Local or world space vertices /// Gameobject the collider will be attached to /// are the vertices already in local space? /// public Mesh CreateMesh_QuickHull(List vertices, GameObject attachTo, bool isLocal = false, GameObject selected = null) { List localVerts = isLocal ? vertices : ToLocalVerts(attachTo.transform, vertices); EasyColliderQuickHull qh = EasyColliderQuickHull.CalculateHull(localVerts); if (ECEPreferences.SaveConvexHullAsAsset) { if (selected == null) { EasyColliderSaving.CreateAndSaveMeshAsset(qh.Result, attachTo); } else { EasyColliderSaving.CreateAndSaveMeshAsset(qh.Result, selected); } } return qh.Result; } #endif #endregion // creating colliders uses undos, the data itself can be used during runtime. #region CreatePrimitiveColliders /// /// Creates a Box collider /// /// data to create box from /// properties to set on collider /// Created collider private BoxCollider CreateBoxCollider(BoxColliderData data, EasyColliderProperties properties, bool postProcess = true) { #if (UNITY_EDITOR) BoxCollider boxCollider; if (UndoEnabled) { boxCollider = Undo.AddComponent(properties.AttachTo); } else { boxCollider = properties.AttachTo.AddComponent(); } #else BoxCollider boxCollider = properties.AttachTo.AddComponent(); #endif boxCollider.size = data.Size; boxCollider.center = data.Center; PostColliderCreation(boxCollider, properties, postProcess); return boxCollider; } /// /// Creates a box collider using a list of world space vertices /// /// List of world space vertices /// Properties of collider /// The created box collider public BoxCollider CreateBoxCollider(List vertices, EasyColliderProperties properties, bool isLocal = false) { if (vertices.Count >= 2) { BoxColliderData data; if (properties.Orientation == COLLIDER_ORIENTATION.ROTATED) { if (vertices.Count >= 3) { GameObject obj = CreateGameObjectOrientation(vertices, properties.AttachTo, "Rotated Box Collider"); // still want to recalculate using the transform matrix, as it better handles uneven scale / shearing across multiple children if (obj != null) { obj.layer = properties.Layer; properties.AttachTo = obj; } data = CalculateBox(vertices, properties.AttachTo.transform, true); } else { Debug.LogWarning("Easy Collider Editor: Creating a Rotated Box Collider requires at least 3 points to be selected."); return null; } } else { if (!isLocal) { data = CalculateBox(vertices, properties.AttachTo.transform); } else { data = CalculateBoxLocal(vertices); } } return CreateBoxCollider(data, properties); } return null; } /// /// Creates a capsule collider (editor undoable) /// /// data to create capsule from /// properties to set on collider /// created capsule collider private CapsuleCollider CreateCapsuleCollider(CapsuleColliderData data, EasyColliderProperties properties, bool postProcess = true) { #if (UNITY_EDITOR) CapsuleCollider capsuleCollider; if (UndoEnabled) { capsuleCollider = Undo.AddComponent(properties.AttachTo); } else { capsuleCollider = properties.AttachTo.AddComponent(); } #else CapsuleCollider capsuleCollider = properties.AttachTo.AddComponent(); #endif capsuleCollider.direction = data.Direction; capsuleCollider.height = data.Height; capsuleCollider.center = data.Center; capsuleCollider.radius = data.Radius; // set properties PostColliderCreation(capsuleCollider, properties); return capsuleCollider; } /// /// Creates a capsule collider usintg the best fit algorithm and a list of world space vertices /// /// List of world vertices /// Properties of collider /// The created capsule collider public CapsuleCollider CreateCapsuleCollider_BestFit(List worldVertices, EasyColliderProperties properties) { if (worldVertices.Count >= 3) { CapsuleColliderData data = new CapsuleColliderData(); if (properties.Orientation == COLLIDER_ORIENTATION.ROTATED) { GameObject obj = CreateGameObjectOrientation(worldVertices, properties.AttachTo, "Rotated Capsule Collider"); if (obj != null) { properties.AttachTo = obj; obj.layer = properties.Layer; } data = CalculateCapsuleBestFit(worldVertices, properties.AttachTo.transform, true); } else { data = CalculateCapsuleBestFit(worldVertices, properties.AttachTo.transform, false); } return CreateCapsuleCollider(data, properties); } return null; } /// /// Creates a capsule collider using the Min-Max method and a list of world space vertices /// /// List of world space vertices /// Properties to set on collider /// Min-Max method to use to add radius' to height. /// The created capsule collider public CapsuleCollider CreateCapsuleCollider_MinMax(List worldVertices, EasyColliderProperties properties, CAPSULE_COLLIDER_METHOD method, bool isLocal = false) { CapsuleColliderData data; if (properties.Orientation == COLLIDER_ORIENTATION.ROTATED && worldVertices.Count >= 3) { GameObject obj = CreateGameObjectOrientation(worldVertices, properties.AttachTo, "Rotated Capsule Collider"); if (obj != null) { properties.AttachTo = obj; obj.layer = properties.AttachTo.layer; } data = CalculateCapsuleMinMax(worldVertices, properties.AttachTo.transform, method, true); } else { if (!isLocal) { data = CalculateCapsuleMinMax(worldVertices, properties.AttachTo.transform, method, false); } else { data = CalculateCapsuleMinMaxLocal(worldVertices, method); } } return CreateCapsuleCollider(data, properties); } /// /// Creates a convex mesh collider component from the mesh using all cooking options, so mesh does not have to be "valid" /// /// Mesh to make a convex hull from /// Gameobject the convex hull will be attached to /// Parameters to set on created collider public MeshCollider CreateConvexMeshCollider(Mesh mesh, GameObject attachToObject, EasyColliderProperties properties, bool postProcess = true) { // Create a mesh collider #if (UNITY_EDITOR) MeshCollider createdCollider = Undo.AddComponent(attachToObject); #else MeshCollider createdCollider = attachToObject.AddComponent(); #endif createdCollider.sharedMesh = mesh; //enable all cooking options. #if UNITY_2018_3_OR_NEWER createdCollider.cookingOptions = ~MeshColliderCookingOptions.None; #elif UNITY_2017_3_OR_NEWER createdCollider.cookingOptions = ~MeshColliderCookingOptions.None; // Auto inflate mesh to the minimum amount createdCollider.skinWidth = 0.000001f; #else // for very old unity versions that didn't have cooking options. createdCollider.inflateMesh = true; createdCollider.skinWidth = 0.000001f; #endif // Would be nice if we could do a try/catch on the baking to only inflate if we have to, but that doesn't work. createdCollider.convex = true; PostColliderCreation(createdCollider, properties, postProcess); //disable read/write to save memory based on user preferences. //for limitations see bottom of: https://docs.unity3d.com/Manual/class-MeshCollider.html #if (UNITY_EDITOR) if (!ECEPreferences.ConvexMeshReadWriteEnabled) { mesh.UploadMeshData(true); // fixes issue where mesh read/write was not correctly saving. EditorUtility.SetDirty(mesh); } #endif return createdCollider; } /// /// Creates a sphere collider, editor undo-able /// /// data to create the sphere collider from /// properties to set on the collider /// the created sphere collider private SphereCollider CreateSphereCollider(SphereColliderData data, EasyColliderProperties properties, bool postProcess = true) { #if (UNITY_EDITOR) SphereCollider sphereCollider; if (UndoEnabled) { sphereCollider = Undo.AddComponent(properties.AttachTo); } else { sphereCollider = properties.AttachTo.AddComponent(); } #else SphereCollider sphereCollider = properties.AttachTo.AddComponent(); #endif sphereCollider.radius = data.Radius; sphereCollider.center = data.Center; PostColliderCreation(sphereCollider, properties, postProcess); return sphereCollider; } /// /// Creates a sphere collider using the best fit sphere algorithm and a list of world space vertices /// /// List of world space vertices /// Properties of collider /// public SphereCollider CreateSphereCollider_BestFit(List worldVertices, EasyColliderProperties properties) { if (worldVertices.Count >= 2) { // Convert to local space. SphereColliderData data = CalculateSphereBestFit(worldVertices, properties.AttachTo.transform); return CreateSphereCollider(data, properties); } return null; } /// /// Creates a Sphere Collider using the distance method and a list of world space vertices /// /// List of world space vertices /// Properties of collider /// public SphereCollider CreateSphereCollider_Distance(List worldVertices, EasyColliderProperties properties) { if (worldVertices.Count >= 2) { SphereColliderData data = CalculateSphereDistance(worldVertices, properties.AttachTo.transform); return CreateSphereCollider(data, properties); } return null; } /// /// Creates a sphere collider using the min max method and a list of world space vertices /// /// List of world space vertices /// Properties of collider /// public SphereCollider CreateSphereCollider_MinMax(List worldVertices, EasyColliderProperties properties, bool isLocal = false) { if (worldVertices.Count >= 2) { if (!isLocal) { SphereColliderData data = CalculateSphereMinMax(worldVertices, properties.AttachTo.transform); return CreateSphereCollider(data, properties); } else { SphereColliderData data = CalculateSphereMinMaxLocal(worldVertices); return CreateSphereCollider(data, properties); } } return null; } #endregion #region PostCreationProcessing /// /// can be used at runtime to set a specific custom post processor if desired, otherwise defaults to EasyColliderPostProccessor /// public static IEasyColliderPostProcessor EasyColliderPostProcessor; /// /// Can add any custom processing to a manually created collider here. /// Current this handles re-centering the pivot of colliders if specified in preferences. (IE: always make collider holders & pivot at center) /// These should only be things that have show no visual effect on the collider preview. /// IE: things like rotating and re-aligning a collider along the new axis'. Or recentering the position of a collider center to 0, and moving it's collider holder to compensate. /// /// /// public void PostColliderCreationProcess(Collider createdCollider, EasyColliderProperties properties) { // set to default if null if (EasyColliderPostProcessor == null) { EasyColliderPostProcessor = new EasyColliderPostProccessor(); } if (createdCollider is BoxCollider) { // adjust pivot to the center of the collider if specified. BoxCollider bc = (BoxCollider)createdCollider; #if(UNITY_EDITOR) if (ECEPreferences.RotatedColliderPivotAtCenter && properties.Orientation == COLLIDER_ORIENTATION.ROTATED) { bc.transform.position = bc.transform.TransformPoint(bc.center); bc.center = Vector3.zero; // bc.transform.localPosition = bc.center; // bc.center = Vector3.zero; } #endif // handle other box related things in future here. if (EasyColliderPostProcessor != null) { EasyColliderPostProcessor.PostProcessCollider(bc, properties); } } else if (createdCollider is SphereCollider) { SphereCollider sc = (SphereCollider)createdCollider; #if(UNITY_EDITOR) if (ECEPreferences.RotatedColliderPivotAtCenter && properties.Orientation == COLLIDER_ORIENTATION.ROTATED) { sc.transform.position = sc.transform.TransformPoint(sc.center); sc.center = Vector3.zero; } #endif //handle other sphere collider things here. if (EasyColliderPostProcessor != null) { EasyColliderPostProcessor.PostProcessCollider(sc, properties); } } else if (createdCollider is CapsuleCollider) { CapsuleCollider cc = (CapsuleCollider)createdCollider; #if (UNITY_EDITOR) Transform t = cc.transform; if (ECEPreferences.RotatedColliderPivotAtCenter && properties.Orientation == COLLIDER_ORIENTATION.ROTATED) { cc.transform.position = cc.transform.TransformPoint(cc.center); cc.center = Vector3.zero; } if (ECEPreferences.CylinderAsCapsuleOrientation) { // aligning to cylinder axis as well. int dir = cc.direction; int prefDir = (int)ECEPreferences.CylinderOrientation - 1; // not set to automatic alignment, so user wants a specific alignment (above would make it -1) if (prefDir >= 0 && prefDir != dir) { Vector3 forward = Vector3.zero; Vector3 up = Vector3.zero; if (dir == 0) { // currently aligned with x-axis (right) if (prefDir == 1) // up { forward = t.transform.up; up = t.transform.right; } else if (prefDir == 2) // forward { forward = t.transform.right; up = t.transform.forward; } } else if (dir == 1) { // current aligned with y-axis (up) if (prefDir == 0) // right { forward = t.transform.right; up = t.transform.forward; } else if (prefDir == 2) // forward { forward = t.transform.up; up = t.transform.right; } } else if (dir == 2) { if (prefDir == 0) // right { forward = t.transform.right; up = -t.transform.up; } else if (prefDir == 1) // up { forward = t.transform.up; up = t.transform.forward; } } if (!cc.transform.name.Contains("Rotated Capsule Collider")) { // need to create a collider holder to align if it's not a rotated collider. GameObject o = new GameObject("EasyColliderHolder"); Undo.RegisterCreatedObjectUndo(o, "Create Collider Holder"); o.transform.SetParent(cc.transform); // move the capsule to the new object. CapsuleCollider replacedCapsule = Undo.AddComponent(o); replacedCapsule.center = cc.center; replacedCapsule.radius = cc.radius; replacedCapsule.direction = cc.direction; replacedCapsule.height = cc.height; GameObject.DestroyImmediate(cc); // correct it's position. cc = replacedCapsule; cc.transform.localPosition = cc.center; cc.center = Vector3.zero; // correct it's rotation and direction to align. cc.transform.rotation = Quaternion.LookRotation(forward, up); cc.direction = prefDir; } else { if (!ECEPreferences.RotatedColliderPivotAtCenter) { // the pivot isn't at center, so we need to transform the point to world space, then back to local after rotating it's transform. Vector3 wsCenter = cc.transform.TransformPoint(cc.center); cc.transform.rotation = Quaternion.LookRotation(forward, up); cc.center = cc.transform.InverseTransformPoint(wsCenter); cc.direction = prefDir; } else { // the pivot is already at the center, so we're good, just align it. cc.transform.rotation = Quaternion.LookRotation(forward, up); cc.direction = prefDir; } } } } #endif if (EasyColliderPostProcessor != null) { EasyColliderPostProcessor.PostProcessCollider(cc, properties); } } else if (createdCollider is MeshCollider) { MeshCollider mc = (MeshCollider)createdCollider; if (EasyColliderPostProcessor != null) { EasyColliderPostProcessor.PostProcessCollider(mc, properties); } } } #endregion #region OtherHelperMethods public List CalculateCylinderPointsLocal(List vertices, Transform attachTo, int numberOfSides, CYLINDER_ORIENTATION orientation, float cylinderOffset) { BoxColliderData data = CalculateBoxLocal(vertices); // calculate height and direction based on the height. // height is the max value between the box's directions. float height = 0.0f; // direction is the height's axis int direction = 0; if (orientation == CYLINDER_ORIENTATION.Automatic) { // calculate height as max of each axis, and direction as whichever value it is. height = Mathf.Max(Mathf.Max(data.Size.x, data.Size.y), data.Size.z); direction = (height == data.Size.x) ? 0 : (height == data.Size.y) ? 1 : 2; } else { height = data.Size[(int)orientation - 1]; // x=1, y=2, z=3 to their vector3 indexs. direction = (int)orientation - 1; // direction is also the same } // calculate max distance to the center of the box. float distance = 0.0f; // max distance is the radius float maxDistance = 0.0f; Vector3 current = Vector3.zero; // calculate radius for the given dimensions by squasing along height axis and measuring distance. foreach (Vector3 v in vertices) { current.x = (direction == 0) ? data.Center.x : v.x; current.y = (direction == 1) ? data.Center.y : v.y; current.z = (direction == 2) ? data.Center.z : v.z; distance = Vector3.Distance(current, data.Center); if (distance > maxDistance) maxDistance = distance; } // half height for offset of points from center. float halfHeight = height / 2; // amount to increment the angle when adding points. float angleIncrement = 360f / numberOfSides; // top and bottom points to build a circle around. Vector3 top, bottom = top = data.Center; // adjust the top / bottom x or y or z depending on direction by the half height. top.x = (direction == 0) ? top.x + halfHeight : top.x; top.y = (direction == 1) ? top.y + halfHeight : top.y; top.z = (direction == 2) ? top.z + halfHeight : top.z; bottom.x = (direction == 0) ? bottom.x - halfHeight : bottom.x; bottom.y = (direction == 1) ? bottom.y - halfHeight : bottom.y; bottom.z = (direction == 2) ? bottom.z - halfHeight : bottom.z; // list of points List points = new List(); for (float a = 0 + cylinderOffset; a < 360f + cylinderOffset; a += angleIncrement) { // calculate pair of offset to use for a circle float b = maxDistance * Mathf.Sin(a * Mathf.Deg2Rad); float c = maxDistance * Mathf.Cos(a * Mathf.Deg2Rad); // only adjust the axis if it is not the height axis. if (direction == 0) { top.y = b + data.Center.y; top.z = c + data.Center.z; bottom.y = b + data.Center.y; bottom.z = c + data.Center.z; } else if (direction == 1) { top.x = b + data.Center.x; top.z = c + data.Center.z; bottom.x = b + data.Center.x; bottom.z = c + data.Center.z; } else { top.y = b + data.Center.y; top.x = c + data.Center.x; bottom.y = b + data.Center.y; bottom.x = c + data.Center.x; } points.Add(top); points.Add(bottom); } return points; } /// /// Creates a gameobject attach to parent with it's local position at zero, and it's up direction oriented in the direction of the first 2 world vertices. /// /// List of world space vertices /// Parent to attach gameobject to /// Name of gameobject to create /// private GameObject CreateGameObjectOrientation(List worldVertices, GameObject parent, string name) { GameObject obj = new GameObject(name); if (worldVertices.Count >= 3) { // calculate forward and up. Vector3 forward = worldVertices[1] - worldVertices[0]; Vector3 up = Vector3.Cross(forward, worldVertices[2] - worldVertices[1]); obj.transform.rotation = Quaternion.LookRotation(forward, up); obj.transform.SetParent(parent.transform); obj.transform.localPosition = Vector3.zero; // we definitely want the local scale of a rotated object to be uniform 1,1,1 obj.transform.localScale = Vector3.one; #if (UNITY_EDITOR) // Rotated collider pivot at center is editor-only, if (ECEPreferences.RotatedColliderPivotAtCenter) { Vector3 min = new Vector3(Mathf.Infinity, Mathf.Infinity, Mathf.Infinity); Vector3 max = new Vector3(-Mathf.Infinity, -Mathf.Infinity, -Mathf.Infinity); foreach (Vector3 v in worldVertices) { Vector3 localV = obj.transform.InverseTransformPoint(v); min.x = localV.x < min.x ? localV.x : min.x; min.y = localV.y < min.y ? localV.y : min.y; min.z = localV.z < min.z ? localV.z : min.z; max.x = localV.x > max.x ? localV.x : max.x; max.y = localV.y > max.y ? localV.y : max.y; max.z = localV.z > max.z ? localV.z : max.z; } Vector3 center = (max + min) / 2; center = obj.transform.TransformPoint(center); center = parent.transform.InverseTransformPoint(center); obj.transform.localPosition = center; } Undo.RegisterCreatedObjectUndo(obj, "Create Rotated GameObject"); #endif return obj; } return null; } /// /// Just a helper method to draw a point in world space /// /// /// private void DebugDrawPoint(Vector3 worldLoc, Color color, float dist = 0.01f) { Debug.DrawLine(worldLoc - Vector3.up * dist, worldLoc + Vector3.up * dist, color, 0.01f, false); Debug.DrawLine(worldLoc - Vector3.left * dist, worldLoc + Vector3.left * dist, color, 0.01f, false); Debug.DrawLine(worldLoc - Vector3.forward * dist, worldLoc + Vector3.forward * dist, color, 0.01f, false); } /// /// A method that can be ran on any collider for things like setting collider properties or post processing. /// /// Collider that was created /// Properties object with the properties to set private void PostColliderCreation(Collider collider, EasyColliderProperties properties, bool postProcess = true) { SetPropertiesOnCollider(collider, properties); if (postProcess) { PostColliderCreationProcess(collider, properties); } } private void SetPropertiesOnCollider(Collider collider, EasyColliderProperties properties) { if (collider != null) { collider.isTrigger = properties.IsTrigger; collider.sharedMaterial = properties.PhysicMaterial; #if (UNITY_2022_2_OR_NEWER) collider.layerOverridePriority = properties.LayerOverridePriority; collider.includeLayers = properties.IncludeLayers; collider.excludeLayers = properties.ExcludeLayers; collider.providesContacts = properties.ProvidesContacts; #endif } } /// /// Converts the list of world vertices to local positions /// /// Transform to use for local space /// World space position of vertices /// Localspace position w.r.t transform of worldVertices private List ToLocalVerts(Transform transform, List worldVertices) { List localVerts = new List(worldVertices.Count); foreach (Vector3 v in worldVertices) { localVerts.Add(transform.InverseTransformPoint(v)); } return localVerts; } #endregion } }