-
Notifications
You must be signed in to change notification settings - Fork 169
Add MinkowskiSum and MinkowskiDifference #666
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
Conversation
|
The non-convex/non-convex test appears to be failing on some of the GH Actions test runners due to triangulations not coming out CCW... It passes on Linux (and my personal Windows machine), but fails on Github's Mac and Windows Runners. I wonder if these will pass on the We can also say that Non-Convex/Non-Convex is not supported, but that would make me sad... |
How did you generate this image? Since this implementation is just unioning hulls of triangle pairs, I believe it's acting like a fuzzer for the union operation, where every hull is the worst case of "just barely touching" the other hulls. Whatever assumptions about floating point Manifold makes seem to hold true on some platforms, but not so much on others... One mysterious thing to note is that the number of triangles output by both the sequential and parallel unions is not constant between runs; I think there might be an indeterminism sneaking into the library somewhere... I'll force the CI to run again to see if different machines pass and fail... |
|
We have non-determinism by design. We can force determinism by setting The plot: import numpy as np
import matplotlib.pyplot as plt
array = np.array
show = lambda x: plt.plot(*np.hsplit(np.append(x, x[0, :].reshape((1, 2)), axis=0), 2), marker='.')
show(array([
[0.0500000268, 0.435410291],
[-0.00156550109, 0.413770884],
[0.0366442874, 0.405546039],
[0.0366442874, 0.405546039],
[0.0500000268, 0.402671158],
]))
show(array([
[-0.0454634838, 0.395349145],
[-0.0454634838, 0.395349145],
[0.0500000268, 0.435410291],
[-0.0131011149, 0.397831321],
[0.0500000268, 0.402671158],
[0.0366442874, 0.405546039],
[0.0366442874, 0.405546039],
[-0.00156550109, 0.413770884],
]))
plt.show() |
|
It is interesting to note that different jobs are failing now, which suggests it might be a quirk of the order of operations... I don't suppose Manifold can catch itself as soon as it generates an inconsistent triangulation, and dump the two manifolds that it tried to union just before doing so? There's also a chance that the test is just too expensive for the runner, and it's inconsistently running out of memory 🤔 |
|
OOM: I don't think that will cause this behavior.
Actually we can do that with some hacking, need to modify |
|
Thanks for this! I've been toying with a similar idea but just for offsetting (minkowski with a sphere). I'll put up a PR soon so we can compare and contrast. Anyway, I ran into I believe the same trouble as you and came to the same conclusion - that this is a good fuzz test of our Booleans. I have a hunch that I see two Boolean bugs I may be able to fix - one regarding our symbolic perturbation and another that's creating those self-intersecting polygons. |
|
By the way, there’s a neat trick with Minkowski Sums: “Linear” Shape Interpolation
It’s the old lerp formula This method even works on shapes with different Genuses. Not sure what it can be used for in modeling just yet 😅 |
|
Okay, that is pretty cool - Minkowski is growing on me. |
|
Yeah, this PR's minkowski-based offset produces virtually no slivers compared to #668 and #669 's edge -> cylinder and edge -> wedge based offset methods. It's a bit slower, but broadly seems to produce the fewest vertices + tris in the final product for a given circularSegments resolution, and the best overall topology. This'll be easiest to see if I have claude make a comparison webpage that side-by-sides all the models from each method with visible wireframes... |
|
Hold on, I need to fix my algorithm first! It makes the slivers because the facing quads don't use the same bisector edge. Should be a pretty simple fix, but I've been very short on time lately. It should work more like this. That test does a simple version: expanding a cube to double its size, but without the corner rounding. |
|
@elalish I'm not entirely sure myself why quads wouldn't have matching edge diagonals (since we're extruding on a per-triangle, not a per-quad basis), but I had Opus 4.5 try to incorporate the suggested bisector edge fixes from your comment and linked code; it made a new "MakePrism" function used in the two Offset functions 🤔 Code changes: f3d6021 It seems like it might have helped with internal slivers during positive offsets, but negative offsets are still pretty slivery (for both of our offset algorithms with cylinders and wedges; minkowski still king...) I'll try to get the comparison page going... |
|
Ha, I think Claude understood what I was referring to better than you did 😄 - the quads are the sides of the extrusions, one per input edge. The only other change I'd make to that commit is to extrude from the verts themselves in either the +normal direction for sum and -normal for difference, rather than from -normal to +normal. |
|
And it might need some of the same convexity tolerance logic that yours uses - can't remember how that's done currently. |
This made some of the tests freeze forever on both Windows and Linux; not sure why 💀
Adding similar Convexity tolerances had no measurable effect on the number of slivers 🫠 Here is the sliver comparison page for the offset mechanisms with as many working fixes as Claude and I could cram in; the cylinder model is particularly dramatic. (Please excuse the naming scheme: "Minkowski" is this PR #666, "Simple" corresponds to PR #668 and "Elegant" corresponds to PR #669 ) For negative offsets, you generally end up with exterior slivers, and for positive offsets, you get interior slivers 😅 The L-Shape, "Fun Shape", and Star examples are worthwhile to check out too. |
|
Also, as a refresher, #669 was written to address the crunchy "dimple" issues #668 has with stitching edge segments to corner caps, visible here: Minkowski naturally handles cap -> edge stitching by ensuring that an edge expands to the convex hull of the two sphere caps at each end. It seems that the general minkowski method is significantly more robust than the specialized method... and I'm having a tough time getting minkowski to make any slivers at all now! EDIT: Just fixed the Genus display bug: wow! It shows the full extent of the sliver issues for the specialized offsets! Also it looks like the Sphere Offset Minkowski is actually correct (no slivers) rather than just appearing correct. 😄 I think it's because the sphere case never has any kissing faces; the spheres are always ~50% overlapping. If you'd like to mess with the algorithms here, I'm running this script on this branch (after building and installing Python) to regenerate the online viewer/comparer: I can ask Claude to put the deploy functionality into a Github Actions CI if desired? All commits to manifold should probably be generating something like this as a "health report"... (Also putting Claude on investigating the source of the sliver issue on its own, since the genus is now an easy way it can verify if what it's trying has worked... EDIT: Claude's suggestions here were dumb and rejected; some things are still beyond Opus 4.5...) |
- Add Impl::Minkowski method in impl.cpp with batch processing optimization - Manifold::Minkowski now delegates to Impl::Minkowski - This is more consistent with the codebase style and avoids repeated GetCsgLeafNode().GetImpl() calls - Includes BATCH_SIZE=1000 optimization for hull operations Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Flatten nested face pair loops into single index space - Process pairs in parallel batches using for_each_n - Use validHull flag array to handle coplanar face filtering - Collect valid hulls after parallel execution for BatchBoolean Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Merge latest changes from origin/master - Move Impl::Minkowski to dedicated minkowski.cpp file - Add minkowski.cpp to CMakeLists.txt - Consistent with codebase organization (separate files for distinct features) Co-Authored-By: Claude Opus 4.5 <[email protected]>
|
Oooh! All my NonConvex <-> NonConvex cases work too! No Genus issues! (Though, the topology could be cleaner 😅 ) This could either be due to the symbolic perturbation changes or adding @pca006132 's batching to the NonConvex <-> NonConvex path too. If you have any additional tests you'd like it to check, I'll ask Claude to add them to the suite 😄 Otherwise, I believe I have addressed all of the review comments now and it should be ready to go. EDIT: Recreated one of my favorite Minkowski pieces from CGAL’s docs: the Spoon + Star: |
Replace face-pair batching with per-A-face processing and periodic reduction every 200 faces. This approach: - Processes A faces sequentially, creating B-face hulls in parallel - Merges accumulated results every REDUCE_THRESHOLD faces - Prevents memory from growing unboundedly with large meshes Enables complex meshes like spoon (904 tris) to complete without crashing while maintaining similar performance for smaller meshes. Co-Authored-By: Claude Opus 4.5 <[email protected]>
bfd4654 to
70f894b
Compare
|
btw the batch size can take into account of the size of the other object, my previous way was just a hack to quickly make it work, without considering too much about memory usage and performance |
elalish
left a comment
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.
Okay, you've convinced me, and you've done a really thorough job building this out. I appreciate that it's not a lot of code. A few details to clean up, but looking good!
|
|
||
| if (!validFaceHulls.empty()) { | ||
| accumulated.push_back( | ||
| Manifold::BatchBoolean(validFaceHulls, OpType::Add)); |
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.
@pca006132 Does this trigger an evaluation? Because if not, it's not actually changing behavior compared to doing the one giant batch boolean at the end, right?
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.
Ah yeah, this probably doesn't trigger an evaluation, but this should change the CSG tree evaluation because we have references in the accumulated array, so the CSG tree cannot be flattened.
I am curious about the performance of forcing an evaluation, but that is not a priority.
This also means that we should look into optimizing batch union/compose for better memory performance.
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.
Given that this Minkowski is always(?) joining faces at shared vertices, there’s an alternative algorithm that might speed it up in the future…
- Accumulate all the hulls
- Merge coincident vertices
- Prune interior triangles
- Prune unused vertices
I believe it’s described in “A Simple Method for Computing Minkowski Sum Boundary in 3D Using Collision Detection”
The BatchUnion is already doing this, but I bet a lot of steps could be skipped and merged for speed. It’s nice to have a reference function to compare against for genus etc.
EDIT: Actually; I’m not sure this is true; would have to prototype…
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.
@pca006132 Hmm, perhaps this needs some documentation? I was under the impression that your batching memory fix was because it forced an evaluation, thus freeing the memory associated with all of those input manifolds. If it's more complicated than that, we should probably describe it somewhere. Or else just change the CSG tree so that it decides when to evaluate in order to free memory automatically - or does it already do this to some degree?
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 am going to open an issue for this. I think we want to do it automatically, or at least look into why it is using so much memory.
- Pre-allocate composedHulls vector - Simplify validHulls loop by checking IsEmpty() instead of separate bool vector - Add surface area check to ConvexConvexMinkowski test - Remove private Minkowski wrapper function, call Impl directly - Add performance warning to MinkowskiSum/MinkowskiDifference docs Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Pre-allocate composedHulls vector - Simplify validHulls loop by checking IsEmpty() instead of separate bool vector - Add surface area check to ConvexConvexMinkowski test - Remove private Minkowski wrapper function, call Impl directly - Add performance warning to MinkowskiSum/MinkowskiDifference docs Co-Authored-By: Claude Opus 4.5 <[email protected]>
|
How do these changes look? I had Claude rebake the website too and there weren’t any regressions there either. EDIT: I added some new extra hard tests. Somehow the NonConvex NonConvex coplanarity tolerance got bumped from 1e-12 to 1e-15, which was too much and causing some of my newly added 30 second long NC-NC tests to just hang forever. Bumped that back and they also complete fine now. |
68ceb83 to
be91763
Compare
Increase kCoplanarTol from 1e-15 to 1e-9 to more reliably skip nearly-coplanar face pairs that could cause degenerate hull computations and potential hangs. This tolerance is conservative enough to skip genuinely coplanar faces while still allowing valid near-coplanar face pairs to be processed. Co-Authored-By: Claude Opus 4.5 <[email protected]>
be91763 to
653dcc6
Compare
elalish
left a comment
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.
LGTM - I think you hold the record here for oldest PR to be merged. Thanks for sticking with it!
I didn't notice any new tests compared to the last time I reviewed - did those get pushed? |
|
@elalish 🎉 Sometimes good things really do come to those who wait 😄 Most of these tests are in the secondary branch as part of that web comparison GUI; they’re kind of long-executing and might make the GH Actions CI take a lot longer… Should I include just the harshest ones? Most of the benefit could probably fit into the 30 second long Star <-> Hollow Sphere test (the one which was giving me a hard time with hanging) and maybe some offsets of the Hollow Sphere as well… |
|
Well, the whole test suite runs in 30 seconds right now (on my machine at least). If you can cut a harsh test down to 5 seconds or less, sure - otherwise let's skip it for now. |
|
Thank you all very much! I've been watching and waiting for a long time. |
|
Holy Carp! 🐟 I missed this going in. Congratulations @zalo! |
This is the minimal code to get the naive minkowksi sum working
with basic affordances for multithreading.This is essentially the other PR, but without the in-progress experimentation or the controversial
voro++dependency.The benefits of the Naive Technique are that:
The drawbacks to the Naive Technique are that:
There are two basic versions of the function, one with significant threading and the other without.I expect we'll consolidate to one or the other as their respective pros/cons make themselves apparent; the speed appears to be pretty comparable on my windows machine (but perhaps that's not true on Linux?).
I've decided the speed difference is too negligible (1-2% at best), and it's now a source of failing unit tests. So no more threading!
Tests and Bindings have been added 😎