Commit Graph

870 Commits

Author SHA1 Message Date
Arseny Kapoulkine 8dd018ac65 Update version to 0.22 everywhere 2024-10-23 16:36:37 -07:00
Arseny Kapoulkine e10e83b46b Mark meshopt_decodeFilter* and meshopt_encodeFilter* as stable
These functions have existed for several years now, with stable
implementation and interface. They are exposed via JS interface and used
by gltfpack, and thus are unlikely to change dramatically.

A recent change added meshopt_EncodeExpClamped, which also had a
possible alternative of adding more fine-grained exponent clamping
control to meshopt_encodeFilterExp. Instead of marking the entire
function as experimental, let's mark just the enum entry as
experimental. If more control is necessary in the future, we can remove
this enum entry and add a meshopt_encodeFilterExpClamped instead.
2024-10-23 16:36:37 -07:00
Arseny Kapoulkine 0e7cfa545c clusterizer: Prioritize triangles with valence 2 edges less
Our initial heuristic adjustment is a little too aggressive wrt global
flow: it is very happy to leave the last vertex of a meshlet unused
(when a triangle with extra=2 and a valence-2 edge exists it will always
be selected over a triangle with extra=1), and that increases the
not-full meshlet count more than optimal.

Using 1+extra instead fixes this by using priority 3 for these triangles
so that they can be weighted against extra=1 triangles. This is a
compromise, as fully separating the priorities results in worse global
outcome.
2024-10-22 15:56:57 -07:00
Arseny Kapoulkine 6f01a2cb97 clusterizer: Prioritize triangles with valence 2 edges
When a triangle has two vertices with valence 2, it's typically
advantageous to prioritize it above others even if that may require an
extra vertex; after removing this triangle, we'll have between one and
two isolated triangles with valence=1 vertex which we'll pick up
immediately.

Avoiding this triangle risks leaving the small triangle cluster alone
and going in a different direction only to collect it later.

This significantly reduces the number of disconnected clusters due to a
better global flow; note that it's important to check for the edge, not
just a single valence=2 vertex, as that results in a worse local
decision that doesn't sufficiently improve global traversal.
2024-10-22 08:12:15 -07:00
Arseny Kapoulkine f0b98e359f clusterizer: Rename extra to priority
To avoid future confusion, we now use extra only to mean "extra vertex
count" and the adjustments are done to priority.
2024-10-22 08:21:59 -07:00
Arseny Kapoulkine 57532e2a45 clusterizer: Fix rescoring to only affect new meshlets
When getNeighborTriangle's best selection can't fit into the current
meshlet, we redo the search process using an adjusted criteria that
ignores position/normal as that is irrelevant. However, internally
getNeighborTriangle adjusted best_extra to act as a topology score
instead of just the extra count. This would lead to the last triangle of
the meshlet being reselected in addition to the first one in some cases.

Also change _extra variables to int as these are small.

Note: by itself, this change actually slightly reduces the
clusterization quality due to non-local effects of the rescoring.
2024-10-21 19:18:03 -07:00
Arseny Kapoulkine f6b26e5deb vertexfilter: Equalize encodeExp behavior for positive and negative zero
When we switched from frexp to optlog2, the behavior for negative zero
changed: instead of being encoded as if it had exponent 0, it was
mistakenly encoded as if it had exponent min_exp.

Both encodings are equivalent; in fact, there is some leeway in encoding
zeroes that we are not exploiting as they could repeat the last non-zero
exponent - but this is an unexpected change and should be corrected for
consistency.
2024-10-17 17:02:15 -07:00
Arseny Kapoulkine 549b388254 vertexfilter: Add meshopt_EncodeExpClamped mode for exponent encoding
When using SharedComponent or SharedVector, exponents compress fairly
well on their own because their range is normalized in a more or less
optimal way; when using Separate mode, the reduced bit count truncates
mantissa which fixes some issues with input entropy, but if the values
are clustered around zero then the exponent will also see a lot of
variance that is generally unwarranted for precision, when the input
range is known to be further away from zero.

While there are ways to solve this in a more general fashion, eg by
exposing min_exp as well as mode, this is probably too involved and it's
simple and mostly sufficient to add a clamped mode.

This is helpful when encoding values like texture coordinates, where
SharedVector or SharedComponent may not have enough precision in case
where a component is tiled with a high repeat value, but we know we need
a limited precision around 0 so clamping the exponent works well.

