diff --git a/demo/tests.cpp b/demo/tests.cpp index 995b032bb..02873cf6e 100644 --- a/demo/tests.cpp +++ b/demo/tests.cpp @@ -1338,6 +1338,78 @@ static void simplifyErrorAbsolute() assert(fabsf(error - 0.85f) < 0.01f); } +static void simplifySeam() +{ + // xyz+attr + float vb[] = { + 0, 0, 0, 0, + 0, 1, 0, 0, + 0, 1, 0, 1, + 0, 2, 0, 1, + 1, 0, 0, 0, + 1, 1, 0.3f, 0, + 1, 1, 0.3f, 1, + 1, 2, 0, 1, + 2, 0, 0, 0, + 2, 1, 0.1f, 0, + 2, 1, 0.1f, 1, + 2, 2, 0, 1, + 3, 0, 0, 0, + 3, 1, 0, 0, + 3, 1, 0, 1, + 3, 2, 0, 1, // clang-format :-/ + }; + + // 0 1-2 3 + // 4 5-6 7 + // 8 9-10 11 + // 12 13-14 15 + + unsigned int ib[] = { + 0, 1, 4, + 4, 1, 5, + 2, 3, 6, + 6, 3, 7, + 4, 5, 8, + 8, 5, 9, + 6, 7, 10, + 10, 7, 11, + 8, 9, 12, + 12, 9, 13, + 10, 11, 14, + 14, 11, 15, // clang-format :-/ + }; + + // note: vertices 1-2 and 13-14 are classified as locked, because they are on a seam & a border + // since seam->locked collapses are restriced, we only get to 3 triangles on each side as the seam is simplified to 3 vertices + + // so we get this structure initially, and then one of the internal seam vertices is collapsed to the other one: + // 0 1-2 3 + // 5-6 + // 9-10 + // 12 13-14 15 + unsigned int expected[] = { + 0, 1, 5, + 2, 3, 6, + 0, 5, 12, + 12, 5, 13, + 6, 3, 14, + 14, 3, 15, // clang-format :-/ + }; + + unsigned int res[36]; + float error = 0.f; + + assert(meshopt_simplify(res, ib, 36, vb, 16, 16, 18, 1.f, 0, &error) == 18); + assert(memcmp(res, expected, sizeof(expected)) == 0); + assert(fabsf(error - 0.04f) < 0.01f); // note: the error is not zero because there is a small difference in height between the seam vertices + + float aw = 1; + assert(meshopt_simplifyWithAttributes(res, ib, 36, vb, 16, 16, vb + 3, 16, &aw, 1, NULL, 18, 2.f, 0, &error) == 18); + assert(memcmp(res, expected, sizeof(expected)) == 0); + assert(fabsf(error - 0.04f) < 0.01f); // note: this is the same error as above because the attribute is constant on either side of the seam +} + static void adjacency() { // 0 1/4 @@ -1556,6 +1628,7 @@ void runTests() simplifyLockFlags(); simplifySparse(); simplifyErrorAbsolute(); + simplifySeam(); adjacency(); tessellation(); diff --git a/src/simplifier.cpp b/src/simplifier.cpp index a7264ff7c..26e46dac7 100644 --- a/src/simplifier.cpp +++ b/src/simplifier.cpp @@ -850,7 +850,7 @@ static void fillEdgeQuadrics(Quadric* vertex_quadrics, const unsigned int* indic } } -static void fillAttributeQuadrics(Quadric* attribute_quadrics, QuadricGrad* attribute_gradients, const unsigned int* indices, size_t index_count, const Vector3* vertex_positions, const float* vertex_attributes, size_t attribute_count, const unsigned int* remap) +static void fillAttributeQuadrics(Quadric* attribute_quadrics, QuadricGrad* attribute_gradients, const unsigned int* indices, size_t index_count, const Vector3* vertex_positions, const float* vertex_attributes, size_t attribute_count) { for (size_t i = 0; i < index_count; i += 3) { @@ -862,14 +862,13 @@ static void fillAttributeQuadrics(Quadric* attribute_quadrics, QuadricGrad* attr QuadricGrad G[kMaxAttributes]; quadricFromAttributes(QA, G, vertex_positions[i0], vertex_positions[i1], vertex_positions[i2], &vertex_attributes[i0 * attribute_count], &vertex_attributes[i1 * attribute_count], &vertex_attributes[i2 * attribute_count], attribute_count); - // TODO: This blends together attribute weights across attribute discontinuities, which is probably not a great idea - quadricAdd(attribute_quadrics[remap[i0]], QA); - quadricAdd(attribute_quadrics[remap[i1]], QA); - quadricAdd(attribute_quadrics[remap[i2]], QA); + quadricAdd(attribute_quadrics[i0], QA); + quadricAdd(attribute_quadrics[i1], QA); + quadricAdd(attribute_quadrics[i2], QA); - quadricAdd(&attribute_gradients[remap[i0] * attribute_count], G, attribute_count); - quadricAdd(&attribute_gradients[remap[i1] * attribute_count], G, attribute_count); - quadricAdd(&attribute_gradients[remap[i2] * attribute_count], G, attribute_count); + quadricAdd(&attribute_gradients[i0 * attribute_count], G, attribute_count); + quadricAdd(&attribute_gradients[i1 * attribute_count], G, attribute_count); + quadricAdd(&attribute_gradients[i2 * attribute_count], G, attribute_count); } } @@ -914,7 +913,13 @@ static bool hasTriangleFlips(const EdgeAdjacency& adjacency, const Vector3* vert // early-out when at least one triangle flips due to a collapse if (hasTriangleFlip(vertex_positions[a], vertex_positions[b], v0, v1)) + { +#if TRACE >= 2 + printf("edge block %d -> %d: flip welded %d %d %d\n", i0, i1, a, i0, b); +#endif + return true; + } } return false; @@ -1017,16 +1022,31 @@ static void rankEdgeCollapses(Collapse* collapses, size_t collapse_count, const float ei = quadricError(vertex_quadrics[remap[i0]], vertex_positions[i1]); float ej = quadricError(vertex_quadrics[remap[j0]], vertex_positions[j1]); +#if TRACE >= 2 + float di = ei, dj = ej; +#endif + if (attribute_count) { - ei += quadricError(attribute_quadrics[remap[i0]], &attribute_gradients[remap[i0] * attribute_count], attribute_count, vertex_positions[i1], &vertex_attributes[i1 * attribute_count]); - ej += quadricError(attribute_quadrics[remap[j0]], &attribute_gradients[remap[j0] * attribute_count], attribute_count, vertex_positions[j1], &vertex_attributes[j1 * attribute_count]); + // note: ideally we would evaluate max/avg of attribute errors for seam edges, but it's not clear if it's worth the extra cost + ei += quadricError(attribute_quadrics[i0], &attribute_gradients[i0 * attribute_count], attribute_count, vertex_positions[i1], &vertex_attributes[i1 * attribute_count]); + ej += quadricError(attribute_quadrics[j0], &attribute_gradients[j0 * attribute_count], attribute_count, vertex_positions[j1], &vertex_attributes[j1 * attribute_count]); } // pick edge direction with minimal error c.v0 = ei <= ej ? i0 : j0; c.v1 = ei <= ej ? i1 : j1; c.error = ei <= ej ? ei : ej; + +#if TRACE >= 2 + if (i0 == j0) // c.bidi has been overwritten + printf("edge eval %d -> %d: error %f (pos %f, attr %f)\n", c.v0, c.v1, + sqrtf(c.error), sqrtf(ei <= ej ? di : dj), sqrtf(ei <= ej ? ei - di : ej - dj)); + else + printf("edge eval %d -> %d: error %f (pos %f, attr %f); reverse %f (pos %f, attr %f)\n", c.v0, c.v1, + sqrtf(ei <= ej ? ei : ej), sqrtf(ei <= ej ? di : dj), sqrtf(ei <= ej ? ei - di : ej - dj), + sqrtf(ei <= ej ? ej : ei), sqrtf(ei <= ej ? dj : di), sqrtf(ei <= ej ? ej - dj : ei - di)); +#endif } } @@ -1108,6 +1128,8 @@ static size_t performEdgeCollapses(unsigned int* collapse_remap, unsigned char* unsigned int r0 = remap[i0]; unsigned int r1 = remap[i1]; + unsigned char kind = vertex_kind[i0]; + // we don't collapse vertices that had source or target vertex involved in a collapse // it's important to not move the vertices twice since it complicates the tracking/remapping logic // it's important to not move other vertices towards a moved vertex to preserve error since we don't re-rank collapses mid-pass @@ -1126,6 +1148,10 @@ static size_t performEdgeCollapses(unsigned int* collapse_remap, unsigned char* continue; } +#if TRACE >= 2 + printf("edge commit %d -> %d: kind %d->%d, error %f\n", i0, i1, vertex_kind[i0], vertex_kind[i1], sqrtf(c.error)); +#endif + assert(collapse_remap[r0] == r0); assert(collapse_remap[r1] == r1); @@ -1133,26 +1159,35 @@ static size_t performEdgeCollapses(unsigned int* collapse_remap, unsigned char* if (attribute_count) { - quadricAdd(attribute_quadrics[r1], attribute_quadrics[r0]); - quadricAdd(&attribute_gradients[r1 * attribute_count], &attribute_gradients[r0 * attribute_count], attribute_count); + quadricAdd(attribute_quadrics[i1], attribute_quadrics[i0]); + quadricAdd(&attribute_gradients[i1 * attribute_count], &attribute_gradients[i0 * attribute_count], attribute_count); + + // note: this is intentionally missing handling for Kind_Complex; we assume that complex vertices have similar attribute values so just using the primary vertex is fine + if (kind == Kind_Seam) + { + // seam collapses involve two edges so we need to update attribute quadrics for both target vertices; position quadrics are shared + unsigned int s0 = wedge[i0], s1 = wedge[i1]; + + quadricAdd(attribute_quadrics[s1], attribute_quadrics[s0]); + quadricAdd(&attribute_gradients[s1 * attribute_count], &attribute_gradients[s0 * attribute_count], attribute_count); + } } - if (vertex_kind[i0] == Kind_Complex) + if (kind == Kind_Complex) { + // remap all vertices in the complex to the target vertex unsigned int v = i0; do { - collapse_remap[v] = r1; + collapse_remap[v] = i1; v = wedge[v]; } while (v != i0); } - else if (vertex_kind[i0] == Kind_Seam) + else if (kind == Kind_Seam) { // remap v0 to v1 and seam pair of v0 to seam pair of v1 - unsigned int s0 = wedge[i0]; - unsigned int s1 = wedge[i1]; - + unsigned int s0 = wedge[i0], s1 = wedge[i1]; assert(s0 != i0 && s1 != i1); assert(wedge[s0] == i0 && wedge[s1] == i1); @@ -1170,7 +1205,7 @@ static size_t performEdgeCollapses(unsigned int* collapse_remap, unsigned char* collapse_locked[r1] = 1; // border edges collapse 1 triangle, other edges collapse 2 or more - triangle_collapses += (vertex_kind[i0] == Kind_Border) ? 1 : 2; + triangle_collapses += (kind == Kind_Border) ? 1 : 2; edge_collapses++; result_error = result_error < c.error ? c.error : result_error; @@ -1641,7 +1676,7 @@ size_t meshopt_simplifyEdge(unsigned int* destination, const unsigned int* indic fillEdgeQuadrics(vertex_quadrics, result, index_count, vertex_positions, remap, vertex_kind, loop, loopback); if (attribute_count) - fillAttributeQuadrics(attribute_quadrics, attribute_gradients, result, index_count, vertex_positions, vertex_attributes, attribute_count, remap); + fillAttributeQuadrics(attribute_quadrics, attribute_gradients, result, index_count, vertex_positions, vertex_attributes, attribute_count); #if TRACE size_t pass_count = 0; @@ -1673,6 +1708,10 @@ size_t meshopt_simplifyEdge(unsigned int* destination, const unsigned int* indic if (edge_collapse_count == 0) break; +#if TRACE + printf("pass %d:%c", int(pass_count++), TRACE >= 2 ? '\n' : ' '); +#endif + rankEdgeCollapses(edge_collapses, edge_collapse_count, vertex_positions, vertex_attributes, vertex_quadrics, attribute_quadrics, attribute_gradients, attribute_count, remap); sortEdgeCollapses(collapse_order, edge_collapses, edge_collapse_count); @@ -1684,10 +1723,6 @@ size_t meshopt_simplifyEdge(unsigned int* destination, const unsigned int* indic memset(collapse_locked, 0, vertex_count); -#if TRACE - printf("pass %d: ", int(pass_count++)); -#endif - size_t collapses = performEdgeCollapses(collapse_remap, collapse_locked, vertex_quadrics, attribute_quadrics, attribute_gradients, attribute_count, edge_collapses, edge_collapse_count, collapse_order, remap, wedge, vertex_kind, vertex_positions, adjacency, triangle_collapse_goal, error_limit, result_error); // no edges can be collapsed any more due to hitting the error limit or triangle collapse limit