diff --git a/README.md b/README.md index 1a08aa4d8..afecb604f 100644 --- a/README.md +++ b/README.md @@ -503,7 +503,7 @@ lod.resize(meshopt_simplifyWithAttributes(&lod[0], indices, index_count, &vertic target_index_count, target_error, /* options= */ meshopt_SimplifyPermissive, &lod_error)); ``` -To maintain appearance, it's highly recommended to use this option together with attribute-aware simplification, as shown above, as it allows the simplifier to maintain attribute appearance. In this mode, it is often desirable to selectively preserve certain attribute seams, such as UV seams or sharp creases. This can be achieved by using the `vertex_lock` array with flag `meshopt_SimplifyVertex_Protect` set for individual vertices to protect specific discontinuities. To fill this array, use `meshopt_generatePositionRemap` to create a mapping table for vertices with identical positions, and then compare each vertex to the remapped vertex to determine which attributes are different: +To maintain appearance, it's highly recommended to use this option together with attribute-aware simplification, as shown above, as it allows the simplifier to maintain attribute quality. In this mode, it is often desirable to selectively preserve certain attribute seams, such as UV seams or sharp creases. This can be achieved by using the `vertex_lock` array with flag `meshopt_SimplifyVertex_Protect` set for individual vertices to protect specific discontinuities. To fill this array, use `meshopt_generatePositionRemap` to create a mapping table for vertices with identical positions, and then compare each vertex to the remapped vertex to determine which attributes are different: ```c++ std::vector remap(vertices.size()); @@ -515,9 +515,6 @@ for (size_t i = 0; i < vertices.size(); ++i) { if (r != i && (vertices[r].tx != vertices[i].tx || vertices[r].ty != vertices[i].ty)) locks[i] |= meshopt_SimplifyVertex_Protect; // protect UV seams - - if (r != i && (vertices[r].nx * vertices[i].nx + vertices[r].ny * vertices[i].ny + vertices[r].nz * vertices[i].nz < 0.25f)) - locks[i] |= meshopt_SimplifyVertex_Protect; // protect sharp normal creases } ``` diff --git a/demo/simplify.html b/demo/simplify.html index b2fcdb8c0..192eef4f9 100644 --- a/demo/simplify.html +++ b/demo/simplify.html @@ -155,7 +155,7 @@ debugOverlay: false, sloppy: false, permissive: false, - permissiveProtection: 3, + permissiveProtection: 2, // uvs lockBorder: false, preprune: 0.0, prune: true, @@ -207,7 +207,7 @@ guiSimplify.add(settings, 'ratio', 0, 1, 0.01).onChange(simplify); guiSimplify.add(settings, 'sloppy').onChange(simplify); guiSimplify.add(settings, 'permissive').onChange(simplify); - guiSimplify.add(settings, 'permissiveProtection', { none: 0, normals: 1, uvs: 2, all: 3 }).onChange(simplify); + guiSimplify.add(settings, 'permissiveProtection', { none: 0, uvs: 2, normals: 1, all: 3 }).onChange(simplify); guiSimplify.add(settings, 'lockBorder').onChange(simplify); guiSimplify.add(settings, 'prune').onChange(simplify); guiSimplify.add(settings, 'regularize').onChange(simplify); @@ -610,7 +610,7 @@ var mask = (1 << 28) - 1; - for (var kind = 1; kind <= 5; ++kind) { + for (var kind = 1; kind <= 6; ++kind) { var offset = dind.length; for (var i = 0; i < dlen; i += 3) { @@ -620,8 +620,8 @@ var ak = (a >> 28) & 7, bk = (b >> 28) & 7; - if ((a >> 31 != 0 || kind == 3) && (ak == kind || bk == kind) && (ak == kind || ak == 4) && (bk == kind || bk == 4)) { - // edge of current kind (allow one of the vertices to be locked) + if (a >> 31 != 0 && (ak == kind || bk == kind) && (ak == kind || ak == 4) && (bk == kind || bk == 4)) { + // loop edge of current kind (allow one of the vertices to be locked) // note: complex edges ignore loop metadata dind.push(a & mask); dind.push(a & mask); @@ -631,8 +631,13 @@ dind.push(a & mask); dind.push(a & mask); dind.push(b & mask); - } else if (kind == 4 && ak == kind && bk == kind) { - // locked edge (not be marked as a loop) + } else if (a >> 31 == 0 && kind == 4 && ak == kind && bk == kind) { + // locked edge (not marked as a loop) + dind.push(a & mask); + dind.push(a & mask); + dind.push(b & mask); + } else if (a >> 31 == 0 && kind == 6 && ak == 3 && bk == 3) { + // complex edge (not marked as a loop) dind.push(a & mask); dind.push(a & mask); dind.push(b & mask); @@ -793,6 +798,7 @@ new THREE.MeshBasicMaterial({ color: 0x009f9f, wireframe: true }), // complex new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true }), // locked new THREE.MeshBasicMaterial({ color: 0xff9f00, wireframe: true }), // mixed edge + new THREE.MeshBasicMaterial({ color: 0x9f5f9f, wireframe: true }), // complex, no loop ]; } diff --git a/src/simplifier.cpp b/src/simplifier.cpp index efbdf91a6..14d4d42fe 100644 --- a/src/simplifier.cpp +++ b/src/simplifier.cpp @@ -367,8 +367,8 @@ static void classifyVertices(unsigned char* result, unsigned int* loop, unsigned memset(loopback, -1, vertex_count * sizeof(unsigned int)); // incoming & outgoing open edges: ~0u if no open edges, i if there are more than 1 - // note that this is the same data as required in loop[] arrays; loop[] data is only valid for border/seam - // but here it's okay to fill the data out for other types of vertices as well + // note that this is the same data as required in loop[] arrays; loop[] data is only used for border/seam by default + // in permissive mode we also use it to guide complex-complex collapses, so we fill it for all vertices unsigned int* openinc = loopback; unsigned int* openout = loop; @@ -1271,6 +1271,20 @@ static float getNeighborhoodRadius(const EdgeAdjacency& adjacency, const Vector3 return sqrtf(result); } +static unsigned int getComplexTarget(unsigned int v, unsigned int target, const unsigned int* remap, const unsigned int* loop, const unsigned int* loopback) +{ + unsigned int r = remap[target]; + + // use loop metadata to guide complex collapses towards the correct wedge + // this works for edges on attribute discontinuities because loop/loopback track the single half-edge without a pair, similar to seams + if (loop[v] != ~0u && remap[loop[v]] == r) + return loop[v]; + else if (loopback[v] != ~0u && remap[loopback[v]] == r) + return loopback[v]; + else + return target; +} + static size_t boundEdgeCollapses(const EdgeAdjacency& adjacency, size_t vertex_count, size_t index_count, unsigned char* vertex_kind) { size_t dual_count = 0; @@ -1393,15 +1407,22 @@ static void rankEdgeCollapses(Collapse* collapses, size_t collapse_count, const } else { - // complex edges can have multiple wedges, so we need to aggregate errors for all wedges - // this is different from seams (where we aggregate pairwise) because all wedges collapse onto the same target + // complex edges can have multiple wedges, so we need to aggregate errors for all wedges based on the selected target if (vertex_kind[i0] == Kind_Complex) for (unsigned int v = wedge[i0]; v != i0; v = wedge[v]) - ei += quadricError(attribute_quadrics[v], &attribute_gradients[v * attribute_count], attribute_count, vertex_positions[i1], &vertex_attributes[i1 * attribute_count]); + { + unsigned int t = getComplexTarget(v, i1, remap, loop, loopback); + + ei += quadricError(attribute_quadrics[v], &attribute_gradients[v * attribute_count], attribute_count, vertex_positions[t], &vertex_attributes[t * attribute_count]); + } if (vertex_kind[i1] == Kind_Complex && bidi) for (unsigned int v = wedge[i1]; v != i1; v = wedge[v]) - ej += quadricError(attribute_quadrics[v], &attribute_gradients[v * attribute_count], attribute_count, vertex_positions[i0], &vertex_attributes[i0 * attribute_count]); + { + unsigned int t = getComplexTarget(v, i0, remap, loop, loopback); + + ej += quadricError(attribute_quadrics[v], &attribute_gradients[v * attribute_count], attribute_count, vertex_positions[t], &vertex_attributes[t * attribute_count]); + } } } @@ -1553,7 +1574,9 @@ static size_t performEdgeCollapses(unsigned int* collapse_remap, unsigned char* do { - collapse_remap[v] = i1; + unsigned int t = getComplexTarget(v, i1, remap, loop, loopback); + + collapse_remap[v] = t; v = wedge[v]; } while (v != i0); }