update quickhull lib

This commit is contained in:
Garux 2023-05-16 13:54:27 +06:00
parent 45ed010114
commit 9dff9434c6
10 changed files with 264 additions and 184 deletions

1
libs/quickhull/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.DS_Store

View File

@ -92,7 +92,8 @@ namespace quickhull {
if (faceStack.size()==0) {
return;
}
const size_t iCCW = CCW ? 1 : 0;
const size_t finalMeshFaceCount = mesh.m_faces.size() - mesh.m_disabledFaces.size();
m_indices.reserve(finalMeshFaceCount*3);
@ -116,26 +117,20 @@ namespace quickhull {
auto vertices = mesh.getVertexIndicesOfFace(mesh.m_faces[top]);
if (!useOriginalIndices) {
for (auto& v : vertices) {
auto it = vertexIndexMapping.find(v);
if (it == vertexIndexMapping.end()) {
auto itV = vertexIndexMapping.find(v);
if (itV == vertexIndexMapping.end()) {
m_optimizedVertexBuffer->push_back(pointCloud[v]);
vertexIndexMapping[v] = m_optimizedVertexBuffer->size()-1;
v = m_optimizedVertexBuffer->size()-1;
}
else {
v = it->second;
v = itV->second;
}
}
}
m_indices.push_back(vertices[0]);
if (CCW) {
m_indices.push_back(vertices[2]);
m_indices.push_back(vertices[1]);
}
else {
m_indices.push_back(vertices[1]);
m_indices.push_back(vertices[2]);
}
m_indices.push_back(vertices[1 + iCCW]);
m_indices.push_back(vertices[2 - iCCW]);
}
}
@ -151,12 +146,20 @@ namespace quickhull {
return m_indices;
}
const std::vector<size_t>& getIndexBuffer() const {
return m_indices;
}
VertexDataSource<T>& getVertexBuffer() {
return m_vertices;
}
const VertexDataSource<T>& getVertexBuffer() const {
return m_vertices;
}
// Export the mesh to a Waveform OBJ file
void writeWaveformOBJ(const std::string& filename, const std::string& objectName = "quickhull")
void writeWaveformOBJ(const std::string& filename, const std::string& objectName = "quickhull") const
{
std::ofstream objFile;
objFile.open (filename);

View File

@ -4,17 +4,20 @@
#include <cassert>
#include <iostream>
#include <algorithm>
#include <deque>
#include <limits>
#include "Structs/Mesh.hpp"
namespace quickhull {
template<>
float defaultEps() {
return 0.0001f;
}
template<>
const float QuickHull<float>::Epsilon = 0.0001f;
template<>
const double QuickHull<double>::Epsilon = 0.0000001;
double defaultEps() {
return 0.0000001;
}
/*
* Implementation of the algorithm
@ -39,14 +42,19 @@ namespace quickhull {
}
template<typename FloatType>
HalfEdgeMesh<FloatType, IndexType> QuickHull<FloatType>::getConvexHullAsMesh(const FloatType* vertexData, size_t vertexCount, bool CCW, FloatType epsilon) {
HalfEdgeMesh<FloatType, size_t> QuickHull<FloatType>::getConvexHullAsMesh(const FloatType* vertexData, size_t vertexCount, bool CCW, FloatType epsilon) {
VertexDataSource<FloatType> vertexDataSource((const vec3*)vertexData,vertexCount);
buildMesh(vertexDataSource, CCW, false, epsilon);
return HalfEdgeMesh<FloatType, IndexType>(m_mesh, m_vertexData);
return HalfEdgeMesh<FloatType, size_t>(m_mesh, m_vertexData);
}
template<typename T>
void QuickHull<T>::buildMesh(const VertexDataSource<T>& pointCloud, bool CCW, bool useOriginalIndices, T epsilon) {
// CCW is unused for now
(void)CCW;
// useOriginalIndices is unused for now
(void)useOriginalIndices;
if (pointCloud.size()==0) {
m_mesh = MeshBuilder<T>();
return;
@ -86,33 +94,27 @@ namespace quickhull {
template<typename T>
void QuickHull<T>::createConvexHalfEdgeMesh() {
// Temporary variables used during iteration
std::vector<IndexType> visibleFaces;
std::vector<IndexType> horizonEdges;
struct FaceData {
IndexType m_faceIndex;
IndexType m_enteredFromHalfEdge; // If the face turns out not to be visible, this half edge will be marked as horizon edge
FaceData(IndexType fi, IndexType he) : m_faceIndex(fi),m_enteredFromHalfEdge(he) {}
};
std::vector<FaceData> possiblyVisibleFaces;
m_visibleFaces.clear();
m_horizonEdges.clear();
m_possiblyVisibleFaces.clear();
// Compute base tetrahedron
m_mesh = getInitialTetrahedron();
setupInitialTetrahedron();
assert(m_mesh.m_faces.size()==4);
// Init face stack with those faces that have points assigned to them
std::deque<IndexType> faceList;
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) {
faceList.push_back(i);
m_faceList.push_back(i);
f.m_inFaceStack = 1;
}
}
// Process faces until the face list is empty.
size_t iter = 0;
while (!faceList.empty()) {
while (!m_faceList.empty()) {
iter++;
if (iter == std::numeric_limits<size_t>::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
@ -120,8 +122,8 @@ namespace quickhull {
iter = 0;
}
const IndexType topFaceIndex = faceList.front();
faceList.pop_front();
const size_t topFaceIndex = m_faceList.front();
m_faceList.pop_front();
auto& tf = m_mesh.m_faces[topFaceIndex];
tf.m_inFaceStack = 0;
@ -136,13 +138,13 @@ namespace quickhull {
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.
horizonEdges.clear();
possiblyVisibleFaces.clear();
visibleFaces.clear();
possiblyVisibleFaces.emplace_back(topFaceIndex,std::numeric_limits<size_t>::max());
while (possiblyVisibleFaces.size()) {
const auto faceData = possiblyVisibleFaces.back();
possiblyVisibleFaces.pop_back();
m_horizonEdges.clear();
m_possiblyVisibleFaces.clear();
m_visibleFaces.clear();
m_possiblyVisibleFaces.emplace_back(topFaceIndex,std::numeric_limits<size_t>::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());
@ -158,10 +160,10 @@ namespace quickhull {
if (d>0) {
pvf.m_isVisibleFaceOnCurrentIteration = 1;
pvf.m_horizonEdgesOnCurrentIteration = 0;
visibleFaces.push_back(faceData.m_faceIndex);
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) {
possiblyVisibleFaces.emplace_back( m_mesh.m_halfEdges[m_mesh.m_halfEdges[heIndex].m_opp].m_face,heIndex );
m_possiblyVisibleFaces.emplace_back( m_mesh.m_halfEdges[m_mesh.m_halfEdges[heIndex].m_opp].m_face,heIndex );
}
}
continue;
@ -171,16 +173,16 @@ namespace quickhull {
// The face is not visible. Therefore, the halfedge we came from is part of the horizon edge.
pvf.m_isVisibleFaceOnCurrentIteration = 0;
horizonEdges.push_back(faceData.m_enteredFromHalfEdge);
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<<ind);
}
const size_t horizonEdgeCount = horizonEdges.size();
const size_t horizonEdgeCount = m_horizonEdges.size();
// Order horizon edges so that they form a loop. This may fail due to numerical instability in which case we give up trying to solve horizon edge for this point and accept a minor degeneration in the convex hull.
if (!reorderHorizonEdges(horizonEdges)) {
if (!reorderHorizonEdges(m_horizonEdges)) {
m_diagnostics.m_failedHorizonEdges++;
std::cerr << "Failed to solve horizon edge." << std::endl;
auto it = std::find(tf.m_pointsOnPositiveSide->begin(),tf.m_pointsOnPositiveSide->end(),activePointIndex);
@ -198,7 +200,7 @@ namespace quickhull {
m_newHalfEdgeIndices.clear();
m_disabledFacePointVectors.clear();
size_t disableCounter = 0;
for (auto faceIndex : visibleFaces) {
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++) {
@ -231,19 +233,19 @@ namespace quickhull {
// Create new faces using the edgeloop
for (size_t i = 0; i < horizonEdgeCount; i++) {
const IndexType AB = horizonEdges[i];
const size_t AB = m_horizonEdges[i];
auto horizonEdgeVertexIndices = m_mesh.getVertexIndicesOfHalfEdge(m_mesh.m_halfEdges[AB]);
IndexType A,B,C;
size_t A,B,C;
A = horizonEdgeVertexIndices[0];
B = horizonEdgeVertexIndices[1];
C = activePointIndex;
const IndexType newFaceIndex = m_mesh.addFace();
const size_t newFaceIndex = m_mesh.addFace();
m_newFaceIndices.push_back(newFaceIndex);
const IndexType CA = m_newHalfEdgeIndices[2*i+0];
const IndexType BC = m_newHalfEdgeIndices[2*i+1];
const size_t CA = m_newHalfEdgeIndices[2*i+0];
const size_t BC = m_newHalfEdgeIndices[2*i+1];
m_mesh.m_halfEdges[AB].m_next = BC;
m_mesh.m_halfEdges[BC].m_next = CA;
@ -289,7 +291,7 @@ namespace quickhull {
if (newFace.m_pointsOnPositiveSide) {
assert(newFace.m_pointsOnPositiveSide->size()>0);
if (!newFace.m_inFaceStack) {
faceList.push_back(newFaceIndex);
m_faceList.push_back(newFaceIndex);
newFace.m_inFaceStack = 1;
}
}
@ -305,48 +307,48 @@ namespace quickhull {
*/
template <typename T>
std::array<IndexType,6> QuickHull<T>::getExtremeValues() {
std::array<IndexType,6> outIndices{0,0,0,0,0,0};
std::array<size_t,6> QuickHull<T>::getExtremeValues() {
std::array<size_t,6> 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<T>& pos = m_vertexData[i];
if (pos.x>extremeVals[0]) {
extremeVals[0]=pos.x;
outIndices[0]=(IndexType)i;
outIndices[0]=i;
}
else if (pos.x<extremeVals[1]) {
extremeVals[1]=pos.x;
outIndices[1]=(IndexType)i;
outIndices[1]=i;
}
if (pos.y>extremeVals[2]) {
extremeVals[2]=pos.y;
outIndices[2]=(IndexType)i;
outIndices[2]=i;
}
else if (pos.y<extremeVals[3]) {
extremeVals[3]=pos.y;
outIndices[3]=(IndexType)i;
outIndices[3]=i;
}
if (pos.z>extremeVals[4]) {
extremeVals[4]=pos.z;
outIndices[4]=(IndexType)i;
outIndices[4]=i;
}
else if (pos.z<extremeVals[5]) {
extremeVals[5]=pos.z;
outIndices[5]=(IndexType)i;
outIndices[5]=i;
}
}
return outIndices;
}
template<typename T>
bool QuickHull<T>::reorderHorizonEdges(std::vector<IndexType>& horizonEdges) {
bool QuickHull<T>::reorderHorizonEdges(std::vector<size_t>& horizonEdges) {
const size_t horizonEdgeCount = horizonEdges.size();
for (size_t i=0;i<horizonEdgeCount-1;i++) {
const IndexType endVertex = m_mesh.m_halfEdges[ horizonEdges[i] ].m_endVertex;
const size_t endVertex = m_mesh.m_halfEdges[ horizonEdges[i] ].m_endVertex;
bool foundNext = false;
for (size_t j=i+1;j<horizonEdgeCount;j++) {
const IndexType beginVertex = m_mesh.m_halfEdges[ m_mesh.m_halfEdges[horizonEdges[j]].m_opp ].m_endVertex;
const size_t beginVertex = m_mesh.m_halfEdges[ m_mesh.m_halfEdges[horizonEdges[j]].m_opp ].m_endVertex;
if (beginVertex == endVertex) {
std::swap(horizonEdges[i+1],horizonEdges[j]);
foundNext = true;
@ -362,7 +364,7 @@ namespace quickhull {
}
template <typename T>
T QuickHull<T>::getScale(const std::array<IndexType,6>& extremeValues) {
T QuickHull<T>::getScale(const std::array<size_t,6>& extremeValues) {
T s = 0;
for (size_t i=0;i<6;i++) {
const T* v = (const T*)(&m_vertexData[extremeValues[i]]);
@ -375,24 +377,24 @@ namespace quickhull {
return s;
}
template <typename T>
MeshBuilder<T> QuickHull<T>::getInitialTetrahedron() {
template<typename T>
void QuickHull<T>::setupInitialTetrahedron() {
const size_t vertexCount = m_vertexData.size();
// If we have at most 4 points, just return a degenerate tetrahedron:
if (vertexCount <= 4) {
IndexType v[4] = {0,std::min((size_t)1,vertexCount-1),std::min((size_t)2,vertexCount-1),std::min((size_t)3,vertexCount-1)};
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<T> N = mathutils::getTriangleNormal(m_vertexData[v[0]],m_vertexData[v[1]],m_vertexData[v[2]]);
const Plane<T> trianglePlane(N,m_vertexData[v[0]]);
if (trianglePlane.isPointOnPositiveSide(m_vertexData[v[3]])) {
std::swap(v[0],v[1]);
}
return MeshBuilder<T>(v[0],v[1],v[2],v[3]);
return m_mesh.setup(v[0],v[1],v[2],v[3]);
}
// Find two most distant extreme points.
T maxD = m_epsilonSquared;
std::pair<IndexType,IndexType> selectedPoints;
std::pair<size_t,size_t> 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] ] );
@ -404,7 +406,7 @@ namespace quickhull {
}
if (maxD == m_epsilonSquared) {
// A degenerate case: the point cloud seems to consists of a single point
return MeshBuilder<T>(0,std::min((size_t)1,vertexCount-1),std::min((size_t)2,vertexCount-1),std::min((size_t)3,vertexCount-1));
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);
@ -426,12 +428,12 @@ namespace quickhull {
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 IndexType thirdPoint = (it == m_vertexData.end()) ? selectedPoints.first : std::distance(m_vertexData.begin(),it);
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 IndexType fourthPoint = (it == m_vertexData.end()) ? selectedPoints.first : std::distance(m_vertexData.begin(),it);
return MeshBuilder<T>(selectedPoints.first,selectedPoints.second,thirdPoint,fourthPoint);
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.
@ -454,10 +456,10 @@ namespace quickhull {
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 N = mathutils::getTriangleNormal(baseTriangleVertices[1],baseTriangleVertices[2],baseTriangleVertices[0]);
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 = N + m_vertexData[0];
const vec3 extraPoint = N1 + m_vertexData[0];
m_planarPointCloudTemp.push_back(extraPoint);
maxI = m_planarPointCloudTemp.size()-1;
m_vertexData = VertexDataSource<T>(m_planarPointCloudTemp);
@ -470,26 +472,25 @@ namespace quickhull {
}
// Create a tetrahedron half edge mesh and compute planes defined by each triangle
MeshBuilder<T> mesh(baseTriangle[0],baseTriangle[1],baseTriangle[2],maxI);
for (auto& f : mesh.m_faces) {
auto v = mesh.getVertexIndicesOfFace(f);
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<T>& va = m_vertexData[v[0]];
const Vector3<T>& vb = m_vertexData[v[1]];
const Vector3<T>& vc = m_vertexData[v[2]];
const Vector3<T> N = mathutils::getTriangleNormal(va, vb, vc);
const Plane<T> trianglePlane(N,va);
f.m_P = trianglePlane;
const Vector3<T> N1 = mathutils::getTriangleNormal(va, vb, vc);
const Plane<T> 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<vCount;i++) {
for (auto& face : mesh.m_faces) {
for (auto& face : m_mesh.m_faces) {
if (addPointToFace(face, i)) {
break;
}
}
}
return mesh;
}
/*

View File

@ -1,6 +1,6 @@
#ifndef QUICKHULL_HPP_
#define QUICKHULL_HPP_
#include <deque>
#include <vector>
#include <array>
#include <limits>
@ -12,7 +12,6 @@
#include "HalfEdgeMesh.hpp"
#include "MathUtils.hpp"
/*
* Implementation of the 3D QuickHull algorithm by Antti Kuukka
*
@ -56,48 +55,58 @@ namespace quickhull {
DiagnosticsData() : m_failedHorizonEdges(0) { }
};
template<typename FloatType>
FloatType defaultEps();
template<typename FloatType>
class QuickHull {
using vec3 = Vector3<FloatType>;
static const FloatType Epsilon;
FloatType m_epsilon, m_epsilonSquared, m_scale;
bool m_planar;
std::vector<vec3> m_planarPointCloudTemp;
VertexDataSource<FloatType> m_vertexData;
MeshBuilder<FloatType> m_mesh;
std::array<IndexType,6> m_extremeValues;
std::array<size_t,6> m_extremeValues;
DiagnosticsData m_diagnostics;
// Temporary variables used during iteration process
std::vector<IndexType> m_newFaceIndices;
std::vector<IndexType> m_newHalfEdgeIndices;
std::vector< std::unique_ptr<std::vector<IndexType>> > m_disabledFacePointVectors;
std::vector<size_t> m_newFaceIndices;
std::vector<size_t> m_newHalfEdgeIndices;
std::vector< std::unique_ptr<std::vector<size_t>> > m_disabledFacePointVectors;
std::vector<size_t> m_visibleFaces;
std::vector<size_t> m_horizonEdges;
struct FaceData {
size_t m_faceIndex;
size_t m_enteredFromHalfEdge; // If the face turns out not to be visible, this half edge will be marked as horizon edge
FaceData(size_t fi, size_t he) : m_faceIndex(fi),m_enteredFromHalfEdge(he) {}
};
std::vector<FaceData> m_possiblyVisibleFaces;
std::deque<size_t> m_faceList;
// Create a half edge mesh representing the base tetrahedron from which the QuickHull iteration proceeds. m_extremeValues must be properly set up when this is called.
MeshBuilder<FloatType> getInitialTetrahedron();
void setupInitialTetrahedron();
// Given a list of half edges, try to rearrange them so that they form a loop. Return true on success.
bool reorderHorizonEdges(std::vector<IndexType>& horizonEdges);
bool reorderHorizonEdges(std::vector<size_t>& 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<IndexType,6> getExtremeValues();
std::array<size_t,6> getExtremeValues();
// Compute scale of the vertex data.
FloatType getScale(const std::array<IndexType,6>& extremeValues);
FloatType getScale(const std::array<size_t,6>& extremeValues);
// 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<std::vector<IndexType>> m_indexVectorPool;
inline std::unique_ptr<std::vector<IndexType>> getIndexVectorFromPool();
inline void reclaimToIndexVectorPool(std::unique_ptr<std::vector<IndexType>>& ptr);
Pool<std::vector<size_t>> m_indexVectorPool;
inline std::unique_ptr<std::vector<size_t>> getIndexVectorFromPool();
inline void reclaimToIndexVectorPool(std::unique_ptr<std::vector<size_t>>& 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<FloatType>::Face& f, IndexType pointIndex);
inline bool addPointToFace(typename MeshBuilder<FloatType>::Face& f, size_t pointIndex);
// This will update m_mesh from which we create the ConvexHull object that getConvexHull function returns
void createConvexHalfEdgeMesh();
@ -115,7 +124,10 @@ namespace quickhull {
// useOriginalIndices: should the output mesh use same vertex indices as the original point cloud. If this is false,
// then we generate a new vertex buffer which contains only the vertices that are part of the convex hull.
// eps: minimum distance to a plane to consider a point being on positive of it (for a point cloud with scale 1)
ConvexHull<FloatType> getConvexHull(const std::vector<Vector3<FloatType>>& pointCloud, bool CCW, bool useOriginalIndices, FloatType eps = Epsilon);
ConvexHull<FloatType> getConvexHull(const std::vector<Vector3<FloatType>>& pointCloud,
bool CCW,
bool useOriginalIndices,
FloatType eps = defaultEps<FloatType>());
// Computes convex hull for a given point cloud.
// Params:
@ -124,8 +136,12 @@ namespace quickhull {
// CCW: whether the output mesh triangles should have CCW orientation
// useOriginalIndices: should the output mesh use same vertex indices as the original point cloud. If this is false,
// then we generate a new vertex buffer which contains only the vertices that are part of the convex hull.
// eps: minimum distance to a plane to consider a point being on positive of it (for a point cloud with scale 1)
ConvexHull<FloatType> getConvexHull(const Vector3<FloatType>* vertexData, size_t vertexCount, bool CCW, bool useOriginalIndices, FloatType eps = Epsilon);
// eps: minimum distance to a plane to consider a point being on positive side of it (for a point cloud with scale 1)
ConvexHull<FloatType> getConvexHull(const Vector3<FloatType>* vertexData,
size_t vertexCount,
bool CCW,
bool useOriginalIndices,
FloatType eps = defaultEps<FloatType>());
// Computes convex hull for a given point cloud. This function assumes that the vertex data resides in memory
// in the following format: x_0,y_0,z_0,x_1,y_1,z_1,...
@ -135,8 +151,12 @@ namespace quickhull {
// CCW: whether the output mesh triangles should have CCW orientation
// useOriginalIndices: should the output mesh use same vertex indices as the original point cloud. If this is false,
// then we generate a new vertex buffer which contains only the vertices that are part of the convex hull.
// eps: minimum distance to a plane to consider a point being on positive of it (for a point cloud with scale 1)
ConvexHull<FloatType> getConvexHull(const FloatType* vertexData, size_t vertexCount, bool CCW, bool useOriginalIndices, FloatType eps = Epsilon);
// eps: minimum distance to a plane to consider a point being on positive side of it (for a point cloud with scale 1)
ConvexHull<FloatType> getConvexHull(const FloatType* vertexData,
size_t vertexCount,
bool CCW,
bool useOriginalIndices,
FloatType eps = defaultEps<FloatType>());
// Computes convex hull for a given point cloud. This function assumes that the vertex data resides in memory
// in the following format: x_0,y_0,z_0,x_1,y_1,z_1,...
@ -144,10 +164,13 @@ namespace quickhull {
// vertexData: pointer to the X component of the first point of the point cloud.
// vertexCount: number of vertices in the point cloud
// CCW: whether the output mesh triangles should have CCW orientation
// eps: minimum distance to a plane to consider a point being on positive of it (for a point cloud with scale 1)
// 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 a mesh object with half edge structure.
HalfEdgeMesh<FloatType, IndexType> getConvexHullAsMesh(const FloatType* vertexData, size_t vertexCount, bool CCW, FloatType eps = Epsilon);
HalfEdgeMesh<FloatType, size_t> getConvexHullAsMesh(const FloatType* vertexData,
size_t vertexCount,
bool CCW,
FloatType eps = defaultEps<FloatType>());
// Get diagnostics about last generated convex hull
const DiagnosticsData& getDiagnostics() {
@ -160,14 +183,14 @@ namespace quickhull {
*/
template<typename T>
std::unique_ptr<std::vector<IndexType>> QuickHull<T>::getIndexVectorFromPool() {
std::unique_ptr<std::vector<size_t>> QuickHull<T>::getIndexVectorFromPool() {
auto r = std::move(m_indexVectorPool.get());
r->clear();
return r;
}
template<typename T>
void QuickHull<T>::reclaimToIndexVectorPool(std::unique_ptr<std::vector<IndexType>>& ptr) {
void QuickHull<T>::reclaimToIndexVectorPool(std::unique_ptr<std::vector<size_t>>& ptr) {
const size_t oldSize = ptr->size();
if ((oldSize+1)*128 < ptr->capacity()) {
// Reduce memory usage! Huge vectors are needed at the beginning of iteration when faces have many points on their positive side. Later on, smaller vectors will suffice.
@ -178,7 +201,7 @@ namespace quickhull {
}
template<typename T>
bool QuickHull<T>::addPointToFace(typename MeshBuilder<T>::Face& f, IndexType pointIndex) {
bool QuickHull<T>::addPointToFace(typename MeshBuilder<T>::Face& f, size_t pointIndex) {
const T D = mathutils::getSignedDistanceToPlane(m_vertexData[ pointIndex ],f.m_P);
if (D>0 && D*D > m_epsilonSquared*f.m_P.m_sqrNLength) {
if (!f.m_pointsOnPositiveSide) {

View File

@ -14,15 +14,17 @@ Basic usage:
// Add points to point cloud
...
auto hull = qh.getConvexHull(pointCloud, true, false);
auto indexBuffer = hull.getIndexBuffer();
auto vertexBuffer = hull.getVertexBuffer();
const auto& indexBuffer = hull.getIndexBuffer();
const auto& vertexBuffer = hull.getVertexBuffer();
// Do what you want with the convex triangle mesh
Vertex data can be passed as a pointer to float/double as long as the data is in X_0,Y_0,Z_0,X_1,Y_1,Z_1,...,X_N,Y_N_Z_N format:
auto hull = qh.getConvexHull(&pointCloud[0].x, pointCloud.size(), true, false);
The first boolean parameter of getConvexHull specifies whether the output mesh should have its triangles in CCW orientation. The second boolean parameter specifies whether the output mesh should use vertex indices of the original point cloud. If it is false, a new vertex buffer is generated which consists only of those vertices that are part of the convex hull.
The first boolean parameter of getConvexHull specifies whether the resulting mesh should have its triangles in CCW orientation.
The second boolean parameter specifies whether the mesh should use vertex indices of the original point cloud. If it is false, a new vertex buffer is generated which consists only of those vertices that are part of the convex hull. In this case, the new vertex buffer is owned by the returned ConvexHull object. Otherwise, the original point cloud is used as vertex buffer and since the vertices are not copied, make sure you don't call ConvexHull::getVertexBuffer after releasing the memory that contains the original point cloud data.
This implementation is fast, because the convex hull is internally built using a half edge mesh representation which provides quick access to adjacent faces. It is also possible to get the output convex hull as a half edge mesh:

View File

@ -5,8 +5,6 @@
#include "Vector3.hpp"
#include "Plane.hpp"
#include "Pool.hpp"
#include "../Types.hpp"
#include <string>
#include <array>
#include <cassert>
#include <limits>
@ -20,32 +18,32 @@ namespace quickhull {
class MeshBuilder {
public:
struct HalfEdge {
IndexType m_endVertex;
IndexType m_opp;
IndexType m_face;
IndexType m_next;
size_t m_endVertex;
size_t m_opp;
size_t m_face;
size_t m_next;
void disable() {
m_endVertex = std::numeric_limits<IndexType>::max();
m_endVertex = std::numeric_limits<size_t>::max();
}
bool isDisabled() const {
return m_endVertex == std::numeric_limits<IndexType>::max();
return m_endVertex == std::numeric_limits<size_t>::max();
}
};
struct Face {
IndexType m_he;
size_t m_he;
Plane<T> m_P;
T m_mostDistantPointDist;
IndexType m_mostDistantPoint;
size_t m_mostDistantPoint;
size_t m_visibilityCheckedOnIteration;
std::uint8_t m_isVisibleFaceOnCurrentIteration : 1;
std::uint8_t m_inFaceStack : 1;
std::uint8_t m_horizonEdgesOnCurrentIteration : 3; // Bit for each half edge assigned to this face, each being 0 or 1 depending on whether the edge belongs to horizon edge
std::unique_ptr<std::vector<IndexType>> m_pointsOnPositiveSide;
std::unique_ptr<std::vector<size_t>> m_pointsOnPositiveSide;
Face() : m_he(std::numeric_limits<IndexType>::max()),
Face() : m_he(std::numeric_limits<size_t>::max()),
m_mostDistantPointDist(0),
m_mostDistantPoint(0),
m_visibilityCheckedOnIteration(0),
@ -57,11 +55,11 @@ namespace quickhull {
}
void disable() {
m_he = std::numeric_limits<IndexType>::max();
m_he = std::numeric_limits<size_t>::max();
}
bool isDisabled() const {
return m_he == std::numeric_limits<IndexType>::max();
return m_he == std::numeric_limits<size_t>::max();
}
};
@ -72,11 +70,11 @@ namespace quickhull {
// 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.
std::vector<IndexType> m_disabledFaces,m_disabledHalfEdges;
std::vector<size_t> m_disabledFaces,m_disabledHalfEdges;
IndexType addFace() {
size_t addFace() {
if (m_disabledFaces.size()) {
IndexType index = m_disabledFaces.back();
size_t index = m_disabledFaces.back();
auto& f = m_faces[index];
assert(f.isDisabled());
assert(!f.m_pointsOnPositiveSide);
@ -88,9 +86,9 @@ namespace quickhull {
return m_faces.size()-1;
}
IndexType addHalfEdge() {
size_t addHalfEdge() {
if (m_disabledHalfEdges.size()) {
const IndexType index = m_disabledHalfEdges.back();
const size_t index = m_disabledHalfEdges.back();
m_disabledHalfEdges.pop_back();
return index;
}
@ -99,14 +97,14 @@ namespace quickhull {
}
// Mark a face as disabled and return a pointer to the points that were on the positive of it.
std::unique_ptr<std::vector<IndexType>> disableFace(IndexType faceIndex) {
std::unique_ptr<std::vector<size_t>> disableFace(size_t faceIndex) {
auto& f = m_faces[faceIndex];
f.disable();
m_disabledFaces.push_back(faceIndex);
return std::move(f.m_pointsOnPositiveSide);
}
void disableHalfEdge(IndexType heIndex) {
void disableHalfEdge(size_t heIndex) {
auto& he = m_halfEdges[heIndex];
he.disable();
m_disabledHalfEdges.push_back(heIndex);
@ -115,7 +113,15 @@ namespace quickhull {
MeshBuilder() = default;
// Create a mesh with initial tetrahedron ABCD. Dot product of AB with the normal of triangle ABC should be negative.
MeshBuilder(IndexType a, IndexType b, IndexType c, IndexType d) {
void setup(size_t a, size_t b, size_t c, size_t d) {
m_faces.clear();
m_halfEdges.clear();
m_disabledFaces.clear();
m_disabledHalfEdges.clear();
m_faces.reserve(4);
m_halfEdges.reserve(12);
// Create halfedges
HalfEdge AB;
AB.m_endVertex = b;
@ -219,8 +225,8 @@ namespace quickhull {
m_faces.push_back(std::move(CBD));
}
std::array<IndexType,3> getVertexIndicesOfFace(const Face& f) const {
std::array<IndexType,3> v;
std::array<size_t,3> getVertexIndicesOfFace(const Face& f) const {
std::array<size_t,3> v;
const HalfEdge* he = &m_halfEdges[f.m_he];
v[0] = he->m_endVertex;
he = &m_halfEdges[he->m_next];
@ -230,11 +236,11 @@ namespace quickhull {
return v;
}
std::array<IndexType,2> getVertexIndicesOfHalfEdge(const HalfEdge& he) const {
std::array<size_t,2> getVertexIndicesOfHalfEdge(const HalfEdge& he) const {
return {m_halfEdges[he.m_opp].m_endVertex,he.m_endVertex};
}
std::array<IndexType,3> getHalfEdgeIndicesOfFace(const Face& f) const {
std::array<size_t,3> getHalfEdgeIndicesOfFace(const Face& f) const {
return {f.m_he,m_halfEdges[f.m_he].m_next,m_halfEdges[m_halfEdges[f.m_he].m_next].m_next};
}
};

View File

@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.11)
project(QuickHullTests)
set(SOURCE_FILES
${CMAKE_SOURCE_DIR}/QuickHullTests.cpp
${CMAKE_SOURCE_DIR}/main.cpp
${CMAKE_SOURCE_DIR}/../QuickHull.cpp
)
add_definitions(-std=c++11)
add_executable(QuickHullTests ${SOURCE_FILES})

View File

@ -3,6 +3,7 @@
#include <iostream>
#include <random>
#include <cassert>
#include <chrono>
namespace quickhull {
@ -19,11 +20,11 @@ namespace quickhull {
return from + (FloatType)dist(rng)*(to-from);
};
void assertSameValue(FloatType a, FloatType b) {
static void assertSameValue(FloatType a, FloatType b) {
assert(std::abs(a-b)<0.0001f);
}
void testVector3() {
static void testVector3() {
typedef Vector3<FloatType> vec3;
vec3 a(1,0,0);
vec3 b(1,0,0);
@ -38,7 +39,7 @@ namespace quickhull {
}
template <typename T>
std::vector<Vector3<T>> createSphere(T radius, size_t M, Vector3<T> offset = Vector3<T>(0,0,0)) {
static std::vector<Vector3<T>> createSphere(T radius, size_t M, Vector3<T> offset = Vector3<T>(0,0,0)) {
std::vector<Vector3<T>> pc;
const T pi = 3.14159f;
for (int i=0;i<=M;i++) {
@ -55,7 +56,7 @@ namespace quickhull {
return pc;
}
void sphereTest() {
static void sphereTest() {
QuickHull<FloatType> qh;
FloatType y = 1;
for (;;) {
@ -75,7 +76,6 @@ namespace quickhull {
for (;;) {
auto pc = createSphere<FloatType>(1, i, Vector3<FloatType>(0,0,0));
auto hull = qh.getConvexHull(pc,true,false,eps);
std::cout << i << ":" << pc.size() << " : " << hull.getVertexBuffer().size() << " at eps=" << eps << std::endl;
if (qh.getDiagnostics().m_failedHorizonEdges) {
// This should not happen
assert(false);
@ -87,16 +87,14 @@ namespace quickhull {
}
else {
eps *= 0.5f;
std::cout << "Epsilon to " << eps << std::endl;
}
if (i == 500) {
if (i == 100) {
break;
}
}
}
void testPlanarCase() {
static void testPlanarCase() {
QuickHull<FloatType> qh;
std::vector<vec3> pc;
pc.emplace_back(-3.000000f, -0.250000f, -0.800000f);
@ -108,10 +106,12 @@ namespace quickhull {
assert(hull.getVertexBuffer().size()==4);
}
void testHalfEdgeOutput() {
QuickHull<FloatType> qh;
static void testHalfEdgeOutput() {
QuickHull<FloatType> qh;
// 8 corner vertices of a cube + tons of vertices inside. Output should be a half edge mesh with 12 faces (6 cube faces with 2 triangles per face) and 36 half edges (3 half edges per face).
// 8 corner vertices of a cube + tons of vertices inside.
// Output should be a half edge mesh with 12 faces (6 cube faces with 2 triangles
// per face) and 36 half edges (3 half edges per face).
std::vector<vec3> pc;
for (int h=0;h<1000;h++) {
pc.emplace_back(rnd(-1,1),rnd(-1,1),rnd(-1,1));
@ -123,9 +123,17 @@ namespace quickhull {
assert(mesh.m_faces.size() == 12);
assert(mesh.m_halfEdges.size() == 36);
assert(mesh.m_vertices.size() == 8);
// Verify that for each face f, f.halfedgeIndex equals next(next(next(f.halfedgeIndex))).
for (const auto& f : mesh.m_faces) {
size_t next = mesh.m_halfEdges[f.m_halfEdgeIndex].m_next;
next = mesh.m_halfEdges[next].m_next;
next = mesh.m_halfEdges[next].m_next;
assert(next == f.m_halfEdgeIndex);
}
}
void testPlanes() {
static void testPlanes() {
Vector3<FloatType> N(1,0,0);
Vector3<FloatType> p(2,0,0);
Plane<FloatType> P(N,p);
@ -140,8 +148,46 @@ namespace quickhull {
dist = mathutils::getSignedDistanceToPlane(Vector3<FloatType>(6,0,0), P);
assertSameValue(dist,8);
}
void run() {
static void testVertexBufferAddress() {
QuickHull<FloatType> qh;
std::vector<vec3> pc;
pc.emplace_back(0, 0, 0);
pc.emplace_back(1, 0, 0);
pc.emplace_back(0, 1, 0);
for (size_t i=0;i<2;i++) {
const bool useOriginalIndices = i != 0;
const auto hull = qh.getConvexHull(pc,false, useOriginalIndices);
const auto vertices = hull.getVertexBuffer();
assert(vertices.size() > 0);
assert((&vertices[0] == &pc[0]) == useOriginalIndices);
}
}
static void testNormals() {
QuickHull<FloatType> qh;
std::vector<vec3> pc;
pc.emplace_back(0, 0, 0);
pc.emplace_back(1, 0, 0);
pc.emplace_back(0, 1, 0);
std::array<vec3, 2> normal;
for (size_t i=0;i<2;i++) {
const bool CCW = i;
const auto hull = qh.getConvexHull(pc,CCW,false);
const auto vertices = hull.getVertexBuffer();
const auto indices = hull.getIndexBuffer();
assert(vertices.size() == 3);
assert(indices.size() >= 6);
const vec3 triangle[3] = { vertices[indices[0]], vertices[indices[1]], vertices[indices[2]] };
normal[i] = mathutils::getTriangleNormal(triangle[0], triangle[1], triangle[2]);
}
const auto dot = normal[0].dotProduct(normal[1]);
assertSameValue(dot, -1);
}
int run() {
// Setup test env
const size_t N = 200;
std::vector<vec3> pc;
@ -252,27 +298,15 @@ namespace quickhull {
}
}
// Test 7
for (int h=0;h<100;h++) {
pc.clear();
const vec3 v1(rnd(-1,1),rnd(-1,1),rnd(-1,1));
const vec3 v2(rnd(-1,1),rnd(-1,1),rnd(-1,1));
pc.push_back(v1);
pc.push_back(v2);
for (int i=0;i<N;i++) {
auto t1 = rnd(0,1);
auto t2 = rnd(0,1);
pc.push_back(t1*v1 + t2*v2);
}
hull = qh.getConvexHull(pc,true,false);
}
// Other tests
testVertexBufferAddress();
testNormals();
testPlanes();
sphereTest();
testVector3();
testHalfEdgeOutput();
std::cout << "QuickHull tests successfully passed." << std::endl;
sphereTest();
std::cout << "QuickHull tests succesfully passed." << std::endl;
return 0;
}
}

View File

@ -0,0 +1,9 @@
namespace quickhull {
namespace tests {
int run();
}
}
int main(int, char** argv) {
return quickhull::tests::run();
}

View File

@ -1,10 +0,0 @@
#ifndef QuickHull_Types_hpp
#define QuickHull_Types_hpp
namespace quickhull {
typedef size_t IndexType;
}
#endif