8000 improved decimation by elalish · Pull Request #1147 · elalish/manifold · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

improved decimation #1147

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

Merged
merged 8 commits into from
Feb 19, 2025
Merged

improved decimation #1147

merged 8 commits into from
Feb 19, 2025

Conversation

elalish
Copy link
Owner
@elalish elalish commented Feb 17, 2025

Fixes #1017

Added Simplify() which is a lot like SetTolerance(), but with a clearer name that matches our CrossSection API.

We now have the beginnings of a halfway-decent decimator; in 3 seconds we can take a sphere with 8 million triangles and reduce it to under 2,500, looking like this:
image

I've also updated our edge collapse to only operate on intersected edges when it runs as part of the Boolean cleanup, which has been requested by several people, and should increase performance a bit too. I tried to apply that logic to the edge swapping as well, but it broke a few tests, so I'll come back to that.

@elalish elalish self-assigned this Feb 17, 2025
Copy link
codecov bot commented Feb 17, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 91.68%. Comparing base (a857f00) to head (cec28f7).
Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1147      +/-   ##
==========================================
- Coverage   91.70%   91.68%   -0.03%     
==========================================
  Files          30       30              
  Lines        5931     5951      +20     
==========================================
+ Hits         5439     5456      +17     
- Misses        492      495       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

src/manifold.cpp Outdated
return Manifold(impl);
// const double oldTolerance = GetCsgLeafNode().GetImpl()->tolerance_;
// Manifold result = SetTolerance(tolerance);
// result.GetCsgLeafNode().GetImpl()->tolerance_ = oldTolerance;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pca006132 What do you think is the right pattern to reduce duplication between these functions? The one I've commented out here didn't compile.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eh, I guess this is fine - only a few lines duplicated really.

@@ -77,14 +77,14 @@ TEST(Properties, Tolerance) {
}

