Skip to content
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 MinkowskiSum and MinkowskiDifference #666

Open
wants to merge 54 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
bcd06a4
Add Naive Minkowski Sum Implementation
zalo Dec 19, 2023
89a9dea
Fix CI Bellyaching
zalo Dec 20, 2023
30be9a0
Add Python Minkowski Test
zalo Dec 20, 2023
f1e245f
Enable Determinism for the Failing CI Tests
zalo Dec 20, 2023
d51976a
Fix Python Test
zalo Dec 20, 2023
12990f8
Fix Python Test Again
zalo Dec 20, 2023
5f52540
Make Python Test Cheaper so it doesn't segfault
zalo Dec 20, 2023
755a55c
Add Insetting Option
zalo Dec 20, 2023
42b8cf6
Update Python Binding Too
zalo Dec 20, 2023
00d92de
Switch to a Member Function
zalo Dec 21, 2023
93b6f30
Remove the useThreading option
zalo Dec 21, 2023
0b7e34d
Merge remote-tracking branch 'upstream/master' into feat-naive-minkowski
zalo Dec 21, 2023
953eaf3
Fix the Python Binding
zalo Dec 21, 2023
2d93b10
And the example
zalo Dec 21, 2023
b668659
Fix Formatting
zalo Dec 21, 2023
847ae97
Split into Add/Subtract Functions, Add C Binding
zalo Dec 21, 2023
1587164
Make Methods const
zalo Dec 21, 2023
ebe2baa
Update the tests; ensure that the inset operation works with convex s…
zalo Dec 21, 2023
5f20c54
Fix Python Example Again
zalo Dec 21, 2023
622556e
Add JS Bindings
zalo Dec 21, 2023
ca0a35d
Fix Formatting
zalo Dec 21, 2023
807910e
Fix Formatting Properly
zalo Dec 21, 2023
3ba8302
Change Function Names to Sum/Difference
zalo Dec 21, 2023
49a50c2
Fix formatting
zalo Dec 21, 2023
0f840b4
Move Tests to boolean_test
zalo Dec 22, 2023
98d9ca9
Fix Formatting
zalo Dec 22, 2023
952a54f
Fix NonConvexConvex Test
zalo Dec 22, 2023
3928719
Fix Formatting
zalo Dec 22, 2023
f5ceb05
Add Surface Area
zalo Dec 22, 2023
84e08f2
passing the last test
elalish Dec 22, 2023
8fdf164
forgot these
elalish Dec 22, 2023
48f174a
Merge pull request #1 from elalish/fixMinkowski
zalo Dec 22, 2023
377d58b
Fix Test?
zalo Dec 22, 2023
cac0610
Fix for Disconnected Manifolds
zalo Dec 23, 2023
1a5611e
Codify IsConvex, Make Python Cheaper
zalo Dec 27, 2023
038e896
Refactor to use Impl
zalo Dec 27, 2023
560b3cb
Merge remote-tracking branch 'upstream/master' into feat-naive-minkow…
zalo Dec 27, 2023
cc6cbd5
Merge remote-tracking branch 'upstream/master' into feat-naive-minkowski
zalo Dec 27, 2023
6ca9645
Remove Dangling Line Return
zalo Dec 27, 2023
418cf1c
Merge branch 'feat-naive-minkowski-refactor' into feat-naive-minkowski
zalo Dec 29, 2023
8599a82
Add another early exit
zalo Jan 4, 2024
d87180b
Merge remote-tracking branch 'upstream/master' into feat-naive-minkowski
zalo Sep 16, 2024
c530b7a
Fix Glaring Bugs...
zalo Sep 16, 2024
114d2c1
Fix Formatting
zalo Sep 16, 2024
adc1163
Again
zalo Sep 16, 2024
922e6b4
Fix Mesh Export
zalo Sep 16, 2024
3ee19b8
Run SimplifyTopology on the output of Minkowski
zalo Sep 16, 2024
31700fc
Update Tests
zalo Sep 17, 2024
baefc51
Update Formatting
zalo Sep 17, 2024
098d2b1
Make more work multithreaded
zalo Sep 17, 2024
2dd5b8b
Fix formatting
zalo Sep 17, 2024
d1f35e2
Disable test cheating?
zalo Sep 17, 2024
be0f63b
Revert "Disable test cheating?"
zalo Sep 17, 2024
7dd55dd
Merge remote-tracking branch 'upstream/master' into feat-naive-minkowski
zalo Oct 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@
"Gyroid",
"halfedge",
"halfedges",
"Minkowski",
"Tris",
"Verts",
"Voxel",
Expand Down
4 changes: 4 additions & 0 deletions bindings/c/include/manifold/manifoldc.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ ManifoldManifoldPair manifold_split_by_plane(void *mem_first, void *mem_second,
ManifoldManifold *manifold_trim_by_plane(void *mem, ManifoldManifold *m,
double normal_x, double normal_y,
double normal_z, double offset);
ManifoldManifold *manifold_minkowski_sum(void *mem, ManifoldManifold *a,
ManifoldManifold *b);
ManifoldManifold *manifold_minkowski_difference(void *mem, ManifoldManifold *a,
ManifoldManifold *b);

// 3D to 2D

Expand Down
12 changes: 12 additions & 0 deletions bindings/c/manifoldc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,18 @@ ManifoldManifold *manifold_trim_by_plane(void *mem, ManifoldManifold *m,
return to_c(new (mem) Manifold(trimmed));
}

ManifoldManifold *manifold_minkowski_sum(void *mem, ManifoldManifold *a,
ManifoldManifold *b) {
auto m = (*from_c(a)).MinkowskiSum(*from_c(b));
return to_c(new (mem) Manifold(m));
}

ManifoldManifold *manifold_minkowski_difference(void *mem, ManifoldManifold *a,
ManifoldManifold *b) {
auto m = (*from_c(a)).MinkowskiDifference(*from_c(b));
return to_c(new (mem) Manifold(m));
}

ManifoldPolygons *manifold_slice(void *mem, ManifoldManifold *m,
double height) {
auto poly = from_c(m)->Slice(height);
Expand Down
22 changes: 22 additions & 0 deletions bindings/python/examples/minkowski.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import numpy as np
from manifold3d import Manifold


def run():
small_cube = Manifold.cube([0.1, 0.1, 0.1], True)
cube_vertices = small_cube.to_mesh().vert_properties[:, :3]
star = Manifold.as_original(small_cube)
for offset in [
[[0.2, 0.0, 0.0]],
[[-0.2, 0.0, 0.0]],
[[0.0, 0.2, 0.0]],
[[0.0, -0.2, 0.0]],
[[0.0, 0.0, 0.2]],
[[0.0, 0.0, -0.2]],
]:
star += Manifold.hull_points(np.concatenate((cube_vertices, offset), axis=0))

sphere = Manifold.sphere(0.6, 15)
cube = Manifold.cube([1.0, 1.0, 1.0], True)
sphereless_cube = cube - sphere
return sphereless_cube.minkowski_sum(star)
4 changes: 4 additions & 0 deletions bindings/python/manifold3d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,10 @@ NB_MODULE(manifold3d, m) {
.def("trim_by_plane", &Manifold::TrimByPlane, nb::arg("normal"),
nb::arg("origin_offset"),
manifold__trim_by_plane__normal__origin_offset)
.def("minkowski_sum", &Manifold::MinkowskiSum, nb::arg("other"),
manifold__minkowski_sum__other)
.def("minkowski_difference", &Manifold::MinkowskiDifference,
nb::arg("other"), manifold__minkowski_difference__other)
.def(
"slice",
[](const Manifold &self, double height) {
Expand Down
2 changes: 2 additions & 0 deletions bindings/wasm/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ EMSCRIPTEN_BINDINGS(whatever) {
.function("_Split", &man_js::Split)
.function("_SplitByPlane", &man_js::SplitByPlane)
.function("_TrimByPlane", &Manifold::TrimByPlane)
.function("_MinkowskiSum", &Manifold::MinkowskiSum)
.function("_MinkowskiDifference", &Manifold::MinkowskiDifference)
.function("_Slice", &Manifold::Slice)
.function("_Project", &Manifold::Project)
.function("hull", select_overload<Manifold() const>(&Manifold::Hull))
Expand Down
4 changes: 3 additions & 1 deletion bindings/wasm/examples/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ const manifoldMemberFunctions = [
'splitByPlane',
'slice',
'project',
'hull'
'hull',
'minkowskiSum',
'minkowskiDifference'
];
// CrossSection static methods (that return a new cross-section)
const crossSectionStaticFunctions = [
Expand Down
16 changes: 16 additions & 0 deletions bindings/wasm/manifold-encapsulated-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,22 @@ export class Manifold {
*/
trimByPlane(normal: Vec3, originOffset: number): Manifold;

/**
* Compute the minkowski sum of this manifold with another.
* This corresponds to the morphological dilation of the manifold.
*
* @param other The other manifold to minkowski sum to this one.
*/
minkowskiSum(other: Manifold): Manifold;

/**
* Subtract the sweep of the other manifold across this manifold's surface.
* This corresponds to the morphological erosion of the manifold.
*
* @param other The other manifold to minkowski subtract from this one.
*/
minkowskiDifference(other: Manifold): Manifold;

/**
* Returns the cross section of this object parallel to the X-Y plane at the
* specified height. Using a height equal to the bottom
Expand Down
4 changes: 4 additions & 0 deletions include/manifold/manifold.h
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,8 @@ class Manifold {
std::pair<Manifold, Manifold> SplitByPlane(vec3 normal,
double originOffset) const;
Manifold TrimByPlane(vec3 normal, double originOffset) const;
Manifold MinkowskiSum(const Manifold&) const;
Manifold MinkowskiDifference(const Manifold&) const;
///@}

/** @name 2D from 3D
Expand Down Expand Up @@ -325,6 +327,8 @@ class Manifold {
mutable std::shared_ptr<CsgNode> pNode_;

CsgLeafNode& GetCsgLeafNode() const;

Manifold Minkowski(const Manifold&, bool inset = false) const;
};
/** @} */
} // namespace manifold
32 changes: 32 additions & 0 deletions src/edge_op.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -693,4 +693,36 @@ void Manifold::Impl::SplitPinchedVerts() {
});
}
}

// Return true if Manifold is Genus 0 and contains no concave edges
bool Manifold::Impl::IsConvex(float tolerance) const {
// Convex Shape Must have Genus of 0
int chi = NumVert() - NumEdge() + NumTri();
int genus = 1 - chi / 2;
if (genus != 0) return false;

// Iterate across all edges; return false if any edges are concave
const Impl* pImpl = this;
const size_t nbEdges = halfedge_.size();
auto policy = autoPolicy(nbEdges, 1e5);
bool anyConcave = false;
for_each_n(
policy, countAt(0), nbEdges, [&anyConcave, &pImpl, &tolerance](int idx) {
Halfedge edge = pImpl->halfedge_[idx];
if (!edge.IsForward()) return;

const vec3 normal0 = pImpl->faceNormal_[edge.face];
const vec3 normal1 =
pImpl->faceNormal_[pImpl->halfedge_[edge.pairedHalfedge].face];
if (glm::all(glm::equal(normal0, normal1))) return;

const vec3 edgeVec =
pImpl->vertPos_[edge.endVert] - pImpl->vertPos_[edge.startVert];
const bool convex =
glm::dot(edgeVec, glm::cross(normal0, normal1)) > tolerance;
if (!convex) anyConcave = true;
});

return !anyConcave;
}
} // namespace manifold
1 change: 1 addition & 0 deletions src/impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ struct Manifold::Impl {
void FormLoop(int current, int end);
void CollapseTri(const ivec3& triEdge);
void SplitPinchedVerts();
bool IsConvex(float tolerance = 1e-8f) const;

// subdivision.cpp
int GetNeighbor(int tri) const;
Expand Down
100 changes: 100 additions & 0 deletions src/manifold.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,106 @@ Manifold Manifold::TrimByPlane(vec3 normal, double originOffset) const {
return *this ^ Halfspace(BoundingBox(), normal, originOffset);
}

/**
* Compute the minkowski sum of two manifolds.
*
* @param other The other manifold to minkowski sum to this one.
* @param inset Whether it should add or subtract from the manifold.
*/
Manifold Manifold::Minkowski(const Manifold& other, bool inset) const {
std::vector<Manifold> composedHulls({*this});
auto aImpl = this->GetCsgLeafNode().GetImpl();
auto bImpl = other.GetCsgLeafNode().GetImpl();

bool aConvex = aImpl->IsConvex();
bool bConvex = bImpl->IsConvex();

// If the convex manifold was supplied first, swap them!
Manifold a = *this, b = other;
if (aConvex && !bConvex) {
a = other;
b = *this;
aImpl = other.GetCsgLeafNode().GetImpl();
bImpl = this->GetCsgLeafNode().GetImpl();
aConvex = !aConvex;
bConvex = !bConvex;
}

// Early-exit if either input is empty
if (b.IsEmpty()) return a;
if (a.IsEmpty()) return b;

// Convex-Convex Minkowski: Very Fast
if (!inset && aConvex && bConvex) {
std::vector<Manifold> simpleHull;
for (const vec3& vertex : aImpl->vertPos_) {
simpleHull.push_back(b.Translate(vertex));
}
composedHulls.push_back(Manifold::Hull(simpleHull));
// Convex - Non-Convex Minkowski: Slower
} else if ((inset || !aConvex) && bConvex) {
const size_t numTri = aImpl->NumTri();
std::vector<Manifold> newHulls(numTri);
auto policy = autoPolicy(numTri, 100);
for_each_n(
policy, countAt(0), numTri, [&newHulls, &b, &aImpl](const int face) {
newHulls[face] = Manifold::Hull(
{b.Translate(
aImpl->vertPos_[aImpl->halfedge_[(face * 3) + 0].startVert]),
b.Translate(
aImpl->vertPos_[aImpl->halfedge_[(face * 3) + 1].startVert]),
b.Translate(aImpl->vertPos_[aImpl->halfedge_[(face * 3) + 2]
.startVert])});
});
composedHulls.insert(composedHulls.end(), newHulls.begin(), newHulls.end());
// Non-Convex - Non-Convex Minkowski: Very Slow
} else if (!aConvex && !bConvex) {
for (size_t aFace = 0; aFace < aImpl->NumTri(); aFace++) {
for (size_t bFace = 0; bFace < bImpl->NumTri(); bFace++) {
const bool coplanar = glm::all(glm::equal(aImpl->faceNormal_[aFace],
bImpl->faceNormal_[bFace])) ||
glm::all(glm::equal(aImpl->faceNormal_[aFace],
-bImpl->faceNormal_[bFace]));
if (coplanar) continue; // Skip Coplanar Triangles

vec3 a1 = aImpl->vertPos_[aImpl->halfedge_[(aFace * 3) + 0].startVert];
vec3 a2 = aImpl->vertPos_[aImpl->halfedge_[(aFace * 3) + 1].startVert];
vec3 a3 = aImpl->vertPos_[aImpl->halfedge_[(aFace * 3) + 2].startVert];
vec3 b1 = bImpl->vertPos_[bImpl->halfedge_[(bFace * 3) + 0].startVert];
vec3 b2 = bImpl->vertPos_[bImpl->halfedge_[(bFace * 3) + 1].startVert];
vec3 b3 = bImpl->vertPos_[bImpl->halfedge_[(bFace * 3) + 2].startVert];
composedHulls.push_back(
Manifold::Hull({a1 + b1, a1 + b2, a1 + b3, a2 + b1, a2 + b2,
a2 + b3, a3 + b1, a3 + b2, a3 + b3}));
}
}
}
return Manifold::BatchBoolean(composedHulls, inset
? manifold::OpType::Subtract
: manifold::OpType::Add)
.AsOriginal();
}

/**
* Compute the minkowski sum of this manifold with another.
* This corresponds to the morphological dilation of the manifold.
*
* @param other The other manifold to minkowski sum to this one.
*/
Manifold Manifold::MinkowskiSum(const Manifold& other) const {
return this->Minkowski(other, false);
}

/**
* Subtract the sweep of the other manifold across this manifold's surface.
* This corresponds to the morphological erosion of the manifold.
*
* @param other The other manifold to minkowski subtract from this one.
*/
Manifold Manifold::MinkowskiDifference(const Manifold& other) const {
return this->Minkowski(other, true);
}

/**
* Returns the cross section of this object parallel to the X-Y plane at the
* specified Z height, defaulting to zero. Using a height equal to the bottom of
Expand Down
75 changes: 75 additions & 0 deletions test/boolean_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,81 @@ TEST(Boolean, SplitByPlane60) {
splits.second.GetProperties().volume, 1e-5);
}

TEST(Boolean, ConvexConvexMinkowski) {
float offsetRadius = 0.1f;
float cubeWidth = 2.0f;
Manifold sphere = Manifold::Sphere(offsetRadius, 20);
Manifold cube = Manifold::Cube({cubeWidth, cubeWidth, cubeWidth});
Manifold sum = cube.MinkowskiSum(sphere);
EXPECT_NEAR(sum.GetProperties().volume, 10.589364051818848f, 1e-5);
EXPECT_EQ(sum.Genus(), 0);
Manifold difference = Manifold::Cube({cubeWidth, cubeWidth, cubeWidth})
.MinkowskiDifference(sphere);
EXPECT_NEAR(difference.GetProperties().volume, 5.8319993019104004f, 1e-5);
EXPECT_NEAR(difference.GetProperties().surfaceArea, 19.439998626708984, 1e-5);
EXPECT_EQ(difference.Genus(), 0);

#ifdef MANIFOLD_EXPORT
if (options.exportModels)
ExportMesh("minkowski-convex-convex.glb", sum.GetMeshGL(), {});
#endif
}

TEST(Boolean, NonConvexConvexMinkowski) {
bool oldDeterministic = ManifoldParams().deterministic;
ManifoldParams().deterministic = true;
ManifoldParams().processOverlaps = true;

Manifold sphere = Manifold::Sphere(1.2, 20);
Manifold cube = Manifold::Cube({2.0, 2.0, 2.0}, true);
Manifold nonConvex = cube - sphere;
Manifold sum = nonConvex.MinkowskiSum(Manifold::Sphere(0.1, 20));
EXPECT_NEAR(sum.GetProperties().volume, 4.8406339f, 1e-5);
EXPECT_NEAR(sum.GetProperties().surfaceArea, 34.063014984130859f, 1e-5);
EXPECT_EQ(sum.Genus(), 5);
Manifold difference =
nonConvex.MinkowskiDifference(Manifold::Sphere(0.05, 20));
EXPECT_NEAR(difference.GetProperties().volume, 0.77841246128082275f, 1e-5);
zalo marked this conversation as resolved.
Show resolved Hide resolved
EXPECT_NEAR(difference.GetProperties().surfaceArea, 16.703740785913258, 1e-5);
EXPECT_EQ(difference.Genus(), 5);

#ifdef MANIFOLD_EXPORT
if (options.exportModels)
ExportMesh("minkowski-nonconvex-convex.glb", sum.GetMeshGL(), {});
#endif

ManifoldParams().deterministic = oldDeterministic;
ManifoldParams().processOverlaps = false;
}

TEST(Boolean, NonConvexNonConvexMinkowski) {
bool oldDeterministic = ManifoldParams().deterministic;
ManifoldParams().deterministic = true;
ManifoldParams().processOverlaps = true;

Manifold tet = Manifold::Tetrahedron();
Manifold nonConvex = tet - tet.Rotate(0, 0, 90).Translate(vec3(1));

Manifold sum = nonConvex.MinkowskiSum(nonConvex.Scale(vec3(0.5)));
EXPECT_NEAR(sum.GetProperties().volume, 8.65625f, 1e-5);
EXPECT_NEAR(sum.GetProperties().surfaceArea, 31.176914f, 1e-5);
EXPECT_EQ(sum.Genus(), 0);

Manifold difference =
nonConvex.MinkowskiDifference(nonConvex.Scale(vec3(0.1)));
EXPECT_NEAR(difference.GetProperties().volume, 0.81554f, 1e-5);
EXPECT_NEAR(difference.GetProperties().surfaceArea, 6.95045f, 1e-5);
EXPECT_EQ(difference.Genus(), 0);

#ifdef MANIFOLD_EXPORT
if (options.exportModels)
ExportMesh("minkowski-nonconvex-nonconvex.glb", sum.GetMeshGL(), {});
#endif

ManifoldParams().deterministic = oldDeterministic;
ManifoldParams().processOverlaps = false;
}

/**
* This tests that non-intersecting geometry is properly retained.
*/
Expand Down
Loading