This API is still experimental so it's easy to rework it later if we
discover a more general solution is warranted.
2024-10-17 14:54:09 -07:00
Arseny Kapoulkine f68e09b0af Improve .clang-format setup
This change fixes a few idiosyncrasies in clang-format integration:

- extern/ code should not be formatted
- All other code, including meshoptimizer.h/gltfpack.h, should be
- Fix indentation issues because of extern "C" block
- Work around clang-format bug wrt calling convention qualifiers
2024-10-15 15:00:51 -07:00
Arseny Kapoulkine 8e2f61396d simplify: Silence clang-tidy diagnostics
Division by 3 here is exact so it is safe to do this despite the fact
that the result is cast to float.
2024-10-15 08:28:41 -07:00
Arseny Kapoulkine cec7d028ba simplify: Move seam pair computation into collapse handling
The only reason why seam pair was computed separately was to reuse that
computation between collapse and quadric updates as it is fragile; now
that we have split quadric updates completely, sx is only used for
collapse update and it's thus better and cheaper to process inline.
2024-10-10 13:58:20 -07:00
Arseny Kapoulkine 30b4bc8889 simplify: Move vertex_error update to updateQuadrics
This way performEdgeCollapses no longer needs access to quadrics data,
and when attributes are used and we need to compute distance error, the
access to vertex quadrics is maximally local as we use them right after
aggregation, which saves 1-2% on large meshes.

For now we keep the optimization of not recomputing the distance error
unless attributes are used; it makes the data flow slightly awkward and
seems to have <1% performance impact but we can remove it later if it
becomes more problematic.
2024-10-10 13:41:04 -07:00
Arseny Kapoulkine 92e5a184bd simplify: Split quadrics update into a separate function
Instead of updating quadrics as we are collapsing edges, we can update
them in batch after all collapses are done. This makes the code cleaner
since we do not need to replicate the collapse structure explicitly for
both remap and quadric update, and also makes it ~5% faster on many
large meshes - presumably due to better data locality during the update.

The only subtle part here is vertex_quadrics update: we need to do this
just once, and we *know* the primary vertex will have to move if any
wedge moved, so we do that during the corresponding collapse. The
target vertex may not have had any collapses to the primary vertex in
case some vertex gets collapsed into a locked vertex.

This change results in exactly identical results; in the future, this
would result in a more careful handling of Complex collapses as our note
about the attribute quadrics not requiring a merge was incorrect - even
though the attributes might be similar, merging quadrics is still
valuable to get area accumulation.
2024-10-10 13:13:34 -07:00
Arseny Kapoulkine 376f7288ea simplify: Use error returned by vertex quadrics to guide pruning
When attributes are incorporated into the metric, depending on the
attribute weights we may see divergence between the error as returned by
vertex quadrics alone and the result. Especially when the simplification
does not have a set limit, this may lead to unexpectedly aggressive
component pruning.

While ideally pruning should only be used with a reasonable error limit
that negates these concerns, for now it is probably better to accomodate
this use case by tracking the positional error separately.

To reduce the cost of tracking this error we recompute it as collapses
are performed; this may seem counter-intuitive since we've already
computed it during ranking, but conserving memory is more important
here, and since we only need to do it when using attributes and only for
collapses we end up taking, the performance cost ends up being
negligible.
2024-10-08 18:27:20 -07:00
Arseny Kapoulkine d8d17345c4 Merge pull request #780 from zeux/vcodec-trace
vertexcodec: Reintroduce tracing and add roundtrip fuzzing
2024-10-04 10:46:51 -07:00
Arseny Kapoulkine 8507d4d354 vertexcodec: Fix tabular alignment for longer files
Tabs only give us a limited amount of alignment if some byte streams are
much longer than others; instead, explicitly specify the width for all
numbers.
2024-10-04 10:07:28 -07:00
Arseny Kapoulkine f44a682a78 vertexcodec: Reduce the amount of printed traces
bitg[0] is always 0 because we accumulate only sizes, and we store zero
data bits for bit0... we might need to print count percentages in the
future instead but this is probably ok for now.

Also reduce the space wasted by extra whitespace.
2024-10-03 14:10:22 -07:00
Arseny Kapoulkine f6ea4692ab vertexcodec: Add tracing for bit consistency
Within each byte group, we count the number of consistent bits: bits
that, in that byte group, all match the corresponding bit from the last
vertex. Consistent bits amplify compression rates when they are properly
aligned.

