Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@zalo
Copy link
Contributor

@zalo zalo commented Dec 20, 2023

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:

  1. The output mesh topology looks much nicer without any need for surface simplification.
  2. It doesn't need any additional dependencies.

The drawbacks to the Naive Technique are that:

  1. Sometimes, it's a bit slower than the general technique (especially for Non-Convex/Non-Convex).
  2. Sometimes, it appears to run out of memory when performing the final union (especially during Non-Convex/Non-Convex).
  3. Sometimes, the topology gets messed up (especially during Non-Convex/Non-Convex).

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 😎

@zalo
Copy link
Contributor Author

zalo commented Dec 20, 2023

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 float->double branch 🤔

We can also say that Non-Convex/Non-Convex is not supported, but that would make me sad...

@pca006132
Copy link
Collaborator

It seems that there is something weird going on:

image

The epsilon is small (7e-6), no way this is a valid polygon, and I don't think it is caused by precision issue.

@zalo
Copy link
Contributor Author

zalo commented Dec 20, 2023

It seems that there is something weird going on

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...

@pca006132
Copy link
Collaborator

We have non-determinism by design. We can force determinism by setting ManifoldParams().deterministic = true. Whether we should make this opt-in or opt-out is open for debate...

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()

@zalo
Copy link
Contributor Author

zalo commented Dec 20, 2023

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 🤔

@pca006132
Copy link
Collaborator

OOM: I don't think that will cause this behavior.

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?

Actually we can do that with some hacking, need to modify csg_tree a bit to catch the triangulator error.

@elalish
Copy link
Owner

elalish commented Dec 20, 2023

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.

@elalish elalish mentioned this pull request Dec 20, 2023
@zalo
Copy link
Contributor Author

zalo commented Dec 21, 2023

By the way, there’s a neat trick with Minkowski Sums: “Linear” Shape Interpolation

