-
Notifications
You must be signed in to change notification settings - Fork 68
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
Lazy boolean #131
Lazy boolean #131
Conversation
maybe we should disable running actions on draft PRs as well, it is expected to fail anyway |
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.
Do you have a sample that demonstrates the improved performance here? I'm curious how much difference it makes.
glm::mat4x3 GetTransform() const override; | ||
|
||
static Manifold::Impl Compose( | ||
const std::vector<std::shared_ptr<CsgLeafNode>> &nodes); |
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 wonder if Compose should be another Op type (OpenSCAD calls it a lazy union, not to be confused with your lazy boolean) rather than a unique function? I don't actually have a strong opinion on this, just curious what you think.
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'm fine with this, but I can't do any reordering to this because it seems that decompose depends on this and would not work if the operation is replaced with union for example.
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 don't think we need to worry about that. Technically I don't guarantee anything for input that's self-overlapping anyway, so I don't really consider a Compose of intersecting objects to be valid. Ideally I'll make a function to remove self-overlaps eventually, but that's non-trivial.
precision_ *= glm::max(1.0f, newScale / oldScale) * | ||
glm::max(glm::length(transform_[0]), | ||
glm::max(glm::length(transform_[1]), | ||
glm::length(transform_[2]))); |
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.
Hmm, I was going to ask why you removed these lines, but now I see this looks like a bug of mine. I should have used the transform size before I reset it.
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 this is somehow causing the Precision test to fail. That's a real failure; it's testing that objects below the floating-point precision limit get collapsed to nothing. This could easily be causing the other failures on the big tests too.
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.
Yes, I think I tried using the transform size before resetting it but got some errors, I will try to change this and see how it goes.
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 found an error in my patch: forgot to use result.precision_
in result.SetPrecision
.
Adding these lines back triggered a few more errors (Boolean.Precision2
, Manifold.Precision
, Samples.TetPuzzle
).
Manifold::Impl Manifold::Impl::Transform(const glm::mat4x3 &transform_) const {
if (transform_ == glm::mat4x3(1.0f)) return *this;
auto policy = autoPolicy(NumVert());
@@ -589,13 +558,15 @@ Manifold::Impl Manifold::Impl::Transform(const glm::mat4x3 &transform_) const {
result.CalculateBBox();
const float newScale = result.bBox_.Scale();
- result.precision_ *= glm::max(1.0f, newScale / oldScale);
+ result.precision_ *= glm::max(1.0f, newScale / oldScale) *
+ glm::max(glm::length(transform_[0]),
+ glm::max(glm::length(transform_[1]),
+ glm::length(transform_[2])));
// Maximum of inherited precision loss and translational precision loss.
- result.SetPrecision(precision_);
+ result.SetPrecision(result.precision_);
return result;
}
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.
Not sure how clear my code is here, but the idea is that for geometric intersections, floating point precision doesn't make a lot of sense. Fixed point would be better, but that's not how modern chips are made, so I just keep track of a fixed precision based on worst-case rounding errors. I use this as my tolerance for collapsing edges, since I can't trust geometry smaller than this tolerance. Looking at the old version of this function, I have the feeling I had a bug already. Would be great to have another set of eyes on this.
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.
newScale ~ oldScale * transform_
. It seems to me that you are multiplying the factor twice here. Maybe you should multiply precision
by just newScale/oldScale
or max length?
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.
Perhaps so; I think the transform size was always one before since it was getting reset, so maybe that's how it effectively was.
|
||
/** | ||
* This returns a Mesh of simple vectors of vertices and triangles suitable for | ||
* saving or other operations outside of the context of this library. | ||
*/ | ||
Mesh Manifold::GetMesh() const { | ||
pImpl_->ApplyTransform(); | ||
const Impl& impl = *GetCsgLeafNode().GetImpl(); |
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 this where the Boolean operations are actually triggered? What if, say, we union/difference a few things together into a sub-module, then union a bunch of those submodules to make the final output? Will those submodule nodes (portions of the tree) know they are related and only evaluate the submodule once? Or will it happen over and over?
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, I see the cache_
now in the OpNode, which sounds like it answers my question. Looks good!
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.
Actually it depends on the tree. For example, consider A + (B + C + D)
and A' + (B + C + D)
, in this case B + C + D
will be evaluated 2 times because it will try to use compose to perform A + B + C + D
and A' + B + C + D
. Whether this is faster or combining B + C + D -> TMP
and do A + TMP
and A' + TMP
is faster depends on the bounding box and the size of the manifolds.
I think it should be possible to tune this to make it combine the operands when the number of objects or the complexity (measured by number of vertices for example) exceeded a certain threshold. But this would be complicated, depends on the test case and build configuration. If users are really concerned about this, they can use some methods that depends on the Impl
object to force the CSG tree to evaluate and cache the result.
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.
Would that be GetMesh
? It kind of reminds me of OpenSCAD's render
method, which really meant cache here. I wonder if an explicit method like that would be helpful? No rush, but something to ponder.
samples/src/bracelet.cpp
Outdated
@@ -52,7 +52,7 @@ Manifold Base(float width, float radius, float decorRadius, float twistRadius, | |||
} | |||
|
|||
base = Manifold::Extrude(stretch, width) ^ base; | |||
base.SetAsOriginal(); | |||
base = base.SetAsOriginal().second; |
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 wonder if this function should have a different name now that it's const
. Maybe just AsOriginal
?
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.
Yes it sounds better
result.SetAsOriginal(); | ||
hole = hole.Rotate(90); | ||
result -= hole; | ||
hole = hole.Rotate(0, 0, 90); |
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.
It's not actually important to me that these rotations are sequential. We can probably put it back to how it was and it should still work, though we should probably wait until we've debugged this test first.
For the performance improvement example, you can try the python tests. The idea is that the more the objects (without using compose), the larger the performance difference. Previously (CPP backend):
Lazy boolean (CPP backend):
(I need to export the models or they won't be actually evaluated) Update: By flattening the tree on the fly, I managed to further strip ~10ms for these test cases
|
OK I know the reason why Fixed via running |
This allows lazy-boolean to reuse Imp for meshes with different transformations.
so it becomes not noticeable when running in precommit hook
Wondering if we can incrementally construct the collider. That way the performance of |
Is this still a draft? |
Not quite sure what you mean by incrementally construct the collider; it involves sorting, so I'm guessing that'll be tricky. For disjointness, perhaps just check for bounding box overlap? If no intersections are detected in the boolean, it ought to reduce to being pretty close to |
Probably not a draft now, but still not mergeable until the test failures are fixed I guess. For union, yes I'm doing overlap check for now (https://github.com/elalish/manifold/pull/131/files#diff-3c6caf3e903fbd0bbc1b99ece9f47113782f7493fb54ba7cfca5eafc005c35c2R344). Just wondering if I can replace the vector of boxes with collider for potentially faster overlap detection, but the current implementation should already be pretty fast right now. |
Oh, yeah, that's a good point. The collider is intended to be a fairly generic BVH implementation, so yes, you can probably construct a higher-level one for whole meshes instead of triangles. |
But it seems that there is no method to append new boxes to the collider. Maybe I will have a look at it later to see how to implement that, this PR is already complicated enough I guess. |
Interestingly #137 did not fix the test failures here, but introduced another failure for |
Let's go ahead and mark this as not a draft so I can see the test failures more easily. |
Not sure how can I trigger the checks without committing anything |
Just got some time to have a look at this. Calling diff --git a/manifold/src/csg_tree.cpp b/manifold/src/csg_tree.cpp
index f767d30..e70bd5c 100644
--- a/manifold/src/csg_tree.cpp
+++ b/manifold/src/csg_tree.cpp
@@ -324,8 +324,14 @@ void CsgOpNode::BatchBoolean(
results.pop_back();
// boolean operation
Boolean3 boolean(*a, *b, operation);
- results.push_back(
- std::make_shared<const Manifold::Impl>(boolean.Result(operation)));
+ auto result = std::make_shared<Manifold::Impl>(
+ boolean.Result(operation));
+ ALWAYS_ASSERT(result->IsManifold(), topologyErr, "batch boolean produced "
+ "non-manifold result");
+ result->InitializeNewReference();
+ result->SimplifyTopology();
+
+ results.push_back(result);
std::push_heap(results.begin(), results.end(), cmpFn);
}
} fixes the test failure for
Although I shouldn't call |
I think this PR might just be a little too big. It's using |
Sure, it would be nice to get the APIs updated first. |
Now the csg tree will not perform any reordering, so the performance may not be great. I tried disabling compose only and the tests still failed, so using fuzzing to reproduce the errors should not be too hard. I just added a commit to disable reordering in csg tree, which allows us to enable it easily later. Let me know if you want me to do a rebase instead for a cleaner history. |
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.
Looks great, thanks! Just a couple of nits.
glm::mat4x3 GetTransform() const override; | ||
|
||
static Manifold::Impl Compose( | ||
const std::vector<std::shared_ptr<CsgLeafNode>> &nodes); |
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 don't think we need to worry about that. Technically I don't guarantee anything for input that's self-overlapping anyway, so I don't really consider a Compose of intersecting objects to be valid. Ideally I'll make a function to remove self-overlaps eventually, but that's non-trivial.
|
||
/** | ||
* This returns a Mesh of simple vectors of vertices and triangles suitable for | ||
* saving or other operations outside of the context of this library. | ||
*/ | ||
Mesh Manifold::GetMesh() const { | ||
pImpl_->ApplyTransform(); | ||
const Impl& impl = *GetCsgLeafNode().GetImpl(); |
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.
Would that be GetMesh
? It kind of reminds me of OpenSCAD's render
method, which really meant cache here. I wonder if an explicit method like that would be helpful? No rush, but something to ponder.
/** | ||
* Flatten the children to a list of leaf nodes and return them. | ||
* If finalize is true, the list will be guaranteed to be a list of leaf nodes | ||
* (i.e. no ops). Otherwise, the list may contain ops. |
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.
This seems odd; it says it will flatten the list, but if finalize is not true, then it might not? I try to avoid functions taking boolean inputs if I can, but maybe this is just an issue with the 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.
The idea is that if finalize
is false, we will try to flatten the tree if it is not expensive (if we don't have to evaluate the boolean expressions), and we will perform the evaluation if finalize
is true. But yes, this is a bit confusing.
Actually this comment is outdated because we don't do any reordering for now, so finalize
is kind of useless.
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.
Originally it will call this function with finalize=false
when constructing the CSG tree, so unions will be flattened into a large union node with many children, which improves performance a bit. This is not necessary though.
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, well let's deal with this in the next PR.
* Efficient union operation on a set of nodes by doing Compose as much as | ||
* possible. | ||
*/ | ||
void CsgOpNode::BatchUnion() const { |
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, so these functions are still here, but just currently unused? Sounds good for now.
Edit: It turns out I just forgot to call |
FYI: with reordering enabled and not using compose for union, the only test that fails is the
The number of tri that does not match normal depends on the backend, 1 for CPP backend and many for TBB backend. The generated mesh looks pretty good to my untrained eyes, but it seems that there are a lot of sliver triangles there: |
The reordering thing with Sponge4 is interesting; can you repro that without the change, but simply by manually switching the order? There's only 3 ops after all. |
Lazy boolean
Implemented lazy boolean operation as mentioned in #114.
SetAsOriginal
, andwarp
, as they need to copy the underlyingImpl
.However, there are 3 test failures that I have no idea about how to fix.
1.
Boolean.PrecisionFixed.2.
Samples.Bracelet
triggered triangulation failure. Probably related to #102.3.
Samples.Sponge4
reported a bunch ofTri ... does not match normal
and with a large number ofNumDegenerateTris
.Might be related to my changes to the compose function but I am not sure about this.Also will have to fix some of the outdated documentation. Just submit it earlier for review as this patch is rather complicated.