Note that this is not exactly the same as having small deltas, eg 0xff
vs 0x00 is still consistent from the delta perspective in most high
bits, but it would be the same if we used xor deltas (which may be an
option worth having).

This is also *not* the same as having bits consistent between
consecutive bytes: in a very random sequence, about half of the bits in
each position usually match from one value to the next, but that's a
very low consistency as it's hard to take advantage of.
2024-10-03 14:10:22 -07:00
Arseny Kapoulkine 88e9e5bedc vertexcodec: Clean up and improve trace reporting
For now we just need a single trace level that incorporates both the
macro stats (per byte) and the micro stats (within the individual byte).

Instead of separately collecting occurrences/sizes for bit groups we
just record the size and print all numbers as percentages within the
given group.
2024-10-03 12:15:52 -07:00
Arseny Kapoulkine 5e5288f9d4 vertexcodec: Reintroduce previously removed tracing
This is a partial revert of a10c311fb2
which removed tracing support from both codecs; to make further progress
on vertex codec v1 we need some amount of tracing back, and while the
tracing we used to have isn't sufficient it's a good start.
2024-10-03 10:59:37 -07:00
Arseny Kapoulkine 737c2697e0 simplify: Rework vertex_lock implementation for better wedge behavior
Previously, we would only read vertex_lock data for the primary vertex
(the vertex with the smallest index among those with the same position).
When using sparse mode, this would be the first vertex referenced by the
index buffer due to how the sparse remap works.

If the input locks were inconsistent between all copies, there could be
a situation where a vertex with the wrong index was asked to be locked,
and the vertex would still be not-locked and could move. When *not*
using sparse mode, this was particularly problematic as the flags would
be read from the vertex that might not be referenced by the index buffer
at all.

We now propagate the locked status into the primary vertex in a separate
pass; this requires re-locking the wedges in a separate pass, but all of
this is very quick especially when using sparse mode.
2024-10-02 11:08:06 -07:00
Arseny Kapoulkine 7bd79e6583 simplify: Add a note about locked target vertices
The collapse scheduling mechanics actually permit collapsing more than
one vertex to the same target. We normally don't do this because that
may lead to collapses v0->v1 even if v1->v0 has better error, since we
haven't had a chance to recompute the error.

However, if the target vertex can not move (if it's kind is locked)
anyway, this constraint does not apply: we dutifully update quadrics of
locked vertices but these updates aren't used for any evaluation. This
could allow us to collapse more vertices in a single pass if the target
is locked.

Trying this on a set of test meshes, however, a few percentage of meshes
exhibit somewhat higher resulting error, and this error is also
apparent visually. This is probably due to an unfair advantage that
collapses to locked get; even though the error is technically correct,
we may go further in the original sorted collapse list and select edges
that we would not need to collapse as edges with smaller error become
available.

So this change is a no-op but this discovery and testing results seem
important to document for the future.
2024-09-28 22:28:52 -07:00
Arseny Kapoulkine 87c107f399 simplify: Minor tracing cleanup
- Edge eval is very verbose because it gets printed for every edge, even
  the ones that we do not collapse, so this bumps it to the next tracing
  level;
- Component measure should be using TRACE >= for consistency.
2024-09-27 15:21:34 -07:00
Arseny Kapoulkine 5c357dde18 vertexcodec: Add a note about quantization and locality of reference
For optimum compression, the vertex buffer should carry quantized values
and have consecutive vertices be reasonably local which can be achieved
with regular vertex cache/fetch optimization for most meshes.

An alternative to quantization is filtering but since it is unlikely to
be as broadly used it's probably enough to just call out quantization
here.
2024-09-27 13:09:39 -07:00
Arseny Kapoulkine b0bbadfa56 simplify: Remove a seemingly redundant call to pruneComponents
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.
2024-09-25 13:58:03 -07:00
Arseny Kapoulkine d6b24c50a3 simplify: Slightly improve tracing for pruning
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.
2024-09-25 13:24:51 -07:00
Arseny Kapoulkine 122e6e9d16 simplify: Keep track of the next component 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).
2024-09-25 13:07:25 -07:00
Arseny Kapoulkine 82250c66f7 simplify: Iteratively prune components until error limit
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.
2024-09-25 11:42:30 -07:00
Arseny Kapoulkine 954e88ca00 simplify: Switch to approximate spherical error for components
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.
2024-09-24 12:48:56 -07:00
Arseny Kapoulkine a59426d115 simplify: Fix call to buildComponents in sparse mode
In sparse mode, indices becomes invalid after a remap (prior to
component build), so the components computed would be invalid.
2024-09-23 18:06:25 -07:00
Arseny Kapoulkine ad7bc72f4c simplify: Implement component pruning
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.
2024-09-23 16:08:04 -07:00
Arseny Kapoulkine 04566590ce simplify: Compute collapse error for each component
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.
2024-09-23 15:03:47 -07:00
Arseny Kapoulkine 5173299de0 simplify: Fix MSVC warning for size_t->unsigned cast 2024-09-24 22:48:13 -07:00
Arseny Kapoulkine 42bb4613e2 simplify: Minor refactoring of connected components
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.
2024-09-23 14:25:59 -07:00
Arseny Kapoulkine eab402ea07 simplify: Initial implementation of connected components
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.
2024-09-23 14:12:33 -07:00
Arseny Kapoulkine 61a98ad7b6 simplify: Add initial scaffolding for component pruning
meshopt_SimplifyPrune will be used to remove individual components.
Since this is experimental, the JS version is using _ prefix for now.
2024-09-23 12:26:02 -07:00
Arseny Kapoulkine 2b0d76401c simplify: Refactor seam pair computation and validation
Since we need seam pair for gradient update as well as collapse remap,
including a temporary safety measure for invalid loop data, we now share
this code between the two blocks instead of duplicating parts of it.

