#include "QuickHull.hpp" #include "MathUtils.hpp" #include #include #include #include #include #include "Structs/Mesh.hpp" namespace quickhull { template<> float defaultEps() { return 0.0001f; } template<> double defaultEps() { return 0.0000001; } /* * Implementation of the algorithm */ template ConvexHull QuickHull::getConvexHull(const std::vector>& pointCloud, bool CCW, bool useOriginalIndices, T epsilon) { VertexDataSource vertexDataSource(pointCloud); return getConvexHull(vertexDataSource,CCW,useOriginalIndices,epsilon); } template ConvexHull QuickHull::getConvexHull(const Vector3* vertexData, size_t vertexCount, bool CCW, bool useOriginalIndices, T epsilon) { VertexDataSource vertexDataSource(vertexData,vertexCount); return getConvexHull(vertexDataSource,CCW,useOriginalIndices,epsilon); } template ConvexHull QuickHull::getConvexHull(const T* vertexData, size_t vertexCount, bool CCW, bool useOriginalIndices, T epsilon) { VertexDataSource vertexDataSource((const vec3*)vertexData,vertexCount); return getConvexHull(vertexDataSource,CCW,useOriginalIndices,epsilon); } template HalfEdgeMesh QuickHull::getConvexHullAsMesh(const FloatType* vertexData, size_t vertexCount, bool CCW, FloatType epsilon) { VertexDataSource vertexDataSource((const vec3*)vertexData,vertexCount); buildMesh(vertexDataSource, epsilon); return HalfEdgeMesh(m_mesh, m_vertexData); } template void QuickHull::buildMesh(const VertexDataSource& pointCloud, T epsilon) { if (pointCloud.size()==0) { m_mesh = MeshBuilder(); return; } m_vertexData = pointCloud; // Very first: find extreme values and use them to compute the scale of the point cloud. m_extremeValues = getExtremeValues(); m_scale = getScale(m_extremeValues); // Epsilon we use depends on the scale m_epsilon = epsilon*m_scale; m_epsilonSquared = m_epsilon*m_epsilon; // Reset diagnostics m_diagnostics = DiagnosticsData(); // The planar case happens when all the points appear to lie on a two dimensional // subspace of R^3. m_planar = false; createConvexHalfEdgeMesh(); if (m_planar) { const size_t extraPointIndex = m_planarPointCloudTemp.size() - 1; for (auto& he : m_mesh.m_halfEdges) { if (he.m_endVertex == extraPointIndex) { he.m_endVertex = 0; } } m_vertexData = pointCloud; m_planarPointCloudTemp.clear(); } } template ConvexHull QuickHull::getConvexHull(const VertexDataSource& pointCloud, bool CCW, bool useOriginalIndices, T epsilon) { buildMesh(pointCloud, epsilon); return ConvexHull(m_mesh,m_vertexData, CCW, useOriginalIndices); } template void QuickHull::createConvexHalfEdgeMesh() { m_visibleFaces.clear(); m_horizonEdges.clear(); m_possiblyVisibleFaces.clear(); // Compute base tetrahedron setupInitialTetrahedron(); assert(m_mesh.m_faces.size() == 4); // Init face stack with those faces that have points assigned to them m_faceList.clear(); for (size_t i=0;i < 4;i++) { auto& f = m_mesh.m_faces[i]; if (f.m_pointsOnPositiveSide && f.m_pointsOnPositiveSide->size()>0) { m_faceList.push_back(i); f.m_inFaceStack = 1; } } // Process faces until the face list is empty. size_t iter = 0; while (!m_faceList.empty()) { iter++; if (iter == std::numeric_limits::max()) { // Visible face traversal marks visited faces with iteration counter // (to mark that the face has been visited on this iteration) and the max // value represents unvisited faces. At this point we have to reset iteration // counter. This shouldn't be an issue on 64 bit machines. iter = 0; } const size_t topFaceIndex = m_faceList.front(); m_faceList.pop_front(); auto& tf = m_mesh.m_faces[topFaceIndex]; tf.m_inFaceStack = 0; assert(!tf.m_pointsOnPositiveSide || tf.m_pointsOnPositiveSide->size()>0); if (!tf.m_pointsOnPositiveSide || tf.isDisabled()) { continue; } // Pick the most distant point to this triangle plane as the point to which we extrude const vec3& activePoint = m_vertexData[tf.m_mostDistantPoint]; const size_t activePointIndex = tf.m_mostDistantPoint; // Find out the faces that have our active point on their positive side // (these are the "visible faces"). The face on top of the stack of course is // one of them. At the same time, we create a list of horizon edges. m_horizonEdges.clear(); m_possiblyVisibleFaces.clear(); m_visibleFaces.clear(); m_possiblyVisibleFaces.emplace_back(topFaceIndex,std::numeric_limits::max()); while (m_possiblyVisibleFaces.size()) { const auto faceData = m_possiblyVisibleFaces.back(); m_possiblyVisibleFaces.pop_back(); auto& pvf = m_mesh.m_faces[faceData.m_faceIndex]; assert(!pvf.isDisabled()); if (pvf.m_visibilityCheckedOnIteration == iter) { if (pvf.m_isVisibleFaceOnCurrentIteration) { continue; } } else { const Plane& P = pvf.m_P; pvf.m_visibilityCheckedOnIteration = iter; const T d = P.m_N.dotProduct(activePoint)+P.m_D; if (d>0) { pvf.m_isVisibleFaceOnCurrentIteration = 1; pvf.m_horizonEdgesOnCurrentIteration = 0; m_visibleFaces.push_back(faceData.m_faceIndex); for (auto heIndex : m_mesh.getHalfEdgeIndicesOfFace(pvf)) { if (m_mesh.m_halfEdges[heIndex].m_opp != faceData.m_enteredFromHalfEdge) { m_possiblyVisibleFaces.emplace_back( m_mesh.m_halfEdges[m_mesh.m_halfEdges[heIndex].m_opp].m_face,heIndex ); } } continue; } assert(faceData.m_faceIndex != topFaceIndex); } // The face is not visible. Therefore, the halfedge we entered from // is part of the horizon edge. pvf.m_isVisibleFaceOnCurrentIteration = 0; m_horizonEdges.push_back(faceData.m_enteredFromHalfEdge); // Store which half edge is the horizon edge. The other half edges of the face // will not be part of the final mesh so their data slots can by recycled. const auto halfEdges = m_mesh.getHalfEdgeIndicesOfFace(m_mesh.m_faces[m_mesh.m_halfEdges[faceData.m_enteredFromHalfEdge].m_face]); const std::int8_t ind = (halfEdges[0]==faceData.m_enteredFromHalfEdge) ? 0 : (halfEdges[1]==faceData.m_enteredFromHalfEdge ? 1 : 2); m_mesh.m_faces[m_mesh.m_halfEdges[faceData.m_enteredFromHalfEdge].m_face].m_horizonEdgesOnCurrentIteration |= (1<begin(), tf.m_pointsOnPositiveSide->end(), activePointIndex); tf.m_pointsOnPositiveSide->erase(it); if (tf.m_pointsOnPositiveSide->size()==0) { reclaimToIndexVectorPool(tf.m_pointsOnPositiveSide); } continue; } // Except for the horizon edges, all half edges of the visible faces can be marked as disabled. Their data slots will be reused. // The faces will be disabled as well, but we need to remember the points that were on the positive side of them - therefore // we save pointers to them. m_newFaceIndices.clear(); m_newHalfEdgeIndices.clear(); m_disabledFacePointVectors.clear(); size_t disableCounter = 0; for (auto faceIndex : m_visibleFaces) { auto& disabledFace = m_mesh.m_faces[faceIndex]; auto halfEdges = m_mesh.getHalfEdgeIndicesOfFace(disabledFace); for (size_t j=0;j<3;j++) { if ((disabledFace.m_horizonEdgesOnCurrentIteration & (1<size()); // Because we should not assign point vectors to faces unless needed... m_disabledFacePointVectors.push_back(std::move(t)); } } if (disableCounter < horizonEdgeCount*2) { const size_t newHalfEdgesNeeded = horizonEdgeCount*2-disableCounter; for (size_t i=0;i planeNormal = mathutils::getTriangleNormal(m_vertexData[A],m_vertexData[B],activePoint); newFace.m_P = Plane(planeNormal,activePoint); newFace.m_he = AB; m_mesh.m_halfEdges[CA].m_opp = m_newHalfEdgeIndices[i>0 ? i*2-1 : 2*horizonEdgeCount-1]; m_mesh.m_halfEdges[BC].m_opp = m_newHalfEdgeIndices[((i+1)*2) % (horizonEdgeCount*2)]; } // Assign points that were on the positive side of the disabled faces to the new faces. for (auto& disabledPoints : m_disabledFacePointVectors) { assert(disabledPoints); for (const auto& point : *(disabledPoints)) { if (point == activePointIndex) { continue; } for (size_t j=0;jsize()>0); if (!newFace.m_inFaceStack) { m_faceList.push_back(newFaceIndex); newFace.m_inFaceStack = 1; } } } } // Cleanup m_indexVectorPool.clear(); } /* * Private helper functions */ template std::array QuickHull::getExtremeValues() { std::array outIndices{0, 0, 0, 0, 0, 0}; T extremeVals[6] = { m_vertexData[0].x, m_vertexData[0].x, m_vertexData[0].y, m_vertexData[0].y, m_vertexData[0].z, m_vertexData[0].z }; const size_t vCount = m_vertexData.size(); for (size_t i = 1; i < vCount; i++) { const Vector3& pos = m_vertexData[i]; if (pos.x>extremeVals[0]) { extremeVals[0]=pos.x; outIndices[0]=i; } else if (pos.xextremeVals[2]) { extremeVals[2]=pos.y; outIndices[2]=i; } else if (pos.yextremeVals[4]) { extremeVals[4]=pos.z; outIndices[4]=i; } else if (pos.z bool QuickHull::reorderHorizonEdges(std::vector& horizonEdges) { const size_t horizonEdgeCount = horizonEdges.size(); for (size_t i=0;i T QuickHull::getScale(const std::array& extremeValues) { T s = 0; for (size_t i=0;i<6;i++) { const T* v = (const T*)(&m_vertexData[extremeValues[i]]); v += i/2; auto a = std::abs(*v); if (a>s) { s = a; } } return s; } template void QuickHull::setupInitialTetrahedron() { const size_t vertexCount = m_vertexData.size(); // If we have at most 4 points, just return a degenerate tetrahedron: if (vertexCount <= 4) { size_t v[4] = {0,std::min((size_t)1,vertexCount-1),std::min((size_t)2,vertexCount-1),std::min((size_t)3,vertexCount-1)}; const Vector3 N = mathutils::getTriangleNormal(m_vertexData[v[0]],m_vertexData[v[1]],m_vertexData[v[2]]); const Plane trianglePlane(N,m_vertexData[v[0]]); if (trianglePlane.isPointOnPositiveSide(m_vertexData[v[3]])) { std::swap(v[0],v[1]); } return m_mesh.setup(v[0],v[1],v[2],v[3]); } // Find two most distant extreme points. T maxD = m_epsilonSquared; std::pair selectedPoints; for (size_t i=0;i<6;i++) { for (size_t j=i+1;j<6;j++) { const T d = m_vertexData[ m_extremeValues[i] ].getSquaredDistanceTo( m_vertexData[ m_extremeValues[j] ] ); if (d > maxD) { maxD=d; selectedPoints={m_extremeValues[i],m_extremeValues[j]}; } } } if (maxD == m_epsilonSquared) { // A degenerate case: the point cloud seems to consists of a single point return m_mesh.setup(0,std::min((size_t)1,vertexCount-1),std::min((size_t)2,vertexCount-1),std::min((size_t)3,vertexCount-1)); } assert(selectedPoints.first != selectedPoints.second); // Find the most distant point to the line between the two chosen extreme points. const Ray r(m_vertexData[selectedPoints.first], (m_vertexData[selectedPoints.second] - m_vertexData[selectedPoints.first])); maxD = m_epsilonSquared; size_t maxI=std::numeric_limits::max(); const size_t vCount = m_vertexData.size(); for (size_t i=0;i maxD) { maxD=distToRay; maxI=i; } } if (maxD == m_epsilonSquared) { // It appears that the point cloud belongs to a 1 dimensional subspace of R^3: convex hull has no volume => return a thin triangle // Pick any point other than selectedPoints.first and selectedPoints.second as the third point of the triangle auto it = std::find_if(m_vertexData.begin(),m_vertexData.end(),[&](const vec3& ve) { return ve != m_vertexData[selectedPoints.first] && ve != m_vertexData[selectedPoints.second]; }); const size_t thirdPoint = (it == m_vertexData.end()) ? selectedPoints.first : std::distance(m_vertexData.begin(),it); it = std::find_if(m_vertexData.begin(),m_vertexData.end(),[&](const vec3& ve) { return ve != m_vertexData[selectedPoints.first] && ve != m_vertexData[selectedPoints.second] && ve != m_vertexData[thirdPoint]; }); const size_t fourthPoint = (it == m_vertexData.end()) ? selectedPoints.first : std::distance(m_vertexData.begin(),it); return m_mesh.setup(selectedPoints.first,selectedPoints.second,thirdPoint,fourthPoint); } // These three points form the base triangle for our tetrahedron. assert(selectedPoints.first != maxI && selectedPoints.second != maxI); std::array baseTriangle{selectedPoints.first, selectedPoints.second, maxI}; const Vector3 baseTriangleVertices[]={ m_vertexData[baseTriangle[0]], m_vertexData[baseTriangle[1]], m_vertexData[baseTriangle[2]] }; // Next step is to find the 4th vertex of the tetrahedron. We naturally choose the point farthest away from the triangle plane. maxD=m_epsilon; maxI=0; const Vector3 N = mathutils::getTriangleNormal(baseTriangleVertices[0],baseTriangleVertices[1],baseTriangleVertices[2]); Plane trianglePlane(N,baseTriangleVertices[0]); for (size_t i=0;imaxD) { maxD=d; maxI=i; } } if (maxD == m_epsilon) { // All the points seem to lie on a 2D subspace of R^3. How to handle this? Well, let's add one extra point to the point cloud so that the convex hull will have volume. m_planar = true; const vec3 N1 = mathutils::getTriangleNormal(baseTriangleVertices[1],baseTriangleVertices[2],baseTriangleVertices[0]); m_planarPointCloudTemp.clear(); m_planarPointCloudTemp.insert(m_planarPointCloudTemp.begin(),m_vertexData.begin(),m_vertexData.end()); const vec3 extraPoint = N1 + m_vertexData[0]; m_planarPointCloudTemp.push_back(extraPoint); maxI = m_planarPointCloudTemp.size()-1; m_vertexData = VertexDataSource(m_planarPointCloudTemp); } // Enforce CCW orientation (if user prefers clockwise orientation, swap two vertices in each triangle when final mesh is created) const Plane triPlane(N,baseTriangleVertices[0]); if (triPlane.isPointOnPositiveSide(m_vertexData[maxI])) { std::swap(baseTriangle[0],baseTriangle[1]); } // Create a tetrahedron half edge mesh and compute planes defined by each triangle m_mesh.setup(baseTriangle[0],baseTriangle[1],baseTriangle[2],maxI); for (auto& f : m_mesh.m_faces) { auto v = m_mesh.getVertexIndicesOfFace(f); const Vector3& va = m_vertexData[v[0]]; const Vector3& vb = m_vertexData[v[1]]; const Vector3& vc = m_vertexData[v[2]]; const Vector3 N1 = mathutils::getTriangleNormal(va, vb, vc); const Plane plane(N1,va); f.m_P = plane; } // Finally we assign a face for each vertex outside the tetrahedron (vertices inside the tetrahedron have no role anymore) for (size_t i=0;i; template class QuickHull; }