// Copyright 2024 The Manifold Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // Derived from the public domain work of Antti Kuukka at // https://github.com/akuukka/quickhull /* * INPUT: a list of points in 3D space (for example, vertices of a 3D mesh) * * OUTPUT: a ConvexHull object which provides vertex and index buffers of the *generated convex hull as a triangle mesh. * * * * The implementation is thread-safe if each thread is using its own QuickHull *object. * * * SUMMARY OF THE ALGORITHM: * - Create initial simplex (tetrahedron) using extreme points. We have *four faces now and they form a convex mesh M. * - For each point, assign them to the first face for which they are on *the positive side of (so each point is assigned to at most one face). Points *inside the initial tetrahedron are left behind now and no longer affect the *calculations. * - Add all faces that have points assigned to them to Face Stack. * - Iterate until Face Stack is empty: * - Pop topmost face F from the stack * - From the points assigned to F, pick the point P that is *farthest away from the plane defined by F. * - Find all faces of M that have P on their positive side. Let us *call these the "visible faces". * - Because of the way M is constructed, these faces are *connected. Solve their horizon edge loop. * - "Extrude to P": Create new faces by connecting *P with the points belonging to the horizon edge. Add the new faces to M and *remove the visible faces from M. * - Each point that was assigned to visible faces is now assigned *to at most one of the newly created faces. * - Those new faces that have points assigned to them are added to *the top of Face Stack. * - M is now the convex hull. * * */ #pragma once #include #include #include #include "./shared.h" #include "./vec.h" namespace manifold { class Pool { std::vector>> data; public: void clear() { data.clear(); } void reclaim(std::unique_ptr>& ptr) { data.push_back(std::move(ptr)); } std::unique_ptr> get() { if (data.size() == 0) { return std::make_unique>(); } auto it = data.end() - 1; std::unique_ptr> r = std::move(*it); data.erase(it); return r; } }; class Plane { public: vec3 N; // Signed distance (if normal is of length 1) to the plane from origin double D; // Normal length squared double sqrNLength; bool isPointOnPositiveSide(const vec3& Q) const { double d = la::dot(N, Q) + D; if (d >= 0) return true; return false; } Plane() = default; // Construct a plane using normal N and any point P on the plane Plane(const vec3& N, const vec3& P) : N(N), D(la::dot(-N, P)), sqrNLength(la::dot(N, N)) {} }; struct Ray { const vec3 S; const vec3 V; const double VInvLengthSquared; Ray(const vec3& S, const vec3& V) : S(S), V(V), VInvLengthSquared(1 / (la::dot(V, V))) {} }; class MeshBuilder { public: struct Face { int he; Plane P{}; double mostDistantPointDist = 0.0; size_t mostDistantPoint = 0; size_t visibilityCheckedOnIteration = 0; std::uint8_t isVisibleFaceOnCurrentIteration : 1; std::uint8_t inFaceStack : 1; // Bit for each half edge assigned to this face, each being 0 or 1 depending // on whether the edge belongs to horizon edge std::uint8_t horizonEdgesOnCurrentIteration : 3; std::unique_ptr> pointsOnPositiveSide; Face(size_t he) : he(he), isVisibleFaceOnCurrentIteration(0), inFaceStack(0), horizonEdgesOnCurrentIteration(0) {} Face() : he(-1), isVisibleFaceOnCurrentIteration(0), inFaceStack(0), horizonEdgesOnCurrentIteration(0) {} void disable() { he = -1; } bool isDisabled() const { return he == -1; } }; // Mesh data std::vector faces; Vec halfedges; Vec halfedgeToFace; Vec halfedgeNext; // When the mesh is modified and faces and half edges are removed from it, we // do not actually remove them from the container vectors. Insted, they are // marked as disabled which means that the indices can be reused when we need // to add new faces and half edges to the mesh. We store the free indices in // the following vectors. Vec disabledFaces, disabledHalfedges; size_t addFace(); size_t addHalfedge(); // Mark a face as disabled and return a pointer to the points that were on the // positive of it. std::unique_ptr> disableFace(size_t faceIndex) { auto& f = faces[faceIndex]; f.disable(); disabledFaces.push_back(faceIndex); return std::move(f.pointsOnPositiveSide); } void disableHalfedge(size_t heIndex) { auto& he = halfedges[heIndex]; he.pairedHalfedge = -1; disabledHalfedges.push_back(heIndex); } MeshBuilder() = default; // Create a mesh with initial tetrahedron ABCD. Dot product of AB with the // normal of triangle ABC should be negative. void setup(int a, int b, int c, int d); std::array getVertexIndicesOfFace(const Face& f) const; std::array getVertexIndicesOfHalfEdge(const Halfedge& he) const { return {halfedges[he.pairedHalfedge].endVert, he.endVert}; } std::array getHalfEdgeIndicesOfFace(const Face& f) const { return {f.he, halfedgeNext[f.he], halfedgeNext[halfedgeNext[f.he]]}; } }; class HalfEdgeMesh { public: Vec vertices; // Index of one of the half edges of the faces std::vector halfEdgeIndexFaces; Vec halfedges; Vec halfedgeToFace; Vec halfedgeNext; HalfEdgeMesh(const MeshBuilder& builderObject, const VecView& vertexData); }; double defaultEps(); class QuickHull { struct FaceData { int faceIndex; // If the face turns out not to be visible, this half edge will be marked as // horizon edge int enteredFromHalfedge; }; double m_epsilon, epsilonSquared, scale; bool planar; Vec planarPointCloudTemp; VecView originalVertexData; MeshBuilder mesh; std::array extremeValues; size_t failedHorizonEdges = 0; // Temporary variables used during iteration process Vec newFaceIndices; Vec newHalfedgeIndices; Vec visibleFaces; Vec horizonEdgesData; Vec possiblyVisibleFaces; std::vector>> disabledFacePointVectors; std::deque faceList; // Create a half edge mesh representing the base tetrahedron from which the // QuickHull iteration proceeds. extremeValues must be properly set up when // this is called. void setupInitialTetrahedron(); // Given a list of half edges, try to rearrange them so that they form a loop. // Return true on success. bool reorderHorizonEdges(VecView& horizonEdges); // Find indices of extreme values (max x, min x, max y, min y, max z, min z) // for the given point cloud std::array getExtremeValues(); // Compute scale of the vertex data. double getScale(const std::array& extremeValuesInput); // Each face contains a unique pointer to a vector of indices. However, many - // often most - faces do not have any points on the positive side of them // especially at the the end of the iteration. When a face is removed from the // mesh, its associated point vector, if such exists, is moved to the index // vector pool, and when we need to add new faces with points on the positive // side to the mesh, we reuse these vectors. This reduces the amount of // std::vectors we have to deal with, and impact on performance is remarkable. Pool indexVectorPool; inline std::unique_ptr> getIndexVectorFromPool(); inline void reclaimToIndexVectorPool(std::unique_ptr>& ptr); // Associates a point with a face if the point resides on the positive side of // the plane. Returns true if the points was on the positive side. inline bool addPointToFace(typename MeshBuilder::Face& f, size_t pointIndex); // This will create HalfedgeMesh from which we create the ConvexHull object // that buildMesh function returns void createConvexHalfedgeMesh(); public: // This function assumes that the pointCloudVec data resides in memory in the // following format: x_0,y_0,z_0,x_1,y_1,z_1,... QuickHull(VecView pointCloudVec) : originalVertexData(VecView(pointCloudVec)) {} // Computes convex hull for a given point cloud. Params: eps: minimum distance // to a plane to consider a point being on positive side of it (for a point // cloud with scale 1) Returns: Convex hull of the point cloud as halfEdge // vector and vertex vector std::pair, Vec> buildMesh(double eps = defaultEps()); }; } // namespace manifold