Lazy collider (#1473)

* lazy collider

* format

* address comments

* avoid std::shared_ptr<T>::unique()

* try to fix maybe uninitialized warning

* remove unused recorder wrapper
This commit is contained in:
pca006132
2025-12-28 15:43:10 +08:00
committed by GitHub
parent b2c6ddd2e3
commit b918447ad1
10 changed files with 277 additions and 84 deletions
+2
View File
@@ -21,6 +21,7 @@ set(
edge_op.cpp
face_op.cpp
impl.cpp
lazy_collider.cpp
manifold.cpp
polygon.cpp
properties.cpp
@@ -43,6 +44,7 @@ set(
hashtable.h
impl.h
iters.h
lazy_collider.h
mesh_fixes.h
parallel.h
quickhull.h
+2 -3
View File
@@ -371,7 +371,7 @@ Intersections Intersect12_(const Manifold::Impl& inP,
a.vertPos_[a.halfedge_[i].endVert])
: Box();
};
b.collider_.Collisions<false>(f, a.halfedge_.size(), recorder);
b.collider_->Collisions<false>(recorder, f, a.halfedge_.size());
Intersections result = recorder.get();
auto& p1q2 = result.p1q2;
@@ -454,8 +454,7 @@ Vec<int> Winding03_(const Manifold::Impl& inP, const Manifold::Impl& inQ,
};
auto recorder = MakeSimpleRecorder(recorderf);
auto f = [&](int i) { return a.vertPos_[verts[i]]; };
b.collider_.Collisions<false, decltype(f), decltype(recorder)>(
f, verts.size(), recorder);
b.collider_->Collisions<false>(recorder, f, verts.size());
// flood fill
for_each(autoPolicy(w03.size()), countAt(0), countAt(w03.size()),
[&](size_t i) {
+34 -47
View File
@@ -157,17 +157,21 @@ struct CreateRadixTree {
}
};
template <typename F, const bool selfCollision, typename Recorder>
template <typename F, const bool selfCollision, const bool hasTransform,
typename Recorder>
struct FindCollision {
F& f;
VecView<const Box> nodeBBox_;
VecView<const std::pair<int, int>> internalChildren_;
Recorder& recorder;
mat3x4 transform;
using Local = typename Recorder::Local;
inline int RecordCollision(int node, const int queryIdx, Local& local) {
bool overlaps = nodeBBox_[node].DoesOverlap(f(queryIdx));
auto box = nodeBBox_[node];
if (hasTransform) box = box.Transform(transform);
bool overlaps = box.DoesOverlap(f(queryIdx));
if (overlaps && IsLeaf(node)) {
int leafIdx = Node2Leaf(node);
if (!selfCollision || leafIdx != queryIdx) {
@@ -224,11 +228,6 @@ struct BuildInternalBoxes {
}
};
struct TransformBox {
const mat3x4 transform;
void operator()(Box& box) { box = box.Transform(transform); }
};
constexpr inline uint32_t SpreadBits3(uint32_t v) {
v = 0xFF0000FFu & (v * 0x00010001u);
v = 0x0F00F00Fu & (v * 0x00000101u);
@@ -264,6 +263,7 @@ class Collider {
ZoneScoped;
DEBUG_ASSERT(leafBB.size() == leafMorton.size(), userErr,
"vectors must be the same length");
if (leafBB.size() == 0) return;
int num_nodes = 2 * leafBB.size() - 1;
// assign and allocate members
nodeBBox_.resize_nofill(num_nodes);
@@ -276,24 +276,6 @@ class Collider {
UpdateBoxes(leafBB);
}
bool Transform(mat3x4 transform) {
ZoneScoped;
bool axisAligned = true;
for (int row : {0, 1, 2}) {
int count = 0;
for (int col : {0, 1, 2}) {
if (transform[col][row] == 0.0) ++count;
}
if (count != 2) axisAligned = false;
}
if (axisAligned) {
for_each(autoPolicy(nodeBBox_.size(), 1e5), nodeBBox_.begin(),
nodeBBox_.end(),
[transform](Box& box) { box = box.Transform(transform); });
}
return axisAligned;
}
void UpdateBoxes(const VecView<const Box>& leafBB) {
ZoneScoped;
DEBUG_ASSERT(leafBB.size() == NumLeaves(), userErr,
@@ -309,6 +291,29 @@ class Collider {
{nodeBBox_, counter, nodeParent_, internalChildren_}));
}
template <const bool selfCollision = false, typename F, typename Recorder>
void Collisions(Recorder& recorder, std::optional<mat3x4> transform, F f,
int n, bool parallel = true) const {
ZoneScoped;
using collider_internal::FindCollision;
if (internalChildren_.empty()) return;
if (transform) {
for_each_n(
parallel ? autoPolicy(n, collider_internal::kSequentialThreshold)
: ExecutionPolicy::Seq,
countAt(0), n,
FindCollision<decltype(f), selfCollision, true, Recorder>{
f, nodeBBox_, internalChildren_, recorder, transform.value()});
} else {
for_each_n(parallel
? autoPolicy(n, collider_internal::kSequentialThreshold)
: ExecutionPolicy::Seq,
countAt(0), n,
FindCollision<decltype(f), selfCollision, false, Recorder>{
f, nodeBBox_, internalChildren_, recorder, la::identity});
}
}
// This function iterates over queriesIn and calls recorder.record(queryIdx,
// leafIdx, local) for each collision it found.
// If selfCollisionl is true, it will skip the case where queryIdx == leafIdx.
@@ -319,30 +324,12 @@ class Collider {
//
// If thread local storage is not needed, use SimpleRecorder.
template <const bool selfCollision = false, typename T, typename Recorder>
void Collisions(const VecView<const T>& queriesIn, Recorder& recorder,
void Collisions(Recorder& recorder, std::optional<mat3x4> transform,
const VecView<const T>& queriesIn,
bool parallel = true) const {
ZoneScoped;
using collider_internal::FindCollision;
if (internalChildren_.empty()) return;
auto f = [queriesIn](const int i) { return queriesIn[i]; };
for_each_n(parallel ? autoPolicy(queriesIn.size(),
collider_internal::kSequentialThreshold)
: ExecutionPolicy::Seq,
countAt(0), queriesIn.size(),
FindCollision<decltype(f), selfCollision, Recorder>{
f, nodeBBox_, internalChildren_, recorder});
}
template <const bool selfCollision = false, typename F, typename Recorder>
void Collisions(F f, int n, Recorder& recorder, bool parallel = true) const {
ZoneScoped;
using collider_internal::FindCollision;
if (internalChildren_.empty()) return;
for_each_n(parallel ? autoPolicy(n, collider_internal::kSequentialThreshold)
: ExecutionPolicy::Seq,
countAt(0), n,
FindCollision<decltype(f), selfCollision, Recorder>{
f, nodeBBox_, internalChildren_, recorder});
Collisions<selfCollision>(recorder, transform, f, queriesIn.size(),
parallel);
}
static uint32_t MortonCode(vec3 position, Box bBox) {
+1 -1
View File
@@ -274,7 +274,7 @@ Polygons Manifold::Impl::Slice(double height) const {
};
auto recorder = MakeSimpleRecorder(recordCollision);
collider_.Collisions<false>(query.cview(), recorder, false);
collider_->Collisions<false>(recorder, query.cview(), false);
Polygons polys;
while (!tris.empty()) {
+14 -18
View File
@@ -572,18 +572,6 @@ void Manifold::Impl::CreateHalfedges(const Vec<ivec3>& triProp,
});
}
/**
* Does a full recalculation of the face bounding boxes, including updating
* the collider, but does not resort the faces.
*/
void Manifold::Impl::Update() {
CalculateBBox();
Vec<Box> faceBox;
Vec<uint32_t> faceMorton;
GetFaceBoxMorton(faceBox, faceMorton);
collider_.UpdateBoxes(faceBox);
}
void Manifold::Impl::MakeEmpty(Error status) {
bBox_ = Box();
vertPos_.clear();
@@ -592,6 +580,7 @@ void Manifold::Impl::MakeEmpty(Error status) {
faceNormal_.clear();
halfedgeTangent_.clear();
meshRelation_ = MeshRelationD();
collider_ = std::make_shared<LazyCollider>(LazyCollider::Empty());
status_ = status;
}
@@ -608,7 +597,7 @@ void Manifold::Impl::WarpBatch(std::function<void(VecView<vec3>)> warpFunc) {
MakeEmpty(Error::NonFiniteVertex);
return;
}
Update();
CalculateBBox();
faceNormal_.clear(); // force recalculation of triNormal
SetEpsilon();
Finish();
@@ -629,7 +618,6 @@ Manifold::Impl Manifold::Impl::Transform(const mat3x4& transform_) const {
result.MakeEmpty(Error::NonFiniteVertex);
return result;
}
result.collider_ = collider_;
result.meshRelation_ = meshRelation_;
result.epsilon_ = epsilon_;
result.tolerance_ = tolerance_;
@@ -669,15 +657,23 @@ Manifold::Impl Manifold::Impl::Transform(const mat3x4& transform_) const {
FlipTris({result.halfedge_}));
}
// This optimization does a cheap collider update if the transform is
// axis-aligned.
if (!result.collider_.Transform(transform_)) result.Update();
result.CalculateBBox();
// Scale epsilon by the norm of the 3x3 portion of the transform.
result.epsilon_ *= SpectralNorm(mat3(transform_));
// Maximum of inherited epsilon loss and translational epsilon loss.
result.SetEpsilon(result.epsilon_);
if (LazyCollider::IsAxisAligned(transform_)) {
result.collider_ = std::make_shared<LazyCollider>(collider_, transform_);
} else if (!result.IsEmpty()) {
Vec<Box> faceBox;
Vec<uint32_t> faceMorton;
result.GetFaceBoxMorton(faceBox, faceMorton);
result.collider_ =
std::make_shared<LazyCollider>(collider_, std::move(faceBox));
} else {
result.collider_ = std::make_shared<LazyCollider>(LazyCollider::Empty());
}
return result;
}
+3 -4
View File
@@ -15,7 +15,7 @@
#pragma once
#include <map>
#include "collider.h"
#include "lazy_collider.h"
#include "manifold/common.h"
#include "manifold/manifold.h"
#include "shared.h"
@@ -54,12 +54,12 @@ struct Manifold::Impl {
Vec<vec3> faceNormal_;
Vec<vec4> halfedgeTangent_;
MeshRelationD meshRelation_;
Collider collider_;
std::shared_ptr<LazyCollider> collider_ = std::make_shared<LazyCollider>();
static std::atomic<uint32_t> meshIDCounter_;
static uint32_t ReserveIDs(uint32_t);
Impl() {}
Impl() : collider_{std::make_shared<LazyCollider>(LazyCollider::Empty())} {};
enum class Shape { Tetrahedron, Cube, Octahedron };
Impl(Shape, const mat3x4 = la::identity);
@@ -300,7 +300,6 @@ struct Manifold::Impl {
void CalculateNormals();
void IncrementMeshIDs();
void Update();
void MakeEmpty(Error status);
void Warp(std::function<void(vec3&)> warpFunc);
void WarpBatch(std::function<void(VecView<vec3>)> warpFunc);
+130
View File
@@ -0,0 +1,130 @@
// Copyright 2025 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "lazy_collider.h"
namespace manifold {
LazyCollider::LazyCollider() = default;
LazyCollider::LazyCollider(LeafData&& leafData) : leafData_{leafData} {}
LazyCollider::LazyCollider(std::shared_ptr<const LazyCollider> base,
const mat3x4& transform) {
DEBUG_ASSERT(base != nullptr, logicErr, "invalid base ptr");
mat3x4 composed = transform;
// attempt to unwrap one level, if the base is also a transformed collider
// we do not unwrap if the base has updatedLeafBox
std::lock_guard<std::mutex> lock(base->mutex_);
if (base->base_ && !base->base_->updatedLeafBox.has_value()) {
composed = composed * Mat4(base->base_->transform);
base = base->base_->base;
}
base_ = Base{base, std::nullopt, composed};
}
LazyCollider::LazyCollider(std::shared_ptr<const LazyCollider> base,
std::optional<Vec<Box>> updatedLeafBox) {
DEBUG_ASSERT(base != nullptr, logicErr, "invalid base ptr");
// attempt to unwrap base
// there are two cases:
// 1. base contains updatedLeafBox, which means that its points to the root
// 2. base is a transformed node, which either points to the root or points to
// a node with updatedLeafBox, which is case 1
// For case 2, the max depth is 2, and in any case we ignore transforms
std::lock_guard<std::mutex> lock(base->mutex_);
if (base->base_) base = base->base_->base;
if (base->base_) base = base->base_->base;
base_ = Base{base, std::move(updatedLeafBox), la::identity};
}
LazyCollider::LazyCollider(const LazyCollider& other) {
std::lock_guard<std::mutex> lock(other.mutex_);
built_ = other.built_;
leafData_ = other.leafData_;
base_ = other.base_;
}
LazyCollider::LazyCollider(LazyCollider&& other) noexcept {
std::lock_guard<std::mutex> lock(other.mutex_);
built_ = std::move(other.built_);
leafData_ = std::move(other.leafData_);
base_ = std::move(other.base_);
}
LazyCollider& LazyCollider::operator=(const LazyCollider& other) {
if (this == &other) return *this;
std::scoped_lock lock(mutex_, other.mutex_);
built_ = other.built_;
leafData_ = other.leafData_;
base_ = other.base_;
return *this;
}
LazyCollider& LazyCollider::operator=(LazyCollider&& other) noexcept {
if (this == &other) return *this;
std::scoped_lock lock(mutex_, other.mutex_);
built_ = std::move(other.built_);
leafData_ = std::move(other.leafData_);
base_ = std::move(other.base_);
return *this;
}
bool LazyCollider::IsBuilt() const {
std::lock_guard<std::mutex> lock(mutex_);
return built_.has_value();
}
bool LazyCollider::IsAxisAligned(const mat3x4& transform) {
for (int row : {0, 1, 2}) {
int count = 0;
for (int col : {0, 1, 2}) {
if (transform[col][row] == 0.0) ++count;
}
if (count != 2) return false;
}
return true;
}
const LazyCollider::Built& LazyCollider::EnsureBuilt() const {
std::lock_guard<std::mutex> lock(mutex_);
if (built_) return built_.value();
if (base_) {
const Built& baseBuilt = base_->base->EnsureBuilt();
// no need to propagate transform from built, we already computed the
// composed transform in the constructor
built_ = {baseBuilt.collider, base_->transform};
if (base_->updatedLeafBox) {
auto leafBox = std::move(base_->updatedLeafBox.value());
base_ = std::nullopt;
// need a unique collider, it may be possible that the base reference to
// the collider was dropped when we reset the base_ pointer
if (built_->collider.use_count() != 1)
built_->collider = std::make_shared<Collider>(*built_->collider);
built_->collider->UpdateBoxes(leafBox);
// note that after updating the boxes, we don't need transform
built_->transform = std::nullopt;
}
base_ = std::nullopt;
} else {
DEBUG_ASSERT(leafData_.has_value(), logicErr, "uninitialized collider");
built_ = {
std::make_shared<Collider>(leafData_->leafBox, leafData_->leafMorton),
std::nullopt};
leafData_ = std::nullopt;
}
return built_.value();
}
} // namespace manifold
+76
View File
@@ -0,0 +1,76 @@
// Copyright 2025 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <memory>
#include <mutex>
#include "collider.h"
namespace manifold {
class LazyCollider {
public:
struct LeafData {
Vec<Box> leafBox;
Vec<uint32_t> leafMorton;
};
struct Built {
std::shared_ptr<Collider> collider;
std::optional<mat3x4> transform;
};
struct Base {
std::shared_ptr<const LazyCollider> base;
std::optional<Vec<Box>> updatedLeafBox;
mat3x4 transform;
};
static LazyCollider Empty() { return LazyCollider(LeafData{{}, {}}); }
LazyCollider();
LazyCollider(LeafData&& leafData);
LazyCollider(std::shared_ptr<const LazyCollider> base,
const mat3x4& transform);
LazyCollider(std::shared_ptr<const LazyCollider> base,
std::optional<Vec<Box>> updatedLeafBox);
LazyCollider(const LazyCollider& other);
LazyCollider(LazyCollider&& other) noexcept;
LazyCollider& operator=(const LazyCollider& other);
LazyCollider& operator=(LazyCollider&& other) noexcept;
~LazyCollider() = default;
bool IsBuilt() const;
template <const bool selfCollision = false, typename Recorder,
typename... Args>
void Collisions(Recorder& recorder, Args... args) const {
const Built& built = EnsureBuilt();
built.collider->Collisions<false>(recorder, built.transform, args...);
}
static bool IsAxisAligned(const mat3x4& transform);
private:
const Built& EnsureBuilt() const;
mutable std::mutex mutex_;
mutable std::optional<Built> built_;
mutable std::optional<LeafData> leafData_;
mutable std::optional<Base> base_;
};
} // namespace manifold
+2 -3
View File
@@ -204,7 +204,7 @@ bool Manifold::Impl::IsSelfIntersecting() const {
};
auto recorder = MakeSimpleRecorder(f);
collider_.Collisions<true>(faceBox.cview(), recorder);
collider_->Collisions<true>(recorder, faceBox.cview());
return intersecting.load();
}
@@ -416,8 +416,7 @@ double Manifold::Impl::MinGap(const Manifold::Impl& other,
});
MinDistanceRecorder recorder{*this, other};
collider_.Collisions<false, Box, MinDistanceRecorder>(faceBoxOther.cview(),
recorder, false);
collider_->Collisions<false>(recorder, faceBoxOther.cview(), false);
double minDistanceSquared =
std::min(recorder.get(), searchLength * searchLength);
return sqrt(minDistanceSquared);
+13 -8
View File
@@ -159,7 +159,7 @@ bool MergeMeshGLP(MeshGLP<Precision, I>& mesh) {
return uf.unite(openVerts[a], openVerts[b]);
};
auto recorder = MakeSimpleRecorder(f);
collider.Collisions<true>(vertBox.cview(), recorder, false);
collider.Collisions<true>(recorder, std::nullopt, vertBox.cview(), false);
for (size_t i = 0; i < mesh.mergeFromVert.size(); ++i) {
uf.unite(static_cast<int>(mesh.mergeFromVert[i]),
@@ -188,7 +188,10 @@ namespace manifold {
* and halfedges flagged for removal (NaN verts and -1 halfedges).
*/
void Manifold::Impl::Finish() {
if (halfedge_.size() == 0) return;
if (halfedge_.size() == 0) {
collider_ = std::make_shared<LazyCollider>(LazyCollider::Empty());
return;
}
CalculateBBox();
SetEpsilon(epsilon_);
@@ -199,11 +202,14 @@ void Manifold::Impl::Finish() {
}
SortVerts();
Vec<Box> faceBox;
Vec<uint32_t> faceMorton;
GetFaceBoxMorton(faceBox, faceMorton);
SortFaces(faceBox, faceMorton);
if (halfedge_.size() == 0) return;
LazyCollider::LeafData leafData;
GetFaceBoxMorton(leafData.leafBox, leafData.leafMorton);
SortFaces(leafData.leafBox, leafData.leafMorton);
if (halfedge_.size() == 0) {
collider_ = std::make_shared<LazyCollider>(LazyCollider::Empty());
return;
}
collider_ = std::make_shared<LazyCollider>(std::move(leafData));
CompactProps();
DEBUG_ASSERT(halfedge_.size() % 6 == 0, topologyErr,
@@ -247,7 +253,6 @@ void Manifold::Impl::Finish() {
"faceNormal size = " + std::to_string(faceNormal_.size()) +
", NumTri = " + std::to_string(NumTri()));
CalculateNormals();
collider_ = Collider(faceBox, faceMorton);
DEBUG_ASSERT(Is2Manifold(), logicErr, "mesh is not 2-manifold!");
}