This code never worked if an actual array was passed in as it required
an element per float instead of an element per vertex.
Since we need to change this anyway, change the interface to accept
Uint8Array instead of a boolean array. This is more consistent with
other parameters and allows an upgrade path if the C interface changes
from "lock flags" to just "flags".
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.
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.
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.
This might be a little misleading in the future, but uniform meshlets
can almost never reach full vertex utilization as the triangle count
should be reached first on regular meshes. So for now it would be more
useful to count meshlets that don't exhaust the triangle limit even
though this makes the metric differ between two types.
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.
It's difficult to work with meshlet demo right now due to the amount of
output; when multiple meshes are specified and multiple algorithms are
tested it becomes distracting.
Additionally, we only test 64/126 configuration right now, which is a
different mode from 64/64: 64/126 almost always terminates meshlets by
running out of vertices, whereas 64/64 almost always runs out of
triangles first. This means some algoritmic changes need to ideally be
tested for both.
Since 8-bit cutoff as well as the alternative culling formula have been
extensively tested, we no longer compute those and consolidate the cone
culling stats into the Bounds row.
This is a similar efficiency measure to strip index count and can be
used to compare the results to other published algorithms. For
simplicity, we only measure the strip length when restart indices are
used; the results should not depend on whether that's the case (compared
to strip index percentage which does).
Document ErrorAbsolute and Sparse options for simplification. Sparse is
probably less useful in JS as the cost of copying the vertex data is
paid in full but would still be useful to reduce memory consumption and
improve performance; Prune is only available as an experimental option
and we currently don't document those.
We previously used Clamped in -c mode for normal deltas; but normal
deltas may be fairly small. Normally preserving these requires more bits
but if the user already opted into floating point normals it would be
more reasonable to use SharedComponent to dynamically adjust to the
delta range. This also fixes the odd corner case where the deltas may be
erased at -c but kept at -cc.
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.
Unlike previous changes, this is not *exactly* equivalent in terms of
binary output. It should be the same when using `-c`, but when using
`-cc` there may be cases where the shared component encoding selects an
exponent that is smaller than 0 for normals or texture coordinates. This
should not negatively affect compression ratio though, just produce
slightly different files.
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.
meshopt doesn't provide an encode function for snorm encoding as it's a
trivial extension of scalar encoding, but here we need this in a few
places so our own function simplifies the data flow.
This is a useful mode of operation since exp encoding shares source &
destination stride, but it's valuable to make sure it works as
implementation changes may accidentally break it if they read data after
writing destination.
This simplifies the code and keeps the output identical since the
encoding logic is the same.
Note that since the stride here is 12 bytes, we can't encode directly
from the source attribute data (we need to skip W), but we can encode
using aliased destination & source.
This almost never comes up in practice, but occasionally scenes might
have the same geometry attached to the same nodes. This results in
counterintuitive results because we create node lists with duplicate
nodes for this, which never happened before, and end up producing more
quantization sub-nodes than necessary.
For now we can just filter these attachments out to avoid redundancy.
Neither std::map nor std::unordered_map are particularly good
containers, but since we're now using unordered_map for deduplication
filtering we might as well use it for the primitive cache. This requires
a custom hash function for pair<uint64, uint64>, but our hash halves are
sufficiently decorellated that a xor suffices.