-
Notifications
You must be signed in to change notification settings - Fork 114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Convex Decomposition and Minkowski Sum #663
Changes from 17 commits
ef30739
56e026b
804d29a
15579ef
c039b8a
3f6248d
a32be23
2aef6ff
21f1daa
f0b9faf
ae5114d
595c933
395e94b
7e1fa10
dfe8e62
1034970
76ca0d9
4921354
d8fd65d
04ae42e
7909f59
2505b15
f86adc5
c574580
1cb3a83
e84e05c
a4d20c0
9c04c6d
33c69d6
b6978bf
7ab872b
3446bb0
258612c
3dcd3a5
6a8a407
c4537e7
19c58ca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -316,4 +316,24 @@ CrossSection Manifold::Impl::Project() const { | |
|
||
return CrossSection(polys).Simplify(precision_); | ||
} | ||
|
||
glm::dvec4 Manifold::Impl::Circumcircle(Vec<glm::dvec3> verts, int face) const { | ||
glm::dvec3 va = verts[this->halfedge_[(face * 3) + 0].startVert]; | ||
glm::dvec3 vb = verts[this->halfedge_[(face * 3) + 1].startVert]; | ||
glm::dvec3 vc = verts[this->halfedge_[(face * 3) + 2].startVert]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about a |
||
|
||
glm::dvec3 a = va - vc; | ||
glm::dvec3 b = vb - vc; | ||
double a_length = glm::length(a); | ||
double b_length = glm::length(b); | ||
glm::dvec3 numerator = | ||
glm::cross((((a_length * a_length) * b) - ((b_length * b_length) * a)), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we remove a few parens? |
||
glm::cross(a, b)); | ||
double crs = glm::length(glm::cross(a, b)); | ||
double denominator = 2.0 * (crs * crs); | ||
glm::dvec3 circumcenter = (numerator / denominator) + vc; | ||
double circumradius = glm::length(circumcenter - vc); | ||
return glm::dvec4(circumcenter.x, circumcenter.y, circumcenter.z, | ||
circumradius); | ||
} | ||
} // namespace manifold |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,12 +15,15 @@ | |
#include <algorithm> | ||
#include <map> | ||
#include <numeric> | ||
#include <random> | ||
#include <unordered_set> | ||
|
||
#include "QuickHull.hpp" | ||
#include "boolean3.h" | ||
#include "csg_tree.h" | ||
#include "impl.h" | ||
#include "par.h" | ||
#include "voro++.hh" | ||
|
||
namespace { | ||
using namespace manifold; | ||
|
@@ -845,4 +848,141 @@ Manifold Manifold::Hull() const { return Hull(GetMesh().vertPos); } | |
Manifold Manifold::Hull(const std::vector<Manifold>& manifolds) { | ||
return Compose(manifolds).Hull(); | ||
} | ||
|
||
// TODO: Handle Joggling in the Fracture Function Directly? | ||
/** | ||
* Compute the voronoi fracturing of this Manifold into convex chunks. | ||
* | ||
* @param pts A vector of points over which to fracture the manifold. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we have some detail on what these inputs do and the purpose of this function? |
||
* @param pts A vector of weights controlling the relative size of each chunk. | ||
*/ | ||
std::vector<Manifold> Manifold::Fracture( | ||
const std::vector<glm::dvec3>& pts, | ||
const std::vector<double>& weights) const { | ||
std::vector<Manifold> output; | ||
output.reserve(pts.size()); | ||
|
||
Box bounds = BoundingBox(); | ||
glm::vec3 min = bounds.min - 0.1f; | ||
glm::vec3 max = bounds.max + 0.1f; | ||
double V = (max.x - min.x) * (max.y - min.y) * (max.z - min.z); | ||
double Nthird = powf((double)pts.size() / V, 1.0f / 3.0f); | ||
voro::container_poly container(min.x, max.x, min.y, max.y, min.z, max.z, | ||
std::round(Nthird * (max.x - min.x)), | ||
std::round(Nthird * (max.y - min.y)), | ||
std::round(Nthird * (max.z - min.z)), false, | ||
false, false, pts.size()); | ||
|
||
bool hasWeights = weights.size() == pts.size(); | ||
for (size_t i = 0; i < pts.size(); i++) { | ||
container.put(i, pts[i].x, pts[i].y, pts[i].z, | ||
hasWeights ? weights[i] : 1.0f); | ||
} | ||
voro::voronoicell c(container); | ||
voro::c_loop_all vl(container); | ||
if (vl.start()) do { // TODO: Parallelize this loop! | ||
if (container.compute_cell(c, vl)) { | ||
std::vector<glm::vec3> verts; | ||
verts.reserve(c.p); | ||
int id; | ||
double x, y, z, r; | ||
vl.pos(id, x, y, z, r); | ||
for (size_t i = 0; i < c.p; i++) { | ||
verts.push_back(glm::vec3(x + 0.5 * c.pts[(vl.ps * i) + 0], | ||
y + 0.5 * c.pts[(vl.ps * i) + 1], | ||
z + 0.5 * c.pts[(vl.ps * i) + 2])); | ||
} | ||
Manifold outputManifold = | ||
Hull(verts) ^ | ||
*this; // TODO: Replace this intersection with a voro++ wall | ||
// implementation or cutting the cell directly... | ||
if (outputManifold.GetProperties().volume > 0.0) { | ||
output.push_back(outputManifold); | ||
} | ||
verts.clear(); | ||
} | ||
} while (vl.inc()); | ||
return output; | ||
} | ||
std::vector<Manifold> Manifold::Fracture( | ||
const std::vector<glm::vec3>& pts, | ||
const std::vector<float>& weights) const { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like to avoid having two APIs for different precisions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it okay to expect everything to converge to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO yes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes |
||
std::vector<glm::dvec3> highpVerts(pts.size()); | ||
std::vector<double> highpWeights(weights.size()); | ||
for (size_t i = 0; i < pts.size(); i++) { | ||
highpVerts[i] = pts[i]; | ||
highpWeights[i] = weights[i]; | ||
} | ||
return Fracture(highpVerts, highpWeights); | ||
} | ||
|
||
std::vector<Manifold> Manifold::ConvexDecomposition() const { | ||
// Step 1. Get a list of all unique triangle faces with at least one reflex | ||
// edge | ||
std::unordered_set<int> uniqueReflexFaceSet; | ||
const Impl& impl = *GetCsgLeafNode().GetImpl(); | ||
for (size_t i = 0; i < impl.halfedge_.size(); i++) { | ||
Halfedge halfedge = impl.halfedge_[i]; | ||
int faceA = halfedge.face; | ||
int faceB = impl.halfedge_[halfedge.pairedHalfedge].face; | ||
glm::vec3 tangent = | ||
glm::cross(impl.faceNormal_[faceA], | ||
impl.vertPos_[impl.halfedge_[i].endVert] - | ||
impl.vertPos_[impl.halfedge_[i].startVert]); | ||
float tangentProjection = glm::dot(impl.faceNormal_[faceB], tangent); | ||
// If we've found a pair of reflex triangles, add them to the set | ||
if (tangentProjection > 1e-6) { | ||
uniqueReflexFaceSet.insert(faceA); | ||
uniqueReflexFaceSet.insert(faceB); | ||
} | ||
} | ||
std::vector<int> uniqueFaces; // Copy to a vector for indexed access | ||
uniqueFaces.insert(uniqueFaces.end(), uniqueReflexFaceSet.begin(), | ||
uniqueReflexFaceSet.end()); | ||
|
||
// Step 2. Calculate the Circumcircles (centers + radii) of these triangles | ||
std::vector<glm::dvec3> circumcenters(uniqueFaces.size()); | ||
std::vector<double> circumradii(uniqueFaces.size()); | ||
Vec<glm::dvec3> joggledVerts(impl.vertPos_.size()); | ||
for (size_t i = 0; i < impl.vertPos_.size(); i++) { | ||
joggledVerts[i] = impl.vertPos_[i]; | ||
} | ||
for (size_t i = 0; i < uniqueFaces.size(); i++) { | ||
glm::dvec4 circumcircle = impl.Circumcircle(joggledVerts, uniqueFaces[i]); | ||
circumcenters[i] = | ||
glm::dvec3(circumcircle.x, circumcircle.y, circumcircle.z); | ||
circumradii[i] = circumcircle.w; | ||
} | ||
|
||
// Step 3. If any two circumcenters are identical, joggle one of the triangle | ||
// vertices, TODO: and store in a hashmap | ||
std::mt19937 mt(1337); | ||
std::uniform_real_distribution<double> dist(0.0, 1.0); | ||
double randOffset = 0.0; | ||
for (size_t i = 0; i < circumcenters.size() - 1; i++) { | ||
for (size_t j = i + 1; j < circumcenters.size(); j++) { | ||
if (glm::distance(circumcenters[i], circumcenters[j]) < 1e-6) { | ||
joggledVerts[impl.halfedge_[(uniqueFaces[i] * 3) + 0].startVert] += | ||
glm::dvec3(dist(mt), dist(mt), dist(mt)) * 1e-11; | ||
} | ||
} | ||
} | ||
|
||
// Step 4. Recalculate the circumcenters | ||
for (size_t i = 0; i < uniqueFaces.size(); i++) { | ||
glm::dvec4 circumcircle = impl.Circumcircle(joggledVerts, uniqueFaces[i]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we joggle the circumcircles instead of the verts? Overriding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My ultimate consideration is to record which verts were joggled before computing the voronoi diagram, and then to unjoggle them afterward so that they occupy their original exact coordinates again... I'm considering alternative strategies, like applying an invertible spatial distortion to all the input vertices, and then backwards to all of the output voronoi diagram vertices.... just have to find the right strategy that preserves precision... |
||
circumcenters[i] = | ||
glm::dvec3(circumcircle.x, circumcircle.y, circumcircle.z); | ||
circumradii[i] = circumcircle.w; | ||
} | ||
|
||
// Step 5. Calculate the Voronoi Fracturing | ||
std::vector<Manifold> output = Fracture(circumcenters, circumradii); | ||
|
||
// TODO: | ||
// Step 6. Unjoggle the voronoi region vertices | ||
// Step 7. Hull and Intersect with the original Manifold | ||
|
||
return output; | ||
} | ||
} // namespace manifold |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is fracture really meant to be a public function, or is it just a means to ConvexDecomposition?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's worth making it public because it's a non-trival operation that generalizes splitting a singular cutting plane by limiting the influence of each half-space. This can help make modelling cuts and edits more "local". Like, if you wanted to take a complicated, winding assembly, and break it apart into multiple pieces, it's nice to be able to make cuts with a localized area of effect to avoid mangling the model far from the cut.
Also, it's useful for people looking to implement their own approximate decompositions based on their particular speed and accuracy needs.
And it's good for modelling lattices and packings, which is one of the primary things Voro++ is used for... it could even help replicate NTopology lattice generation functionality if each cell is contracted and then subtracted from a solid region.
There's a universe with a dual version of this function, where the user directly specifies splitting planes and their relative area of influence. This could be implemented by inserting two voronoi cells right next to each other with the plane normal as the offset between them. Perhaps this would be more intuitive?