ZeroVR/ZeroPacientVR/Assets/EasyColliderEditor/Scripts/EasyColliderQuickHull.cs

1240 lines
46 KiB
C#

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
{
/// <summary>
/// Calculates a convex hull for a list of local-space points.
/// </summary>
/// <param name="points">Local-Space points to generate a hull on.</param>
/// <returns>the class with the result already calculated</returns>
public static EasyColliderQuickHull CalculateHull(List<Vector3> points)
{
EasyColliderQuickHull qh = new EasyColliderQuickHull();
// Calculate and return the quickhull.
qh.GenerateHull(points);
return qh;
}
/// <summary>
/// Calculates a convex hull for a list of world-space points.
/// </summary>
/// <param name="points">World-Space points to generate a hull on.</param>
/// <param name="attachTo">Transform the result will be attached to.</param>
/// <returns>class with result calculated</returns>
public static EasyColliderQuickHull CalculateHullWorld(List<Vector3> points, Transform attachTo)
{
List<Vector3> localPoints = new List<Vector3>();
foreach (Vector3 point in points)
{
localPoints.Add(attachTo.InverseTransformPoint(point));
}
EasyColliderQuickHull qh = new EasyColliderQuickHull();
qh.GenerateHull(localPoints);
return qh;
}
/// <summary>
/// Calculates a convex hull for a list of world-space points for use by the previwer.
/// </summary>
/// <param name="points">world-space points to generate a hull on</param>
/// <param name="attachTo"></param>
/// <returns>EasyColliderData with mesh, matrix, and validity</returns>
public static MeshColliderData CalculateHullData(List<Vector3> 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;
}
/// <summary>
/// Calculates a convex hull for a list of local-space points for use by the previewer
/// </summary>
/// <param name="points">local space points</param>
/// <returns>EasyColliderData with mesh</returns>
public static MeshColliderData CalculateHullData(List<Vector3> 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;
}
/// <summary>
/// class representing a triangle / face
/// </summary>
private class Face
{
/// <summary>
/// v0 to v1 face
/// </summary>
public int F0;
/// <summary>
/// v1 to v2 face
/// </summary>
public int F1;
/// <summary>
/// v2 to v0 face
/// </summary>
public int F2;
/// <summary>
/// Normal of the face
/// </summary>
public Vector3 Normal;
/// <summary>
/// is the face on the convex hull?
/// </summary>
public bool OnConvexHull;
/// <summary>
/// List of vertices on the outside of the triangle (signed distance from plane is positive)
/// </summary>
public List<int> OutsideVertices;
// vertex index on points list.
public int V0;
public int V1;
public int V2;
/// <summary>
/// Creates a face
/// </summary>
/// <param name="v0">vertex 0</param>
/// <param name="v1">vertex 1</param>
/// <param name="v2">vertex 2</param>
/// <param name="normal">normal of the face</param>
/// <param name="f0">face connected to v0-v1 edge</param>
/// <param name="f1">face connected to v1-v2edge</param>
/// <param name="f2">face connected to v2-v0 edge</param>
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<int>();
F0 = f0;
F1 = f1;
F2 = f2;
OnConvexHull = true;
}
}
/// <summary>
/// Class to hold vertex and face data of the current horizon edge
/// </summary>
private class Horizon
{
/// <summary>
/// Index of Face crossed over to
/// </summary>
public int Face;
/// <summary>
/// Index of Face crossed over from
/// </summary>
public int From;
/// <summary>
/// Is the edge on the convex hull?
/// </summary>
public bool OnConvexHull;
/// <summary>
/// Index of vertex 0 of edge
/// </summary>
public int V0;
/// <summary>
/// Index of vertex 1 of edge
/// </summary>
public int V1;
/// <summary>
/// Create a new horizon edge, automatically marked on convex hull
/// </summary>
/// <param name="v0">Index of vertex 0 of edge</param>
/// <param name="v1">Index of vertex 1 of edge</param>
/// <param name="face">Face we cross edge to</param>
/// <param name="from">Face we cross edge from</param>
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;
/// <summary>
/// list of assigned vertices from add to outside set.
/// </summary>
/// <typeparam name="int"></typeparam>
/// <returns></returns>
private HashSet<int> AssignedVertices = new HashSet<int>();
/// <summary>
/// List of vertices that area already done (in/on the convex hull)
/// </summary>
private HashSet<int> ClosedVertices = new HashSet<int>();
/// <summary>
/// List of current horizon edges
/// </summary>
/// <typeparam name="Horizon"></typeparam>
/// <returns></returns>
private List<Horizon> CurrentHorizon = new List<Horizon>();
/// <summary>
/// Just a small value for float comparisons
/// </summary>
private float Epsilon = 0.000001f;
/// <summary>
/// List of faces in the convex hull.
/// </summary>
private List<Face> Faces = new List<Face>();
/// <summary>
/// List of new faces created after finding the horizon edge.
/// </summary>
private List<int> NewFaces = new List<int>();
/// <summary>
/// result mesh of quick hull calculation
/// </summary>
public Mesh Result = null;
/// <summary>
/// list of unasigned vertices for add to outside set.
/// </summary>
private HashSet<int> UnAssignedVertices = new HashSet<int>();
/// <summary>
/// List of all original vertices.
/// </summary>
/// <typeparam name="Vector3"></typeparam>
/// <returns></returns>
private List<Vector3> VerticesList = new List<Vector3>();
/// <summary>
/// Adds vertices to a faces outside set and adds them to the assigned vertices set.
/// Also closes / merges vertices
/// </summary>
/// <param name="face">Face to assign vertices to</param>
/// <param name="vertices">Set of unassigned vertices</param>
private void AddToOutsideSet(Face face, HashSet<int> 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);
}
}
}
/// <summary>
/// Checks if vertices a, and b, are coincident using an epsilon value.
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns>true if coincident, false otherwise</returns>
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;
}
/// <summary>
/// Checks if vertices a and b are approximately coincident (x, y, and z differences are all < epsilon)
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns>true if coincident, false otherwise.</returns>
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;
}
/// <summary>
/// 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.
/// </summary>
private void CloseUnAssignedVertsOnFaces()
{
HashSet<int> newClosedVertices = new HashSet<int>();
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);
}
/// <summary>
/// Checks to see if vertex at index i is on a face.
/// </summary>
/// <param name="i">index</param>
/// <param name="face">face</param>
/// <returns>true if vertex at index i is on the face</returns>
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;
}
/// <summary>
/// Calculates the normal of a face with points a, b, and c.
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="c"></param>
/// <returns>Normal of the face formed by points a, b, and c.</returns>
private Vector3 CalcNormal(Vector3 a, Vector3 b, Vector3 c)
{
return Vector3.Cross(b - a, c - a).normalized;
}
/// <summary>
/// Calculates a normal given vertex index's a, b, and c.
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="c"></param>
/// <returns>Normal of the face formed by the vertices at indexs a, b, and c</returns>
private Vector3 CalcNormal(int a, int b, int c)
{
return Vector3.Cross(VerticesList[b] - VerticesList[a], VerticesList[c] - VerticesList[a]).normalized;
}
/// <summary>
/// Calculates the area of a tringle with points v0, v1, and v1.
/// </summary>
/// <param name="v0"></param>
/// <param name="v1"></param>
/// <param name="v2"></param>
/// <returns>Area of the triangle</returns>
private float CalcTriangleArea(int v0, int v1, int v2)
{
return (0.5f) * Vector3.Cross(VerticesList[v1] - VerticesList[v0], VerticesList[v2] - VerticesList[v1]).magnitude;
}
/// <summary>
/// Calculate the horizon edge recursively
/// </summary>
/// <param name="eyePoint">index in vertices list of the current eyepoint</param>
/// <param name="crossedEdge">last edge that was crossed to get to currFace</param>
/// <param name="currFace">index in list of faces to check horizon on</param>
/// <param name="firstFace">is this the first face?</param>
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);
}
}
}
}
/// <summary>
/// Creates a mesh from the face data of the convex hull.
/// </summary>
/// <param name="allFaces"></param>
/// <returns>A mesh of the convex hull</returns>
private Mesh CreateMesh(List<Face> allFaces)
{
Mesh m = new Mesh();
List<Vector3> vertices = new List<Vector3>();
// filter faces based on which are still on the convex hull.
List<Face> faces = allFaces.Where(face => face.OnConvexHull).ToList();
List<Vector3> normals = new List<Vector3>();
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;
}
/// <summary>
/// Calculates the distance a point is from a line.
/// </summary>
/// <param name="point">how far away is this point</param>
/// <param name="line">direction of line</param>
/// <param name="pointOnLine">a point on the line</param>
/// <returns></returns>
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);
}
/// <summary>
/// Calculates the signed distance of a point from a plane
/// </summary>
/// <param name="point">how far is this point</param>
/// <param name="p">from this plane</param>
/// <returns>Distance point is from plane.</returns>
float DistanceFromPlane(Vector3 point, Plane p)
{
return p.GetDistanceToPoint(point);
}
/// <summary>
/// Calculate the signed distance of a point from a plane
/// </summary>
/// <param name="point">point</param>
/// <param name="normal">normal of the plane</param>
/// <param name="pointOnPlane">a point on the plane.</param>
/// <returns></returns>
float DistanceFromPlane(Vector3 point, Vector3 normal, Vector3 pointOnPlane)
{
return Vector3.Dot(normal, point - pointOnPlane);
}
/// <summary>
/// Finds the initial max points from which to build a hull from. Creates faces from these points.
/// </summary>
/// <param name="points">list of points</param>
/// <returns>true if it found and made the faces, false otherwise.</returns>
bool FindInitialHull(List<Vector3> points)
{
List<int> 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<int>();
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<int>();
ClosedVertices.UnionWith(UnAssignedVertices);
ClosedVertices.ExceptWith(AssignedVertices);
return true;
}
return false;
}
/// <summary>
/// Fallback old method of finding initial points.
/// </summary>
/// <param name="points"></param>
/// <param name="initialPoints"></param>
/// <returns></returns>
bool FindInitialPointsFallBack(List<Vector3> points, out List<int> initialPoints)
{
List<int> ips = new List<int>(6) { -1, -1, -1, -1, -1, -1 };
initialPoints = new List<int>(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;
}
/// <summary>
/// Finds initial 6 points of min/max in x, y, z dimensions.
/// Now finds the first 4 non-coplanar points + 2 extra points.
/// </summary>
/// <param name="points">list of points</param>
/// <param name="initialPoints">initial pointss list of length 6.</param>
/// <returns>true if it finds the initial points [xMin, xMax, yMin, yMax, zMin, zMax], false otherwise</returns>
bool FindInitialPoints(List<Vector3> points, out List<int> initialPoints)
{
// just find the first 4 non-coplanar points.
initialPoints = new List<int>(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;
}
/// <summary>
/// Is the calculation finished? (The Result has been generated)
/// </summary>
/// <value>true if result != null, false otherwise</value>
public bool isFinished
{
get
{
return (Result != null);
}
}
/// <summary>
/// Given a set of points, calculate an appropriate epislon value for quickhull-ing
/// </summary>
/// <param name="points">local space points</param>
private void CalculateEpsilon(List<Vector3> 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;
}
/// <summary>
/// Generates a convex hull from a list of local space points.
/// The resulting mesh is placed in the result variable.
/// </summary>
/// <param name="points">List of local space points.</param>
public void GenerateHull(List<Vector3> 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<int>();
// clear the current horizon.
CurrentHorizon = new List<Horizon>();
// 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<int>();
// 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.");
}
}
/// <summary>
/// gets the verticeslist index of the furthest point in a face's outside set (furthest signed distance)
/// </summary>
/// <param name="face">face we want the furthest point from</param>
/// <returns>index of vertex at a positive signed distance furthest from the face</returns>
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;
}
/// <summary>
/// Gets the index of the first face that has a non-empty outside set.
/// </summary>
/// <returns>index of the first face that has a non-empty outside set, -1 if none are found</returns>
private int GetNonEmptyFaceIndex()
{
for (int i = 0; i < Faces.Count; i++)
{
if (Faces[i].OutsideVertices.Count > 0)
{
return i;
}
}
return -1;
}
/// <summary>
/// Do we have a face with a non-empty outside set?
/// </summary>
/// <returns>true if we have a non-empty outside set</returns>
private bool HaveNonEmptyFaceSet()
{
foreach (Face f in Faces)
{
if (f.OutsideVertices.Count > 0)
{
return true;
}
}
return false;
}
/// <summary>
/// Checks if a > b by at least epsilon.
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns>true a > b</returns>
private bool isAGreaterThanB(float a, float b)
{
if (a - b > Epsilon)
{
return true;
}
return false;
}
/// <summary>
/// Checks if a < b by at least epsilon
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns>true if a < b</returns>
private bool isALessThanB(float a, float b)
{
if (b - a > Epsilon)
{
return true;
}
return false;
}
/// <summary>
/// Checks if a and b are approximately equal (the difference between them is < epsilon)
/// </summary>
/// <param name="a">a</param>
/// <param name="b">b</param>
/// <returns>true if they are approximately equal, false otherwise</returns>
private bool isApproxEqual(float a, float b)
{
return Mathf.Abs(a - b) < Epsilon;
}
/// <summary>
/// Checks if value is approximately zero by comparing abs(value) < epsilon.
/// </summary>
/// <param name="a">a</param>
/// <returns>true is a is approximately 0</returns>
private bool IsApproxZero(float a)
{
return Mathf.Abs(a) < Epsilon;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="horizon">horizon edge</param>
/// <param name="newFace">new face index</param>
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.
/// <summary>
/// Calculates the center of a face.
/// </summary>
Vector3 CalcFaceCenter(Face face)
{
return (VerticesList[face.V0] + VerticesList[face.V1] + VerticesList[face.V2]) / 3;
}
void DebugInitialPoints(List<Vector3> points, List<int> initialPoints)
{
string ints = "";
string vals = "";
foreach (int i in initialPoints)
{
ints += i + " : ";
vals += points[i] + " : ";
}
}
/// <summary>
/// Draws a faces points.
/// </summary>
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);
}
/// <summary>
/// Draws the normal of faces conected to the face provided. (f0 = r, f1 = g, f2 = b)
/// </summary>
/// <param name="face">Face to draw neighbours of</param>
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);
}
/// <summary>
/// draws a faces normal
/// </summary>
void DrawFaceNormal(Face face, Color color, float distance = 1.0f)
{
Vector3 center = CalcFaceCenter(face);
Debug.DrawLine(center, center + face.Normal * distance, color, DrawTime);
}
/// <summary>
/// Force verifys faces.
/// Was used to help solve the issue with incorrect horizon finding.
/// Left in for future issues and solutions.
/// </summary>
/// <param name="faceIndex">Index of face</param>
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;
}
}
}
}
/// <summary>
/// Generates a random color.
/// </summary>
/// <returns></returns>
Color RandomColor()
{
return new Color(Random.Range(0f, 1f), Random.Range(0f, 1f), Random.Range(0f, 1f));
}
/// <summary>
/// Draws a point at poisition
/// </summary>
/// <param name="point">point to draw</param>
/// <param name="color">color to draw with</param>
/// <param name="size">size to draw point</param>
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);
}
}
}