Merge pull request #773 from zeux/simp-prune

simplify: Implement experimental support for component pruning
This commit is contained in:
Arseny Kapoulkine
2024-09-26 09:39:13 -07:00
committed by GitHub
11 changed files with 359 additions and 41 deletions
+1
View File
@@ -322,6 +322,7 @@ For basic customization, a number of options can be passed via `options` bitmask
- `meshopt_SimplifyLockBorder` restricts the simplifier from collapsing edges that are on the border of the mesh. This can be useful for simplifying mesh subsets independently, so that the LODs can be combined without introducing cracks.
- `meshopt_SimplifyErrorAbsolute` changes the error metric from relative to absolute both for the input error limit as well as for the resulting error. This can be used instead of `meshopt_simplifyScale`.
- `meshopt_SimplifySparse` improves simplification performance assuming input indices are a sparse subset of the mesh. This can be useful when simplifying small mesh subsets independently, and is intended to be used for meshlet simplification. For consistency, it is recommended to use absolute errors when sparse simplification is desired, as this flag changes the meaning of the relative errors.
- `meshopt_SimplifyPrune` allows the simplifier to remove isolated components regardless of the topological restrictions inside the component. This is generally recommended for full-mesh simplification as it can improve quality and reduce triangle count; note that with this option, triangles connected to locked vertices may be removed as part of their component.
While `meshopt_simplify` is aware of attribute discontinuities by default (and infers them through the supplied index buffer) and tries to preserve them, it can be useful to provide information about attribute values. This allows the simplifier to take attribute error into account which can improve shading (by using vertex normals), texture deformation (by using texture coordinates), and may be necessary to preserve vertex colors when textures are not used in the first place. This can be done by using a variant of the simplification function that takes attribute values and weight factors, `meshopt_simplifyWithAttributes`:
+6 -5
View File
@@ -451,7 +451,7 @@ void packMesh(std::vector<PackedVertexOct>& pv, const std::vector<Vertex>& verti
}
}
void simplify(const Mesh& mesh, float threshold = 0.2f)
void simplify(const Mesh& mesh, float threshold = 0.2f, unsigned int options = 0)
{
Mesh lod;
@@ -462,7 +462,7 @@ void simplify(const Mesh& mesh, float threshold = 0.2f)
float result_error = 0;
lod.indices.resize(mesh.indices.size()); // note: simplify needs space for index_count elements in the destination array, not target_index_count
lod.indices.resize(meshopt_simplify(&lod.indices[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), target_index_count, target_error, 0, &result_error));
lod.indices.resize(meshopt_simplify(&lod.indices[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), target_index_count, target_error, options, &result_error));
lod.vertices.resize(lod.indices.size() < mesh.vertices.size() ? lod.indices.size() : mesh.vertices.size()); // note: this is just to reduce the cost of resize()
lod.vertices.resize(meshopt_optimizeVertexFetch(&lod.vertices[0], &lod.indices[0], lod.indices.size(), &mesh.vertices[0], mesh.vertices.size(), sizeof(Vertex)));
@@ -476,7 +476,7 @@ void simplify(const Mesh& mesh, float threshold = 0.2f)
(end - start) * 1000);
}
void simplifyAttr(const Mesh& mesh, float threshold = 0.2f)
void simplifyAttr(const Mesh& mesh, float threshold = 0.2f, unsigned int options = 0)
{
Mesh lod;
@@ -490,7 +490,7 @@ void simplifyAttr(const Mesh& mesh, float threshold = 0.2f)
const float attr_weights[3] = {nrm_weight, nrm_weight, nrm_weight};
lod.indices.resize(mesh.indices.size()); // note: simplify needs space for index_count elements in the destination array, not target_index_count
lod.indices.resize(meshopt_simplifyWithAttributes(&lod.indices[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), &mesh.vertices[0].nx, sizeof(Vertex), attr_weights, 3, NULL, target_index_count, target_error, 0, &result_error));
lod.indices.resize(meshopt_simplifyWithAttributes(&lod.indices[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), &mesh.vertices[0].nx, sizeof(Vertex), attr_weights, 3, NULL, target_index_count, target_error, options, &result_error));
lod.vertices.resize(lod.indices.size() < mesh.vertices.size() ? lod.indices.size() : mesh.vertices.size()); // note: this is just to reduce the cost of resize()
lod.vertices.resize(meshopt_optimizeVertexFetch(&lod.vertices[0], &lod.indices[0], lod.indices.size(), &mesh.vertices[0], mesh.vertices.size(), sizeof(Vertex)));
@@ -1372,6 +1372,7 @@ void process(const char* path)
encodeVertex<PackedVertexOct>(copy, "O");
simplify(mesh);
simplify(mesh, 0.1f, meshopt_SimplifyPrune);
simplifyAttr(mesh);
simplifySloppy(mesh);
simplifyComplete(mesh);
@@ -1391,7 +1392,7 @@ void processDev(const char* path)
if (!loadMesh(mesh, path))
return;
simplifyAttr(mesh);
simplifyAttr(mesh, 0.1f, meshopt_SimplifyPrune);
}
void processNanite(const char* path)
+5
View File
@@ -67,6 +67,7 @@
ratio: 1.0,
debugOverlay: false,
lockBorder: false,
prune: false,
weldVertices: false,
useAttributes: false,
errorThresholdLog10: 1,
@@ -113,6 +114,7 @@
var guiSimplify = gui.addFolder('Simplify');
guiSimplify.add(settings, 'ratio', 0, 1, 0.01).onChange(simplify);
guiSimplify.add(settings, 'lockBorder').onChange(simplify);
guiSimplify.add(settings, 'prune').onChange(simplify);
guiSimplify.add(settings, 'weldVertices').onChange(simplify);
guiSimplify.add(settings, 'errorThresholdLog10', 0, 3, 0.1).onChange(simplify);
guiSimplify.add(settings, 'useAttributes').onChange(simplify).onFinishChange(updateSettings);
@@ -199,6 +201,9 @@
if (settings.lockBorder) {
flags.push('LockBorder');
}
if (settings.prune) {
flags.push('Prune');
}
var stride = geo.attributes.position instanceof THREE.InterleavedBufferAttribute ? geo.attributes.position.data.stride : 3;
+124 -30
View File
@@ -909,39 +909,19 @@ static void simplify()
// 1 2
// 3 4 5
unsigned int ib[] = {
0,
2,
1,
1,
2,
3,
3,
2,
4,
2,
5,
4,
0, 2, 1,
1, 2, 3,
3, 2, 4,
2, 5, 4, // clang-format :-/
};
float vb[] = {
0,
4,
0,
0,
1,
0,
2,
2,
0,
0,
0,
0,
1,
0,
0,
4,
0,
0,
0, 4, 0,
0, 1, 0,
2, 2, 0,
0, 0, 0,
1, 0, 0,
4, 0, 0, // clang-format :-/
};
unsigned int expected[] = {
@@ -1431,6 +1411,117 @@ static void simplifySeamFake()
assert(meshopt_simplify(ib, ib, 6, vb, 4, 16, 0, 1.f, 0, NULL) == 6);
}
static void simplifyDebug()
{
// 0
// 1 2
// 3 4 5
unsigned int ib[] = {
0, 2, 1,
1, 2, 3,
3, 2, 4,
2, 5, 4, // clang-format :-/
};
float vb[] = {
0, 4, 0,
0, 1, 0,
2, 2, 0,
0, 0, 0,
1, 0, 0,
4, 0, 0, // clang-format :-/
};
unsigned int expected[] = {
0 | (9u << 28),
5 | (9u << 28),
3 | (9u << 28),
};
const unsigned int meshopt_SimplifyInternalDebug = 1 << 30;
float error;
assert(meshopt_simplify(ib, ib, 12, vb, 6, 12, 3, 1e-2f, meshopt_SimplifyInternalDebug, &error) == 3);
assert(error == 0.f);
assert(memcmp(ib, expected, sizeof(expected)) == 0);
}
static void simplifyPrune()
{
// 0
// 1 2
// 3 4 5
// +
// 6 7 8 (same position)
unsigned int ib[] = {
0, 2, 1,
1, 2, 3,
3, 2, 4,
2, 5, 4,
6, 7, 8, // clang-format :-/
};
float vb[] = {
0, 4, 0,
0, 1, 0,
2, 2, 0,
0, 0, 0,
1, 0, 0,
4, 0, 0,
1, 1, 1,
1, 1, 1,
1, 1, 1, // clang-format :-/
};
unsigned int expected[] = {
0,
5,
3,
};
float error;
assert(meshopt_simplify(ib, ib, 15, vb, 9, 12, 3, 1e-2f, meshopt_SimplifyPrune, &error) == 3);
assert(error == 0.f);
assert(memcmp(ib, expected, sizeof(expected)) == 0);
// re-run prune with and without sparsity on a small subset to make sure the component code correctly handles sparse subsets
assert(meshopt_simplify(ib, ib, 3, vb, 9, 12, 3, 1e-2f, meshopt_SimplifyPrune, &error) == 3);
assert(meshopt_simplify(ib, ib, 3, vb, 9, 12, 3, 1e-2f, meshopt_SimplifyPrune | meshopt_SimplifySparse, &error) == 3);
assert(memcmp(ib, expected, sizeof(expected)) == 0);
}
static void simplifyPruneCleanup()
{
unsigned int ib[] = {
0, 1, 2,
3, 4, 5,
6, 7, 8, // clang-format :-/
};
float vb[] = {
0, 0, 0,
0, 1, 0,
1, 0, 0,
0, 0, 1,
0, 2, 1,
2, 0, 1,
0, 0, 2,
0, 4, 2,
4, 0, 2, // clang-format :-/
};
unsigned int expected[] = {
6,
7,
8,
};
float error;
assert(meshopt_simplify(ib, ib, 9, vb, 9, 12, 3, 1.f, meshopt_SimplifyLockBorder | meshopt_SimplifyPrune, &error) == 3);
assert(fabsf(error - 0.37f) < 0.01f);
assert(memcmp(ib, expected, sizeof(expected)) == 0);
}
static void adjacency()
{
// 0 1/4
@@ -1687,6 +1778,9 @@ void runTests()
simplifyErrorAbsolute();
simplifySeam();
simplifySeamFake();
simplifyDebug();
simplifyPrune();
simplifyPruneCleanup();
adjacency();
tessellation();
+4
View File
@@ -625,9 +625,13 @@ static void simplifyMesh(Mesh& mesh, float threshold, float error, bool attribut
size_t target_index_count = size_t(double(mesh.indices.size() / 3) * threshold) * 3;
float target_error = error;
float target_error_aggressive = 1e-1f;
unsigned int options = 0;
if (lock_borders)
options |= meshopt_SimplifyLockBorder;
else
options |= meshopt_SimplifyPrune;
if (debug)
options |= meshopt_SimplifyInternalDebug;
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
// This file is part of meshoptimizer library and is distributed under the terms of MIT License.
// Copyright (C) 2016-2024, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
export type Flags = 'LockBorder' | 'Sparse' | 'ErrorAbsolute';
export type Flags = 'LockBorder' | 'Sparse' | 'ErrorAbsolute' | 'Prune';
export const MeshoptSimplifier: {
supported: boolean;
File diff suppressed because one or more lines are too long
+2
View File
@@ -349,6 +349,8 @@ enum
meshopt_SimplifySparse = 1 << 1,
/* Treat error limit and resulting error as absolute instead of relative to mesh extents. */
meshopt_SimplifyErrorAbsolute = 1 << 2,
/* Experimental: remove disconnected parts of the mesh during simplification incrementally, regardless of the topological restrictions inside components. */
meshopt_SimplifyPrune = 1 << 3,
};
/**
+207 -1
View File
@@ -1322,6 +1322,164 @@ static void remapEdgeLoops(unsigned int* loop, size_t vertex_count, const unsign
}
}
static unsigned int follow(unsigned int* parents, unsigned int index)
{
while (index != parents[index])
{
unsigned int parent = parents[index];
parents[index] = parents[parent];
index = parent;
}
return index;
}
static size_t buildComponents(unsigned int* components, size_t vertex_count, const unsigned int* indices, size_t index_count, const unsigned int* remap)
{
for (size_t i = 0; i < vertex_count; ++i)
components[i] = unsigned(i);
// compute a unique (but not sequential!) index for each component via union-find
for (size_t i = 0; i < index_count; i += 3)
{
static const int next[4] = {1, 2, 0, 1};
for (int e = 0; e < 3; ++e)
{
unsigned int i0 = indices[i + e];
unsigned int i1 = indices[i + next[e]];
unsigned int r0 = remap[i0];
unsigned int r1 = remap[i1];
r0 = follow(components, r0);
r1 = follow(components, r1);
// merge components with larger indices into components with smaller indices
// this guarantees that the root of the component is always the one with the smallest index
if (r0 != r1)
components[r0 < r1 ? r1 : r0] = r0 < r1 ? r0 : r1;
}
}
// make sure each element points to the component root *before* we renumber the components
for (size_t i = 0; i < vertex_count; ++i)
if (remap[i] == i)
components[i] = follow(components, unsigned(i));
unsigned int next_component = 0;
// renumber components using sequential indices
// a sequential pass is sufficient because component root always has the smallest index
// note: it is unsafe to use follow() in this pass because we're replacing component links with sequential indices inplace
for (size_t i = 0; i < vertex_count; ++i)
{
if (remap[i] == i)
{
unsigned int root = components[i];
assert(root <= i); // make sure we already computed the component for non-roots
components[i] = (root == i) ? next_component++ : components[root];
}
else
{
assert(remap[i] < i); // make sure we already computed the component
components[i] = components[remap[i]];
}
}
return next_component;
}
static void measureComponents(float* component_errors, size_t component_count, const unsigned int* components, const Vector3* vertex_positions, size_t vertex_count)
{
memset(component_errors, 0, component_count * 4 * sizeof(float));
// compute approximate sphere center for each component as an average
for (size_t i = 0; i < vertex_count; ++i)
{
unsigned int c = components[i];
assert(components[i] < component_count);
Vector3 v = vertex_positions[i]; // copy avoids aliasing issues
component_errors[c * 4 + 0] += v.x;
component_errors[c * 4 + 1] += v.y;
component_errors[c * 4 + 2] += v.z;
component_errors[c * 4 + 3] += 1; // weight
}
// complete the center computation, and reinitialize [3] as a radius
for (size_t i = 0; i < component_count; ++i)
{
float w = component_errors[i * 4 + 3];
float iw = w == 0.f ? 0.f : 1.f / w;
component_errors[i * 4 + 0] *= iw;
component_errors[i * 4 + 1] *= iw;
component_errors[i * 4 + 2] *= iw;
component_errors[i * 4 + 3] = 0; // radius
}
// compute squared radius for each component
for (size_t i = 0; i < vertex_count; ++i)
{
unsigned int c = components[i];
float dx = vertex_positions[i].x - component_errors[c * 4 + 0];
float dy = vertex_positions[i].y - component_errors[c * 4 + 1];
float dz = vertex_positions[i].z - component_errors[c * 4 + 2];
float r = dx * dx + dy * dy + dz * dz;
component_errors[c * 4 + 3] = component_errors[c * 4 + 3] < r ? r : component_errors[c * 4 + 3];
}
// we've used the output buffer as scratch space, so we need to move the results to proper indices
for (size_t i = 0; i < component_count; ++i)
{
#if TRACE > 1
printf("component %d: center %f %f %f, error %e\n", int(i),
component_errors[i * 4 + 0], component_errors[i * 4 + 1], component_errors[i * 4 + 2], sqrtf(component_errors[i * 4 + 3]));
#endif
// note: we keep the squared error to make it match quadric error metric
component_errors[i] = component_errors[i * 4 + 3];
}
}
static size_t pruneComponents(unsigned int* indices, size_t index_count, const unsigned int* components, const float* component_errors, size_t component_count, float error_cutoff, float& nexterror)
{
size_t write = 0;
for (size_t i = 0; i < index_count; i += 3)
{
unsigned int c = components[indices[i]];
assert(c == components[indices[i + 1]] && c == components[indices[i + 2]]);
if (component_errors[c] > error_cutoff)
{
indices[write + 0] = indices[i + 0];
indices[write + 1] = indices[i + 1];
indices[write + 2] = indices[i + 2];
write += 3;
}
}
#if TRACE
size_t pruned_components = 0;
for (size_t i = 0; i < component_count; ++i)
pruned_components += (component_errors[i] >= nexterror && component_errors[i] <= error_cutoff);
printf("pruned %d triangles in %d components (goal %e)\n", int((index_count - write) / 3), int(pruned_components), sqrtf(error_cutoff));
#endif
// update next error with the smallest error of the remaining components for future pruning
nexterror = FLT_MAX;
for (size_t i = 0; i < component_count; ++i)
if (component_errors[i] > error_cutoff)
nexterror = nexterror > component_errors[i] ? component_errors[i] : nexterror;
return write;
}
struct CellHasher
{
const unsigned int* vertex_ids;
@@ -1645,7 +1803,7 @@ size_t meshopt_simplifyEdge(unsigned int* destination, const unsigned int* indic
assert(vertex_positions_stride % sizeof(float) == 0);
assert(target_index_count <= index_count);
assert(target_error >= 0);
assert((options & ~(meshopt_SimplifyLockBorder | meshopt_SimplifySparse | meshopt_SimplifyErrorAbsolute | meshopt_SimplifyInternalDebug)) == 0);
assert((options & ~(meshopt_SimplifyLockBorder | meshopt_SimplifySparse | meshopt_SimplifyErrorAbsolute | meshopt_SimplifyPrune | meshopt_SimplifyInternalDebug)) == 0);
assert(vertex_attributes_stride >= attribute_count * sizeof(float) && vertex_attributes_stride <= 256);
assert(vertex_attributes_stride % sizeof(float) == 0);
assert(attribute_count <= kMaxAttributes);
@@ -1736,6 +1894,28 @@ size_t meshopt_simplifyEdge(unsigned int* destination, const unsigned int* indic
if (attribute_count)
fillAttributeQuadrics(attribute_quadrics, attribute_gradients, result, index_count, vertex_positions, vertex_attributes, attribute_count);
unsigned int* components = NULL;
float* component_errors = NULL;
size_t component_count = 0;
float component_nexterror = 0;
if (options & meshopt_SimplifyPrune)
{
components = allocator.allocate<unsigned int>(vertex_count);
component_count = buildComponents(components, vertex_count, result, index_count, remap);
component_errors = allocator.allocate<float>(component_count * 4); // overallocate for temporary use inside measureComponents
measureComponents(component_errors, component_count, components, vertex_positions, vertex_count);
component_nexterror = FLT_MAX;
for (size_t i = 0; i < component_count; ++i)
component_nexterror = component_nexterror > component_errors[i] ? component_errors[i] : component_nexterror;
#if TRACE
printf("components: %d (min error %e)\n", int(component_count), sqrtf(component_nexterror));
#endif
}
#if TRACE
size_t pass_count = 0;
#endif
@@ -1794,6 +1974,32 @@ size_t meshopt_simplifyEdge(unsigned int* destination, const unsigned int* indic
assert(new_count < result_count);
result_count = new_count;
if ((options & meshopt_SimplifyPrune) && result_count > target_index_count && component_nexterror <= result_error)
result_count = pruneComponents(result, result_count, components, component_errors, component_count, result_error, component_nexterror);
}
// we're done with the regular simplification but we're still short of the target; try pruning more aggressively towards error_limit
while ((options & meshopt_SimplifyPrune) && result_count > target_index_count && component_nexterror <= error_limit)
{
#if TRACE
printf("pass %d: cleanup; ", int(pass_count++));
#endif
float component_cutoff = component_nexterror * 1.5f < error_limit ? component_nexterror * 1.5f : error_limit;
// track maximum error in eligible components as we are increasing resulting error
float component_maxerror = 0;
for (size_t i = 0; i < component_count; ++i)
if (component_errors[i] > component_maxerror && component_errors[i] <= component_cutoff)
component_maxerror = component_errors[i];
size_t new_count = pruneComponents(result, result_count, components, component_errors, component_count, component_cutoff, component_nexterror);
if (new_count == result_count)
break;
result_count = new_count;
result_error = result_error < component_maxerror ? component_maxerror : result_error;
}
#if TRACE
+1
View File
@@ -33,6 +33,7 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* buffer, size_t size)
meshopt_simplify(res, ib, indices, vb[0], 100, sizeof(float) * 4, 0, FLT_MAX, 0, NULL);
meshopt_simplify(res, ib, indices, vb[0], 100, sizeof(float) * 4, 0, FLT_MAX, meshopt_SimplifyLockBorder, NULL);
meshopt_simplify(res, ib, indices, vb[0], 100, sizeof(float) * 4, 0, FLT_MAX, meshopt_SimplifySparse, NULL);
meshopt_simplify(res, ib, indices, vb[0], 100, sizeof(float) * 4, 0, FLT_MAX, meshopt_SimplifyPrune, NULL);
return 0;
}