TEST(Properties, ToleranceSphere) {
const int n = 100;
const int n = 1000;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This used to fail dramatically, but now works well.

@elalish elalish requested a review from pca006132 February 18, 2025 09:18
@@ -567,7 +570,7 @@ void Manifold::Impl::CollapseEdge(const int edge, std::vector<int>& edges) {
const vec3 pNew = vertPos_[endVert];
const vec3 pOld = vertPos_[toRemove.startVert];
const vec3 delta = pNew - pOld;
const bool shortEdge = la::dot(delta, delta) < tolerance_ * tolerance_;
const bool shortEdge = la::dot(delta, delta) < epsilon_ * epsilon_;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be epsilon? I thought we want to remove edges smaller than tolerance (tolerance is the shortest edges that the user want).

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, actually the check was already changed to epsilon above, but this line was forgotten. Really the flagged edge collapse should be a superset of the short edge collapse, but the actually the short edges relax a few things. I think that's really only valid for degenerate triangles, rather than "within tolerance" - all the longer edges should really go through the additional flagged edge checks.

src/edge_op.cpp Outdated
@@ -317,36 +320,35 @@ void Manifold::Impl::SimplifyTopology() {
numFlagged = 0;
ShortEdge se{halfedge_, vertPos_, epsilon_};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this ShortEdge check limit to newly added edges? Adding the check to this seems fine.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried, but it fails when two retained verts are at the same position in space - that edge can't be collapsed, even though it's zero length.

#endif
}

{
ZoneScopedN("RecursiveEdgeSwap");
Copy link
Collaborator
@pca006132 pca006132 Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edge swap as well: Should we swap old edges? Adding the check makes 3 tests fail with too many degenerate triangles, wondering why.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I guess I know why, should check for both candidates. Will try that later...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably put the check within the actual edge swap function as well, because we recursively do edge swapping?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we should look into this one more - I was surprised by those failures too.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice job, that was easier than I'd feared.

@pca006132
Copy link
Collaborator
diff --git a/src/edge_op.cpp b/src/edge_op.cpp
index 6ed9e1fd..b5a5f065 100644
--- a/src/edge_op.cpp
+++ b/src/edge_op.cpp
@@ -48,10 +48,13 @@ struct ShortEdge {
   VecView<const Halfedge> halfedge;
   VecView<const vec3> vertPos;
   const double tolerance;
+  const int firstNewVert;
 
   bool operator()(int edge) const {
     const Halfedge& half = halfedge[edge];
-    if (half.pairedHalfedge < 0) return false;
+    if (half.pairedHalfedge < 0 ||
+        (half.startVert < firstNewVert && half.endVert < firstNewVert))
+      return false;
     // Flag short edges
     const vec3 delta = vertPos[half.endVert] - vertPos[half.startVert];
     return la::dot(delta, delta) < tolerance * tolerance;
@@ -94,10 +97,15 @@ struct SwappableEdge {
   VecView<const vec3> vertPos;
   VecView<const vec3> triNormal;
   const double tolerance;
+  const int firstNewVert;
 
   bool operator()(int edge) const {
     const Halfedge& half = halfedge[edge];
     if (half.pairedHalfedge < 0) return false;
+    if (half.startVert < firstNewVert && half.endVert < firstNewVert &&
+        halfedge[NextHalfedge(edge)].endVert < firstNewVert &&
+        halfedge[NextHalfedge(half.pairedHalfedge)].endVert < firstNewVert)
+      return false;
 
     int tri = edge / 3;
     ivec3 triEdge = TriOf(edge);
@@ -318,7 +326,7 @@ void Manifold::Impl::SimplifyTopology(int firstNewVert) {
   {
     ZoneScopedN("CollapseShortEdge");
     numFlagged = 0;
-    ShortEdge se{halfedge_, vertPos_, epsilon_};
+    ShortEdge se{halfedge_, vertPos_, epsilon_, firstNewVert};
     s.run(nbEdges, se, [&](size_t i) {
       const bool didCollapse = CollapseEdge(i, scratchBuffer);
       if (didCollapse) numFlagged++;
@@ -353,7 +361,8 @@ void Manifold::Impl::SimplifyTopology(int firstNewVert) {
   {
     ZoneScopedN("RecursiveEdgeSwap");
     numFlagged = 0;
-    SwappableEdge se{halfedge_, vertPos_, faceNormal_, tolerance_};
+    SwappableEdge se{halfedge_, vertPos_, faceNormal_, tolerance_,
+                     firstNewVert};
     std::vector<int> edgeSwapStack;
     std::vector<int> visited(halfedge_.size(), -1);
     int tag = 0;

@pca006132
Copy link
Collaborator

OK parallelized pinched verts splitting:

void Manifold::Impl::SplitPinchedVerts() {
  ZoneScoped;

#if MANIFOLD_PAR == 1
  if (halfedge_.size() > 1e5) {
    std::mutex mutex;
    std::vector<size_t> problematic;
    std::vector<size_t> largestEdge(NumVert(),
                                    std::numeric_limits<size_t>::max());
    auto halfedgeCount = halfedge_.size();
    tbb::combinable<std::vector<bool>> store(
        [halfedgeCount]() { return std::vector<bool>(halfedgeCount, false); });
    tbb::parallel_for(
        tbb::blocked_range<size_t>(0, halfedge_.size()),
        [&store, &mutex, &problematic, &largestEdge, this](const auto& r) {
          auto& local = store.local();
          for (auto i = r.begin(); i < r.end(); ++i) {
            if (local[i]) continue;
            local[i] = true;
            const int vert = halfedge_[i].startVert;
            size_t largest = i;
            ForVert(i, [&local, &largest](int current) {
              local[current] = true;
              largest = std::max(largest, static_cast<size_t>(current));
            });
            auto expected = std::numeric_limits<size_t>::max();
            if (!reinterpret_cast<std::atomic<size_t>*>(largestEdge.data() +
                                                        vert)
                     ->compare_exchange_strong(expected, largest) &&
                expected != largest) {
              // we know that there is another loop...
              mutex.lock();
              problematic.push_back(largest);
              mutex.unlock();
            }
          }
        
6DB6
});

    std::vector<bool> halfedgeProcessed(halfedge_.size(), false);
    for (size_t i : problematic) {
      if (halfedgeProcessed[i]) continue;
      vertPos_.push_back(vertPos_[halfedge_[i].startVert]);
      const int vert = NumVert() - 1;
      ForVert(i, [this, vert, &halfedgeProcessed](int current) {
        halfedgeProcessed[current] = true;
        halfedge_[current].startVert = vert;
        halfedge_[halfedge_[current].pairedHalfedge].endVert = vert;
      });
    }
  } else
#endif
  {
    std::vector<bool> vertProcessed(NumVert(), false);
    std::vector<bool> halfedgeProcessed(halfedge_.size(), false);
    for (size_t i = 0; i < halfedge_.size(); ++i) {
      if (halfedgeProcessed[i]) continue;
      int vert = halfedge_[i].startVert;
      if (vertProcessed[vert]) {
        vertPos_.push_back(vertPos_[vert]);
        vert = NumVert() - 1;
      } else {
        vertProcessed[vert] = true;
      }
      ForVert(i, [this, &halfedgeProcessed, vert](int current) {
        halfedgeProcessed[current] = true;
        halfedge_[current].startVert = vert;
        halfedge_[halfedge_[current].pairedHalfedge].endVert = vert;
      });
    }
  }
}

@pca006132
Copy link
Collaborator

And thinking about it, edge deduplication shouldn't require sorting all the entries. We are guaranteed that there are no pinched vertices, so we can just check locally around the starting vertex to search for edge deduplication. That should allow us to eliminate the expensive sorting operation for rare edge duplication cases.

@elalish
Copy link
Owner Author
elalish commented Feb 19, 2025

I'm impressed with your pinched vert parallelization - I looked at that and found I couldn't figure out how to parallelize it. Care to open a PR with that? And the deduplication stuff too?

@pca006132
Copy link
Collaborator

I'm impressed with your pinched vert parallelization - I looked at that and found I couldn't figure out how to parallelize it. Care to open a PR with that? And the deduplication stuff too?

Done.

And I think my patch for SwappableEdge (above the parallelization stuff) is working. We can go from there.

@pca006132
Copy link
Collaborator

Hmm, weird:

[ RUN      ] BooleanComplex.CraycloudBool
/home/pca006132/code/manifold/test/boolean_complex_test.cpp:915: Failure
Value of: res.IsEmpty()
  Actual: false
Expected: true

With my patch, this one is failing. Probably because I forgot to turn on MANIFOLD_EXPORT when testing yesterday.

@elalish elalish force-pushed the onlyCollapseNewEdges branch from 87e6ad9 to cec28f7 Compare February 19, 2025 09:06
@elalish
Copy link
Owner Author
elalish commented Feb 19, 2025

I tweaked that test slightly and it passes - I think the behavior is expected. Thanks for the patch!

@elalish elalish merged commit c16b521 into master Feb 19, 2025
27 checks passed
@elalish elalish deleted the onlyCollapseNewEdges branch February 19, 2025 09:47
@elalish elalish mentioned this pull request May 14, 2025
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.

better handling of tolerance for simplification
2 participants
0