#include "TextureBakeSubdivide.hpp" #include #include #include #include #include #include namespace Slic3r { namespace TextureBake { namespace { double edge_len_sq(const VertStore &v, int a, int b) { return (v.pos[size_t(a)] - v.pos[size_t(b)]).squaredNorm(); } // Both indexers accumulate raw, area-weighted cross products and normalise once at the end. void normalize_store_normals(VertStore &verts) { for (Vec3d &n : verts.nrm) { const double len = n.norm(); n = (len > 0.0) ? Vec3d(n / len) : Vec3d(0.0, 0.0, 1.0); } } // Keyed by the raw parent-vertex pair rather than by position: two sharp-edge copies of one point // need their own midpoints, since their normals differ even though the position does not. int get_midpoint(VertStore &verts, QuantizedPointMap &cache, int a, int b, QuantizedPointMap *pos_canon_map) { const int lo = std::min(a, b), hi = std::max(a, b); if (const int cached = cache.get_key(lo, hi, 0); cached != -1) return cached; const Vec3d m = (verts.pos[size_t(a)] + verts.pos[size_t(b)]) * 0.5; Vec3d n = verts.nrm[size_t(a)] + verts.nrm[size_t(b)]; const double nl = n.norm(); n = (nl > 0.0) ? Vec3d(n / nl) : verts.nrm[size_t(a)]; const int idx = verts.push(m, n); if (!verts.wgt.empty()) verts.wgt.push_back((verts.wgt[size_t(a)] + verts.wgt[size_t(b)]) * 0.5); if (!verts.canon.empty() && pos_canon_map != nullptr) verts.canon.push_back(pos_canon_map->get_or_set(float(m.x()), float(m.y()), float(m.z()), idx)); cache.get_or_set_key(lo, hi, 0, idx); return idx; } struct PassResult { std::vector indices; std::vector face_excluded; std::vector face_parent_id; bool changed = false; bool capped = false; }; // Three steps, so that no T-junction can appear: // 1. Mark every too-long edge globally, so both triangles on a shared edge decide alike. // 1.5 Predict the exact resulting count from the marks (0->1, 1->2, 2->3, 3->4) and abort the // *whole* pass if it exceeds the cap - a partial pass leaves split parents beside unsplit // neighbours, the very crack step 1 prevents. // 2. Rebuild, allocating once at the now-known size. PassResult subdivide_pass(VertStore &verts, const std::vector &indices, double max_edge_length, int safety_cap, const std::vector &face_excluded, QuantizedPointMap *pos_canon_map, const std::vector &face_parent_id) { PassResult out; const double max_sq = max_edge_length * max_edge_length; const size_t tri_count = indices.size() / 3; const bool have_canon = !verts.canon.empty(); // Sized from the pass rather than grown from 64 k: a fine pass marks on the order of one edge per // triangle, and growing to that by doubling rehashes the whole table a dozen times. const size_t expect = std::max(size_t(1) << 16, tri_count); QuantizedPointMap mid_cache(1.0, expect); QuantizedPointMap split_edges(1.0, expect); // With canonical ids the key is the canonical *position* id, so split copies either side of a // sharp edge see one another's decision; without them the vertex index serves. const auto key_of = [&](int v) -> int64_t { return have_canon ? verts.canon[size_t(v)] : v; }; const auto mark_edge = [&](int a, int b) { const int64_t u = key_of(a), v = key_of(b); if (u < v) split_edges.get_or_set_key(u, v, 0, 1); else split_edges.get_or_set_key(v, u, 0, 1); }; const auto is_marked = [&](int a, int b) { const int64_t u = key_of(a), v = key_of(b); return (u < v ? split_edges.get_key(u, v, 0) : split_edges.get_key(v, u, 0)) != -1; }; // Step 1. An excluded triangle marks none of its own edges, so its interior never refines; its // boundary edges are still marked by an included neighbour, and it follows that split. for (size_t t = 0; t < tri_count; ++t) { if (!face_excluded.empty() && face_excluded[t]) continue; const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2]; if (edge_len_sq(verts, a, b) > max_sq) mark_edge(a, b); if (edge_len_sq(verts, b, c) > max_sq) mark_edge(b, c); if (edge_len_sq(verts, c, a) > max_sq) mark_edge(c, a); } if (split_edges.size() == 0) { out.indices = indices; out.face_excluded = face_excluded; out.face_parent_id = face_parent_id; return out; // changed stays false: nothing left to refine } // Step 1.5. Read-only against the finished mark set, so it runs in parallel - and it keeps each // triangle's three marks (bit 0 = ab, 1 = bc, 2 = ca), so the serial rebuild below reads a byte // instead of probing the hash map three more times per triangle. std::vector tri_marks(tri_count); const size_t predicted = tbb::parallel_reduce( tbb::blocked_range(0, tri_count), size_t(0), [&](const tbb::blocked_range &range, size_t acc) { for (size_t t = range.begin(); t < range.end(); ++t) { const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2]; const uint8_t m = uint8_t(is_marked(a, b)) | uint8_t(is_marked(b, c) << 1) | uint8_t(is_marked(c, a) << 2); tri_marks[t] = m; const int n = (m & 1) + ((m >> 1) & 1) + ((m >> 2) & 1); acc += (n == 0) ? 1 : size_t(n + 1); } return acc; }, std::plus()); if (predicted > size_t(safety_cap)) { out.indices = indices; out.face_excluded = face_excluded; out.face_parent_id = face_parent_id; out.capped = true; return out; // coarser than asked for, but watertight } // Step 2. out.indices.resize(predicted * 3); if (!face_excluded.empty()) out.face_excluded.resize(predicted); if (!face_parent_id.empty()) out.face_parent_id.resize(predicted); size_t wi = 0, fi = 0; const auto emit_face_data = [&](uint8_t excl, int pid, int times) { for (int k = 0; k < times; ++k) { if (!out.face_excluded.empty()) out.face_excluded[fi] = excl; if (!out.face_parent_id.empty()) out.face_parent_id[fi] = pid; ++fi; } }; const auto emit = [&](int x, int y, int z) { out.indices[wi++] = x; out.indices[wi++] = y; out.indices[wi++] = z; }; for (size_t t = 0; t < tri_count; ++t) { const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2]; const uint8_t excl = face_excluded.empty() ? uint8_t(0) : face_excluded[t]; const int pid = face_parent_id.empty() ? 0 : face_parent_id[t]; const bool s_ab = (tri_marks[t] & 1) != 0, s_bc = (tri_marks[t] & 2) != 0, s_ca = (tri_marks[t] & 4) != 0; const int n = int(s_ab) + int(s_bc) + int(s_ca); if (n == 0) { emit(a, b, c); emit_face_data(excl, pid, 1); } else if (n == 3) { // a // / \ // mCA-mAB // / \ / \ // c--mBC--b const int m_ab = get_midpoint(verts, mid_cache, a, b, pos_canon_map); const int m_bc = get_midpoint(verts, mid_cache, b, c, pos_canon_map); const int m_ca = get_midpoint(verts, mid_cache, c, a, pos_canon_map); emit(a, m_ab, m_ca); emit(m_ab, b, m_bc); emit(m_ca, m_bc, c); emit(m_ab, m_bc, m_ca); emit_face_data(excl, pid, 4); } else if (n == 1) { if (s_ab) { const int m = get_midpoint(verts, mid_cache, a, b, pos_canon_map); emit(a, m, c); emit(m, b, c); } else if (s_bc) { const int m = get_midpoint(verts, mid_cache, b, c, pos_canon_map); emit(a, b, m); emit(a, m, c); } else { const int m = get_midpoint(verts, mid_cache, c, a, pos_canon_map); emit(a, b, m); emit(m, b, c); } emit_face_data(excl, pid, 2); } else { // A corner triangle on the untouched-edge vertex, then the remaining quadrilateral split // along the midpoint-to-midpoint diagonal, which keeps the winding consistent. // // A sliver parent propagates: that inner diagonal inherits half the short edge and hands // the sliver to two children per pass. No better diagonal exists - one avoiding the // midpoints must pass through one of them, giving a zero-area triangle. Regularization // removes such slivers before the mesh reaches here. if (!s_ab) { // fan from c const int m_bc = get_midpoint(verts, mid_cache, b, c, pos_canon_map); const int m_ca = get_midpoint(verts, mid_cache, c, a, pos_canon_map); emit(a, b, m_bc); emit(a, m_bc, m_ca); emit(c, m_ca, m_bc); } else if (!s_bc) { // fan from a const int m_ab = get_midpoint(verts, mid_cache, a, b, pos_canon_map); const int m_ca = get_midpoint(verts, mid_cache, c, a, pos_canon_map); emit(a, m_ab, m_ca); emit(m_ab, b, c); emit(m_ab, c, m_ca); } else { // fan from b const int m_ab = get_midpoint(verts, mid_cache, a, b, pos_canon_map); const int m_bc = get_midpoint(verts, mid_cache, b, c, pos_canon_map); emit(b, m_bc, m_ab); emit(a, m_ab, m_bc); emit(a, m_bc, c); } emit_face_data(excl, pid, 3); } } out.changed = true; return out; } } // namespace IndexedMesh to_indexed_fast(const TriSoup &geometry) { // Preview path: a plain position merge - no clustering, no sharp-edge splitting, no canonical ids. IndexedMesh out; const size_t n = geometry.pos.size(); QuantizedPointMap vert_map(WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22)); out.indices.resize(n); const bool has_w = !geometry.exclude_weight.empty(); for (size_t i = 0; i < n; ++i) { const Vec3f &p = geometry.pos[i]; const Vec3f nf = geometry.nrm.empty() ? Vec3f(0.f, 0.f, 1.f) : geometry.nrm[i]; const int idx = vert_map.get_or_set(p, int(out.verts.count())); if (vert_map.inserted()) { out.verts.push(p.cast(), nf.cast()); if (has_w) out.verts.wgt.push_back(double(geometry.exclude_weight[i])); } else { out.verts.nrm[size_t(idx)] += nf.cast(); // Merge exclusion by maximum: any excluded face marks the shared vertex. if (has_w && double(geometry.exclude_weight[i]) > out.verts.wgt[size_t(idx)]) out.verts.wgt[size_t(idx)] = double(geometry.exclude_weight[i]); } out.indices[i] = idx; } normalize_store_normals(out.verts); return out; } IndexedMesh to_indexed(const TriSoup &geometry) { // Export path. Two vertices at one position merge only when their face normals agree to within // SUBDIVIDE_SHARP_ANGLE_DEG, which keeps a cylinder from faceting while stopping a cube's edge // normal from leaking into the flat face interiors as subdivision carries it inward. IndexedMesh out; out.has_canon = true; const size_t n = geometry.pos.size(); const bool has_w = !geometry.exclude_weight.empty(); const double sharp_cos = std::cos(SUBDIVIDE_SHARP_ANGLE_DEG * M_PI / 180.0); // Per-face normals: unit for the angle test, raw for the area-weighted accumulation. std::vector face_unit(n), face_raw(n); tbb::parallel_for(tbb::blocked_range(0, n / 3), [&](const tbb::blocked_range &range) { for (size_t f = range.begin(); f < range.end(); ++f) { const size_t t = f * 3; const Vec3d a = geometry.pos[t].cast(); const Vec3d r = (geometry.pos[t + 1].cast() - a).cross( geometry.pos[t + 2].cast() - a); const double len = r.norm(); const Vec3d u = (len > 0.0) ? Vec3d(r / len) : Vec3d(0.0, 0.0, 1.0); for (int v = 0; v < 3; ++v) { face_unit[t + size_t(v)] = u; face_raw[t + size_t(v)] = r; } } }); out.indices.resize(n); out.pos_canon_map = QuantizedPointMap(WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22)); struct Cluster { int idx; Vec3d fn_unit; }; std::unordered_map> clusters_by_canon; for (size_t i = 0; i < n; ++i) { const Vec3f &p = geometry.pos[i]; // The first vertex at a position becomes its canonical id; later split copies share it. const int canon_id = out.pos_canon_map.get_or_set(p, int(out.verts.count())); const bool fresh_position = out.pos_canon_map.inserted(); const auto add_vertex = [&](int canon) { const int idx = out.verts.push(p.cast(), face_raw[i]); if (has_w) out.verts.wgt.push_back(double(geometry.exclude_weight[i])); out.verts.canon.push_back(canon); return idx; }; if (fresh_position) { const int idx = add_vertex(canon_id); clusters_by_canon[canon_id].push_back({ idx, face_unit[i] }); out.indices[i] = idx; continue; } std::vector &clusters = clusters_by_canon[canon_id]; bool matched = false; for (Cluster &cl : clusters) { if (cl.fn_unit.dot(face_unit[i]) < sharp_cos) continue; out.verts.nrm[size_t(cl.idx)] += face_raw[i]; if (has_w && double(geometry.exclude_weight[i]) > out.verts.wgt[size_t(cl.idx)]) out.verts.wgt[size_t(cl.idx)] = double(geometry.exclude_weight[i]); // Track the running average, so gradual curvature stays in one cluster instead of // fragmenting when a distant face exceeds the threshold against the seed's fixed normal. cl.fn_unit += face_unit[i]; if (const double rl = cl.fn_unit.norm(); rl > 0.0) cl.fn_unit /= rl; out.indices[i] = cl.idx; matched = true; break; } if (!matched) { // A sharp-edge split: a new vertex at the same position, sharing its canonical id. const int idx = add_vertex(canon_id); clusters.push_back({ idx, face_unit[i] }); out.indices[i] = idx; } } normalize_store_normals(out.verts); return out; } TriSoup to_non_indexed(const VertStore &verts, const std::vector &indices, const std::vector &face_excluded) { TriSoup out; const size_t tri_count = indices.size() / 3; out.pos.resize(tri_count * 3); out.nrm.resize(tri_count * 3); const bool want_weights = !face_excluded.empty() || !verts.wgt.empty(); if (want_weights) out.exclude_weight.resize(tri_count * 3); // Each triangle writes only its own three slots, so this is a plain parallel gather. tbb::parallel_for(tbb::blocked_range(0, tri_count), [&](const tbb::blocked_range &r) { for (size_t t = r.begin(); t < r.end(); ++t) { // The per-face flag, not the interpolated weight: merging by maximum can push an *included* // face's corners to 1 when it borders two excluded neighbours, wrongly excluding it. const bool have_face_flag = !face_excluded.empty(); const float face_w = have_face_flag ? (face_excluded[t] ? 1.f : 0.f) : 0.f; for (int v = 0; v < 3; ++v) { const size_t vidx = size_t(indices[t * 3 + size_t(v)]); out.pos[t * 3 + size_t(v)] = verts.pos[vidx].cast(); out.nrm[t * 3 + size_t(v)] = verts.nrm[vidx].cast(); if (want_weights) out.exclude_weight[t * 3 + size_t(v)] = have_face_flag ? face_w : float(verts.wgt[vidx]); } } }); return out; } SubdivideResult subdivide(const TriSoup &geometry, double max_edge_length, const std::vector &face_excluded, bool fast, int safety_cap, const SubdivideProgressFn &on_progress) { SubdivideResult result; if (geometry.empty() || max_edge_length <= 0.0) { result.geometry = geometry; return result; } IndexedMesh indexed = fast ? to_indexed_fast(geometry) : to_indexed(geometry); QuantizedPointMap *canon_map = indexed.has_canon ? &indexed.pos_canon_map : nullptr; const size_t initial_tris = indexed.indices.size() / 3; std::vector current_indices = std::move(indexed.indices); // nothing reads it again std::vector current_excluded = face_excluded; std::vector current_parent(initial_tris); for (size_t i = 0; i < initial_tris; ++i) current_parent[i] = int(i); for (int iter = 0; iter < SUBDIVIDE_MAX_ITERATIONS; ++iter) { if (current_indices.size() / 3 >= size_t(safety_cap)) { result.safety_cap_hit = true; break; } PassResult pass = subdivide_pass(indexed.verts, current_indices, max_edge_length, safety_cap, current_excluded, canon_map, current_parent); current_indices = std::move(pass.indices); if (!pass.face_excluded.empty()) current_excluded = std::move(pass.face_excluded); if (!pass.face_parent_id.empty()) current_parent = std::move(pass.face_parent_id); if (pass.capped || current_indices.size() / 3 >= size_t(safety_cap)) result.safety_cap_hit = true; if (on_progress) { // Reported after the pass, so the value falls each iteration instead of lagging a step. // A max is order-independent, so the scan reduces in parallel to the same value. const double max_edge_sq = tbb::parallel_reduce( tbb::blocked_range(0, current_indices.size() / 3), 0.0, [&](const tbb::blocked_range &r, double acc) { for (size_t f = r.begin(); f < r.end(); ++f) { const int a = current_indices[f * 3], b = current_indices[f * 3 + 1], c = current_indices[f * 3 + 2]; acc = std::max({ acc, edge_len_sq(indexed.verts, a, b), edge_len_sq(indexed.verts, b, c), edge_len_sq(indexed.verts, c, a) }); } return acc; }, [](double x, double y) { return std::max(x, y); }); if (!on_progress(std::min(0.95, double(iter + 1) / SUBDIVIDE_MAX_ITERATIONS), current_indices.size() / 3, std::sqrt(max_edge_sq))) break; // whole passes only, so what we have is still crack-free } if (!pass.changed || result.safety_cap_hit) break; } result.geometry = to_non_indexed(indexed.verts, current_indices, current_excluded); result.face_parent_id = std::move(current_parent); return result; } } // namespace TextureBake } // namespace Slic3r