From 6cf1d3c13e4ecc0f50f689e9001be954b7ae6eff Mon Sep 17 00:00:00 2001 From: "K. S. Ernest (iFire) Lee" Date: Tue, 3 Dec 2024 04:12:36 -0800 Subject: [PATCH] Print better manifold errors and avoid crash on non manifold csg input. * Manifold does not have a snap property. * Tolerance means simplification amount. * CSG snap has been removed * Add better error messages. * Verbose print manifold meshgl64 properties as json. * Update manifold for error fixes --- modules/csg/csg_shape.cpp | 134 +++- modules/csg/csg_shape.h | 2 + modules/csg/doc_classes/CSGShape3D.xml | 4 +- thirdparty/README.md | 2 +- thirdparty/manifold/src/constructors.cpp | 2 +- thirdparty/manifold/src/csg_tree.cpp | 573 +++++++++--------- thirdparty/manifold/src/csg_tree.h | 25 +- thirdparty/manifold/src/impl.cpp | 11 +- .../{include/manifold => src}/iters.h | 0 thirdparty/manifold/src/parallel.h | 2 +- thirdparty/manifold/src/properties.cpp | 9 +- 11 files changed, 443 insertions(+), 321 deletions(-) rename thirdparty/manifold/{include/manifold => src}/iters.h (100%) diff --git a/modules/csg/csg_shape.cpp b/modules/csg/csg_shape.cpp index cc4595929f31..eed5a7673792 100644 --- a/modules/csg/csg_shape.cpp +++ b/modules/csg/csg_shape.cpp @@ -30,6 +30,9 @@ #include "csg_shape.h" +#ifdef DEV_ENABLED +#include "core/io/json.h" +#endif // DEV_ENABLED #include "core/math/geometry_2d.h" #include @@ -142,6 +145,7 @@ bool CSGShape3D::is_root_shape() const { return !parent_shape; } +#ifndef DISABLE_DEPRECATED void CSGShape3D::set_snap(float p_snap) { if (snap == p_snap) { return; @@ -154,6 +158,7 @@ void CSGShape3D::set_snap(float p_snap) { float CSGShape3D::get_snap() const { return snap; } +#endif // DISABLE_DEPRECATED void CSGShape3D::_make_dirty(bool p_parent_removing) { if ((p_parent_removing || is_root_shape()) && !dirty) { @@ -232,13 +237,111 @@ static void _unpack_manifold( r_mesh_merge->_regen_face_aabbs(); } +// Errors matching `thirdparty/manifold/include/manifold/manifold.h`. +static String manifold_error_to_string(const manifold::Manifold::Error &p_error) { + switch (p_error) { + case manifold::Manifold::Error::NoError: + return "No Error"; + case manifold::Manifold::Error::NonFiniteVertex: + return "Non Finite Vertex"; + case manifold::Manifold::Error::NotManifold: + return "Not Manifold"; + case manifold::Manifold::Error::VertexOutOfBounds: + return "Vertex Out Of Bounds"; + case manifold::Manifold::Error::PropertiesWrongLength: + return "Properties Wrong Length"; + case manifold::Manifold::Error::MissingPositionProperties: + return "Missing Position Properties"; + case manifold::Manifold::Error::MergeVectorsDifferentLengths: + return "Merge Vectors Different Lengths"; + case manifold::Manifold::Error::MergeIndexOutOfBounds: + return "Merge Index Out Of Bounds"; + case manifold::Manifold::Error::TransformWrongLength: + return "Transform Wrong Length"; + case manifold::Manifold::Error::RunIndexWrongLength: + return "Run Index Wrong Length"; + case manifold::Manifold::Error::FaceIDWrongLength: + return "Face ID Wrong Length"; + case manifold::Manifold::Error::InvalidConstruction: + return "Invalid Construction"; + default: + return "Unknown Error"; + } +} + +#ifdef DEV_ENABLED +static String _export_meshgl_as_json(const manifold::MeshGL64 &p_mesh) { + Dictionary mesh_dict; + mesh_dict["numProp"] = p_mesh.numProp; + + Array vert_properties; + for (const double &val : p_mesh.vertProperties) { + vert_properties.append(val); + } + mesh_dict["vertProperties"] = vert_properties; + + Array tri_verts; + for (const uint64_t &val : p_mesh.triVerts) { + tri_verts.append(val); + } + mesh_dict["triVerts"] = tri_verts; + + Array merge_from_vert; + for (const uint64_t &val : p_mesh.mergeFromVert) { + merge_from_vert.append(val); + } + mesh_dict["mergeFromVert"] = merge_from_vert; + + Array merge_to_vert; + for (const uint64_t &val : p_mesh.mergeToVert) { + merge_to_vert.append(val); + } + mesh_dict["mergeToVert"] = merge_to_vert; + + Array run_index; + for (const uint64_t &val : p_mesh.runIndex) { + run_index.append(val); + } + mesh_dict["runIndex"] = run_index; + + Array run_original_id; + for (const uint32_t &val : p_mesh.runOriginalID) { + run_original_id.append(val); + } + mesh_dict["runOriginalID"] = run_original_id; + + Array run_transform; + for (const double &val : p_mesh.runTransform) { + run_transform.append(val); + } + mesh_dict["runTransform"] = run_transform; + + Array face_id; + for (const uint64_t &val : p_mesh.faceID) { + face_id.append(val); + } + mesh_dict["faceID"] = face_id; + + Array halfedge_tangent; + for (const double &val : p_mesh.halfedgeTangent) { + halfedge_tangent.append(val); + } + mesh_dict["halfedgeTangent"] = halfedge_tangent; + + mesh_dict["tolerance"] = p_mesh.tolerance; + + String json_string = JSON::stringify(mesh_dict); + return json_string; +} +#endif // DEV_ENABLED + static void _pack_manifold( const CSGBrush *const p_mesh_merge, manifold::Manifold &r_manifold, HashMap> &p_mesh_materials, - float p_snap) { + CSGShape3D *p_csg_shape) { ERR_FAIL_NULL_MSG(p_mesh_merge, "p_mesh_merge is null"); - + ERR_FAIL_NULL_MSG(p_csg_shape, "p_shape is null"); HashMap> faces_by_material; for (int face_i = 0; face_i < p_mesh_merge->faces.size(); face_i++) { const CSGBrush::Face &face = p_mesh_merge->faces[face_i]; @@ -246,7 +349,6 @@ static void _pack_manifold( } manifold::MeshGL64 mesh; - mesh.tolerance = p_snap; mesh.numProp = MANIFOLD_PROPERTY_MAX; mesh.runOriginalID.reserve(faces_by_material.size()); mesh.runIndex.reserve(faces_by_material.size() + 1); @@ -291,12 +393,22 @@ static void _pack_manifold( } // runIndex needs an explicit end value. mesh.runIndex.push_back(mesh.triVerts.size()); + mesh.tolerance = 2 * FLT_EPSILON; ERR_FAIL_COND_MSG(mesh.vertProperties.size() % mesh.numProp != 0, "Invalid vertex properties size."); mesh.Merge(); +#ifdef DEV_ENABLED + print_verbose(_export_meshgl_as_json(mesh)); +#endif // DEV_ENABLED r_manifold = manifold::Manifold(mesh); - manifold::Manifold::Error err = r_manifold.Status(); - if (err != manifold::Manifold::Error::NoError) { - print_error(String("Manifold creation from mesh failed:" + itos((int)err))); + manifold::Manifold::Error error = r_manifold.Status(); + if (error == manifold::Manifold::Error::NoError) { + return; + } + if (p_csg_shape->get_owner()) { + NodePath path = p_csg_shape->get_owner()->get_path_to(p_csg_shape, true); + print_error(vformat("CSGShape3D manifold creation from mesh failed at %s: %s.", path, manifold_error_to_string(error))); + } else { + print_error(vformat("CSGShape3D manifold creation from mesh failed at .: %s.", manifold_error_to_string(error))); } } @@ -330,7 +442,7 @@ CSGBrush *CSGShape3D::_get_brush() { CSGBrush *n = _build_brush(); HashMap> mesh_materials; manifold::Manifold root_manifold; - _pack_manifold(n, root_manifold, mesh_materials, get_snap()); + _pack_manifold(n, root_manifold, mesh_materials, this); manifold::OpType current_op = ManifoldOperation::convert_csg_op(get_operation()); std::vector manifolds; manifolds.push_back(root_manifold); @@ -346,7 +458,7 @@ CSGBrush *CSGShape3D::_get_brush() { CSGBrush transformed_brush; transformed_brush.copy_from(*child_brush, child->get_transform()); manifold::Manifold child_manifold; - _pack_manifold(&transformed_brush, child_manifold, mesh_materials, get_snap()); + _pack_manifold(&transformed_brush, child_manifold, mesh_materials, child); manifold::OpType child_operation = ManifoldOperation::convert_csg_op(child->get_operation()); if (child_operation != current_op) { manifold::Manifold result = manifold::Manifold::BatchBoolean(manifolds, current_op); @@ -834,8 +946,10 @@ void CSGShape3D::_bind_methods() { ClassDB::bind_method(D_METHOD("set_operation", "operation"), &CSGShape3D::set_operation); ClassDB::bind_method(D_METHOD("get_operation"), &CSGShape3D::get_operation); +#ifndef DISABLE_DEPRECATED ClassDB::bind_method(D_METHOD("set_snap", "snap"), &CSGShape3D::set_snap); ClassDB::bind_method(D_METHOD("get_snap"), &CSGShape3D::get_snap); +#endif // DISABLE_DEPRECATED ClassDB::bind_method(D_METHOD("set_use_collision", "operation"), &CSGShape3D::set_use_collision); ClassDB::bind_method(D_METHOD("is_using_collision"), &CSGShape3D::is_using_collision); @@ -864,7 +978,9 @@ void CSGShape3D::_bind_methods() { ClassDB::bind_method(D_METHOD("bake_collision_shape"), &CSGShape3D::bake_collision_shape); ADD_PROPERTY(PropertyInfo(Variant::INT, "operation", PROPERTY_HINT_ENUM, "Union,Intersection,Subtraction"), "set_operation", "get_operation"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "snap", PROPERTY_HINT_RANGE, "0.000001,1,0.000001,suffix:m"), "set_snap", "get_snap"); +#ifndef DISABLE_DEPRECATED + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "snap", PROPERTY_HINT_RANGE, "0.000001,1,0.000001,suffix:m", PROPERTY_USAGE_NONE), "set_snap", "get_snap"); +#endif // DISABLE_DEPRECATED ADD_PROPERTY(PropertyInfo(Variant::BOOL, "calculate_tangents"), "set_calculate_tangents", "is_calculating_tangents"); ADD_GROUP("Collision", "collision_"); diff --git a/modules/csg/csg_shape.h b/modules/csg/csg_shape.h index 8f23ae2f9e2c..9aa612885aa8 100644 --- a/modules/csg/csg_shape.h +++ b/modules/csg/csg_shape.h @@ -155,8 +155,10 @@ class CSGShape3D : public GeometryInstance3D { void set_collision_priority(real_t p_priority); real_t get_collision_priority() const; +#ifndef DISABLE_DEPRECATED void set_snap(float p_snap); float get_snap() const; +#endif // DISABLE_DEPRECATED void set_calculate_tangents(bool p_calculate_tangents); bool is_calculating_tangents() const; diff --git a/modules/csg/doc_classes/CSGShape3D.xml b/modules/csg/doc_classes/CSGShape3D.xml index ac62d8dd83dc..8ea471e62d83 100644 --- a/modules/csg/doc_classes/CSGShape3D.xml +++ b/modules/csg/doc_classes/CSGShape3D.xml @@ -89,8 +89,8 @@ The operation that is performed on this shape. This is ignored for the first CSG child node as the operation is between this node and the previous child of this nodes parent. - - Snap makes the mesh vertices snap to a given distance so that the faces of two meshes can be perfectly aligned. A lower value results in greater precision but may be harder to adjust. The top-level CSG shape's snap value is used for the entire CSG tree. + + This property does nothing. Adds a collision shape to the physics engine for our CSG shape. This will always act like a static body. Note that the collision shape is still active even if the CSG shape itself is hidden. See also [member collision_mask] and [member collision_priority]. diff --git a/thirdparty/README.md b/thirdparty/README.md index 79d7e5b7b6ad..b70738c21a80 100644 --- a/thirdparty/README.md +++ b/thirdparty/README.md @@ -549,7 +549,7 @@ in the MSVC debugger. ## manifold - Upstream: https://github.com/elalish/manifold -- Version: 3.0.0 (5d127e57fbfb89225a8e905d0d914ccc86c139c8, 2024) +- Version: master (36035428bc32302a9d7c9ee1ecc833fb8394a2a3, 2024) - License: Apache 2.0 File extracted from upstream source: diff --git a/thirdparty/manifold/src/constructors.cpp b/thirdparty/manifold/src/constructors.cpp index 17df744ee965..2b3e528c6bbe 100644 --- a/thirdparty/manifold/src/constructors.cpp +++ b/thirdparty/manifold/src/constructors.cpp @@ -436,7 +436,7 @@ Manifold Manifold::Compose(const std::vector& manifolds) { for (const auto& manifold : manifolds) { children.push_back(manifold.pNode_->ToLeafNode()); } - return Manifold(std::make_shared(CsgLeafNode::Compose(children))); + return Manifold(CsgLeafNode::Compose(children)); } /** diff --git a/thirdparty/manifold/src/csg_tree.cpp b/thirdparty/manifold/src/csg_tree.cpp index 8f98fe29895c..a05fce3aa6b6 100644 --- a/thirdparty/manifold/src/csg_tree.cpp +++ b/thirdparty/manifold/src/csg_tree.cpp @@ -19,7 +19,6 @@ #endif #include -#include #include "./boolean3.h" #include "./csg_tree.h" @@ -31,66 +30,10 @@ constexpr int kParallelThreshold = 4096; namespace { using namespace manifold; -struct Transform4x3 { - mat3x4 transform; - - vec3 operator()(vec3 position) const { - return transform * vec4(position, 1.0); - } -}; - -struct UpdateHalfedge { - const int nextVert; - const int nextEdge; - const int nextFace; - - Halfedge operator()(Halfedge edge) { - edge.startVert += nextVert; - edge.endVert += nextVert; - edge.pairedHalfedge += nextEdge; - return edge; - } -}; - -struct UpdateTriProp { - const int nextProp; - - ivec3 operator()(ivec3 tri) { - tri += nextProp; - return tri; - } -}; - -struct UpdateMeshIDs { - const int offset; - - TriRef operator()(TriRef ref) { - ref.meshID += offset; - return ref; - } -}; - -struct CheckOverlap { - VecView boxes; - const size_t i; - bool operator()(size_t j) { return boxes[i].DoesOverlap(boxes[j]); } -}; - -using SharedImpl = std::variant, - std::shared_ptr>; -struct GetImplPtr { - const Manifold::Impl *operator()(const SharedImpl &p) { - if (std::holds_alternative>(p)) { - return std::get_if>(&p)->get(); - } else { - return std::get_if>(&p)->get(); - } - }; -}; - struct MeshCompare { - bool operator()(const SharedImpl &a, const SharedImpl &b) { - return GetImplPtr()(a)->NumVert() < GetImplPtr()(b)->NumVert(); + bool operator()(const std::shared_ptr &a, + const std::shared_ptr &b) { + return a->GetImpl()->NumVert() < b->GetImpl()->NumVert(); } }; @@ -99,11 +42,12 @@ namespace manifold { std::shared_ptr CsgNode::Boolean( const std::shared_ptr &second, OpType op) { - if (auto opNode = std::dynamic_pointer_cast(second)) { + if (second->GetNodeType() != CsgNodeType::Leaf) { // "this" is not a CsgOpNode (which overrides Boolean), but if "second" is // and the operation is commutative, we let it built the tree. if ((op == OpType::Add || op == OpType::Intersect)) { - return opNode->Boolean(shared_from_this(), op); + return std::static_pointer_cast(second)->Boolean( + shared_from_this(), op); } } std::vector> children({shared_from_this(), second}); @@ -154,8 +98,6 @@ std::shared_ptr CsgLeafNode::GetImpl() const { return pImpl_; } -mat3x4 CsgLeafNode::GetTransform() const { return transform_; } - std::shared_ptr CsgLeafNode::ToLeafNode() const { return std::make_shared(*this); } @@ -166,10 +108,14 @@ std::shared_ptr CsgLeafNode::Transform(const mat3x4 &m) const { CsgNodeType CsgLeafNode::GetNodeType() const { return CsgNodeType::Leaf; } +std::shared_ptr ImplToLeaf(Manifold::Impl &&impl) { + return std::make_shared(std::make_shared(impl)); +} + /** * Efficient union of a set of pairwise disjoint meshes. */ -Manifold::Impl CsgLeafNode::Compose( +std::shared_ptr CsgLeafNode::Compose( const std::vector> &nodes) { ZoneScoped; double epsilon = -1; @@ -187,7 +133,7 @@ Manifold::Impl CsgLeafNode::Compose( if (node->pImpl_->status_ != Manifold::Error::NoError) { Manifold::Impl impl; impl.status_ = node->pImpl_->status_; - return impl; + return ImplToLeaf(std::move(impl)); } double nodeOldScale = node->pImpl_->bBox_.Scale(); double nodeNewScale = @@ -242,18 +188,30 @@ Manifold::Impl CsgLeafNode::Compose( copy(node->pImpl_->halfedgeTangent_.begin(), node->pImpl_->halfedgeTangent_.end(), combined.halfedgeTangent_.begin() + edgeIndices[i]); - transform( - node->pImpl_->halfedge_.begin(), node->pImpl_->halfedge_.end(), - combined.halfedge_.begin() + edgeIndices[i], - UpdateHalfedge({vertIndices[i], edgeIndices[i], triIndices[i]})); + const int nextVert = vertIndices[i]; + const int nextEdge = edgeIndices[i]; + const int nextFace = triIndices[i]; + transform(node->pImpl_->halfedge_.begin(), + node->pImpl_->halfedge_.end(), + combined.halfedge_.begin() + edgeIndices[i], + [nextVert, nextEdge, nextFace](Halfedge edge) { + edge.startVert += nextVert; + edge.endVert += nextVert; + edge.pairedHalfedge += nextEdge; + return edge; + }); if (numPropOut > 0) { auto start = combined.meshRelation_.triProperties.begin() + triIndices[i]; if (node->pImpl_->NumProp() > 0) { auto &triProp = node->pImpl_->meshRelation_.triProperties; + const int nextProp = propVertIndices[i]; transform(triProp.begin(), triProp.end(), start, - UpdateTriProp({propVertIndices[i]})); + [nextProp](ivec3 tri) { + tri += nextProp; + return tri; + }); const int numProp = node->pImpl_->NumProp(); auto &oldProp = node->pImpl_->meshRelation_.properties; @@ -282,8 +240,11 @@ Manifold::Impl CsgLeafNode::Compose( } else { // no need to apply the transform to the node, just copy the vertices // and face normals and apply transform on the fly + const mat3x4 transform = node->transform_; auto vertPosBegin = TransformIterator( - node->pImpl_->vertPos_.begin(), Transform4x3({node->transform_})); + node->pImpl_->vertPos_.begin(), [&transform](vec3 position) { + return transform * vec4(position, 1.0); + }); mat3 normalTransform = la::inverse(la::transpose(mat3(node->transform_))); auto faceNormalBegin = @@ -311,7 +272,10 @@ Manifold::Impl CsgLeafNode::Compose( transform(node->pImpl_->meshRelation_.triRef.begin(), node->pImpl_->meshRelation_.triRef.end(), combined.meshRelation_.triRef.begin() + triIndices[i], - UpdateMeshIDs({offset})); + [offset](TriRef ref) { + ref.meshID += offset; + return ref; + }); }); for (size_t i = 0; i < nodes.size(); i++) { @@ -327,173 +291,52 @@ Manifold::Impl CsgLeafNode::Compose( combined.SimplifyTopology(); combined.Finish(); combined.IncrementMeshIDs(); - return combined; -} - -CsgOpNode::CsgOpNode() {} - -CsgOpNode::CsgOpNode(const std::vector> &children, - OpType op) - : impl_(Impl{}) { - auto impl = impl_.GetGuard(); - impl->children_ = children; - SetOp(op); -} - -CsgOpNode::CsgOpNode(std::vector> &&children, - OpType op) - : impl_(Impl{}) { - auto impl = impl_.GetGuard(); - impl->children_ = children; - SetOp(op); -} - -std::shared_ptr CsgOpNode::Boolean( - const std::shared_ptr &second, OpType op) { - std::vector> children; - - auto isReused = [](const auto &node) { return node->impl_.UseCount() > 1; }; - - auto copyChildren = [&](const auto &list, const mat3x4 &transform) { - for (const auto &child : list) { - children.push_back(child->Transform(transform)); - } - }; - - auto self = std::dynamic_pointer_cast(shared_from_this()); - if (IsOp(op) && !isReused(self)) { - auto impl = impl_.GetGuard(); - copyChildren(impl->children_, transform_); - } else { - children.push_back(self); - } - - auto secondOp = std::dynamic_pointer_cast(second); - auto canInlineSecondOp = [&]() { - switch (op) { - case OpType::Add: - case OpType::Intersect: - return secondOp->IsOp(op); - case OpType::Subtract: - return secondOp->IsOp(OpType::Add); - default: - return false; - } - }; - - if (secondOp && canInlineSecondOp() && !isReused(secondOp)) { - auto secondImpl = secondOp->impl_.GetGuard(); - copyChildren(secondImpl->children_, secondOp->transform_); - } else { - children.push_back(second); - } - - return std::make_shared(children, op); -} - -std::shared_ptr CsgOpNode::Transform(const mat3x4 &m) const { - auto node = std::make_shared(); - node->impl_ = impl_; - node->transform_ = m * Mat4(transform_); - node->op_ = op_; - return node; -} - -std::shared_ptr CsgOpNode::ToLeafNode() const { - if (cache_ != nullptr) return cache_; - // turn the children into leaf nodes - GetChildren(); - auto impl = impl_.GetGuard(); - auto &children_ = impl->children_; - if (children_.size() > 1) { - switch (op_) { - case CsgNodeType::Union: - BatchUnion(); - break; - case CsgNodeType::Intersection: { - std::vector> impls; - for (auto &child : children_) { - impls.push_back( - std::dynamic_pointer_cast(child)->GetImpl()); - } - children_.clear(); - children_.push_back(std::make_shared( - BatchBoolean(OpType::Intersect, impls))); - break; - }; - case CsgNodeType::Difference: { - // take the lhs out and treat the remaining nodes as the rhs, perform - // union optimization for them - auto lhs = std::dynamic_pointer_cast(children_.front()); - children_.erase(children_.begin()); - BatchUnion(); - auto rhs = std::dynamic_pointer_cast(children_.front()); - children_.clear(); - Boolean3 boolean(*lhs->GetImpl(), *rhs->GetImpl(), OpType::Subtract); - children_.push_back( - std::make_shared(std::make_shared( - boolean.Result(OpType::Subtract)))); - }; - case CsgNodeType::Leaf: - // unreachable - break; - } - } else if (children_.size() == 0) { - return nullptr; - } - // children_ must contain only one CsgLeafNode now, and its Transform will - // give CsgLeafNode as well - cache_ = std::dynamic_pointer_cast( - children_.front()->Transform(transform_)); - return cache_; + return ImplToLeaf(std::move(combined)); } /** * Efficient boolean operation on a set of nodes utilizing commutativity of the * operation. Only supports union and intersection. */ -std::shared_ptr CsgOpNode::BatchBoolean( - OpType operation, - std::vector> &results) { +std::shared_ptr BatchBoolean( + OpType operation, std::vector> &results) { ZoneScoped; - auto getImplPtr = GetImplPtr(); DEBUG_ASSERT(operation != OpType::Subtract, logicErr, "BatchBoolean doesn't support Difference."); // common cases - if (results.size() == 0) return std::make_shared(); - if (results.size() == 1) - return std::make_shared(*results.front()); + if (results.size() == 0) return std::make_shared(); + if (results.size() == 1) return results.front(); if (results.size() == 2) { - Boolean3 boolean(*results[0], *results[1], operation); - return std::make_shared(boolean.Result(operation)); + Boolean3 boolean(*results[0]->GetImpl(), *results[1]->GetImpl(), operation); + return ImplToLeaf(boolean.Result(operation)); } #if (MANIFOLD_PAR == 1) && __has_include() tbb::task_group group; - tbb::concurrent_priority_queue queue(results.size()); + tbb::concurrent_priority_queue, MeshCompare> + queue(results.size()); for (auto result : results) { queue.emplace(result); } results.clear(); std::function process = [&]() { while (queue.size() > 1) { - SharedImpl a, b; + std::shared_ptr a, b; if (!queue.try_pop(a)) continue; if (!queue.try_pop(b)) { queue.push(a); continue; } group.run([&, a, b]() { - Boolean3 boolean(*getImplPtr(a), *getImplPtr(b), operation); - queue.emplace( - std::make_shared(boolean.Result(operation))); + Boolean3 boolean(*a->GetImpl(), *b->GetImpl(), operation); + queue.emplace(ImplToLeaf(boolean.Result(operation))); return group.run(process); }); } }; group.run_and_wait(process); - SharedImpl r; + std::shared_ptr r; queue.try_pop(r); - return *std::get_if>(&r); + return r; #endif // apply boolean operations starting from smaller meshes // the assumption is that boolean operations on smaller meshes is faster, @@ -508,24 +351,23 @@ std::shared_ptr CsgOpNode::BatchBoolean( auto b = std::move(results.back()); results.pop_back(); // boolean operation - Boolean3 boolean(*a, *b, operation); - auto result = std::make_shared(boolean.Result(operation)); + Boolean3 boolean(*a->GetImpl(), *b->GetImpl(), operation); + auto result = ImplToLeaf(boolean.Result(operation)); if (results.size() == 0) { return result; } results.push_back(result); std::push_heap(results.begin(), results.end(), cmpFn); } - return std::make_shared(*results.front()); + return results.front(); } /** * Efficient union operation on a set of nodes by doing Compose as much as * possible. - * Note: Due to some unknown issues with `Compose`, we are now doing - * `BatchBoolean` instead of using `Compose` for non-intersecting manifolds. */ -void CsgOpNode::BatchUnion() const { +std::shared_ptr BatchUnion( + std::vector> &children) { ZoneScoped; // INVARIANT: children_ is a vector of leaf nodes // this kMaxUnionSize is a heuristic to avoid the pairwise disjoint check @@ -533,26 +375,25 @@ void CsgOpNode::BatchUnion() const { // If the number of children exceeded this limit, we will operate on chunks // with size kMaxUnionSize. constexpr size_t kMaxUnionSize = 1000; - auto impl = impl_.GetGuard(); - auto &children_ = impl->children_; - while (children_.size() > 1) { - const size_t start = (children_.size() > kMaxUnionSize) - ? (children_.size() - kMaxUnionSize) + DEBUG_ASSERT(!children.empty(), logicErr, + "BatchUnion should not have empty children"); + while (children.size() > 1) { + const size_t start = (children.size() > kMaxUnionSize) + ? (children.size() - kMaxUnionSize) : 0; Vec boxes; - boxes.reserve(children_.size() - start); - for (size_t i = start; i < children_.size(); i++) { - boxes.push_back(std::dynamic_pointer_cast(children_[i]) - ->GetImpl() - ->bBox_); + boxes.reserve(children.size() - start); + for (size_t i = start; i < children.size(); i++) { + boxes.push_back(children[i]->GetImpl()->bBox_); } // partition the children into a set of disjoint sets // each set contains a set of children that are pairwise disjoint std::vector> disjointSets; for (size_t i = 0; i < boxes.size(); i++) { auto lambda = [&boxes, i](const Vec &set) { - return std::find_if(set.begin(), set.end(), CheckOverlap({boxes, i})) == - set.end(); + return std::find_if(set.begin(), set.end(), [&boxes, i](size_t j) { + return boxes[i].DoesOverlap(boxes[j]); + }) == set.end(); }; auto it = std::find_if(disjointSets.begin(), disjointSets.end(), lambda); if (it == disjointSets.end()) { @@ -562,82 +403,260 @@ void CsgOpNode::BatchUnion() const { } } // compose each set of disjoint children - std::vector> impls; + std::vector> impls; for (auto &set : disjointSets) { if (set.size() == 1) { - impls.push_back( - std::dynamic_pointer_cast(children_[start + set[0]]) - ->GetImpl()); + impls.push_back(children[start + set[0]]); } else { std::vector> tmp; for (size_t j : set) { - tmp.push_back( - std::dynamic_pointer_cast(children_[start + j])); + tmp.push_back(children[start + j]); } - impls.push_back( - std::make_shared(CsgLeafNode::Compose(tmp))); + impls.push_back(CsgLeafNode::Compose(tmp)); } } - children_.erase(children_.begin() + start, children_.end()); - children_.push_back( - std::make_shared(BatchBoolean(OpType::Add, impls))); + children.erase(children.begin() + start, children.end()); + children.push_back(BatchBoolean(OpType::Add, impls)); // move it to the front as we process from the back, and the newly added // child should be quite complicated - std::swap(children_.front(), children_.back()); + std::swap(children.front(), children.back()); } + return children.front(); } -/** - * Flatten the children to a list of leaf nodes and return them. - * If forceToLeafNodes is true, the list will be guaranteed to be a list of leaf - * nodes (i.e. no ops). Otherwise, the list may contain ops. Note that this - * function will not apply the transform to children, as they may be shared with - * other nodes. - */ -std::vector> &CsgOpNode::GetChildren( - bool forceToLeafNodes) const { +CsgOpNode::CsgOpNode() {} + +CsgOpNode::CsgOpNode(const std::vector> &children, + OpType op) + : impl_(Impl{}), op_(op) { auto impl = impl_.GetGuard(); + impl->children_ = children; +} - if (forceToLeafNodes && !impl->forcedToLeafNodes_) { - impl->forcedToLeafNodes_ = true; - for_each(ExecutionPolicy::Par, impl->children_.begin(), - impl->children_.end(), [](auto &child) { - if (child->GetNodeType() != CsgNodeType::Leaf) { - child = child->ToLeafNode(); - } - }); - } - return impl->children_; +std::shared_ptr CsgOpNode::Boolean( + const std::shared_ptr &second, OpType op) { + std::vector> children; + children.push_back(shared_from_this()); + children.push_back(second); + + return std::make_shared(children, op); } -void CsgOpNode::SetOp(OpType op) { - switch (op) { - case OpType::Add: - op_ = CsgNodeType::Union; - break; - case OpType::Subtract: - op_ = CsgNodeType::Difference; - break; - case OpType::Intersect: - op_ = CsgNodeType::Intersection; - break; +std::shared_ptr CsgOpNode::Transform(const mat3x4 &m) const { + auto node = std::make_shared(); + node->impl_ = impl_; + node->transform_ = m * Mat4(transform_); + node->op_ = op_; + return node; +} + +struct CsgStackFrame { + bool finalize; + OpType parent_op; + mat3x4 transform; + std::vector> *destination; + std::shared_ptr op_node; + std::vector> positive_children; + std::vector> negative_children; + + CsgStackFrame(bool finalize, OpType parent_op, mat3x4 transform, + std::vector> *parent, + std::shared_ptr op_node) + : finalize(finalize), + parent_op(parent_op), + transform(transform), + destination(parent), + op_node(op_node) {} +}; + +std::shared_ptr CsgOpNode::ToLeafNode() const { + if (cache_ != nullptr) return cache_; + + // Note: We do need a pointer here to avoid vector pointers from being + // invalidated after pushing elements into the explicit stack. + // It is a `shared_ptr` because we may want to drop the stack frame while + // still referring to some of the elements inside the old frame. + // It is possible to use `unique_ptr`, extending the lifetime of the frame + // when we remove it from the stack, but it is a bit more complicated and + // there is no measurable overhead from using `shared_ptr` here... + std::vector> stack; + // initial node, destination is a nullptr because we don't need to put the + // result anywhere else (except in the cache_). + stack.push_back(std::make_shared( + false, op_, la::identity, nullptr, + std::static_pointer_cast(shared_from_this()))); + + // Instead of actually using recursion in the algorithm, we use an explicit + // stack, do DFS and store the intermediate states into `CsgStackFrame` to + // avoid stack overflow. + // + // Before performing boolean operations, we should make sure that all children + // are `CsgLeafNodes`, i.e. are actual meshes that can be operated on. Hence, + // we do it in two steps: + // 1. Populate `children` (`left_children` and `right_children`, see below) + // If the child is a `CsgOpNode`, we either collapse it or compute its + // boolean operation result. + // 2. Performs boolean after populating the `children` set. + // After a boolean operation is completed, we put the result back to its + // parent's `children` set. + // + // When we populate `children`, we perform collapsing on-the-fly. + // For example, we want to turn `(Union a (Union b c))` into `(Union a b c)`. + // This allows more efficient `BatchBoolean`/`BatchUnion` calls. + // We can do this when the child operation is the same as the parent + // operation, except when the operation is `Subtract` (see below). + // Note that to avoid repeating work, we will not collapse nodes that are + // reused. And in the special case where the children set only contains one + // element, we don't need any operation, so we can collapse that as well. + // Instead of moving `b` and `c` into the parent, and running this collapsing + // check until a fixed point, we remember the `destination` where we should + // put the `CsgLeafNode` into. Normally, the `destination` pointer point to + // the parent `children` set. However, when a child is being collapsed, we + // keep using the old `destination` pointer for the grandchildren. Hence, + // removing a node by collapsing takes O(1) time. We also need to store the + // parent operation type for checking if the node is eligible for collapsing, + // and transform matrix because we need to re-apply the transformation to the + // children. + // + // `Subtract` is handled differently from `Add` and `Intersect`. It is treated + // as two `Add` nodes, `positive_children` and `negative_children`, that + // should be subtracted later. This allows collapsing children `Add` nodes. + // For normal `Add` and `Intersect`, we only use `positive_children`. + // + // `impl->children_` should always contain either the raw set of children or + // the NOT transformed result, while `cache_` should contain the transformed + // result. This is because `impl` can be shared between `CsgOpNode` that + // differ in `transform_`, so we want it to be able to share the result. + // =========================================================================== + // Recursive version (pseudocode only): + // + // void f(CsgOpNode node, OpType parent_op, mat3x4 transform, + // std::vector *destination) { + // auto impl = node->impl_.GetGuard(); + // // can collapse when we have the same operation as the parent and is + // // unique, or when we have only one children. + // const bool canCollapse = (node->op_ == parent_op && IsUnique(node)) || + // impl->children_.size() == 1; + // const mat3x4 transform2 = canCollapse ? transform * node->transform_ + // : la::identity; + // std::vector positive_children, negative_children; + // // for subtract, we pretend the operation is Add for our children. + // auto op = node->op_ == OpType::Subtract ? OpType::Add : node->op_; + // for (size_t i = 0; i < impl->children_.size(); i++) { + // auto child = impl->children_[i]; + // // negative when it is the remaining operands for Subtract + // auto dest = node->op_ == OpType::Subtract && i != 0 ? + // negative_children : positive_children; + // if (canCollapse) dest = destination; + // if (child->GetNodeType() == CsgNodeType::Leaf) + // dest.push_back(child); + // else + // f(child, op, transform2, dest); + // } + // if (canCollapse) return; + // if (node->op_ == OpType::Add) + // impl->children_ = {BatchUnion(positive_children)}; + // else if (node->op_ == OpType::Intersect) + // impl->children_ = {BatchBoolean(Intersect, positive_children)}; + // else // subtract + // impl->children_ = { BatchUnion(positive_children) - + // BatchUnion(negative_children)}; + // // node local transform + // node->cache_ = impl->children_[0].Transform(node.transform); + // // collapsed node transforms + // if (destination) + // destination->push_back(node->cache_->Transform(transform)); + // } + while (!stack.empty()) { + std::shared_ptr frame = stack.back(); + auto impl = frame->op_node->impl_.GetGuard(); + if (frame->finalize) { + switch (frame->op_node->op_) { + case OpType::Add: + impl->children_ = {BatchUnion(frame->positive_children)}; + break; + case OpType::Intersect: { + impl->children_ = { + BatchBoolean(OpType::Intersect, frame->positive_children)}; + break; + }; + case OpType::Subtract: + if (frame->positive_children.empty()) { + // nothing to subtract from, so the result is empty. + impl->children_ = {std::make_shared()}; + } else { + auto positive = BatchUnion(frame->positive_children); + if (frame->negative_children.empty()) { + // nothing to subtract, result equal to the LHS. + impl->children_ = {frame->positive_children[0]}; + } else { + Boolean3 boolean(*positive->GetImpl(), + *BatchUnion(frame->negative_children)->GetImpl(), + OpType::Subtract); + impl->children_ = {ImplToLeaf(boolean.Result(OpType::Subtract))}; + } + } + break; + } + frame->op_node->cache_ = std::static_pointer_cast( + impl->children_[0]->Transform(frame->op_node->transform_)); + if (frame->destination != nullptr) + frame->destination->push_back(std::static_pointer_cast( + frame->op_node->cache_->Transform(frame->transform))); + stack.pop_back(); + } else { + auto add_children = [&stack](std::shared_ptr &node, OpType op, + mat3x4 transform, auto *destination) { + if (node->GetNodeType() == CsgNodeType::Leaf) + destination->push_back(std::static_pointer_cast( + node->Transform(transform))); + else + stack.push_back(std::make_shared( + false, op, transform, destination, + std::static_pointer_cast(node))); + }; + // op_node use_count == 2 because it is both inside one CsgOpNode + // and in our stack. + // if there is only one child, we can also collapse. + const bool canCollapse = frame->destination != nullptr && + ((frame->op_node->op_ == frame->parent_op && + frame->op_node.use_count() <= 2 && + frame->op_node->impl_.UseCount() == 1) || + impl->children_.size() == 1); + if (canCollapse) + stack.pop_back(); + else + frame->finalize = true; + + const mat3x4 transform = + canCollapse ? (frame->transform * Mat4(frame->op_node->transform_)) + : la::identity; + OpType op = frame->op_node->op_ == OpType::Subtract ? OpType::Add + : frame->op_node->op_; + for (size_t i = 0; i < impl->children_.size(); i++) { + auto dest = canCollapse ? frame->destination + : (frame->op_node->op_ == OpType::Subtract && i != 0) + ? &frame->negative_children + : &frame->positive_children; + add_children(impl->children_[i], op, transform, dest); + } + } } + return cache_; } -bool CsgOpNode::IsOp(OpType op) { - switch (op) { +CsgNodeType CsgOpNode::GetNodeType() const { + switch (op_) { case OpType::Add: - return op_ == CsgNodeType::Union; + return CsgNodeType::Union; case OpType::Subtract: - return op_ == CsgNodeType::Difference; + return CsgNodeType::Difference; case OpType::Intersect: - return op_ == CsgNodeType::Intersection; - default: - return false; + return CsgNodeType::Intersection; } + // unreachable... + return CsgNodeType::Leaf; } -mat3x4 CsgOpNode::GetTransform() const { return transform_; } - } // namespace manifold diff --git a/thirdparty/manifold/src/csg_tree.h b/thirdparty/manifold/src/csg_tree.h index f2a2f692afed..5c24dc5a2050 100644 --- a/thirdparty/manifold/src/csg_tree.h +++ b/thirdparty/manifold/src/csg_tree.h @@ -27,7 +27,6 @@ class CsgNode : public std::enable_shared_from_this { virtual std::shared_ptr ToLeafNode() const = 0; virtual std::shared_ptr Transform(const mat3x4 &m) const = 0; virtual CsgNodeType GetNodeType() const = 0; - virtual mat3x4 GetTransform() const = 0; virtual std::shared_ptr Boolean( const std::shared_ptr &second, OpType op); @@ -52,9 +51,7 @@ class CsgLeafNode final : public CsgNode { CsgNodeType GetNodeType() const override; - mat3x4 GetTransform() const override; - - static Manifold::Impl Compose( + static std::shared_ptr Compose( const std::vector> &nodes); private: @@ -68,8 +65,6 @@ class CsgOpNode final : public CsgNode { CsgOpNode(const std::vector> &children, OpType op); - CsgOpNode(std::vector> &&children, OpType op); - std::shared_ptr Boolean(const std::shared_ptr &second, OpType op) override; @@ -77,9 +72,7 @@ class CsgOpNode final : public CsgNode { std::shared_ptr ToLeafNode() const override; - CsgNodeType GetNodeType() const override { return op_; } - - mat3x4 GetTransform() const override; + CsgNodeType GetNodeType() const override; private: struct Impl { @@ -87,22 +80,10 @@ class CsgOpNode final : public CsgNode { bool forcedToLeafNodes_ = false; }; mutable ConcurrentSharedPtr impl_ = ConcurrentSharedPtr(Impl{}); - CsgNodeType op_; + OpType op_; mat3x4 transform_ = la::identity; // the following fields are for lazy evaluation, so they are mutable mutable std::shared_ptr cache_ = nullptr; - - void SetOp(OpType); - bool IsOp(OpType op); - - static std::shared_ptr BatchBoolean( - OpType operation, - std::vector> &results); - - void BatchUnion() const; - - std::vector> &GetChildren( - bool forceToLeafNodes = true) const; }; } // namespace manifold diff --git a/thirdparty/manifold/src/impl.cpp b/thirdparty/manifold/src/impl.cpp index 9582b2c12638..59cc0293d6da 100644 --- a/thirdparty/manifold/src/impl.cpp +++ b/thirdparty/manifold/src/impl.cpp @@ -194,13 +194,14 @@ int GetLabels(std::vector& components, } void DedupePropVerts(manifold::Vec& triProp, - const Vec>& vert2vert) { + const Vec>& vert2vert, + size_t numPropVert) { ZoneScoped; std::vector vertLabels; - const int numLabels = GetLabels(vertLabels, vert2vert, vert2vert.size()); + const int numLabels = GetLabels(vertLabels, vert2vert, numPropVert); std::vector label2vert(numLabels); - for (size_t v = 0; v < vert2vert.size(); ++v) label2vert[vertLabels[v]] = v; + for (size_t v = 0; v < numPropVert; ++v) label2vert[vertLabels[v]] = v; for (auto& prop : triProp) for (int i : {0, 1, 2}) prop[i] = label2vert[vertLabels[prop[i]]]; } @@ -343,6 +344,8 @@ void Manifold::Impl::CreateFaces() { const int prop1 = meshRelation_ .triProperties[pairFace][jointNum == 2 ? 0 : jointNum + 1]; + if (prop0 == prop1) return; + bool propEqual = true; for (size_t p = 0; p < numProp; ++p) { if (meshRelation_.properties[numProp * prop0 + p] != @@ -355,7 +358,7 @@ void Manifold::Impl::CreateFaces() { vert2vert[edgeIdx] = std::make_pair(prop0, prop1); } }); - DedupePropVerts(meshRelation_.triProperties, vert2vert); + DedupePropVerts(meshRelation_.triProperties, vert2vert, NumPropVert()); } for_each_n(autoPolicy(halfedge_.size(), 1e4), countAt(0), halfedge_.size(), diff --git a/thirdparty/manifold/include/manifold/iters.h b/thirdparty/manifold/src/iters.h similarity index 100% rename from thirdparty/manifold/include/manifold/iters.h rename to thirdparty/manifold/src/iters.h diff --git a/thirdparty/manifold/src/parallel.h b/thirdparty/manifold/src/parallel.h index 221cefac1bb7..a434939494c6 100644 --- a/thirdparty/manifold/src/parallel.h +++ b/thirdparty/manifold/src/parallel.h @@ -17,6 +17,7 @@ #pragma once +#include "./iters.h" #if (MANIFOLD_PAR == 1) #include #include @@ -27,7 +28,6 @@ #include #include -#include "manifold/iters.h" namespace manifold { enum class ExecutionPolicy { diff --git a/thirdparty/manifold/src/properties.cpp b/thirdparty/manifold/src/properties.cpp index 32f79e5b601f..911133776ab3 100644 --- a/thirdparty/manifold/src/properties.cpp +++ b/thirdparty/manifold/src/properties.cpp @@ -157,10 +157,11 @@ struct CheckCCW { "tol = %g, area2 = %g, base2*tol2 = %g\n" "normal = %g, %g, %g\n" "norm = %g, %g, %g\nverts: %d, %d, %d\n", - face, area / base, base, tol, area * area, base2 * tol * tol, - triNormal[face].x, triNormal[face].y, triNormal[face].z, norm.x, - norm.y, norm.z, halfedges[3 * face].startVert, - halfedges[3 * face + 1].startVert, halfedges[3 * face + 2].startVert); + static_cast(face), area / base, base, tol, area * area, + base2 * tol * tol, triNormal[face].x, triNormal[face].y, + triNormal[face].z, norm.x, norm.y, norm.z, + halfedges[3 * face].startVert, halfedges[3 * face + 1].startVert, + halfedges[3 * face + 2].startVert); } #endif return check;