using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Linq; namespace ECE { // TODO: re-implement coroutine for runtime quickhull? improve find initial points? // Implemented with the help of the explanation of the algorithm found here: http://algolist.ru/maths/geom/convhull/qhull3d.php public class EasyColliderQuickHull { /// /// Calculates a convex hull for a list of local-space points. /// /// Local-Space points to generate a hull on. /// the class with the result already calculated public static EasyColliderQuickHull CalculateHull(List points) { EasyColliderQuickHull qh = new EasyColliderQuickHull(); // Calculate and return the quickhull. qh.GenerateHull(points); return qh; } /// /// Calculates a convex hull for a list of world-space points. /// /// World-Space points to generate a hull on. /// Transform the result will be attached to. /// class with result calculated public static EasyColliderQuickHull CalculateHullWorld(List points, Transform attachTo) { List localPoints = new List(); foreach (Vector3 point in points) { localPoints.Add(attachTo.InverseTransformPoint(point)); } EasyColliderQuickHull qh = new EasyColliderQuickHull(); qh.GenerateHull(localPoints); return qh; } /// /// Calculates a convex hull for a list of world-space points for use by the previwer. /// /// world-space points to generate a hull on /// /// EasyColliderData with mesh, matrix, and validity public static MeshColliderData CalculateHullData(List points, Transform attachTo) { if (points == null || points.Count < 4) { // can't calculate yet. return new MeshColliderData(); } EasyColliderQuickHull qh = CalculateHullWorld(points, attachTo); MeshColliderData data = new MeshColliderData(); data.ConvexMesh = qh.Result; data.IsValid = true; data.Matrix = attachTo.localToWorldMatrix; data.ColliderType = CREATE_COLLIDER_TYPE.CONVEX_MESH; return data; } /// /// Calculates a convex hull for a list of local-space points for use by the previewer /// /// local space points /// EasyColliderData with mesh public static MeshColliderData CalculateHullData(List points) { EasyColliderQuickHull qh = CalculateHull(points); MeshColliderData data = new MeshColliderData(); data.ConvexMesh = qh.Result; data.IsValid = true; data.ColliderType = CREATE_COLLIDER_TYPE.CONVEX_MESH; return data; } /// /// class representing a triangle / face /// private class Face { /// /// v0 to v1 face /// public int F0; /// /// v1 to v2 face /// public int F1; /// /// v2 to v0 face /// public int F2; /// /// Normal of the face /// public Vector3 Normal; /// /// is the face on the convex hull? /// public bool OnConvexHull; /// /// List of vertices on the outside of the triangle (signed distance from plane is positive) /// public List OutsideVertices; // vertex index on points list. public int V0; public int V1; public int V2; /// /// Creates a face /// /// vertex 0 /// vertex 1 /// vertex 2 /// normal of the face /// face connected to v0-v1 edge /// face connected to v1-v2edge /// face connected to v2-v0 edge public Face(int v0, int v1, int v2, Vector3 normal, int f0, int f1, int f2) { V0 = v0; V1 = v1; V2 = v2; Normal = normal; OutsideVertices = new List(); F0 = f0; F1 = f1; F2 = f2; OnConvexHull = true; } } /// /// Class to hold vertex and face data of the current horizon edge /// private class Horizon { /// /// Index of Face crossed over to /// public int Face; /// /// Index of Face crossed over from /// public int From; /// /// Is the edge on the convex hull? /// public bool OnConvexHull; /// /// Index of vertex 0 of edge /// public int V0; /// /// Index of vertex 1 of edge /// public int V1; /// /// Create a new horizon edge, automatically marked on convex hull /// /// Index of vertex 0 of edge /// Index of vertex 1 of edge /// Face we cross edge to /// Face we cross edge from public Horizon(int v0, int v1, int face, int from) { V0 = v0; V1 = v1; Face = face; From = from; OnConvexHull = true; } } //Debug variables left in for future use. public bool DebugHorizon; public Color DebugHorizonColor = new Color(1, 0.5f, 0, 1); public int DebugLoopNumber = 0; public int DebugMaxLoopNumber; public bool DebugNewFaces; public bool DebugNormals; public bool DebugOutsideSet; public Color DebugNormalColor = new Color(0.5f, 0, 0.5f, 1); public float DrawTime = 2f; /// /// list of assigned vertices from add to outside set. /// /// /// private HashSet AssignedVertices = new HashSet(); /// /// List of vertices that area already done (in/on the convex hull) /// private HashSet ClosedVertices = new HashSet(); /// /// List of current horizon edges /// /// /// private List CurrentHorizon = new List(); /// /// Just a small value for float comparisons /// private float Epsilon = 0.000001f; /// /// List of faces in the convex hull. /// private List Faces = new List(); /// /// List of new faces created after finding the horizon edge. /// private List NewFaces = new List(); /// /// result mesh of quick hull calculation /// public Mesh Result = null; /// /// list of unasigned vertices for add to outside set. /// private HashSet UnAssignedVertices = new HashSet(); /// /// List of all original vertices. /// /// /// private List VerticesList = new List(); /// /// Adds vertices to a faces outside set and adds them to the assigned vertices set. /// Also closes / merges vertices /// /// Face to assign vertices to /// Set of unassigned vertices private void AddToOutsideSet(Face face, HashSet vertices) { float d = 0; foreach (int i in vertices) { // skip already assigned vertices. if (AssignedVertices.Contains(i) || ClosedVertices.Contains(i)) continue; // vertex is not assigned d = DistanceFromPlane(VerticesList[i], face.Normal, VerticesList[face.V0]); if (IsApproxZero(d)) { if (IsVertOnFace(i, face)) { ClosedVertices.Add(i); } } else if (d > 0) { // claim vertex by removing it from vertices list and adding to the face's set of vertices. AssignedVertices.Add(i); face.OutsideVertices.Add(i); } } } /// /// Checks if vertices a, and b, are coincident using an epsilon value. /// /// /// /// true if coincident, false otherwise private bool AreVertsCoincident(Vector3 a, Vector3 b) { // if one of them is greater than epislon, they aren't coincident. // simpler than checking they are all < Epsilon, as they aren't coincident if any single one fails. if (Mathf.Abs(a.x - b.x) > Epsilon || Mathf.Abs(a.y - b.y) > Epsilon || Mathf.Abs(a.z - b.z) > Epsilon) { return false; } return true; } /// /// Checks if vertices a and b are approximately coincident (x, y, and z differences are all < epsilon) /// /// /// /// true if coincident, false otherwise. private bool AreVertsCoincident(int a, int b) { if (Mathf.Abs(VerticesList[a].x - VerticesList[b].x) > Epsilon || Mathf.Abs(VerticesList[a].y - VerticesList[b].y) > Epsilon || Mathf.Abs(VerticesList[a].z - VerticesList[b].z) > Epsilon) { return false; } return true; } /// /// Updates closed vertices list by checking if unassigned vertices lie on a face on the convex hull. /// Updates the unassigned list by removing the newly closed vertices. /// private void CloseUnAssignedVertsOnFaces() { HashSet newClosedVertices = new HashSet(); foreach (Face f in Faces) { if (!f.OnConvexHull) { continue; } foreach (int i in UnAssignedVertices) { if (ClosedVertices.Contains(i)) { continue; } if (IsVertOnFace(i, f)) { newClosedVertices.Add(i); ClosedVertices.Add(i); } } } UnAssignedVertices.ExceptWith(newClosedVertices); } /// /// Checks to see if vertex at index i is on a face. /// /// index /// face /// true if vertex at index i is on the face private bool IsVertOnFace(int i, Face face) { // same approximate position as one of the corners if (AreVertsCoincident(i, face.V0) || AreVertsCoincident(i, face.V1) || AreVertsCoincident(i, face.V2)) { return true; } // areas of full triangle, and point and edge. float a = CalcTriangleArea(face.V0, face.V1, face.V2); float a1 = CalcTriangleArea(i, face.V0, face.V1); float a2 = CalcTriangleArea(i, face.V1, face.V2); float a3 = CalcTriangleArea(i, face.V2, face.V0); if (isApproxEqual(a, (a1 + a2 + a3))) { return true; } return false; } /// /// Calculates the normal of a face with points a, b, and c. /// /// /// /// /// Normal of the face formed by points a, b, and c. private Vector3 CalcNormal(Vector3 a, Vector3 b, Vector3 c) { return Vector3.Cross(b - a, c - a).normalized; } /// /// Calculates a normal given vertex index's a, b, and c. /// /// /// /// /// Normal of the face formed by the vertices at indexs a, b, and c private Vector3 CalcNormal(int a, int b, int c) { return Vector3.Cross(VerticesList[b] - VerticesList[a], VerticesList[c] - VerticesList[a]).normalized; } /// /// Calculates the area of a tringle with points v0, v1, and v1. /// /// /// /// /// Area of the triangle private float CalcTriangleArea(int v0, int v1, int v2) { return (0.5f) * Vector3.Cross(VerticesList[v1] - VerticesList[v0], VerticesList[v2] - VerticesList[v1]).magnitude; } /// /// Calculate the horizon edge recursively /// /// index in vertices list of the current eyepoint /// last edge that was crossed to get to currFace /// index in list of faces to check horizon on /// is this the first face? private void CalculateHorizon(int eyePoint, Horizon crossedEdge, int currFace, bool firstFace = true) { // if curr face is not on the convex hull (negative distance) float d = DistanceFromPlane(VerticesList[eyePoint], Faces[currFace].Normal, VerticesList[Faces[currFace].V0]); // if the currFace is not on the convex hull if (!Faces[currFace].OnConvexHull) { // mark the crossed edge as not on the convex hull and return crossedEdge.OnConvexHull = false; return; } // if the curr face is visible from the eyepoint (signed distance from plane will be positive) else if (d > 0) { // 1. mark current face as not on the convex hull. Faces[currFace].OnConvexHull = false; // 2. remove all vertices from the currFace's outside set and add them to the list unclaimed vertices. UnAssignedVertices.UnionWith(Faces[currFace].OutsideVertices); Faces[currFace].OutsideVertices.Clear(); // if the crossed edge != null (only null for the first face) then mark the crossed edge as not on the convex hull if (!firstFace) { crossedEdge.OnConvexHull = false; } // cross each of the edges of currface which are still on the convex hull. in counterclockwise order // starting from the edge after the crossed edge (in the case of the first face, pick any edge to start with. for each curr edge recurse with the call.) if (firstFace) { // first face -> we can start with any edge. // add v0 - v1 edge CurrentHorizon.Add(new Horizon(Faces[currFace].V0, Faces[currFace].V1, Faces[currFace].F0, currFace)); // recursive call from that edge. CalculateHorizon(eyePoint, CurrentHorizon[CurrentHorizon.Count - 1], Faces[currFace].F0, false); // add v1 - v2 edge CurrentHorizon.Add(new Horizon(Faces[currFace].V1, Faces[currFace].V2, Faces[currFace].F1, currFace)); CalculateHorizon(eyePoint, CurrentHorizon[CurrentHorizon.Count - 1], Faces[currFace].F1, false); // add v2 - v0 edge. CurrentHorizon.Add(new Horizon(Faces[currFace].V2, Faces[currFace].V0, Faces[currFace].F2, currFace)); CalculateHorizon(eyePoint, CurrentHorizon[CurrentHorizon.Count - 1], Faces[currFace].F2, false); } else { // not the first face, but still visible. if (Faces[currFace].F0 == crossedEdge.From) // crossed edge was v0-v1 edge. { // add v1-v2 edge to horizon CurrentHorizon.Add(new Horizon(Faces[currFace].V1, Faces[currFace].V2, Faces[currFace].F1, currFace)); CalculateHorizon(eyePoint, CurrentHorizon[CurrentHorizon.Count - 1], Faces[currFace].F1, false); // add v2-v0 edge to horizon CurrentHorizon.Add(new Horizon(Faces[currFace].V2, Faces[currFace].V0, Faces[currFace].F2, currFace)); CalculateHorizon(eyePoint, CurrentHorizon[CurrentHorizon.Count - 1], Faces[currFace].F2, false); } else if (Faces[currFace].F1 == crossedEdge.From) // crossed edge was v1-v2 edge { // So much time spent looking for a bug in this method, and it was simply just forgetting // that we need to go in a certain order. // we NEED to go v2-v0 then v0-v1....................... Oops. // add v2-v0 edge to horizon CurrentHorizon.Add(new Horizon(Faces[currFace].V2, Faces[currFace].V0, Faces[currFace].F2, currFace)); CalculateHorizon(eyePoint, CurrentHorizon[CurrentHorizon.Count - 1], Faces[currFace].F2, false); // add v0-v1 edge to horizon CurrentHorizon.Add(new Horizon(Faces[currFace].V0, Faces[currFace].V1, Faces[currFace].F0, currFace)); CalculateHorizon(eyePoint, CurrentHorizon[CurrentHorizon.Count - 1], Faces[currFace].F0, false); } else if (Faces[currFace].F2 == crossedEdge.From) // crossed edge was v2-v0 edge. { // add v0-v1 edge to horizon CurrentHorizon.Add(new Horizon(Faces[currFace].V0, Faces[currFace].V1, Faces[currFace].F0, currFace)); CalculateHorizon(eyePoint, CurrentHorizon[CurrentHorizon.Count - 1], Faces[currFace].F0, false); // add v1-v2 edge to horizon CurrentHorizon.Add(new Horizon(Faces[currFace].V1, Faces[currFace].V2, Faces[currFace].F1, currFace)); CalculateHorizon(eyePoint, CurrentHorizon[CurrentHorizon.Count - 1], Faces[currFace].F1, false); } } } } /// /// Creates a mesh from the face data of the convex hull. /// /// /// A mesh of the convex hull private Mesh CreateMesh(List allFaces) { Mesh m = new Mesh(); List vertices = new List(); // filter faces based on which are still on the convex hull. List faces = allFaces.Where(face => face.OnConvexHull).ToList(); List normals = new List(); int[] triangles = new int[faces.Count * 3]; int t0, t1, t2 = t1 = t0 = 0; for (int i = 0; i < faces.Count; i++) { // QH-SMOOTHED // shared vertices smooth // since we just add normals together and normalize them, and verts can be shared by n faces // it's not really proper smoothing. t0 = vertices.IndexOf(VerticesList[faces[i].V0]); t1 = vertices.IndexOf(VerticesList[faces[i].V1]); t2 = vertices.IndexOf(VerticesList[faces[i].V2]); if (t0 < 0) { normals.Add(faces[i].Normal); vertices.Add(VerticesList[faces[i].V0]); t0 = vertices.Count - 1; } else { normals[t0] = (normals[t0] + faces[i].Normal).normalized; } if (t1 < 0) { normals.Add(faces[i].Normal); vertices.Add(VerticesList[faces[i].V1]); t1 = vertices.Count - 1; } else { normals[t1] = (normals[t1] + faces[i].Normal).normalized; } if (t2 < 0) { normals.Add(faces[i].Normal); vertices.Add(VerticesList[faces[i].V2]); t2 = vertices.Count - 1; } else { normals[t2] = (normals[t2] + faces[i].Normal).normalized; } //QH_SMOOTHED // all vertices are added and arent shared so they aren't smoothed at all. //QH-UNSMOOTHED // vertices.Add(VerticesList[faces[i].V0]); // t0 = vertices.Count - 1; // vertices.Add(VerticesList[faces[i].V1]); // t1 = vertices.Count - 1; // vertices.Add(VerticesList[faces[i].V2]); // t2 = vertices.Count - 1; // normals.Add(faces[i].Normal); // normals.Add(faces[i].Normal); // normals.Add(faces[i].Normal); //QH_UNSMOOTHED triangles[i * 3] = t0; triangles[i * 3 + 1] = t1; triangles[i * 3 + 2] = t2; } m.SetVertices(vertices); m.SetTriangles(triangles, 0); m.SetNormals(normals); // m.RecalculateNormals(0); return m; } /// /// Calculates the distance a point is from a line. /// /// how far away is this point /// direction of line /// a point on the line /// float DistanceFromLine(Vector3 point, Vector3 line, Vector3 pointOnLine) { Vector3 v = point - pointOnLine; float dV = Vector3.Dot(v, line); v = pointOnLine + dV * line; return Vector3.Distance(v, point); } /// /// Calculates the signed distance of a point from a plane /// /// how far is this point /// from this plane /// Distance point is from plane. float DistanceFromPlane(Vector3 point, Plane p) { return p.GetDistanceToPoint(point); } /// /// Calculate the signed distance of a point from a plane /// /// point /// normal of the plane /// a point on the plane. /// float DistanceFromPlane(Vector3 point, Vector3 normal, Vector3 pointOnPlane) { return Vector3.Dot(normal, point - pointOnPlane); } /// /// Finds the initial max points from which to build a hull from. Creates faces from these points. /// /// list of points /// true if it found and made the faces, false otherwise. bool FindInitialHull(List points) { List initialPoints; bool initialPointsFound = false; // Brain isn't working great right now, so two methods of finding initial points. if (FindInitialPoints(points, out initialPoints)) { initialPointsFound = true; } else if (FindInitialPointsFallBack(points, out initialPoints)) { initialPointsFound = true; } if (initialPointsFound) { // we've found 6 valid points xMin,xMax & same for y, z. that are in a 3d point cloud. // find the point which is furthest distance from the line defined by the first two points. float maxDistance = -Mathf.Infinity; int furthestLinePoint = 0; Vector3 line = (points[initialPoints[1]] - points[initialPoints[0]]).normalized; int furthestIndex = 0; for (int i = 2; i < 6; i++) { float d = DistanceFromLine(points[initialPoints[i]], line, points[initialPoints[0]]); if (isAGreaterThanB(d, maxDistance)) { maxDistance = d; furthestLinePoint = initialPoints[i]; furthestIndex = i; } } // swap the points at the furthest index and the 3rd point. initialPoints[furthestIndex] = initialPoints[2]; initialPoints[2] = furthestLinePoint; // find the point which has the largest absolute distance from the plane defined by the first three points. maxDistance = -Mathf.Infinity; Plane p = new Plane(points[initialPoints[0]], points[initialPoints[1]], points[furthestLinePoint]); int furthestPlanePoint = -1; for (int i = 2; i < 6; i++) { if (initialPoints[i] == furthestLinePoint) continue; float d = DistanceFromPlane(points[initialPoints[i]], p); if (!IsApproxZero(d) && isAGreaterThanB(Mathf.Abs(d), maxDistance)) { furthestPlanePoint = initialPoints[i]; maxDistance = d; furthestIndex = i; } } // if the furest plane point is still -1, all points are coplanar. if (furthestPlanePoint == -1) { return false; } // swap the points initialPoints[furthestIndex] = initialPoints[3]; initialPoints[3] = furthestPlanePoint; // remember that if the distance from the fourth point was negative, the order of the first three vertices must be reversed. if (DistanceFromPlane(points[furthestPlanePoint], p) < 0.0f) { int i1 = initialPoints[2]; initialPoints[2] = initialPoints[0]; initialPoints[0] = i1; } // add the faces. (Creating a tetrahedron to start.) Faces.Add(new Face(initialPoints[0], initialPoints[2], initialPoints[1], CalcNormal(points[initialPoints[0]], points[initialPoints[2]], points[initialPoints[1]]), 2, 3, 1)); Faces.Add(new Face(initialPoints[0], initialPoints[1], initialPoints[3], CalcNormal(points[initialPoints[0]], points[initialPoints[1]], points[initialPoints[3]]), 0, 3, 2)); Faces.Add(new Face(initialPoints[0], initialPoints[3], initialPoints[2], CalcNormal(points[initialPoints[0]], points[initialPoints[3]], points[initialPoints[2]]), 1, 3, 0)); Faces.Add(new Face(initialPoints[1], initialPoints[2], initialPoints[3], CalcNormal(points[initialPoints[1]], points[initialPoints[2]], points[initialPoints[3]]), 0, 2, 1)); UnAssignedVertices.UnionWith(Enumerable.Range(0, points.Count)); // keep track of all vertices that were assigned. AssignedVertices = new HashSet(); foreach (Face f in Faces) { AddToOutsideSet(f, UnAssignedVertices); } // remove all vertices that weren't assigned at all, as they are inside or merged, so not part of the convex hull // ClosedVertices = new HashSet(); ClosedVertices.UnionWith(UnAssignedVertices); ClosedVertices.ExceptWith(AssignedVertices); return true; } return false; } /// /// Fallback old method of finding initial points. /// /// /// /// bool FindInitialPointsFallBack(List points, out List initialPoints) { List ips = new List(6) { -1, -1, -1, -1, -1, -1 }; initialPoints = new List(6) { -1, -1, -1, -1, -1, -1 }; // keep track of points of x,y,z min and max. // could just be floats since we're only using them in comparisons and tracking the actual indexs. Vector3 xMin, yMin, zMin = yMin = xMin = new Vector3(Mathf.Infinity, Mathf.Infinity, Mathf.Infinity); Vector3 xMax, yMax, zMax = yMax = xMax = new Vector3(-Mathf.Infinity, -Mathf.Infinity, -Mathf.Infinity); for (int i = 0; i < points.Count; i++) { // using epislon make sure the new point is less than the min // if they are the same as the current, and they are used in multiple places, replace it with the new one. // (otherwise the same point can be (example) both xMin and zMin, and result in the initial faces being coplanar) when there are other points to use. if (isALessThanB(points[i].x, xMin.x) || (isApproxEqual(points[i].x, xMin.x) && initialPoints.FindAll(element => element == ips[0]).Count > 1)) { // keep track of the index of the point initialPoints[0] = i; ips[0] = i; // set the minimum point. xMin = points[i]; } if (isAGreaterThanB(points[i].x, xMax.x) || (isApproxEqual(points[i].x, xMax.x) && initialPoints.FindAll(element => element == ips[1]).Count > 1)) { initialPoints[1] = i; ips[1] = i; xMax = points[i]; } if (isALessThanB(points[i].y, yMin.y) || (isApproxEqual(points[i].y, yMin.y) && initialPoints.FindAll(element => element == ips[2]).Count > 1)) { initialPoints[2] = i; ips[2] = i; yMin = points[i]; } if (isAGreaterThanB(points[i].y, yMax.y) || (isApproxEqual(points[i].y, yMax.y) && initialPoints.FindAll(element => element == ips[3]).Count > 1)) { initialPoints[3] = i; ips[3] = i; yMax = points[i]; } if (isALessThanB(points[i].z, zMin.z) || (isApproxEqual(points[i].z, zMin.z) && initialPoints.FindAll(element => element == ips[4]).Count > 1)) { initialPoints[4] = i; ips[4] = i; zMin = points[i]; } if (isAGreaterThanB(points[i].z, zMax.z) || (isApproxEqual(points[i].z, zMax.z) && initialPoints.FindAll(element => element == ips[5]).Count > 1)) { initialPoints[5] = i; ips[5] = i; zMax = points[i]; } } if (!isApproxEqual(xMin.x, xMax.x) && !isApproxEqual(yMin.y, yMax.y) && !isApproxEqual(zMin.z, zMax.z)) { return true; // we're good. } return false; } /// /// Finds initial 6 points of min/max in x, y, z dimensions. /// Now finds the first 4 non-coplanar points + 2 extra points. /// /// list of points /// initial pointss list of length 6. /// true if it finds the initial points [xMin, xMax, yMin, yMax, zMin, zMax], false otherwise bool FindInitialPoints(List points, out List initialPoints) { // just find the first 4 non-coplanar points. initialPoints = new List(6) { -1, -1, -1, -1, -1, -1 }; Vector3 a, b, c, d = a = b = c = Vector3.zero; // search 4 consecutive points for a polyhedron with a volume. for (int i = 0; i < points.Count; i++) { if (i + 3 >= points.Count || i + 2 >= points.Count || i + 1 >= points.Count) continue; a = points[i]; b = points[i + 1]; c = points[i + 2]; d = points[i + 3]; // volume = | (a-d) dot ((b-d) cross (c - d)) | / 6. float v = Mathf.Abs(Vector3.Dot((a - d), Vector3.Cross((b - d), (c - d)))) / 6; // non zero volume = 4 points are not coplanar. if (!IsApproxZero(v)) { initialPoints[0] = i; initialPoints[1] = i + 1; initialPoints[2] = i + 2; initialPoints[3] = i + 3; if (i + 4 < points.Count) { initialPoints[4] = i + 4; } else { initialPoints[4] = i; } if (i + 5 < points.Count) { initialPoints[5] = i + 5; } else { initialPoints[5] = i; } return true; } else { // just swap the last point to find a non-coplanar point. for (int j = i + 4; j < points.Count; j++) { d = points[j]; v = Mathf.Abs(Vector3.Dot((a - d), Vector3.Cross((b - d), (c - d)))) / 6; if (!IsApproxZero(v)) { initialPoints[0] = i; initialPoints[1] = i + 1; initialPoints[2] = i + 2; initialPoints[3] = j; if (i + 4 < points.Count) { initialPoints[4] = i + 4; } else { initialPoints[4] = i; } if (i + 5 < points.Count) { initialPoints[5] = i + 5; } else { initialPoints[5] = i; } return true; } } } } return false; } /// /// Is the calculation finished? (The Result has been generated) /// /// true if result != null, false otherwise public bool isFinished { get { return (Result != null); } } /// /// Given a set of points, calculate an appropriate epislon value for quickhull-ing /// /// local space points private void CalculateEpsilon(List points) { // given a set of points determine an appropriate epislon value to use for quickhull. // epislon is relative to maximum abs values of x,y,and z. // float maxX, maxY, maxZ = maxY = maxX = -Mathf.Infinity; Vector3 min = new Vector3(Mathf.Infinity, Mathf.Infinity, Mathf.Infinity); Vector3 max = new Vector3(-Mathf.Infinity, -Mathf.Infinity, -Mathf.Infinity); foreach (Vector3 v in points) { if (v.x < min.x) { min.x = v.x; } if (v.y < min.y) { min.y = v.y; } if (v.z < min.z) { min.z = v.z; } if (v.x > max.x) { max.x = v.x; } if (v.y > max.y) { max.y = v.y; } if (v.z > max.z) { max.z = v.z; } } Epsilon = Vector3.Distance(min, max) * 0.000001f; } /// /// Generates a convex hull from a list of local space points. /// The resulting mesh is placed in the result variable. /// /// List of local space points. public void GenerateHull(List points) { CalculateEpsilon(points); VerticesList = points; if (FindInitialHull(points)) { // while there is a current face on the current hull which has a non-empty outside set of vertices. while (HaveNonEmptyFaceSet())// && whileLoopedCount < DebugMaxLoopNumber) { // clear unassigned vertices. UnAssignedVertices = new HashSet(); // clear the current horizon. CurrentHorizon = new List(); // get the non empty face int currFace = GetNonEmptyFaceIndex(); // find the point on the currFaces' surface which is farthest away from the plane of currFace. this is the eyePoint int eyePoint = GetFurthestPointFromFace(currFace); // compute horizon of current poly as seen from eyePoint (CALCULATE_HORIZON) // this will mark all visible faces as not on the convex hull and place all of their outside set points // on the list listUnclaimedVertices. it creates a list HorizonEdges which is // a counter clockwise ordered list of edges around the horizon contour of the polyhedron as viewed from the eyepoint. CalculateHorizon(eyePoint, null, currFace, true); // calculate horizon updates the unassigned vertices, so we need to update the assigned vertices AssignedVertices.ExceptWith(UnAssignedVertices); // construct a cone from the eye point to all of the edges of the horizon. // start face (used for last valid face that is added.) int startFace = Faces.Count; // end face (used for first valid face that is added.) int endFace = Faces.Count + CurrentHorizon.Where(item => item.OnConvexHull).ToList().Count - 1; // total number of valid valids (used to see if we're currently adding the last valid face.) int totalValidHorizons = CurrentHorizon.Where(item => item.OnConvexHull).ToList().Count; // reset the new faces list NewFaces = new List(); // count the number of valid faces we've added. int validHorizonsDone = 0; for (int i = 0; i < CurrentHorizon.Count; i++) { // makes it easier than always typing currenthorizon[i]. Horizon h = CurrentHorizon[i]; if (!h.OnConvexHull) { continue; } if (validHorizonsDone == 0) { // add a new face that has edge v0, v1 and eye point, calculate the normal, the shared edge (v0,v1) face is the the horizon's face. // the next face is the next shared edge, the end face is the last face. Faces.Add(new Face(h.V0, h.V1, eyePoint, CalcNormal(h.V0, h.V1, eyePoint), h.Face, Faces.Count + 1, endFace)); } else if (validHorizonsDone == totalValidHorizons - 1) { // this is the last face, the number of horizon faces we've added is the total count of valid faces. Faces.Add(new Face(h.V0, h.V1, eyePoint, CalcNormal(h.V0, h.V1, eyePoint), h.Face, startFace, Faces.Count - 1)); } else { // add previous face, and next face as it's other faces. Faces.Add(new Face(h.V0, h.V1, eyePoint, CalcNormal(h.V0, h.V1, eyePoint), h.Face, Faces.Count + 1, Faces.Count - 1)); } // keep track of the added face. NewFaces.Add(Faces.Count - 1); // update the face of the horizon to share the new edge with the new face. UpdateFace(h, Faces.Count - 1); validHorizonsDone++; } // We had an error somewhere on the connected face not correctly being set to the correct face but I can't find the source of the bug, // and so we are just going to force verification of all of the new faces that were just added. // The error was in the recursive find horizon method the whole time. // FIX_FACE_VERIFY (So if someone has an issue in the future, they can just uncomment the foreach loop below and it should all work but slower) // foreach (int i in NewFaces) // { // ForceUpdateFace(i); // } // All the vertices of removed faces were added to the unassigned vertices list. // update the closed vertices list before assigning an unassigned vertex to any face's outside set. CloseUnAssignedVertsOnFaces(); // the remaining unassigned vertices can all be added to the new faces that were created. for (int i = 0; i < NewFaces.Count; i++) { // "randomly" assign the points to the new faces outside sets. AddToOutsideSet(Faces[NewFaces[i]], UnAssignedVertices); } // mark vertices as closed that are still unassigned as they are etiher on or in the convex hull UnAssignedVertices.ExceptWith(AssignedVertices); ClosedVertices.UnionWith(UnAssignedVertices); } // create the mesh from the list of faces. Result = CreateMesh(Faces); } else { // Removed warning, as it can happen too often when selecting a face. // Debug.LogWarning("EasyColliderEditor: Unable to find initial points, likely because all points lie on the same plane."); } } /// /// gets the verticeslist index of the furthest point in a face's outside set (furthest signed distance) /// /// face we want the furthest point from /// index of vertex at a positive signed distance furthest from the face private int GetFurthestPointFromFace(int faceIndex) { Face face = Faces[faceIndex]; float maxDistance = -Mathf.Infinity; int furthestIndex = -1; foreach (int i in face.OutsideVertices) { float d = DistanceFromPlane(VerticesList[i], face.Normal, VerticesList[face.V0]); if (d > maxDistance) { furthestIndex = i; maxDistance = d; } } return furthestIndex; } /// /// Gets the index of the first face that has a non-empty outside set. /// /// index of the first face that has a non-empty outside set, -1 if none are found private int GetNonEmptyFaceIndex() { for (int i = 0; i < Faces.Count; i++) { if (Faces[i].OutsideVertices.Count > 0) { return i; } } return -1; } /// /// Do we have a face with a non-empty outside set? /// /// true if we have a non-empty outside set private bool HaveNonEmptyFaceSet() { foreach (Face f in Faces) { if (f.OutsideVertices.Count > 0) { return true; } } return false; } /// /// Checks if a > b by at least epsilon. /// /// /// /// true a > b private bool isAGreaterThanB(float a, float b) { if (a - b > Epsilon) { return true; } return false; } /// /// Checks if a < b by at least epsilon /// /// /// /// true if a < b private bool isALessThanB(float a, float b) { if (b - a > Epsilon) { return true; } return false; } /// /// Checks if a and b are approximately equal (the difference between them is < epsilon) /// /// a /// b /// true if they are approximately equal, false otherwise private bool isApproxEqual(float a, float b) { return Mathf.Abs(a - b) < Epsilon; } /// /// Checks if value is approximately zero by comparing abs(value) < epsilon. /// /// a /// true is a is approximately 0 private bool IsApproxZero(float a) { return Mathf.Abs(a) < Epsilon; } /// /// Updates the faces of a horizon based on the new face created. /// The face that was crossed from is no longer on the convex hull and is replaced with the new face in the correct /// spot on horizon.Face's F0, F1, or F2. /// /// horizon edge /// new face index private void UpdateFace(Horizon horizon, int newFace) { // if the face is on the convex hull if (Faces[horizon.Face].OnConvexHull) { // and f0's was the face we crossed the edge from // the edge we crossed from is no longer on the convex hull and is replaced with the new face. if (Faces[horizon.Face].F0 == horizon.From) { // set that face to the new face Faces[horizon.Face].F0 = newFace; } else if (Faces[horizon.Face].F1 == horizon.From) { Faces[horizon.Face].F1 = newFace; } else if (Faces[horizon.Face].F2 == horizon.From) { Faces[horizon.Face].F2 = newFace; } } } //Debugging methods below left in for future possible use for bug-fixing. /// /// Calculates the center of a face. /// Vector3 CalcFaceCenter(Face face) { return (VerticesList[face.V0] + VerticesList[face.V1] + VerticesList[face.V2]) / 3; } void DebugInitialPoints(List points, List initialPoints) { string ints = ""; string vals = ""; foreach (int i in initialPoints) { ints += i + " : "; vals += points[i] + " : "; } } /// /// Draws a faces points. /// void DrawFace(int face, Color color, float size = 0.08f) { Face f = Faces[face]; DrawPoint(VerticesList[f.V0], color, size); DrawPoint(VerticesList[f.V1], color, size); DrawPoint(VerticesList[f.V2], color, size); } /// /// Draws the normal of faces conected to the face provided. (f0 = r, f1 = g, f2 = b) /// /// Face to draw neighbours of void DrawFaceConnections(int face) { // DrawFaceNormal(Faces[face], Color.black); DrawFaceNormal(Faces[Faces[face].F0], Color.red, 1.025f); DrawFaceNormal(Faces[Faces[face].F1], Color.green, 1.05f); DrawFaceNormal(Faces[Faces[face].F2], Color.blue, 1.075f); } /// /// draws a faces normal /// void DrawFaceNormal(Face face, Color color, float distance = 1.0f) { Vector3 center = CalcFaceCenter(face); Debug.DrawLine(center, center + face.Normal * distance, color, DrawTime); } /// /// Force verifys faces. /// Was used to help solve the issue with incorrect horizon finding. /// Left in for future issues and solutions. /// /// Index of face void ForceUpdateFace(int faceIndex) { bool needsToBeRepaired = true; if (needsToBeRepaired) { Face f = Faces[faceIndex]; Face o = null; for (int i = 0; i < Faces.Count; i++) { if (faceIndex == i) { continue; } if (!Faces[i].OnConvexHull) { continue; } o = Faces[i]; if ((f.V0 == o.V0 || f.V0 == o.V1 || f.V0 == o.V2) && (f.V1 == o.V0 || f.V1 == o.V1 || f.V1 == o.V2)) // v0-v1 edge shared { f.F0 = i; } else if ((f.V2 == o.V0 || f.V2 == o.V1 || f.V2 == o.V2) && (f.V1 == o.V0 || f.V1 == o.V1 || f.V1 == o.V2)) //v1-v2 edge shared { f.F1 = i; } else if ((f.V0 == o.V0 || f.V0 == o.V1 || f.V0 == o.V2) && (f.V2 == o.V0 || f.V2 == o.V1 || f.V2 == o.V2)) //v2-v0 edge shared. { f.F2 = i; } } } } /// /// Generates a random color. /// /// Color RandomColor() { return new Color(Random.Range(0f, 1f), Random.Range(0f, 1f), Random.Range(0f, 1f)); } /// /// Draws a point at poisition /// /// point to draw /// color to draw with /// size to draw point void DrawPoint(Vector3 point, Color color, float size = 0.05f) { Debug.DrawLine(point - Vector3.up * size, point + Vector3.up * size, color, DrawTime); Debug.DrawLine(point - Vector3.left * size, point + Vector3.left * size, color, DrawTime); Debug.DrawLine(point - Vector3.forward * size, point + Vector3.forward * size, color, DrawTime); } } }