This is not performance sensitive but will help if we ever need to
change anything here.
2024-09-19 19:37:37 -07:00
Arseny Kapoulkine 970978fa19 simplify: Fix comment to account for updated table
We can now collapse border and seam vertices onto locked as long as the
collapse follows the border/seam loop.
2024-09-19 14:49:29 -07:00
Arseny Kapoulkine a9540dee8d simplify: Add memory safety guards for seam pair
While the new seam handling code seems correct and robust based on
testing and fuzzing, it's theoretically possible that loop lookup
returns ~0u. Since this is used for indexing various vertex arrays it
would be a memory safety issue if it ever happened. Builds with enabled
assertions will abort, but if assertions are compiled out this may go
unnoticed.

For now, handle the ~0u case that should never happen by collapsing to
the next wedge; this is not correct for locked vertices but should not
create more issues downstream. Eventually this code could be removed.
2024-09-19 14:29:02 -07:00
Arseny Kapoulkine 25fe878ae4 simplify: Enable seam->locked collapses
Similarly to border->locked collapses, enabling seam->locked collapses
is important to reduce topology based restrictions as that allows us to
reach lower triangle counts or same triangle counts at lower error for
meshes with islands of irreducible topology.

A seam-seam edge is bidirectional whereas a seam-locked edge is
unidirectional and may be connected to any wedge out of an arbitrary
count in the locked vertex. Because of this we can't use wedge[] to find
the seam pair anymore, and need to switch to using loop/loopback based
on whether the edge is forward or backward wrt original topology.

This is somewhat risky as it requires loop/loopback to stay valid and
coherent throughout the entire simplification process, whereas
previously bugs here would simply block certain collapses; now it's
theoretically possible to get an out of bounds read if the loop data for
the other wedge points nowhere. However, extensive testing and fuzzing
did not uncover any further issues with this.
2024-09-19 13:33:57 -07:00
Arseny Kapoulkine f9fb515a06 simplify: Do not classify degenerate seams as seams
Sometimes edges that connect two wedges may be classified as seams,
which invalidates assumptions about coherent topology around seam
vertices and prevents future improvements to seam collapses. These are
incredibly rare and should not be classified as seams.
2024-09-19 13:33:57 -07:00
Arseny Kapoulkine b3ddf87c3f simplify: Enable border->locked collapses
While locked vertices can't move by themselves, ideally we should be
able to move any vertex into a locked vertex; currently this is allowed
for manifold but not allowed for border/seam. This artificially limits
the allowed collapses which can lead to larger than necessary result or
worse than necessary error as the simplifier is forced to consider edges
with worse error.

To enable border collapses, we simply need to extend the picking logic;
border-border edges are symmetric so it is enough to validate them in
one direction, but border-locked edges may be forward or backward (as
in, they may align with the topological edge direction or be opposite to
it), so we need to check loop or loopback depending on which vertex is
border.

