//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");
        }
示例#2
0
        /// <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);
        }
示例#3
0
        //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);
        }
        //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);
        }
示例#6
0
        //
        // 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);
        }