// // Is a point within a convex hull? // //If the point is on the hull it's "inside" public static bool PointWithinConvexHull(Vector3 point, HalfEdgeData3 convexHull) { bool isInside = true; float epsilon = MathUtility.EPSILON; //We know a point is within the hull if the point is inside all planes formed by the faces of the hull foreach (HalfEdgeFace3 triangle in convexHull.faces) { //Build a plane Plane3 plane = new Plane3(triangle.edge.v.position, triangle.edge.v.normal); //Find the distance to the plane from the point //The distance is negative if the point is inside the plane float distance = _Geometry.GetSignedDistanceFromPointToPlane(point, plane); //This point is outside, which means we don't need to test more planes if (distance > 0f + epsilon) { isInside = false; break; } } return(isInside); }
// // We have faces, but we also want a list with vertices, edges, etc // public static HalfEdgeData3 GenerateHalfEdgeDataFromFaces(HashSet <HalfEdgeFace3> faces) { HalfEdgeData3 meshData = new HalfEdgeData3(); //What we need to fill HashSet <HalfEdge3> edges = new HashSet <HalfEdge3>(); HashSet <HalfEdgeVertex3> verts = new HashSet <HalfEdgeVertex3>(); foreach (HalfEdgeFace3 f in faces) { //Get all edges in this face List <HalfEdge3> edgesInFace = f.GetEdges(); foreach (HalfEdge3 e in edgesInFace) { edges.Add(e); verts.Add(e.v); } } meshData.faces = faces; meshData.edges = edges; meshData.verts = verts; return(meshData); }
//We have faces, but we also want a list with vertices, edges, etc //Assume the faces are triangles public static HalfEdgeData3 GenerateHalfEdgeDataFromFaces(HashSet <HalfEdgeFace3> faces) { HalfEdgeData3 meshData = new HalfEdgeData3(); //WHat we need to fill HashSet <HalfEdge3> edges = new HashSet <HalfEdge3>(); HashSet <HalfEdgeVertex3> verts = new HashSet <HalfEdgeVertex3>(); foreach (HalfEdgeFace3 f in faces) { edges.Add(f.edge); edges.Add(f.edge.nextEdge); edges.Add(f.edge.nextEdge.nextEdge); verts.Add(f.edge.v); verts.Add(f.edge.nextEdge.v); verts.Add(f.edge.nextEdge.nextEdge.v); } meshData.faces = faces; meshData.edges = edges; meshData.verts = verts; return(meshData); }
public Hole(HalfEdgeData3 holeMeshI, HalfEdgeData3 holeMeshO, HalfEdge3 holeEdgeI, HalfEdge3 holeEdgeO) { this.holeMeshI = holeMeshI; this.holeMeshO = holeMeshO; this.holeEdgeI = holeEdgeI; this.holeEdgeO = holeEdgeO; }
//HalfEdgeData3 public HalfEdgeData3 UnNormalize(HalfEdgeData3 data) { foreach (HalfEdgeVertex3 v in data.verts) { MyVector3 vUnNormalized = UnNormalize(v.position); v.position = vUnNormalized; } return(data); }
//meshData should be triangles only //normalizer is just for debugging public static void Remove(HalfEdgeData3 meshData, Normalizer3 normalizer = null) { //We are going to remove the following (some triangles can be a combination of these): // - Caps. Triangle where one angle is close to 180 degrees. Are difficult to remove. If the vertex is connected to three triangles, we can maybe just remove the vertex and build one big triangle. This can be said to be a flat terahedron? // - Needles. Triangle where the longest edge is much longer than the shortest one. Same as saying that the smallest angle is close to 0 degrees? Can often be removed by collapsing the shortest edge // - Flat tetrahedrons. Find a vertex and if this vertex is surrounded by three triangles, and if all the vertex is roughly on the same plane as one of big triangle, then remove the vertex and replace it with one big triangle bool foundNeedle = false; bool foundFlatTetrahedron = false; int needleCounter = 0; int flatTetrahedronCounter = 0; int safety = 0; do { //Found an edge case where you can't remove needle by just merging the shortest edge, //because it resulted in aflat triangle //foundNeedle = RemoveNeedle(meshData, normalizer); foundFlatTetrahedron = RemoveFlatTetrahedrons(meshData, normalizer); if (foundNeedle) { needleCounter += 1; } if (foundFlatTetrahedron) { flatTetrahedronCounter += 1; } safety += 1; if (safety > 100000) { Debug.LogWarning("Stuck in infinite loop while removing unwanted triangles"); break; } }while (foundNeedle || foundFlatTetrahedron); Debug.Log($"Removed {needleCounter} needles and {flatTetrahedronCounter} flat tetrahedrons"); }
// // 3d space // //Iterative //Normalizer is only needed for debugging //removeUnwantedTriangles means that we will try to improve the quality of the triangles in the hull public static HalfEdgeData3 Iterative_3D(HashSet <Vector3> points, bool removeUnwantedTriangles, Normalizer3 normalizer = null) { List <Vector3> pointsList = new List <Vector3>(points); if (!CanFormConvexHull_3d(pointsList)) { return(null); } HalfEdgeData3 convexHull = IterativeHullAlgorithm3D.GenerateConvexHull(points, removeUnwantedTriangles, normalizer); return(convexHull); }
//Should return null if the mesh couldn't be cut because it doesn't intersect with the plane //Otherwise it should return two new meshes //meshTrans is needed so we can transform the cut plane to the mesh's local space public static List <Mesh> CutMesh(Transform meshTrans, OrientedPlane3 orientedCutPlaneGlobal) { //Validate the input data if (meshTrans == null) { Debug.Log("There's transform to cut"); return(null); } Mesh mesh = meshTrans.GetComponent <MeshFilter>().mesh; if (mesh == null) { Debug.Log("There's no mesh to cut"); return(null); } //The plane with just a normal Plane3 cutPlaneGlobal = orientedCutPlaneGlobal.Plane3; //First check if the AABB of the mesh is intersecting with the plane //Otherwise we can't cut the mesh, so its a waste of time //To get the AABB in world space we need to use the mesh renderer MeshRenderer mr = meshTrans.GetComponent <MeshRenderer>(); if (mr != null) { AABB3 aabb = new AABB3(mr.bounds); //The corners of this box HashSet <MyVector3> corners = aabb.GetCorners(); if (corners != null && corners.Count > 1) { //The points are in world space so use the plane in world space if (ArePointsOnOneSideOfPlane(new List <MyVector3>(corners), cutPlaneGlobal)) { Debug.Log("This mesh can't be cut because its AABB doesnt intersect with the plane"); return(null); } } } //The two meshes we might end up with after the cut //One is in front of the plane and another is in back of the plane HalfEdgeData3 newMeshO = new HalfEdgeData3(); HalfEdgeData3 newMeshI = new HalfEdgeData3(); //The data belonging to the original mesh Vector3[] vertices = mesh.vertices; int[] triangles = mesh.triangles; Vector3[] normals = mesh.normals; //Save the new edges we add when cutting triangles that intersects with the plane //Need to be edges so we can later connect them with each other to fill the hole //And to remove small triangles HashSet <HalfEdge3> newEdgesO = new HashSet <HalfEdge3>(); HashSet <HalfEdge3> newEdgesI = new HashSet <HalfEdge3>(); //Transform the plane from global space to local space of the mesh MyVector3 planePosLocal = meshTrans.InverseTransformPoint(cutPlaneGlobal.pos.ToVector3()).ToMyVector3(); MyVector3 planeNormalLocal = meshTrans.InverseTransformDirection(cutPlaneGlobal.normal.ToVector3()).ToMyVector3(); Plane3 cutPlane = new Plane3(planePosLocal, planeNormalLocal); //Loop through all triangles in the original mesh for (int i = 0; i < triangles.Length; i += 3) { //Get the triangle data we need int triangleIndex1 = triangles[i + 0]; int triangleIndex2 = triangles[i + 1]; int triangleIndex3 = triangles[i + 2]; //Positions Vector3 p1_unity = vertices[triangleIndex1]; Vector3 p2_unity = vertices[triangleIndex2]; Vector3 p3_unity = vertices[triangleIndex3]; MyVector3 p1 = p1_unity.ToMyVector3(); MyVector3 p2 = p2_unity.ToMyVector3(); MyVector3 p3 = p3_unity.ToMyVector3(); //Normals MyVector3 n1 = normals[triangleIndex1].ToMyVector3(); MyVector3 n2 = normals[triangleIndex2].ToMyVector3(); MyVector3 n3 = normals[triangleIndex3].ToMyVector3(); //To make it easier to send data to methods MyMeshVertex v1 = new MyMeshVertex(p1, n1); MyMeshVertex v2 = new MyMeshVertex(p2, n2); MyMeshVertex v3 = new MyMeshVertex(p3, n3); //First check on which side of the plane these vertices are //If they are all on one side we dont have to cut the triangle bool is_p1_front = _Geometry.IsPointOutsidePlane(cutPlane, v1.position); bool is_p2_front = _Geometry.IsPointOutsidePlane(cutPlane, v2.position); bool is_p3_front = _Geometry.IsPointOutsidePlane(cutPlane, v3.position); //Build triangles belonging to respective mesh //All are outside the plane if (is_p1_front && is_p2_front && is_p3_front) { AddTriangleToMesh(v1, v2, v3, newMeshO, newEdges: null); } //All are inside the plane else if (!is_p1_front && !is_p2_front && !is_p3_front) { AddTriangleToMesh(v1, v2, v3, newMeshI, newEdges: null); } //The vertices are on different sides of the plane, so we need to cut the triangle into 3 new triangles else { //We get 6 cases where each vertex is on its own in front or in the back of the plane //p1 is outside if (is_p1_front && !is_p2_front && !is_p3_front) { CutTriangleOneOutside(v1, v2, v3, newMeshO, newMeshI, newEdgesI, newEdgesO, cutPlane); } //p1 is inside else if (!is_p1_front && is_p2_front && is_p3_front) { CutTriangleTwoOutside(v2, v3, v1, newMeshO, newMeshI, newEdgesI, newEdgesO, cutPlane); } //p2 is outside else if (!is_p1_front && is_p2_front && !is_p3_front) { CutTriangleOneOutside(v2, v3, v1, newMeshO, newMeshI, newEdgesI, newEdgesO, cutPlane); } //p2 is inside else if (is_p1_front && !is_p2_front && is_p3_front) { CutTriangleTwoOutside(v3, v1, v2, newMeshO, newMeshI, newEdgesI, newEdgesO, cutPlane); } //p3 is outside else if (!is_p1_front && !is_p2_front && is_p3_front) { CutTriangleOneOutside(v3, v1, v2, newMeshO, newMeshI, newEdgesI, newEdgesO, cutPlane); } //p3 is inside else if (is_p1_front && is_p2_front && !is_p3_front) { CutTriangleTwoOutside(v1, v2, v3, newMeshO, newMeshI, newEdgesI, newEdgesO, cutPlane); } //Something is strange if we end up here... else { Debug.Log("No case was gound where we split triangle into 3 new triangles"); } } } //Generate the new meshes only needed the old mesh intersected with the plane if (newMeshO.verts.Count == 0 || newMeshI.verts.Count == 0) { return(null); } //Find opposite edges to each edge //This is a slow process, so should be done only if the mesh is intersecting with the plane newMeshO.ConnectAllEdges(); newMeshI.ConnectAllEdges(); //Display all edges which have no opposite DebugHalfEdge.DisplayEdgesWithNoOpposite(newMeshO.edges, meshTrans, Color.white); DebugHalfEdge.DisplayEdgesWithNoOpposite(newMeshI.edges, meshTrans, Color.white); //Remove small triangles at the seam where we did the cut because they will cause shading issues if the surface is smooth //RemoveSmallTriangles(F_Mesh, newEdges); //Split each mesh into separate meshes if the original mesh is not connected, meaning it has islands HashSet <HalfEdgeData3> newMeshesO = SeparateMeshIslands(newMeshO); HashSet <HalfEdgeData3> newMeshesI = SeparateMeshIslands(newMeshI); //Fill the holes in the mesh HashSet <Hole> allHoles = FillHoles(newEdgesI, newEdgesO, orientedCutPlaneGlobal, meshTrans, planeNormalLocal); //Connect the holes with respective mesh AddHolesToMeshes(newMeshesO, newMeshesI, allHoles); //Finally generate standardized Unity meshes List <Mesh> cuttedUnityMeshes = new List <Mesh>(); foreach (HalfEdgeData3 meshData in newMeshesO) { Mesh unityMesh = meshData.ConvertToUnityMesh("Outside mesh", shareVertices: true, generateNormals: false); cuttedUnityMeshes.Add(unityMesh); } foreach (HalfEdgeData3 meshData in newMeshesI) { Mesh unityMesh = meshData.ConvertToUnityMesh("Inside mesh", shareVertices: true, generateNormals: false); cuttedUnityMeshes.Add(unityMesh); } return(cuttedUnityMeshes); }
//Initialize by making 2 triangles by using three points, so its a flat triangle with a face on each side //We could use the ideas from Quickhull to make the start triangle as big as possible //Then find a point which is the furthest away as possible from these triangles //Add that point and you have a tetrahedron (triangular pyramid) public static void BuildFirstTetrahedron(HashSet <Vector3> points, HalfEdgeData3 convexHull) { //Of all points, find the two points that are furthes away from each other Edge3 eFurthestApart = FindEdgeFurthestApart(points); //Remove the two points we found points.Remove(eFurthestApart.p1); points.Remove(eFurthestApart.p2); //Find a point which is the furthest away from this edge //TODO: Is this point also on the AABB? So we don't have to search all remaining points... Vector3 pointFurthestAway = FindPointFurthestFromEdge(eFurthestApart, points); //Remove the point points.Remove(pointFurthestAway); //Display the triangle //Debug.DrawLine(eFurthestApart.p1.ToVector3(), eFurthestApart.p2.ToVector3(), Color.white, 1f); //Debug.DrawLine(eFurthestApart.p1.ToVector3(), pointFurthestAway.ToVector3(), Color.blue, 1f); //Debug.DrawLine(eFurthestApart.p2.ToVector3(), pointFurthestAway.ToVector3(), Color.blue, 1f); //Now we can build two triangles //It doesnt matter how we build these triangles as long as they are opposite //But the normal matters, so make sure it is calculated so the triangles are ordered clock-wise while the normal is pointing out Vector3 p1 = eFurthestApart.p1; Vector3 p2 = eFurthestApart.p2; Vector3 p3 = pointFurthestAway; convexHull.AddTriangle(p1, p2, p3); convexHull.AddTriangle(p1, p3, p2); //Debug.Log(convexHull.faces.Count); /* * foreach (HalfEdgeFace3 f in convexHull.faces) * { * TestAlgorithmsHelpMethods.DebugDrawTriangle(f, Color.white, Color.red); * } */ //Find the point which is furthest away from the triangle (this point cant be co-planar) List <HalfEdgeFace3> triangles = new List <HalfEdgeFace3>(convexHull.faces); //Just pick one of the triangles HalfEdgeFace3 triangle = triangles[0]; //Build a plane Plane3 plane = new Plane3(triangle.edge.v.position, triangle.edge.v.normal); //Find the point furthest away from the plane Vector3 p4 = FindPointFurthestAwayFromPlane(points, plane); //Remove the point points.Remove(p4); //Debug.DrawLine(p1.ToVector3(), p4.ToVector3(), Color.green, 1f); //Debug.DrawLine(p2.ToVector3(), p4.ToVector3(), Color.green, 1f); //Debug.DrawLine(p3.ToVector3(), p4.ToVector3(), Color.green, 1f); //Now we have to remove one of the triangles == the triangle the point is outside of HalfEdgeFace3 triangleToRemove = triangles[0]; HalfEdgeFace3 triangleToKeep = triangles[1]; //This means the point is inside the triangle-plane, so we have to switch //We used triangle #0 to generate the plane if (_Geometry.GetSignedDistanceFromPointToPlane(p4, plane) < 0f) { triangleToRemove = triangles[1]; triangleToKeep = triangles[0]; } //Delete the triangle convexHull.DeleteFace(triangleToRemove); //Build three new triangles //The triangle we keep is ordered clock-wise: Vector3 p1_opposite = triangleToKeep.edge.v.position; Vector3 p2_opposite = triangleToKeep.edge.nextEdge.v.position; Vector3 p3_opposite = triangleToKeep.edge.nextEdge.nextEdge.v.position; //But we are looking at it from the back-side, //so we add those vertices counter-clock-wise to make the new triangles clock-wise convexHull.AddTriangle(p1_opposite, p3_opposite, p4); convexHull.AddTriangle(p3_opposite, p2_opposite, p4); convexHull.AddTriangle(p2_opposite, p1_opposite, p4); //Make sure all opposite edges are connected convexHull.ConnectAllEdgesSlow(); //Debug.Log(convexHull.faces.Count); //Display what weve got so far //foreach (HalfEdgeFace3 f in convexHull.faces) //{ // TestAlgorithmsHelpMethods.DebugDrawTriangle(f, Color.white, Color.red); //} /* * //Now we might as well remove all the points that are within the tetrahedron because they are not on the hull * //But this is slow if we have many points and none of them are inside * HashSet<MyVector3> pointsToRemove = new HashSet<MyVector3>(); * * foreach (MyVector3 p in points) * { * bool isWithinConvexHull = _Intersections.PointWithinConvexHull(p, convexHull); * * if (isWithinConvexHull) * { * pointsToRemove.Add(p); * } * } * * Debug.Log($"Removed {pointsToRemove.Count} points because they were within the tetrahedron"); * * foreach (MyVector3 p in pointsToRemove) * { * points.Remove(p); * } */ }
/// <summary> /// /// </summary> /// <param name="points">The points from which we want to build the convex hull</param> /// <param name="removeUnwantedTriangles">At the end of the algorithm, try to remove triangles from the hull that we dont want, //such as needles where one edge is much shorter than the other edges in the triangle</param> /// <param name="normalizer">Is only needed for debugging</param> /// <returns></returns> public static HalfEdgeData3 GenerateConvexHull(HashSet <Vector3> points, bool removeUnwantedTriangles, Normalizer3 normalizer = null) { HalfEdgeData3 convexHull = new HalfEdgeData3(); //Step 1. Init by making a tetrahedron (triangular pyramid) and remove all points within the tetrahedron System.Diagnostics.Stopwatch timer = new System.Diagnostics.Stopwatch(); //timer.Start(); BuildFirstTetrahedron(points, convexHull); //timer.Stop(); //Debug.Log($"Testrahedron {timer.ElapsedMilliseconds/1000f}"); //Debug.Log(convexHull.faces.Count); //return convexHull; //Step 2. For each other point: // -If the point is within the hull constrcuted so far, remove it // - Otherwise, see which triangles are visible to the point and remove them // Then build new triangles from the edges that have no neighbor to the point List <Vector3> pointsToAdd = new List <Vector3>(points); int removedPointsCounter = 0; //int debugCounter = 0; foreach (Vector3 p in pointsToAdd) { //Is this point within the tetrahedron bool isWithinHull = _Intersections.PointWithinConvexHull(p, convexHull); if (isWithinHull) { points.Remove(p); removedPointsCounter += 1; continue; } //Find visible triangles and edges on the border between the visible and invisible triangles HashSet <HalfEdgeFace3> visibleTriangles = null; HashSet <HalfEdge3> borderEdges = null; FindVisibleTrianglesAndBorderEdgesFromPoint(p, convexHull, out visibleTriangles, out borderEdges); //Remove all visible triangles foreach (HalfEdgeFace3 triangle in visibleTriangles) { convexHull.DeleteFace(triangle); } //Make new triangle by connecting all edges on the border with the point //Debug.Log($"Number of border edges: {borderEdges.Count}"); //int debugStop = 11; //Save all ned edges so we can connect them with an opposite edge //To make it faster you can use the ideas in the Valve paper to get a sorted list of newEdges HashSet <HalfEdge3> newEdges = new HashSet <HalfEdge3>(); foreach (HalfEdge3 borderEdge in borderEdges) { //Each edge is point TO a vertex Vector3 p1 = borderEdge.prevEdge.v.position; Vector3 p2 = borderEdge.v.position; /* * if (debugCounter > debugStop) * { * Debug.DrawLine(normalizer.UnNormalize(p1).ToVector3(), normalizer.UnNormalize(p2).ToVector3(), Color.white, 2f); * * Debug.DrawLine(normalizer.UnNormalize(p1).ToVector3(), normalizer.UnNormalize(p).ToVector3(), Color.gray, 2f); * Debug.DrawLine(normalizer.UnNormalize(p2).ToVector3(), normalizer.UnNormalize(p).ToVector3(), Color.gray, 2f); * * convexHull.AddTriangle(p2, p1, p); * } * else * { * //Debug.Log(borderEdge.face); * * convexHull.AddTriangle(p2, p1, p); * } */ //The border edge belongs to a triangle which is invisible //Because triangles are oriented clockwise, we have to add the vertices in the other direction //to build a new triangle with the point HalfEdgeFace3 newTriangle = convexHull.AddTriangle(p2, p1, p); //Connect the new triangle with the opposite edge on the border //When we create the face we give it a reference edge which goes to p2 //So the edge we want to connect is the next edge HalfEdge3 edgeToConnect = newTriangle.edge.nextEdge; edgeToConnect.oppositeEdge = borderEdge; borderEdge.oppositeEdge = edgeToConnect; //Two edges are still not connected, so save those HalfEdge3 e1 = newTriangle.edge; //HalfEdge3 e2 = newTriangle.edge.nextEdge; HalfEdge3 e3 = newTriangle.edge.nextEdge.nextEdge; newEdges.Add(e1); //newEdges.Add(e2); newEdges.Add(e3); } //timer.Start(); //Two edges in each triangle are still not connected with an opposite edge foreach (HalfEdge3 e in newEdges) { if (e.oppositeEdge != null) { continue; } convexHull.TryFindOppositeEdge(e, newEdges); } //timer.Stop(); //Connect all new triangles and the triangles on the border, //so each edge has an opposite edge or flood filling will be impossible //timer.Start(); //convexHull.ConnectAllEdges(); //timer.Stop(); //if (debugCounter > debugStop) //{ // break; //} //debugCounter += 1; } //Debug.Log($"Connect half-edges took {timer.ElapsedMilliseconds/1000f} seconds"); Debug.Log($"Removed {removedPointsCounter} points during the construction of the hull because they were inside the hull"); // // Clean up // //Merge concave edges according to the paper //Remove unwanted triangles, such as slivers and needles //Which is maybe not needed because when you add a Unity convex mesh collider to the result of this algorithm, there are still slivers //Unity's mesh collider is also using quads and not just triangles //But if you add enough points, so you end up with many points on the hull you can see that Unitys convex mesh collider is not capturing all points, so they must be using some simplification algorithm //Run the hull through the mesh simplification algorithm if (removeUnwantedTriangles) { convexHull = MeshSimplification_QEM.Simplify(convexHull, maxEdgesToContract: int.MaxValue, maxError: 0.0001f, normalizeTriangles: true); } return(convexHull); }
//Find all visible triangles from a point //Also find edges on the border between invisible and visible triangles public static void FindVisibleTrianglesAndBorderEdgesFromPoint(Vector3 p, HalfEdgeData3 convexHull, out HashSet <HalfEdgeFace3> visibleTriangles, out HashSet <HalfEdge3> borderEdges) { //Flood-fill from the visible triangle to find all other visible triangles //When you cross an edge from a visible triangle to an invisible triangle, //save the edge because thhose edge should be used to build triangles with the point //These edges should belong to the triangle which is not visible borderEdges = new HashSet <HalfEdge3>(); //Store all visible triangles here so we can't visit triangles multiple times visibleTriangles = new HashSet <HalfEdgeFace3>(); //Start the flood-fill by finding a triangle which is visible from the point //A triangle is visible if the point is outside the plane formed at the triangles //Another sources is using the signed volume of a tetrahedron formed by the triangle and the point HalfEdgeFace3 visibleTriangle = FindVisibleTriangleFromPoint(p, convexHull.faces); //If we didn't find a visible triangle, we have some kind of edge case and should move on for now if (visibleTriangle == null) { Debug.LogWarning("Couldn't find a visible triangle so will ignore the point"); return; } //The queue which we will use when flood-filling Queue <HalfEdgeFace3> trianglesToFloodFrom = new Queue <HalfEdgeFace3>(); //Add the first triangle to init the flood-fill trianglesToFloodFrom.Enqueue(visibleTriangle); List <HalfEdge3> edgesToCross = new List <HalfEdge3>(); int safety = 0; while (true) { //We have visited all visible triangles if (trianglesToFloodFrom.Count == 0) { break; } HalfEdgeFace3 triangleToFloodFrom = trianglesToFloodFrom.Dequeue(); //This triangle is always visible and should be deleted visibleTriangles.Add(triangleToFloodFrom); //Investigate bordering triangles edgesToCross.Clear(); edgesToCross.Add(triangleToFloodFrom.edge); edgesToCross.Add(triangleToFloodFrom.edge.nextEdge); edgesToCross.Add(triangleToFloodFrom.edge.nextEdge.nextEdge); //Jump from this triangle to a bordering triangle foreach (HalfEdge3 edgeToCross in edgesToCross) { HalfEdge3 oppositeEdge = edgeToCross.oppositeEdge; if (oppositeEdge == null) { Debug.LogWarning("Found an opposite edge which is null"); break; } HalfEdgeFace3 oppositeTriangle = oppositeEdge.face; //Have we visited this triangle before (only test visible triangles)? if (trianglesToFloodFrom.Contains(oppositeTriangle) || visibleTriangles.Contains(oppositeTriangle)) { continue; } //Check if this triangle is visible //A triangle is visible from a point the point is outside of a plane formed with the triangles position and normal Plane3 plane = new Plane3(oppositeTriangle.edge.v.position, oppositeTriangle.edge.v.normal); bool isPointOutsidePlane = _Geometry.IsPointOutsidePlane(p, plane); //This triangle is visible so save it so we can flood from it if (isPointOutsidePlane) { trianglesToFloodFrom.Enqueue(oppositeTriangle); } //This triangle is invisible. Since we only flood from visible triangles, //it means we crossed from a visible triangle to an invisible triangle, so save the crossing edge else { borderEdges.Add(oppositeEdge); } } safety += 1; if (safety > 50000) { Debug.Log("Stuck in infinite loop when flood-filling visible triangles"); break; } } }
//Return all edges going to this vertex = all edges that references this vertex position //meshData is needed if we cant spring around this vertex because there might be a hole in the mesh //If so we have to search through all edges in the entire mesh public HashSet <HalfEdge3> GetEdgesPointingToVertex(HalfEdgeData3 meshData) { HashSet <HalfEdge3> allEdgesGoingToVertex = new HashSet <HalfEdge3>(); //This is the edge that goes to this vertex HalfEdge3 currentEdge = this.edge.prevEdge; //if (currentEdge == null) Debug.Log("Edge is null"); int safety = 0; do { allEdgesGoingToVertex.Add(currentEdge); //This edge is going to the vertex but in another triangle HalfEdge3 oppositeEdge = currentEdge.oppositeEdge; if (oppositeEdge == null) { Debug.LogWarning("We cant rotate around this vertex because there are holes in the mesh"); //Better to clear than to null or we have to create a new hashset when filling it the brute force way allEdgesGoingToVertex.Clear(); break; } //if (oppositeEdge == null) Debug.Log("Opposite edge is null"); currentEdge = oppositeEdge.prevEdge; safety += 1; if (safety > 1000) { Debug.LogWarning("Stuck in infinite loop when getting all edges around a vertex"); allEdgesGoingToVertex.Clear(); break; } }while (currentEdge != this.edge.prevEdge); //If there are holes in the triangulation around the vertex, //we have to use the brute force approach and look at edges if (allEdgesGoingToVertex.Count == 0 && meshData != null) { HashSet <HalfEdge3> edges = meshData.edges; foreach (HalfEdge3 e in edges) { //An edge points TO a vertex if (e.v.position.Equals(position)) { allEdgesGoingToVertex.Add(e); } } } return(allEdgesGoingToVertex); }
//Cut a triangle where two vertices are inside and the other vertex is outside //Make sure they are sorted clockwise: O1-O2-I1 //F means that this vertex is outside the plane private static void CutTriangleTwoOutside(MyMeshVertex O1, MyMeshVertex O2, MyMeshVertex I1, HalfEdgeData3 newMeshO, HalfEdgeData3 newMeshI, HashSet <HalfEdge3> newEdgesI, HashSet <HalfEdge3> newEdgesO, Plane3 cutPlane) { //Cut the triangle by using edge-plane intersection //Triangles in Unity are ordered clockwise, so form edges that intersects with the plane: Edge3 e_O2I1 = new Edge3(O2.position, I1.position); //Edge3 e_F1F2 = new Edge3(F1, F2); //Not needed because never intersects with the plane Edge3 e_I1O1 = new Edge3(I1.position, O1.position); //The positions of the intersection vertices MyVector3 pos_O2I1 = _Intersections.GetLinePlaneIntersectionPoint(cutPlane, e_O2I1); MyVector3 pos_I1O1 = _Intersections.GetLinePlaneIntersectionPoint(cutPlane, e_I1O1); //The normals of the intersection vertices float percentageBetween_O2I1 = MyVector3.Distance(O2.position, pos_O2I1) / MyVector3.Distance(O2.position, I1.position); float percentageBetween_I1O1 = MyVector3.Distance(I1.position, pos_I1O1) / MyVector3.Distance(I1.position, O1.position); MyVector3 normal_O2I1 = _Interpolation.Lerp(O2.normal, I1.normal, percentageBetween_O2I1); MyVector3 normal_I1O1 = _Interpolation.Lerp(I1.normal, O1.normal, percentageBetween_I1O1); //MyVector3 normal_F2B1 = Vector3.Slerp(F2.normal.ToVector3(), B1.normal.ToVector3(), percentageBetween_F2B1).ToMyVector3(); //MyVector3 normal_B1F1 = Vector3.Slerp(B1.normal.ToVector3(), F1.normal.ToVector3(), percentageBetween_B1F1).ToMyVector3(); normal_O2I1 = MyVector3.Normalize(normal_O2I1); normal_I1O1 = MyVector3.Normalize(normal_I1O1); //The intersection vertices MyMeshVertex v_O2I1 = new MyMeshVertex(pos_O2I1, normal_O2I1); MyMeshVertex v_I1O1 = new MyMeshVertex(pos_I1O1, normal_I1O1); //Form 3 new triangles //Outside AddTriangleToMesh(v_O2I1, v_I1O1, O2, newMeshO, newEdgesO); AddTriangleToMesh(O2, v_I1O1, O1, newMeshO, null); //Inside AddTriangleToMesh(v_I1O1, v_O2I1, I1, newMeshI, newEdgesI); }
//Separate a mesh by its islands (if it has islands) private static HashSet <HalfEdgeData3> SeparateMeshIslands(HalfEdgeData3 meshData) { HashSet <HalfEdgeData3> meshIslands = new HashSet <HalfEdgeData3>(); HashSet <HalfEdgeFace3> allFaces = meshData.faces; //Separate by flood-filling //Faces belonging to a separate island HashSet <HalfEdgeFace3> facesOnThisIsland = new HashSet <HalfEdgeFace3>(); //Faces we havent flodded from yet Queue <HalfEdgeFace3> facesToFloodFrom = new Queue <HalfEdgeFace3>(); //Add a first face to the queue HalfEdgeFace3 firstFace = allFaces.FakePop(); facesToFloodFrom.Enqueue(firstFace); int numberOfIslands = 0; List <HalfEdge3> edges = new List <HalfEdge3>(); int safety = 0; while (true) { //If the queue is empty, it means we have flooded this island if (facesToFloodFrom.Count == 0) { numberOfIslands += 1; //Generate the new half-edge data structure from the faces that belong to this island HalfEdgeData3 meshIsland = HalfEdgeData3.GenerateHalfEdgeDataFromFaces(facesOnThisIsland); meshIslands.Add(meshIsland); //We still have faces to visit, so they must be on a new island if (allFaces.Count > 0) { facesOnThisIsland = new HashSet <HalfEdgeFace3>(); //Add a first face to the queue firstFace = allFaces.FakePop(); facesToFloodFrom.Enqueue(firstFace); } else { Debug.Log($"This mesh has {numberOfIslands} islands"); break; } } HalfEdgeFace3 f = facesToFloodFrom.Dequeue(); facesOnThisIsland.Add(f); //Remove from the original mesh so we can identify if we need to start at a new island allFaces.Remove(f); //Find neighboring faces edges.Clear(); edges.Add(f.edge); edges.Add(f.edge.nextEdge); edges.Add(f.edge.nextEdge.nextEdge); foreach (HalfEdge3 e in edges) { if (e.oppositeEdge != null) { HalfEdgeFace3 fNeighbor = e.oppositeEdge.face; //If we haven't seen this face before if (!facesOnThisIsland.Contains(fNeighbor) && !facesToFloodFrom.Contains(fNeighbor)) { facesToFloodFrom.Enqueue(fNeighbor); } } //Here we could mabe save all edges with no opposite, meaning its an edge at the hole } safety += 1; if (safety > 50000) { Debug.Log("Stuck in infinite loop when generating mesh islands"); break; } } return(meshIslands); }
//Needles. Triangle where the longest edge is much longer than the shortest one. private static bool RemoveNeedle(HalfEdgeData3 meshData, Normalizer3 normalizer = null) { HashSet <HalfEdgeFace3> triangles = meshData.faces; bool foundNeedle = false; foreach (HalfEdgeFace3 triangle in triangles) { /* * List<HalfEdge3> edges = triangle.GetEdges(); * * //Sort the edges from shortest to longest * List<HalfEdge3> edgesSorted = edges.OrderBy(e => e.Length()).ToList(); * * //The ratio between the shortest and longest side * float edgeLengthRatio = edgesSorted[0].Length() / edgesSorted[2].Length(); */ //Instead of using a million lists, we know we have just three edges we have to sort, so we can do better HalfEdge3 e1 = triangle.edge; HalfEdge3 e2 = triangle.edge.nextEdge; HalfEdge3 e3 = triangle.edge.nextEdge.nextEdge; //We want e1 to be the shortest and e3 to be the longest if (e1.SqrLength() > e3.SqrLength()) { (e1, e3) = (e3, e1); } if (e1.SqrLength() > e2.SqrLength()) { (e1, e2) = (e2, e1); } //e1 is now the shortest edge, so we just need to check the second and third if (e2.SqrLength() > e3.SqrLength()) { (e2, e3) = (e3, e2); } //The ratio between the shortest and longest edge float edgeLengthRatio = e1.Length() / e3.Length(); //This is a needle if (edgeLengthRatio < NEEDLE_RATIO) { //Debug.Log("We found a needle triangle"); TestAlgorithmsHelpMethods.DebugDrawTriangle(triangle, Color.blue, Color.red, normalizer); //Remove the needle by merging the shortest edge MyVector3 mergePosition = (e1.v.position + e1.prevEdge.v.position) * 0.5f; meshData.ContractTriangleHalfEdge(e1, mergePosition); foundNeedle = true; //Now we have to restart because the triangulation has changed break; } } return(foundNeedle); }
//Remove flat tetrahedrons (a vertex in a triangle) private static bool RemoveFlatTetrahedrons(HalfEdgeData3 meshData, Normalizer3 normalizer = null) { HashSet <HalfEdgeVertex3> vertices = meshData.verts; bool foundFlatTetrahedron = false; foreach (HalfEdgeVertex3 vertex in vertices) { HashSet <HalfEdge3> edgesGoingToVertex = vertex.GetEdgesPointingToVertex(meshData); if (edgesGoingToVertex.Count == 3) { //Find the vertices of the triangle covering this vertex clock-wise HalfEdgeVertex3 v1 = vertex.edge.v; HalfEdgeVertex3 v2 = vertex.edge.prevEdge.oppositeEdge.v; HalfEdgeVertex3 v3 = vertex.edge.oppositeEdge.nextEdge.v; //Build a plane MyVector3 normal = MyVector3.Normalize(MyVector3.Cross(v3.position - v2.position, v1.position - v2.position)); Plane3 plane = new Plane3(v1.position, normal); //Find the distance from the vertex to the plane float distance = _Geometry.GetSignedDistanceFromPointToPlane(vertex.position, plane); distance = Mathf.Abs(distance); if (distance < FLAT_TETRAHEDRON_DISTANCE) { //Debug.Log("Found flat tetrahedron"); Vector3 p1 = normalizer.UnNormalize(v1.position).ToVector3(); Vector3 p2 = normalizer.UnNormalize(v2.position).ToVector3(); Vector3 p3 = normalizer.UnNormalize(v3.position).ToVector3(); TestAlgorithmsHelpMethods.DebugDrawTriangle(p1, p2, p3, normal.ToVector3(), Color.blue, Color.red); foundFlatTetrahedron = true; //Save the opposite edges HashSet <HalfEdge3> oppositeEdges = new HashSet <HalfEdge3>(); oppositeEdges.Add(v1.edge.oppositeEdge); oppositeEdges.Add(v2.edge.oppositeEdge); oppositeEdges.Add(v3.edge.oppositeEdge); //Remove the three triangles foreach (HalfEdge3 e in edgesGoingToVertex) { meshData.DeleteFace(e.face); } //Add the new triangle (could maybe connect it ourselves) HalfEdgeFace3 newTriangle = meshData.AddTriangle(v1.position, v2.position, v3.position, findOppositeEdge: false); meshData.TryFindOppositeEdge(newTriangle.edge, oppositeEdges); meshData.TryFindOppositeEdge(newTriangle.edge.nextEdge, oppositeEdges); meshData.TryFindOppositeEdge(newTriangle.edge.nextEdge.nextEdge, oppositeEdges); break; } } } return(foundFlatTetrahedron); }
//Generate a Voronoi diagram in 3d space given a Delaunay triangulation in 3d space public static HashSet <VoronoiCell3> GenerateVoronoiDiagram(HalfEdgeData3 delaunayTriangulation) { //If we dont need the voronoi sitePos, which is the center of the voronoi cell, we can use the half-edge data structure //If not we have the create a child class for voronoi HashSet <VoronoiCell3> voronoiDiagram = new HashSet <VoronoiCell3>(); //Step 1. Generate a center of circle for each triangle because this process is slow in 3d space Dictionary <HalfEdgeFace3, Vector3> circleCenterLookup = new Dictionary <HalfEdgeFace3, Vector3>(); HashSet <HalfEdgeFace3> delaunayTriangles = delaunayTriangulation.faces; foreach (HalfEdgeFace3 triangle in delaunayTriangles) { Vector3 p1 = triangle.edge.v.position; Vector3 p2 = triangle.edge.nextEdge.v.position; Vector3 p3 = triangle.edge.nextEdge.nextEdge.v.position; Vector3 circleCenter = _Geometry.CalculateCircleCenter(p1, p2, p3); //https://www.redblobgames.com/x/1842-delaunay-voronoi-sphere/ suggested circleCenter should be moved to get a better surface //But it generates a bad result //float d = Mathf.Sqrt(circleCenter.x * circleCenter.x + circleCenter.y * circleCenter.y + circleCenter.z * circleCenter.z); //MyVector3 circleCenterMove = new MyVector3(circleCenter.x / d, circleCenter.y / d, circleCenter.z / d); //circleCenter = circleCenterMove; circleCenterLookup.Add(triangle, circleCenter); } //Step 2. Generate the voronoi cells HashSet <HalfEdgeVertex3> delaunayVertices = delaunayTriangulation.verts; //In the half-edge data structure we have multiple vertices at the same position, //so we have to track which vertex positions have been added HashSet <Vector3> addedSites = new HashSet <Vector3>(); foreach (HalfEdgeVertex3 v in delaunayVertices) { //Has this site already been added? if (addedSites.Contains(v.position)) { continue; } addedSites.Add(v.position); //This vertex is a cite pos in the voronoi diagram VoronoiCell3 cell = new VoronoiCell3(v.position); voronoiDiagram.Add(cell); //All triangles are fully connected so no null opposite edges should exist //So to generate the voronoi cell, we just rotate clock-wise around each vertex in the delaunay triangulation HalfEdge3 currentEdge = v.edge; int safety = 0; while (true) { //Build an edge going from the opposite face to this face //Each vertex has an edge going FROM it HalfEdgeFace3 oppositeTriangle = currentEdge.oppositeEdge.face; HalfEdgeFace3 thisTriangle = currentEdge.face; Vector3 oppositeCircleCenter = circleCenterLookup[oppositeTriangle]; Vector3 thisCircleCenter = circleCenterLookup[thisTriangle]; VoronoiEdge3 edge = new VoronoiEdge3(oppositeCircleCenter, thisCircleCenter, v.position); cell.edges.Add(edge); //Jump to the next triangle //Each vertex has an edge going FROM it //And we want to rotate around a vertex clockwise //So the edge we should jump over is: HalfEdge3 jumpEdge = currentEdge.nextEdge.nextEdge; HalfEdge3 oppositeEdge = jumpEdge.oppositeEdge; //Are we back where we started? if (oppositeEdge == v.edge) { break; } currentEdge = oppositeEdge; safety += 1; if (safety > 10000) { Debug.Log("Stuck in infinite loop when generating voronoi cells"); break; } } } return(voronoiDiagram); }
//Algorithm 2. Voronoi by adding point after point //Algorithm 3. 3D Delaunay to Voronoi public static HashSet <VoronoiCell3> Delaunay3DToVoronoi(HalfEdgeData3 delaunayTriangulation) { HashSet <VoronoiCell3> voronoiCells = Delaunay3DToVoronoiAlgorithm.GenerateVoronoiDiagram(delaunayTriangulation); return(voronoiCells); }
//Fill the hole (or holes) in the mesh private static HashSet <Hole> FillHoles(HashSet <HalfEdge3> holeEdgesI, HashSet <HalfEdge3> holeEdgesO, OrientedPlane3 orientedCutPlane, Transform meshTrans, MyVector3 planeNormal) { if (holeEdgesI == null || holeEdgesI.Count == 0) { Debug.Log("This mesh has no hole"); return(null); } //Find all separate holes HashSet <List <HalfEdge3> > allHoles = IdentifySeparateHoles(holeEdgesI); if (allHoles.Count == 0) { Debug.LogWarning("Couldn't identify any holes even though we have hole edges"); return(null); } //Debug //foreach (List<HalfEdge3> hole in allHoles) //{ // foreach (HalfEdge3 e in hole) // { // Debug.DrawLine(meshTrans.TransformPoint(e.v.position.ToVector3()), Vector3.zero, Color.white, 5f); // } //} //Fill the hole with a mesh HashSet <Hole> holeMeshes = new HashSet <Hole>(); foreach (List <HalfEdge3> hole in allHoles) { HalfEdgeData3 holeMeshI = new HalfEdgeData3(); HalfEdgeData3 holeMeshO = new HalfEdgeData3(); //Transform vertices to local position of the cut plane to make it easier to triangulate with Ear Clipping //Ear CLipping wants vertices in 2d List <MyVector2> sortedEdges_2D = new List <MyVector2>(); Transform planeTrans = orientedCutPlane.planeTrans; foreach (HalfEdge3 e in hole) { MyVector3 pMeshSpace = e.v.position; //Mesh space to Global space Vector3 pGlobalSpace = meshTrans.TransformPoint(pMeshSpace.ToVector3()); //Global space to Plane space Vector3 pPlaneSpace = planeTrans.InverseTransformPoint(pGlobalSpace); //Y is normal direction so should be 0 MyVector2 p2D = new MyVector2(pPlaneSpace.x, pPlaneSpace.z); sortedEdges_2D.Add(p2D); } //Triangulate with Ear Clipping HashSet <Triangle2> triangles = _EarClipping.Triangulate(sortedEdges_2D, null, optimizeTriangles: false); //Debug.Log($"Number of triangles from Ear Clipping: {triangles.Count}"); //Transform vertices to mesh space and half-edge data structure foreach (Triangle2 t in triangles) { //3d space Vector3 p1 = new Vector3(t.p1.x, 0f, t.p1.y); Vector3 p2 = new Vector3(t.p2.x, 0f, t.p2.y); Vector3 p3 = new Vector3(t.p3.x, 0f, t.p3.y); //Plane space to Global space Vector3 p1Global = planeTrans.TransformPoint(p1); Vector3 p2Global = planeTrans.TransformPoint(p2); Vector3 p3Global = planeTrans.TransformPoint(p3); //Global space to Mesh space Vector3 p1Mesh = meshTrans.InverseTransformPoint(p1Global); Vector3 p2Mesh = meshTrans.InverseTransformPoint(p2Global); Vector3 p3Mesh = meshTrans.InverseTransformPoint(p3Global); //For inside mesh MyMeshVertex v1_I = new MyMeshVertex(p1Mesh.ToMyVector3(), planeNormal); MyMeshVertex v2_I = new MyMeshVertex(p2Mesh.ToMyVector3(), planeNormal); MyMeshVertex v3_I = new MyMeshVertex(p3Mesh.ToMyVector3(), planeNormal); //For inside mesh MyMeshVertex v1_O = new MyMeshVertex(p1Mesh.ToMyVector3(), -planeNormal); MyMeshVertex v2_O = new MyMeshVertex(p2Mesh.ToMyVector3(), -planeNormal); MyMeshVertex v3_O = new MyMeshVertex(p3Mesh.ToMyVector3(), -planeNormal); //Now we can finally add this triangle to the half-edge data structure AddTriangleToMesh(v1_I, v2_I, v3_I, holeMeshI, null); AddTriangleToMesh(v1_O, v3_O, v2_O, holeMeshO, null); } //We also need an edge belonging to the mesh (not hole mesh) to easier merge mesh with hole //The hole edges were generated for the Inside mesh HalfEdge3 holeEdgeI = hole[0]; //But we also need an edge for the Outside mesh bool foundCorrespondingEdge = false; MyVector3 eGoingTo = holeEdgeI.v.position; MyVector3 eGoingFrom = holeEdgeI.prevEdge.v.position; foreach (HalfEdge3 holeEdgeO in holeEdgesO) { MyVector3 eOppsiteGoingTo = holeEdgeO.v.position; MyVector3 eOppsiteGoingFrom = holeEdgeO.prevEdge.v.position; if (eOppsiteGoingTo.Equals(eGoingFrom) && eOppsiteGoingFrom.Equals(eGoingTo)) { Hole newHoleMesh = new Hole(holeMeshI, holeMeshO, holeEdgeI, holeEdgeO); holeMeshes.Add(newHoleMesh); foundCorrespondingEdge = true; break; } } if (!foundCorrespondingEdge) { Debug.Log("Couldnt find opposite edge in hole, so no hole was added"); } } return(holeMeshes); }
//TODO: //- Calculate the optimal contraction target v and not just the average between two vertices or one of the two endpoints //- Sometimes at the end of a simplification process, the QEM is NaN because the normal of the triangle has length 0 because two vertices are at the same position. This has maybe to do with "mesh inversion." The reports says that you should compare the normal of each neighboring face before and after the contraction. If the normal flips, undo the contraction or penalize it. The temp solution to solve this problem is to set the matrix to zero matrix if the normal is NaN //- The algorithm can also join vertices that are within ||v1 - v2|| < distance, so test to add that. It should merge the hole in the bunny //- Maybe there's a faster (and simpler) way by using unique edges instead of double the calculations for an edge going in the opposite direction? //- A major bottleneck is finding edges going to a specific vertex. The problem is that if there are holes in the mesh, we can't just rotate around the vertex to find the edges - we have to search through ALL edges. In the regular mesh structure, we have a list of all vertices, so moving a vertex would be fast if we moved it in that list, so all edges should reference a vertex in a list? //- Is edgesToContract the correct way to stop the algorithm? Maybe it should be number of vertices in the final mesh? //- Visualize the error by using some color scale. //- Some times when we contract an edge we end up with invalid triangles, such as triangles with area 0. Are all these automatically removed if we weigh each error with triangle area? Or do we need to check that the triangulation is valid after contracting an edge? /// <summary> /// Merge edges to simplify a mesh /// Based on reports by Garland and Heckbert, "Surface simplification using quadric error metrics" /// Is called: "Iterative pair contraction with the Quadric Error Metric (QEM)" /// </summary> /// <param name="halfEdgeMeshData">Original mesh</param> /// <param name="maxEdgesToContract">How many edges do we want to merge (the algorithm stops if it can't merge more edges)</param> /// <param name="maxError">Stop merging edges if the error is bigger than the maxError, which will prevent the algorithm from changing the shape of the mesh</param> /// <param name="normalizeTriangles">Sometimes the quality improves if we take triangle area into account when calculating ther error</param> /// <param name="normalizer">Is only needed for debugging</param> /// <returns>The simplified mesh</returns> /// If you set edgesToContract to max value, then it will continue until it cant merge any more edges or the maxError is reached /// If you set maxError to max value, then it will continue to merge edges until it cant merge or max edgesToContract is reached public static HalfEdgeData3 Simplify(HalfEdgeData3 halfEdgeMeshData, int maxEdgesToContract, float maxError, bool normalizeTriangles = false, Normalizer3 normalizer = null) { System.Diagnostics.Stopwatch timer = new System.Diagnostics.Stopwatch(); // // Compute the Q matrices for all the initial vertices // //Put the result in a lookup dictionary //This assumes we have no floating point precision issues, so vertices at the same position have to be at the same position Dictionary <Vector3, Matrix4x4> qMatrices = new Dictionary <Vector3, Matrix4x4>(); HashSet <HalfEdgeVertex3> vertices = halfEdgeMeshData.verts; //timer.Start(); //0.142 seconds for the bunny (0.012 for dictionary lookup, 0.024 to calculate the Q matrices, 0.087 to find edges going to vertex) foreach (HalfEdgeVertex3 v in vertices) { //Have we already calculated a Q matrix for this vertex? //Remember that we have multiple vertices at the same position in the half-edge data structure //timer.Start(); if (qMatrices.ContainsKey(v.position)) { continue; } //timer.Stop(); //Calculate the Q matrix for this vertex //timer.Start(); //Find all edges meeting at this vertex HashSet <HalfEdge3> edgesPointingToThisVertex = v.GetEdgesPointingToVertex(halfEdgeMeshData); //timer.Stop(); //timer.Start(); Matrix4x4 Q = CalculateQMatrix(edgesPointingToThisVertex, normalizeTriangles); //timer.Stop(); qMatrices.Add(v.position, Q); } //timer.Stop(); // // Select all valid pairs that can be contracted // List <HalfEdge3> validPairs = new List <HalfEdge3>(halfEdgeMeshData.edges); // // Compute the cost of contraction for each pair // HashSet <QEM_Edge> QEM_edges = new HashSet <QEM_Edge>(); //We need a lookup table to faster remove and update QEM_edges Dictionary <HalfEdge3, QEM_Edge> halfEdge_QEM_Lookup = new Dictionary <HalfEdge3, QEM_Edge>(); foreach (HalfEdge3 halfEdge in validPairs) { Vector3 p1 = halfEdge.prevEdge.v.position; Vector3 p2 = halfEdge.v.position; Matrix4x4 Q1 = qMatrices[p1]; Matrix4x4 Q2 = qMatrices[p2]; QEM_Edge QEM_edge = new QEM_Edge(halfEdge, Q1, Q2); QEM_edges.Add(QEM_edge); halfEdge_QEM_Lookup.Add(halfEdge, QEM_edge); } // // Sort all pairs, with the minimum cost pair at the top // //The fastest way to keep the data sorted is to use a heap Heap <QEM_Edge> sorted_QEM_edges = new Heap <QEM_Edge>(QEM_edges.Count); foreach (QEM_Edge e in QEM_edges) { sorted_QEM_edges.Add(e); } // // Start contracting edges // //For each edge we want to remove for (int i = 0; i < maxEdgesToContract; i++) { //Check that we can simplify the mesh //The smallest mesh we can have is a tetrahedron with 4 faces, itherwise we get a flat triangle if (halfEdgeMeshData.faces.Count <= 4) { Debug.Log($"Cant contract more than {i} edges"); break; } // // Remove the pair (v1,v2) of the least cost and contract the pair // //timer.Start(); QEM_Edge smallestErrorEdge = sorted_QEM_edges.RemoveFirst(); //This means an edge in this face has already been contracted //We are never removing edges from the heap after contracting and edges, //so we do it this way for now, which is maybe better? if (smallestErrorEdge.halfEdge.face == null) { //This edge wasn't contracted so don't add it to iteration i -= 1; continue; } if (smallestErrorEdge.qem > maxError) { Debug.Log($"Cant contract more than {i} edges because reached max error"); break; } //timer.Stop(); //timer.Start(); //Get the half-edge we want to contract HalfEdge3 edgeToContract = smallestErrorEdge.halfEdge; //Need to save the endpoints so we can remove the old Q matrices from the pos-matrix lookup table Edge3 contractedEdgeEndpoints = new Edge3(edgeToContract.prevEdge.v.position, edgeToContract.v.position); //Contract edge HashSet <HalfEdge3> edgesPointingToNewVertex = halfEdgeMeshData.ContractTriangleHalfEdge(edgeToContract, smallestErrorEdge.mergePosition, timer); //timer.Stop(); // // Remove all QEM_edges that belonged to the faces we contracted // //This is not needed if we check if an edge in the triangle has already been contracted /* * //timer.Start(); * * //This edge doesnt exist anymore, so remove it from the lookup * halfEdge_QEM_Lookup.Remove(edgeToContract); * * //Remove the two edges that were a part of the triangle of the edge we contracted * RemoveHalfEdgeFromQEMEdges(edgeToContract.nextEdge, QEM_edges, halfEdge_QEM_Lookup); * RemoveHalfEdgeFromQEMEdges(edgeToContract.nextEdge.nextEdge, QEM_edges, halfEdge_QEM_Lookup); * * //Remove the three edges belonging to the triangle on the opposite side of the edge we contracted * //If there was an opposite side... * if (edgeToContract.oppositeEdge != null) * { * HalfEdge3 oppositeEdge = edgeToContract.oppositeEdge; * * RemoveHalfEdgeFromQEMEdges(oppositeEdge, QEM_edges, halfEdge_QEM_Lookup); * RemoveHalfEdgeFromQEMEdges(oppositeEdge.nextEdge, QEM_edges, halfEdge_QEM_Lookup); * RemoveHalfEdgeFromQEMEdges(oppositeEdge.nextEdge.nextEdge, QEM_edges, halfEdge_QEM_Lookup); * } * //timer.Stop(); */ //Remove the edges start and end vertices from the pos-matrix lookup table qMatrices.Remove(contractedEdgeEndpoints.p1); qMatrices.Remove(contractedEdgeEndpoints.p2); //timer.Stop(); // // Update all QEM_edges that is now connected with the new contracted vertex because their errors have changed // //The contracted position has a new Q matrix Matrix4x4 QNew = CalculateQMatrix(edgesPointingToNewVertex, normalizeTriangles); //Add the Q matrix to the pos-matrix lookup table qMatrices.Add(smallestErrorEdge.mergePosition, QNew); //Update the error of the QEM_edges of the edges that pointed to and from one of the two old Q matrices //Those edges are the same edges that points to the new vertex and goes from the new vertex //timer.Start(); foreach (HalfEdge3 edgeToV in edgesPointingToNewVertex) { //The edge going from the new vertex is the next edge of the edge going to the vertex HalfEdge3 edgeFromV = edgeToV.nextEdge; //To QEM_Edge QEM_edgeToV = halfEdge_QEM_Lookup[edgeToV]; Edge3 edgeToV_endPoints = QEM_edgeToV.GetEdgeEndPoints(); Matrix4x4 Q1_edgeToV = qMatrices[edgeToV_endPoints.p1]; Matrix4x4 Q2_edgeToV = QNew; QEM_edgeToV.UpdateEdge(edgeToV, Q1_edgeToV, Q2_edgeToV); sorted_QEM_edges.UpdateItem(QEM_edgeToV); //From QEM_Edge QEM_edgeFromV = halfEdge_QEM_Lookup[edgeFromV]; Edge3 edgeFromV_endPoints = QEM_edgeFromV.GetEdgeEndPoints(); Matrix4x4 Q1_edgeFromV = QNew; Matrix4x4 Q2_edgeFromV = qMatrices[edgeFromV_endPoints.p2]; QEM_edgeFromV.UpdateEdge(edgeFromV, Q1_edgeFromV, Q2_edgeFromV); sorted_QEM_edges.UpdateItem(QEM_edgeFromV); } //timer.Stop(); } //Timers: 0.78 to generate the simplified bunny (2400 edge contractions) (normalizing triangles is 0.05 seconds slower) //Init: // - 0.1 to convert to half-edge data structure // - 0.14 to calculate a Q matrix for each unique vertex //Loop (total time): // - 0.04 to find smallest QEM error // - 0.25 to merge the edges (the bottleneck is where we have to find all edges pointing to a vertex) // - 0.02 to remove the data that was destroyed when we contracted an edge // - 0.13 to update QEM edges //Debug.Log($"It took {timer.ElapsedMilliseconds / 1000f} seconds to measure whatever we measured"); return(halfEdgeMeshData); }
//Help method to build a triangle and add it to a mesh //v1-v2-v3 should be sorted clock-wise //v1-v2 should be the cut edge (if we have a cut edge), and we know this triangle has a cut edge if newEdges != null private static void AddTriangleToMesh(MyMeshVertex v1, MyMeshVertex v2, MyMeshVertex v3, HalfEdgeData3 mesh, HashSet <HalfEdge3> newEdges) { //Create three new vertices HalfEdgeVertex3 half_v1 = new HalfEdgeVertex3(v1.position, v1.normal); HalfEdgeVertex3 half_v2 = new HalfEdgeVertex3(v2.position, v2.normal); HalfEdgeVertex3 half_v3 = new HalfEdgeVertex3(v3.position, v3.normal); //Create three new half-edges that points TO these vertices HalfEdge3 e_to_v1 = new HalfEdge3(half_v1); HalfEdge3 e_to_v2 = new HalfEdge3(half_v2); HalfEdge3 e_to_v3 = new HalfEdge3(half_v3); //Create the face (which is a triangle) which needs a reference to one of the edges HalfEdgeFace3 f = new HalfEdgeFace3(e_to_v1); //Connect the data: //Connect the edges clock-wise e_to_v1.nextEdge = e_to_v2; e_to_v2.nextEdge = e_to_v3; e_to_v3.nextEdge = e_to_v1; e_to_v1.prevEdge = e_to_v3; e_to_v2.prevEdge = e_to_v1; e_to_v3.prevEdge = e_to_v2; //Each vertex needs a reference to an edge going FROM that vertex half_v1.edge = e_to_v2; half_v2.edge = e_to_v3; half_v3.edge = e_to_v1; //Each edge needs a reference to the face e_to_v1.face = f; e_to_v2.face = f; e_to_v3.face = f; //Each edge needs an opposite edge //This is slow process but we need it to be able to split meshes which are not connected //You could do this afterwards when all triangles have been generate, but Im not sure which is the fastest... //Save the data mesh.verts.Add(half_v1); mesh.verts.Add(half_v2); mesh.verts.Add(half_v3); mesh.edges.Add(e_to_v1); mesh.edges.Add(e_to_v2); mesh.edges.Add(e_to_v3); mesh.faces.Add(f); //Save the new edge if (newEdges != null) { //We know the knew edge goes from v1 to v2, so we should save the half-edge that points to v2 newEdges.Add(e_to_v2); } }
//Merge with another half-edge mesh public void MergeMesh(HalfEdgeData3 otherMesh) { verts.UnionWith(otherMesh.verts); faces.UnionWith(otherMesh.faces); edges.UnionWith(otherMesh.edges); }