The same logic would work for seam collapses but they require changes
elsewhere in the pipeline to be permissible.
2024-09-19 13:33:57 -07:00
Arseny Kapoulkine da800cf309 simplify: Fix edge loop remap creating invalid loops in some cases
When the seam edge is collapsed in the opposite direction to the loop
flow, we need to follow the previous link to discover the new
connection. However, if that link pointed to a remapped vertex, we
previously would create an invalid loop edge. This would result in
inconsistent loop metadata between loop & loopback, and could prevent
future seam collapses around the problematic area.

The correct thing to do is to remap the connection, unless the loop
stopped after that vertex in which case our vertex becomes terminal.
2024-09-19 13:33:52 -07:00
Arseny Kapoulkine 0d45d58b1c simplify: Improve precision of collapse error sorting
We use a subset of bits of collapse error to produce an approximate
ordering of collapses; to keep stack usage and cache utilization
reasonable, we used 11-bit counting sort which uses 8 bits of exponent
and 3 bits of mantissa.

3 bits of mantissa may not be enough and can result in choosing
suboptimal collapse order. Ideally we should probably use 5 bits here,
but that needs much more stack space.

For now, switch to 8+4 bits but to avoid doubling the stack usage,
constrain the exponent range on the high end so that excessively high
errors (>2^32) are bucketed together, as they are not useful.

In the future there's an opportunity to similarly constrain exponent
space on low end by clamping errors that are too low to zero.

In addition to improving the error selection in some cases this also can
sometimes reduce the amount of simplification passes as eligible
collapses in error limited regime get sorted better and can be processed
earlier.
2024-09-17 12:48:39 -07:00
Arseny Kapoulkine 3759ab2787 simplify: Avoid early termination of passes due to error_goal
In large meshes with complex topology we occasionally hit a case where
error_goal cutoff is not monotonic: a previous pass would simplify up to
a certain goal, but a subsequent pass would arrive at a smaller goal
(for example as invalid collapses get eliminated) and hit it before an
error that it already encountered.

If we already collapsed an edge with a given error it is always correct
to collapse other edges with a smaller error. This change tweaks the
conditional, which results in fewer passes on some meshes (eg
thai_buddha gets ~10% faster to simplify), 2-3% smaller error on some
meshes, and occasionally 1-2% larger error; most meshes simplify as they
did before however.
2024-09-16 18:15:06 -07:00
Arseny Kapoulkine f93f40cef3 simplify: Improve pass collapse tracing code
We now record the reason for each pass termination and adjust edge
collapse goal logic to match the code we are actually using for
rejection; previously, goal in the log was used as an "ideal" error
assuming no collapses are locked in progress but that is not realistic.

We now display the actual goal used or, if the collapse list is
exhausted, the error limit, which should be more clear.
2024-09-16 17:43:37 -07:00
Ono-Sendai fecfc00059 Fix spelling mistake. 2024-09-09 22:07:52 +12:00
Arseny Kapoulkine 9957ccce36 simplify: Update header documentation to account for weight changes
For meshopt_simplifyWithAttributes, the weights depend on the attribute
type so it is probably best to leave the guidance to the documentation.

For meshopt_simplifyPoints, we can just suggest 1.0.
2024-09-02 19:48:31 -07:00
Arseny Kapoulkine 1964ad6986 simplify: Adjust meshopt_simplifyPoints color errors to be more uniform
Since the error for points is pos + color_weight * col, it is hard to
tweak color weight to be scale-independent. We had a similar issue in
the edge collapse metric, but here the issue is complicated by the fact
that it's incorrect to compute errors as pos * (1 + col), since multiple
points can have the same position but the color delta must be used for
picking the right replacement.

As a compromise, we scale the color weight by the grid scale; since the
maximum position error along any axis is the grid scale, this makes the
errors compatible, and makes it easier to tweak the weight: similarly to
edge collapse, a weight of 1 is a good default; at a weight of 1 a
distance of 1 in color is equivalent to a worst case positional delta,
so a point like this will only be selected if there is no closer option.
2024-09-02 19:11:21 -07:00
Arseny Kapoulkine 5a96ab4a94 Merge pull request #755 from zeux/provoke
Implement provoking vertex generation for visibility buffers
2024-08-30 17:58:25 -07:00