#include "TextureBakeRegularize.hpp" #include #include #include #include namespace Slic3r { namespace TextureBake { namespace { // Vertex-to-triangle lists as intrusive doubly linked lists of corner slots over flat arrays. Slot s // is corner (triangle * 3 + k), owned by corners[s]. Deleted and moved corners are unlinked, so a // collapse costs no allocation. struct SlotLists { std::vector head, next, prev; void init(size_t vertex_count, size_t slot_count) { head.assign(vertex_count, -1); next.assign(slot_count, -1); prev.assign(slot_count, -1); } void link(int s, const std::vector &corners) { const int v = corners[size_t(s)]; const int h = head[size_t(v)]; prev[size_t(s)] = -1; next[size_t(s)] = h; if (h != -1) prev[size_t(h)] = s; head[size_t(v)] = s; } void unlink(int s, const std::vector &corners) { const int p = prev[size_t(s)], n = next[size_t(s)]; if (p != -1) next[size_t(p)] = n; else head[size_t(corners[size_t(s)])] = n; if (n != -1) prev[size_t(n)] = p; } }; } // namespace RegularizeResult regularize_mesh(const TriSoup &geometry, const std::vector &face_parent_id, double max_edge_length, const RegularizeOptions &opts) { RegularizeResult result; const size_t tri_count = geometry.triangle_count(); if (tri_count == 0 || max_edge_length <= 0.0) { result.geometry = geometry; result.face_parent_id = face_parent_id; return result; } const double base_max_len_sq = (max_edge_length * opts.slack) * (max_edge_length * opts.slack); const double aggr_max_len_sq = (max_edge_length * opts.aggressive_slack) * (max_edge_length * opts.aggressive_slack); const double extreme_aspect2 = opts.extreme_sliver_aspect * opts.extreme_sliver_aspect; const double aspect_thr2 = opts.aspect_threshold * opts.aspect_threshold; // Double precision: a collapse writes a midpoint back and later collapses read it, so rounding // would accumulate. QuantizedPointMap pos_map(WELD_GRID_GEOMETRY, std::min(tri_count * 3, size_t(1) << 22)); std::vector vert; std::vector corners(tri_count * 3); vert.reserve(tri_count); for (size_t i = 0; i < tri_count * 3; ++i) { const Vec3f &p = geometry.pos[i]; const int id = pos_map.get_or_set(p, int(vert.size())); if (pos_map.inserted()) vert.push_back(p.cast()); corners[i] = id; } const size_t vert_count = vert.size(); std::vector tri_nrm(tri_count, Vec3d::Zero()); std::vector tri_deleted(tri_count, 0); result.face_parent_id = face_parent_id; if (result.face_parent_id.size() != tri_count) result.face_parent_id.assign(tri_count, 0); const auto sq_dist = [&](int a, int b) { return (vert[size_t(a)] - vert[size_t(b)]).squaredNorm(); }; const auto recompute_face_normal = [&](size_t t) { const Vec3d &a = vert[size_t(corners[t * 3])]; const Vec3d n = (vert[size_t(corners[t * 3 + 1])] - a).cross(vert[size_t(corners[t * 3 + 2])] - a); const double len = n.norm(); tri_nrm[t] = (len > 0.0) ? Vec3d(n / len) : Vec3d::Zero(); }; for (size_t t = 0; t < tri_count; ++t) recompute_face_normal(t); // Never updated - the normal gate measures against these, so drift cannot compound across rounds. const std::vector orig_nrm = tri_nrm; // Squared thinness, the longest edge over the shortest altitude: // thinness = lmax / hmin = lmax^2 / (2 * area), so thinness^2 = lmax^4 / |AB x AC|^2 // // Not lmax/lmin, which misses what matters here: three near-collinear points can have all edges // similar, so an edge ratio reports about 2 and the gate skips a triangle with near-zero area // whose corners sample three unrelated texels. An equilateral scores about 1.15. const auto tri_aspect_sq = [&](size_t t) -> double { const Vec3d &a = vert[size_t(corners[t * 3])]; const Vec3d ab = vert[size_t(corners[t * 3 + 1])] - a; const Vec3d ac = vert[size_t(corners[t * 3 + 2])] - a; const Vec3d bc = vert[size_t(corners[t * 3 + 2])] - vert[size_t(corners[t * 3 + 1])]; const double lmax2 = std::max({ ab.squaredNorm(), ac.squaredNorm(), bc.squaredNorm() }); const double cross2 = ab.cross(ac).squaredNorm(); return cross2 > 0.0 ? lmax2 * lmax2 / cross2 : std::numeric_limits::infinity(); }; SlotLists slots; slots.init(vert_count, tri_count * 3); for (size_t s = 0; s < tri_count * 3; ++s) slots.link(int(s), corners); // O(1) membership without clearing a set per collapse. std::vector vert_stamp(vert_count, 0), tri_stamp(tri_count, 0); uint32_t stamp_gen = 0; // Both endpoints of a hard edge are barred from being collapse endpoints, preserving such // corners exactly while leaving flat-face interiors free. // // Skipped when either triangle is an extreme sliver: a sliver's normal is dominated by where its // far apex sits, so noise pivots it tens of degrees with no feature behind it, and freezing on // that would lock the very chains this pass exists to dissolve. Genuine features are bordered by // well-shaped triangles and are unaffected. std::vector frozen_vert(vert_count, 0); { std::vector tri_thin2(tri_count); for (size_t t = 0; t < tri_count; ++t) tri_thin2[t] = tri_aspect_sq(t); QuantizedPointMap edge_seen(1.0, std::min(tri_count * 3, size_t(1) << 22)); for (size_t t = 0; t < tri_count; ++t) for (int e = 0; e < 3; ++e) { const int u = corners[t * 3 + size_t(e)]; const int v = corners[t * 3 + size_t((e + 1) % 3)]; const int lo = std::min(u, v), hi = std::max(u, v); const int other = edge_seen.get_or_set_key(lo, hi, 0, int(t)); if (edge_seen.inserted()) continue; if (tri_thin2[t] > extreme_aspect2 || tri_thin2[size_t(other)] > extreme_aspect2) continue; if (tri_nrm[t].dot(tri_nrm[size_t(other)]) < opts.sharp_edge_cos) { frozen_vert[size_t(u)] = 1; frozen_vert[size_t(v)] = 1; } } } // Exclusion freeze. The weight is constant across a face's corners, so the first one answers. if (opts.preserve_excluded && !geometry.exclude_weight.empty()) for (size_t t = 0; t < tri_count; ++t) if (geometry.exclude_weight[t * 3] > 0.99f) for (int k = 0; k < 3; ++k) frozen_vert[size_t(corners[t * 3 + size_t(k)])] = 1; std::vector wing_scratch, affected_scratch; const auto third_vertex = [&](size_t t, int u, int v) { const int a = corners[t * 3], b = corners[t * 3 + 1], c = corners[t * 3 + 2]; if (a != u && a != v) return a; if (b != u && b != v) return b; return c; }; const auto triangles_sharing_edge = [&](int u, int v) -> std::vector & { wing_scratch.clear(); for (int s = slots.head[size_t(u)]; s != -1; s = slots.next[size_t(s)]) { const size_t t = size_t(s) / 3; if (tri_deleted[t]) continue; if (corners[t * 3] == v || corners[t * 3 + 1] == v || corners[t * 3 + 2] == v) wing_scratch.push_back(int(t)); } return wing_scratch; }; RegularizeRejectStats &stats = result.reject_stats; const auto try_collapse = [&](int u, int v) -> bool { if (u == v) return false; if (frozen_vert[size_t(u)] || frozen_vert[size_t(v)]) { ++stats.frozen; return false; } // Two wings means a manifold interior edge. std::vector &wings = triangles_sharing_edge(u, v); if (wings.size() != 2) { ++stats.wing_count; return false; } const size_t w0 = size_t(wings[0]), w1 = size_t(wings[1]); const int apex1 = third_vertex(w0, u, v), apex2 = third_vertex(w1, u, v); if (apex1 == apex2) { ++stats.folded_apex; return false; } // The edge cap loosens if *either* wing is extreme, since the re-subdivision recovers an // over-long edge. The normal cap needs *both*, which is what protects fillets. const double w1a = tri_aspect_sq(w0), w2a = tri_aspect_sq(w1); const bool either_extreme = w1a > extreme_aspect2 || w2a > extreme_aspect2; const bool both_extreme = w1a > extreme_aspect2 && w2a > extreme_aspect2; const double eff_max_len_sq = either_extreme ? aggr_max_len_sq : base_max_len_sq; const double eff_normal_cos = both_extreme ? opts.aggressive_normal_delta_cos : opts.max_normal_delta_cos; // A vertex sharing a triangle with both endpoints, other than the wing apexes, would go // non-manifold. Stamp one side's neighbours, scan the other against them. ++stamp_gen; for (int s = slots.head[size_t(v)]; s != -1; s = slots.next[size_t(s)]) { const size_t t = size_t(s) / 3; if (tri_deleted[t]) continue; for (int k = 0; k < 3; ++k) if (const int x = corners[t * 3 + size_t(k)]; x != v) vert_stamp[size_t(x)] = stamp_gen; } for (int s = slots.head[size_t(u)]; s != -1; s = slots.next[size_t(s)]) { const size_t t = size_t(s) / 3; if (tri_deleted[t]) continue; for (int k = 0; k < 3; ++k) { const int x = corners[t * 3 + size_t(k)]; if (x != u && x != v && x != apex1 && x != apex2 && vert_stamp[size_t(x)] == stamp_gen) { ++stats.link_condition; return false; } } } const Vec3d m = (vert[size_t(u)] + vert[size_t(v)]) * 0.5; // Everything using either endpoint; the wings are being deleted. ++stamp_gen; affected_scratch.clear(); for (const int endpoint : { u, v }) for (int s = slots.head[size_t(endpoint)]; s != -1; s = slots.next[size_t(s)]) { const size_t t = size_t(s) / 3; if (tri_deleted[t] || t == w0 || t == w1) continue; if (tri_stamp[t] != stamp_gen) { tri_stamp[t] = stamp_gen; affected_scratch.push_back(int(t)); } } // Validate every affected triangle before touching anything. for (const int ti : affected_scratch) { const size_t t = size_t(ti); Vec3d p[3]; for (int k = 0; k < 3; ++k) { const int x = corners[t * 3 + size_t(k)]; p[k] = (x == u || x == v) ? m : vert[size_t(x)]; } const double ab2 = (p[1] - p[0]).squaredNorm(); const double bc2 = (p[2] - p[1]).squaredNorm(); const double ca2 = (p[0] - p[2]).squaredNorm(); if (ab2 > eff_max_len_sq || bc2 > eff_max_len_sq || ca2 > eff_max_len_sq) { ++stats.edge_cap; return false; } const Vec3d n = (p[1] - p[0]).cross(p[2] - p[0]); const double nlen = n.norm(); if (nlen <= 0.0) { ++stats.degenerate; return false; } if ((n / nlen).dot(orig_nrm[t]) < eff_normal_cos) { ++stats.normal_change; return false; } } // Apply: move u to the merged position and redirect every reference to v. vert[size_t(u)] = m; for (const size_t w : { w0, w1 }) { tri_deleted[w] = 1; for (int k = 0; k < 3; ++k) slots.unlink(int(w * 3) + k, corners); } // A non-wing triangle contains v exactly once, so moving its slots suffices. for (int s = slots.head[size_t(v)]; s != -1;) { const int ns = slots.next[size_t(s)]; slots.unlink(s, corners); corners[size_t(s)] = u; slots.link(s, corners); recompute_face_normal(size_t(s) / 3); s = ns; } for (int s = slots.head[size_t(u)]; s != -1; s = slots.next[size_t(s)]) { const size_t t = size_t(s) / 3; if (!tri_deleted[t]) recompute_face_normal(t); } return true; }; for (int round = 0; round < opts.maxrounds; ++round) { // Rebuilt each round so earlier collapses inform the priorities. std::vector cand; std::vector cand_aspect; for (size_t t = 0; t < tri_count; ++t) { if (tri_deleted[t]) continue; const int a = corners[t * 3], b = corners[t * 3 + 1], c = corners[t * 3 + 2]; if (std::min({ sq_dist(a, b), sq_dist(b, c), sq_dist(c, a) }) <= 0.0) continue; const double aspect2 = tri_aspect_sq(t); if (aspect2 < aspect_thr2) continue; cand.push_back(int(t)); cand_aspect.push_back(aspect2); } // Worst first; ties keep ascending order so the pass is deterministic. std::vector order(cand.size()); std::iota(order.begin(), order.end(), 0); std::stable_sort(order.begin(), order.end(), [&](int x, int y) { return cand_aspect[size_t(x)] > cand_aspect[size_t(y)]; }); size_t round_collapses = 0; for (const int oi : order) { const size_t t = size_t(cand[size_t(oi)]); if (tri_deleted[t]) continue; const int a = corners[t * 3], b = corners[t * 3 + 1], c = corners[t * 3 + 2]; // All three edges, shortest first: a sliver straddling a seam has its shortest edge // crossing it, which the normal gate refuses, while a long edge along one surface // collapses safely. Trying only the shortest would leave those stuck. struct Cand { double len2; int u, v; }; Cand e[3] = { { sq_dist(a, b), a, b }, { sq_dist(b, c), b, c }, { sq_dist(c, a), c, a } }; std::stable_sort(std::begin(e), std::end(e), [](const Cand &x, const Cand &y) { return x.len2 < y.len2; }); if (try_collapse(e[0].u, e[0].v) || try_collapse(e[1].u, e[1].v) || try_collapse(e[2].u, e[2].v)) ++round_collapses; } result.collapse_count += round_collapses; if (round_collapses == 0) break; } // Drop deleted triangles and rebuild the soup. const bool have_weights = !geometry.exclude_weight.empty(); std::vector out_parent; TriSoup &out = result.geometry; for (size_t t = 0; t < tri_count; ++t) { if (tri_deleted[t]) continue; for (int k = 0; k < 3; ++k) out.pos.push_back(vert[size_t(corners[t * 3 + size_t(k)])].cast()); if (have_weights) { // Constant across a face's corners. const float w = geometry.exclude_weight[t * 3]; out.exclude_weight.insert(out.exclude_weight.end(), { w, w, w }); } out_parent.push_back(result.face_parent_id[t]); } result.face_parent_id = std::move(out_parent); // Rebuilt from the compacted geometry - the collapses moved vertices. out.nrm.assign(out.pos.size(), Vec3f::Zero()); { std::vector accum(out.pos.size(), Vec3d::Zero()); QuantizedPointMap weld(WELD_GRID_GEOMETRY, out.pos.size()); std::vector vid(out.pos.size()); int next = 0; for (size_t i = 0; i < out.pos.size(); ++i) { vid[i] = weld.get_or_set(out.pos[i], next); if (weld.inserted()) ++next; } std::vector vn(size_t(next), Vec3d::Zero()); for (size_t t = 0; t * 3 < out.pos.size(); ++t) { const Vec3d a = out.pos[t * 3].cast(); const Vec3d n = (out.pos[t * 3 + 1].cast() - a).cross(out.pos[t * 3 + 2].cast() - a); for (int k = 0; k < 3; ++k) vn[size_t(vid[t * 3 + size_t(k)])] += n; } for (size_t i = 0; i < out.pos.size(); ++i) { const Vec3d &n = vn[size_t(vid[i])]; const double l = n.norm(); out.nrm[i] = (l > 0.0) ? Vec3d(n / l).cast() : Vec3f(0.f, 0.f, 1.f); } } return result; } } // namespace TextureBake } // namespace Slic3r