image
image
(From: https://liris.cnrs.fr/Documents/Liris-4020.pdf )

It’s the old lerp formula c = (b*t) + ((1-t)*a), but where multiplication is replaced with scaling, and addition is replaced with the Minkowski Sum.

This method even works on shapes with different Genuses. Not sure what it can be used for in modeling just yet 😅

@elalish
Copy link
Owner

elalish commented Dec 21, 2023

Okay, that is pretty cool - Minkowski is growing on me.

@zalo
Copy link
Contributor Author

zalo commented Jan 16, 2026

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...

@elalish
Copy link
Owner

elalish commented Jan 16, 2026

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.

@zalo
Copy link
Contributor Author

zalo commented Jan 16, 2026

@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 🤔

Here's a summary of the fix:                                                                                                                                                                                            
                                                                                                                                                                                                                                
  Problem: The Offset functions created slivers because adjacent triangles sharing an edge used different diagonals when triangulating their quad faces.                                                                        
                                                                                                                                                                                                                                
  Solution: Added a MakePrism() helper function that creates prisms with consistent quad diagonals based on comparing original mesh vertex indices:                                                                             
                                                                                                                                                                                                                                
  // When creating quad for edge (v0, v1), check original indices                                                                                                                                                               
  if (idx1 > idx0) {                                                                                                                                                                                                            
      // Diagonal from inner0 to outer1                                                                                                                                                                                         
  } else {                                                                                                                                                                                                                      
      // Diagonal from inner1 to outer0                                                                                                                                                                                         
  }                                                                                                                                                                                                                             
                                                                                                                                                                                                                                
  This ensures that when two triangles share an edge (e.g., one sees it as vertices 3→7, the other as 7→3), both will choose the same diagonal direction.                                                                       
                                                                                                                                                                                                                                
  Changes:                                                                                                                                                                                                                      
  - Added MakePrism() helper in src/manifold.cpp:35-96                                                                                                                                                                          
  - Updated OffsetSimple (PR #668 ) to use MakePrism() instead of block.Warp()                                                                                                                                                             
  - Updated OffsetElegant (PR #669 ) to use MakePrism() instead of block.Warp()                                                                                                                                                            
                                                                                                                                                                                                                                
  Verification:                                                                                                                                                                                                                 
  - All 301 tests pass, including Boolean.Perturb2 which tests this exact scenario                                                                                                                                              
                                                                                                                                                                                                                                
  Pushed: commit f3d60211 to combo-minkowski-offset    

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...

@elalish
Copy link
Owner

elalish commented Jan 16, 2026

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.

@elalish
Copy link
Owner

elalish commented Jan 16, 2026

And it might need some of the same convexity tolerance logic that yours uses - can't remember how that's done currently.

@zalo
Copy link
Contributor Author

zalo commented Jan 16, 2026

@elalish

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.

This made some of the tests freeze forever on both Windows and Linux; not sure why 💀

And it might need some of the same convexity tolerance logic that yours uses - can't remember how that's done currently.

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.

image

(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.

@zalo
Copy link
Contributor Author

zalo commented Jan 16, 2026

Also, as a refresher, #669 was written to address the crunchy "dimple" issues #668 has with stitching edge segments to corner caps, visible here:
image
image

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! Gotta fix a bug where it's not writing the Genus correctly though...

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.
The NonConvex <-> NonConvex case will probably display similar behavior to the existing offset functions though 🤔 ; that would be the only case probably worth putting a {BETA} or {SLIVER ME TIMBERS} flag around. This is one where the delaunay tetrahedralization would fix the issue (but insetting wouldn't work with it in the NonConvex <-> NonConvex case). This now works too! See below! Maybe batching has saved me 🤔

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:
https://github.com/zalo/manifold/blob/combo-minkowski-offset/bindings/python/examples/offset_comparison_test.py

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...)

zalo and others added 4 commits January 16, 2026 15:33
- 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]>
@zalo
Copy link
Contributor Author

zalo commented Jan 17, 2026

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:
https://manifold-offset-comparison.pages.dev/#type=nc_minkowski&shape=spoon_x_star_element&delta=NaN

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]>
@zalo zalo force-pushed the feat-naive-minkowski branch from bfd4654 to 70f894b Compare January 17, 2026 04:53
@pca006132
Copy link
Collaborator

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

Copy link
Owner

@elalish elalish left a 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));
Copy link
Owner

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?

Copy link
Collaborator

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.

Copy link
Contributor Author

@zalo zalo Jan 17, 2026

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…

  1. Accumulate all the hulls
  2. Merge coincident vertices
  3. Prune interior triangles
  4. 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…

Copy link
Owner

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?

Copy link
Collaborator

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]>
zalo added a commit to zalo/manifold that referenced this pull request Jan 17, 2026
- 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]>
@zalo
Copy link
Contributor Author

zalo commented Jan 17, 2026

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.

https://manifold-offset-comparison.pages.dev/#type=nc_minkowski&shape=hollow_sphere_x_star_element&delta=NaN

@zalo zalo force-pushed the feat-naive-minkowski branch from 68ceb83 to be91763 Compare January 17, 2026 13:39
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]>
@zalo zalo force-pushed the feat-naive-minkowski branch from be91763 to 653dcc6 Compare January 17, 2026 13:47
Copy link
Owner

@elalish elalish left a 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!

@elalish
Copy link
Owner

elalish commented Jan 17, 2026

EDIT: I added some new extra hard tests.

I didn't notice any new tests compared to the last time I reviewed - did those get pushed?

@zalo
Copy link
Contributor Author

zalo commented Jan 17, 2026

@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…

@elalish
Copy link
Owner

elalish commented Jan 17, 2026

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.

@elalish elalish merged commit 301c165 into elalish:master Jan 17, 2026
36 checks passed
@rujialiu
Copy link

Thank you all very much! I've been watching and waiting for a long time.

@tonious
Copy link
Contributor

tonious commented Jan 21, 2026

Holy Carp! 🐟

I missed this going in. Congratulations @zalo!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants