Skip to content
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

simplify: Implement experimental support for component pruning #773

Merged
merged 21 commits into from
Sep 26, 2024
Merged

Conversation

zeux
Copy link
Owner

@zeux zeux commented Sep 25, 2024

On complex meshes with varied topology, we can get small islands of triangles with irreducible topology due to locked vertices. These consume the overall triangle budget and force the simplifier to collapse other, larger, features instead, which can result in meshes not reaching the target triangle counts or reaching them at a higher error.

This change introduces a new experimental flag, meshopt_SimplifyPrune, which adds component pruning to simplification process. Isolated components are identified during classification and can be removed after any pass as long as that does not increase the overall simplification error. For now this analysis disregards external requests to lock vertices, such as vertex_lock or meshopt_SimplifyLockBorders; these usually are not really necessary simultaneously, as external locks are useful for subset simplification whereas pruning is intended more for full-mesh pipelines, but this might change in the future.

Each component may get simplified in isolation before being pruned; this is desirable since that allows components with partially reasonable topology, such as chain links, to be simplified first with minimal quality impact, and only pruned if the rest of the mesh suffers significant deformation.

Currently, pruning simplifies towards a target error cutoff (established automatically during each simplification pass) without considering the target index count. Because of this, sometimes requesting a specific target count with pruning enabled may return a smaller count instead; this is possible even without pruning (as sometimes an edge collapse removes more triangles than planned), and impossible to fully avoid with pruning (as a small but dense component needs to be removed wholesale); still, in the future we could consider doing this one component at a time in error order, instead of in batches.

The performance impact of component pruning is minimal: both preparing the initial data and subsequent scans are fairly quick, with an overall impact of a couple percent.

Based on some limited testing this is probably safe to enable by default, but because of the interaction with external locks and the fact that this is a fairly new mode so not all impact is known, for now this is behind a separate flag (since it is experimental we could remove it entirely if the behavior is considered unambiguously good).

This contribution is sponsored by Valve.

Fixes #764.

meshopt_SimplifyPrune will be used to remove individual components.
Since this is experimental, the JS version is using _ prefix for now.
For pruning, we need to identify small isolated components that can be
removed entirely. This requires computing connected components of the
mesh graph.

There are two ways to look at connectivity, either triangles connected
by edges or vertices connected by edges. Both types of analysis are
possible, but for now we use vertex analysis. This is easier to compute,
and would not require continuous updates as the triangles get collapsed
and recreated during edge simplification.

For computing components, we could use union-find or a graph traversal.
For now we settle on union-find: it's usually measurably faster on
complex meshes, and does not require extra memory for a vertex queue. To
compute the components, we first do a classical union-find pass over all
edges, and then renumber the roots (and other vertices) sequentially,
thus mapping every single vertex into a component id.
Add a few comments to make the code easier to read, and simplify the
component update.

Because the last pass renumbers the components which changes the meaning
of the values from parent links to component ids, we *need* an
intermediate pass to finalize component values; make that clear in
comments.
When pruning is enabled, we perform a second pass filtering of the index
buffer using the current reached error limit; any triangles that are
part of components with a smaller error get removed.

Note that this is done for all components regardless of whether or not
some of them are unnecessary to remove to reach the resulting count (but
only done if we haven't reached the count yet). This means we do not
need to sort the components by error but may result in bursts of triangle
counts depending on how the edge collapses end up working out.
In sparse mode, indices becomes invalid after a remap (prior to
component build), so the components computed would be invalid.
Instead of computing an AABB and using half diagonal as an error,
compute an approximate sphere bounds and use radius as an error.

This makes the error rotationally invariant (modulo inaccuracies of the
computation), and produces a more reasonable pruning order in practice
on a few test meshes, although both measurement methods have their own
issues.
This allows us to test on a wider mesh set with and without pruning.
This is not part of public API but it would be helpful to validate that
this code actually works. Also reformat the basic simplify() test in a
way that clang-format won't unformat back.
Here we have one degenerate triangle that has all three vertices locked
(and is not topologically degenerate); this triangle can not be
collapsed normally, however pruning collapses it as it has zero error,
just as the other collapses here.

We also check that the resulting small mesh can be processed with
pruning enabled without triggering any assertions.
This mostly tests connected components code for correctness on odd
topology and sparse inputs.
Instead of inventing a new way to safeguard experimental features like
Prune in the simplifier, we can use the already-existing method of
requiring useExperimentalFeatures enablement. This is cleaner as we can
advertise this flag via type definitions.
After computing component ids, we can compute a bounding box for each
component and derive an error value from it that will be used for
pruning.

For now we allocate an entire bounding box for each component for
performance, but we might use some other way of computing these in the
future that is less memory intensive; usually, component sizes are
fairly large so this is an acceptable use of memory.

It's not clear yet if AABB diagonal is a reasonable approximation of
component error; this is pending future testing. Note that we square
the value for consistency with quadric errors.
This should probably be unambiguously a good idea, especially given the
default low error target. If this ends up being a bad idea, we can
revert this or expose a separate option; for now, -slb will disable this
because pruning doesn't respect locked borders.
In addition to pruning after each pass, where we are limiting the error
by the current worst-case error reached, we now also prune components at
the end of simplification with a series of passes of increasing error.

This helps on meshes where available edge collapses are very low error
so the total error must increase to prune components; without this
meshes would be stuck at a higher triangle count. This also improves the
behavior with varying target_error: if some edge collapses have error
1.1e-2, and some components have an error 0.9e-2, then without this we
might see components pruned with target error 1.2e-2 but not with 1e-2
because earlier edges might have lower error.
Instead of only computing the next component error at the end of the
collapse loop, we keep track of it as we prune components. This is
fairly cheap, as usually the number of components is much smaller than
the number of indices at any point in time, and this helps us avoid
calling pruneComponents excessively: after this change, while the
behavior of simplification stays the same, only ~1% of calls to
pruneComponents end up removing no triangles (which happens when the
component was entirely removed through edge collapses).
Since we rarely have no-op pruning now, it should be fine to always
print pruning stats; this also makes it easier for us to print single
line traces for the cleanup passes.
This was originally added so that we have a pruneComponents call at the
end of any simplification process. However, since pruneComponents will
only do work if the error has increased since last time, and early-outs
from the edge collapse loop are only possible if the error has not
updated, even before cleanup passes this should have been redundant with
a rare exception of pruning zero-error components in a fully locked mesh.

With the cleanup passes this is entirely redundant now, and is never hit
on any real test meshes.
This adds pruning support as well as aggregated improvements from prior
changes.
Cleanup passes are comparatively rare as they require there to be very
few high error edge collapses so that the intra-pass pruning doesn't get
anywhere on its own. Here we construct a 3-component mesh where all
vertices are locked and verify that two of the components get pruned.
Add meshopt_SimplifyPrune option documentation.
@zeux zeux marked this pull request as ready for review September 25, 2024 21:16
@zeux zeux merged commit 58a0290 into master Sep 26, 2024
12 checks passed
@zeux zeux deleted the simp-prune branch September 26, 2024 16:39
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.

Simplification should prune topologically separate components
1 participant