-
Notifications
You must be signed in to change notification settings - Fork 481
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
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 asvertex_lock
ormeshopt_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.