diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index a0677e332e..56b2a1168f 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -464,6 +464,22 @@ set(lisbslic3r_sources TextConfiguration.hpp TextureDisplacement.cpp TextureDisplacement.hpp + TextureBake/TextureBakeIndex.cpp + TextureBake/TextureBakeIndex.hpp + TextureBake/TextureBakeSubdivide.cpp + TextureBake/TextureBakeSubdivide.hpp + TextureBake/TextureBakeRegularize.cpp + TextureBake/TextureBakeRegularize.hpp + TextureBake/TextureBakeDisplace.cpp + TextureBake/TextureBakeDisplace.hpp + TextureBake/TextureBakeDecimate.cpp + TextureBake/TextureBakeDecimate.hpp + TextureBake/TextureBakeRepair.cpp + TextureBake/TextureBakeRepair.hpp + TextureBake/TextureBakePipeline.cpp + TextureBake/TextureBakePipeline.hpp + TextureBake/TextureBakeMesh.cpp + TextureBake/TextureBakeMesh.hpp Thread.cpp Thread.hpp Time.cpp diff --git a/src/libslic3r/TextureBake/TextureBakeDecimate.cpp b/src/libslic3r/TextureBake/TextureBakeDecimate.cpp new file mode 100644 index 0000000000..d4744f40a7 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeDecimate.cpp @@ -0,0 +1,478 @@ +#include "TextureBakeDecimate.hpp" + +#include +#include +#include +#include +#include + +namespace Slic3r { +namespace TextureBake { + +namespace { + +// Symmetric 4x4 quadric, as its 10 upper-triangle values. +struct Quadric +{ + std::array q{}; + + void add_plane(double a, double b, double c, double d) + { + q[0] += a * a; q[1] += a * b; q[2] += a * c; q[3] += a * d; + q[4] += b * b; q[5] += b * c; q[6] += b * d; + q[7] += c * c; q[8] += c * d; + q[9] += d * d; + } + void operator+=(const Quadric &o) + { + for (int i = 0; i < 10; ++i) + q[size_t(i)] += o.q[size_t(i)]; + } + double eval(double x, double y, double z) const + { + return q[0] * x * x + 2 * q[1] * x * y + 2 * q[2] * x * z + 2 * q[3] * x + + q[4] * y * y + 2 * q[5] * y * z + 2 * q[6] * y + + q[7] * z * z + 2 * q[8] * z + q[9]; + } +}; + +double eval_sum(const std::vector &qs, int v1, int v2, const Vec3d &p) +{ + return qs[size_t(v1)].eval(p.x(), p.y(), p.z()) + qs[size_t(v2)].eval(p.x(), p.y(), p.z()); +} + +// The position minimising the summed quadric, if the system is well conditioned enough to trust. +bool solve_q(const std::vector &qs, int v1, int v2, Vec3d &out) +{ + const auto &A = qs[size_t(v1)].q; + const auto &B = qs[size_t(v2)].q; + const double a00 = A[0] + B[0], a01 = A[1] + B[1], a02 = A[2] + B[2]; + const double a11 = A[4] + B[4], a12 = A[5] + B[5], a22 = A[7] + B[7]; + const double b0 = -(A[3] + B[3]), b1 = -(A[6] + B[6]), b2 = -(A[8] + B[8]); + + const double det = a00 * (a11 * a22 - a12 * a12) - a01 * (a01 * a22 - a12 * a02) + + a02 * (a01 * a12 - a11 * a02); + const double max_el = std::max({ std::abs(a00), std::abs(a01), std::abs(a02), std::abs(a11), + std::abs(a12), std::abs(a22) }); + // Scaled with the matrix, so it means the same at any model scale. + const double threshold = max_el * max_el * max_el * 1e-10; + if (std::abs(det) < std::max(threshold, 1e-30)) + return false; + + const double inv = 1.0 / det; + out.x() = inv * (b0 * (a11 * a22 - a12 * a12) - a01 * (b1 * a22 - a12 * b2) + a02 * (b1 * a12 - a11 * b2)); + out.y() = inv * (a00 * (b1 * a22 - a12 * b2) - b0 * (a01 * a22 - a12 * a02) + a02 * (a01 * b2 - b1 * a02)); + out.z() = inv * (a00 * (a11 * b2 - b1 * a12) - a01 * (a01 * b2 - b1 * a02) + b0 * (a01 * a12 - a11 * a02)); + return true; +} + +Vec3d face_normal_unit(const std::vector &pos, int a, int b, int c) +{ + const Vec3d n = (pos[size_t(b)] - pos[size_t(a)]).cross(pos[size_t(c)] - pos[size_t(a)]); + const double len = n.norm(); + return (len > 0.0) ? Vec3d(n / len) : Vec3d::Zero(); +} + +// Versions are captured at push time; a mismatch on pop means a later collapse invalidated the entry. +// Lazy deletion, far cheaper than removing entries eagerly. +struct HeapEntry +{ + double cost; + int v1, v2; + uint32_t ver1, ver2; + Vec3d p; + bool operator>(const HeapEntry &o) const { return cost > o.cost; } +}; + +} // namespace + +DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool harvest_flat, + double harvest_tol, const std::vector &locked_faces, + const DecimateProgressFn &on_progress) +{ + DecimateResult result; + const size_t n = geometry.pos.size(); + if (n < 3) { + result.geometry = geometry; + return result; + } + + // The finest grid. Anything coarser fuses distinct fine-feature vertices on a displaced mesh, + // leaving it non-manifold before decimation starts and producing open edges afterwards. + QuantizedPointMap vert_map(WELD_GRID_DECIMATION, std::min(n, size_t(1) << 22)); + std::vector pos; + std::vector remap(n); + for (size_t i = 0; i < n; ++i) { + const int idx = vert_map.get_or_set(geometry.pos[i], int(pos.size())); + if (vert_map.inserted()) + pos.push_back(geometry.pos[i].cast()); + remap[i] = idx; + } + const size_t vert_count = pos.size(); + const size_t face_count = n / 3; + std::vector faces(face_count * 3); + for (size_t i = 0; i < n; ++i) + faces[i] = remap[i]; + + if (face_count <= target_triangles && !harvest_flat) { + result.geometry = geometry; + return result; + } + + // An edge with a locked endpoint never reaches the heap. + std::vector locked_vert; + size_t locked_face_count = 0; + if (!locked_faces.empty()) { + locked_vert.assign(vert_count, 0); + for (size_t f = 0; f < face_count && f < locked_faces.size(); ++f) { + if (!locked_faces[f]) + continue; + ++locked_face_count; + for (int k = 0; k < 3; ++k) + locked_vert[size_t(faces[f * 3 + size_t(k)])] = 1; + } + } + // With the locked faces alone at the target, chasing it would grind the free region to its guard + // limit for nothing - harvest only, and say so. + const bool locked_over_budget = + !locked_vert.empty() && face_count > target_triangles && locked_face_count >= target_triangles; + result.locked_over_budget = locked_over_budget; + if (locked_over_budget && !harvest_flat) { + result.geometry = geometry; + return result; + } + + std::vector quadrics(vert_count); + for (size_t f = 0; f < face_count; ++f) { + const int a = faces[f * 3], b = faces[f * 3 + 1], c = faces[f * 3 + 2]; + if (a < 0) + continue; + const Vec3d nrm = face_normal_unit(pos, a, b, c); + if (nrm.isZero()) + continue; + const double d = -nrm.dot(pos[size_t(a)]); + for (const int v : { a, b, c }) + quadrics[size_t(v)].add_plane(nrm.x(), nrm.y(), nrm.z(), d); + } + + // Two penalty planes per endpoint on a sharp interior edge, each perpendicular to one adjacent + // face and containing the edge, constraining the vertex to the crease line. + { + struct EdgeRec { int va, vb, f0, f1; uint8_t count; }; + std::vector edges; + QuantizedPointMap edge_idx(1.0, std::min(face_count * 3, size_t(1) << 22)); + for (size_t f = 0; f < face_count; ++f) { + if (faces[f * 3] < 0) + continue; + for (int e = 0; e < 3; ++e) { + const int va = faces[f * 3 + size_t(e)]; + const int vb = faces[f * 3 + size_t((e + 1) % 3)]; + const int lo = std::min(va, vb), hi = std::max(va, vb); + const int ei = edge_idx.get_or_set_key(lo, hi, 0, int(edges.size())); + if (edge_idx.inserted()) + edges.push_back({ lo, hi, int(f), -1, 1 }); + else if (edges[size_t(ei)].count == 1) { + edges[size_t(ei)].f1 = int(f); + edges[size_t(ei)].count = 2; + } else + // Non-manifold; never feeds a crease. + edges[size_t(ei)].count = 3; + } + } + + const double sqrt_w = std::sqrt(DECIMATE_CREASE_WEIGHT); + for (const EdgeRec &er : edges) { + if (er.count != 2) + continue; // boundary or non-manifold + const Vec3d n0 = face_normal_unit(pos, faces[size_t(er.f0) * 3], faces[size_t(er.f0) * 3 + 1], + faces[size_t(er.f0) * 3 + 2]); + const Vec3d n1 = face_normal_unit(pos, faces[size_t(er.f1) * 3], faces[size_t(er.f1) * 3 + 1], + faces[size_t(er.f1) * 3 + 2]); + if (n0.dot(n1) >= DECIMATE_CREASE_COS) + continue; // smooth enough to be no crease + + const Vec3d e = pos[size_t(er.vb)] - pos[size_t(er.va)]; + const double elen = e.norm(); + if (elen <= 0.0) + continue; + const Vec3d ed = e / elen; + for (const Vec3d &fn : { n0, n1 }) { + Vec3d pn = fn.cross(ed); + const double plen = pn.norm(); + if (plen < 1e-10) + continue; // edge parallel to the face normal + pn /= plen; + const double d = -pn.dot(pos[size_t(er.va)]); + // sqrt(w) on the inputs gives w times the accumulated products. + for (const int v : { er.va, er.vb }) + quadrics[size_t(v)].add_plane(pn.x() * sqrt_w, pn.y() * sqrt_w, pn.z() * sqrt_w, + d * sqrt_w); + } + } + } + + // Vertex-face incidence as intrusive linked lists of slots over flat arrays. + const size_t S = face_count * 3; + std::vector vf_head(vert_count, -1), slot_face(S), slot_vert(S), slot_next(S, -1), + slot_prev(S, -1), face_slot(S, -1); + for (size_t f = 0; f < face_count; ++f) + for (int k = 0; k < 3; ++k) { + const int s = int(f) * 3 + k; + const int v = faces[size_t(s)]; + slot_face[size_t(s)] = int(f); + slot_vert[size_t(s)] = v; + slot_next[size_t(s)] = vf_head[size_t(v)]; + slot_prev[size_t(s)] = -1; + if (vf_head[size_t(v)] >= 0) + slot_prev[size_t(vf_head[size_t(v)])] = s; + vf_head[size_t(v)] = s; + face_slot[size_t(s)] = s; + } + const auto unlink_slot = [&](int s) { + const int p = slot_prev[size_t(s)], nx = slot_next[size_t(s)]; + if (p >= 0) slot_next[size_t(p)] = nx; + else vf_head[size_t(slot_vert[size_t(s)])] = nx; + if (nx >= 0) slot_prev[size_t(nx)] = p; + }; + const auto move_slot = [&](int s, int nv) { + unlink_slot(s); + slot_next[size_t(s)] = vf_head[size_t(nv)]; + slot_prev[size_t(s)] = -1; + if (vf_head[size_t(nv)] >= 0) + slot_prev[size_t(vf_head[size_t(nv)])] = s; + vf_head[size_t(nv)] = s; + slot_vert[size_t(s)] = nv; + }; + + std::vector active(vert_count, 1); + std::vector version(vert_count, 0); + std::vector nb_stamp(vert_count, 0), lk_stamp(vert_count, 0); + uint32_t epoch = 1, lk_epoch = 1; + size_t active_faces = face_count; + + std::priority_queue, std::greater> heap; + + const auto push_edge = [&](int v1, int v2) { + Vec3d p; + if (!solve_q(quadrics, v1, v2, p)) { + const Vec3d mid = (pos[size_t(v1)] + pos[size_t(v2)]) * 0.5; + const double e1 = eval_sum(quadrics, v1, v2, pos[size_t(v1)]); + const double e2 = eval_sum(quadrics, v1, v2, pos[size_t(v2)]); + const double em = eval_sum(quadrics, v1, v2, mid); + const double emin = std::min({ e1, e2, em }); + const double etol = emin * 1e-2 + 1e-12; + // The midpoint when the three are near-equal, i.e. flat: it moves adjacent triangles + // least, so fewer normal flips and no stalling on coplanar geometry. + if (em <= emin + etol) p = mid; + else if (e1 <= e2) p = pos[size_t(v1)]; + else p = pos[size_t(v2)]; + } + // Where quadric costs are all near zero, shorter edges first keeps triangle quality up. + const double len2 = (pos[size_t(v2)] - pos[size_t(v1)]).squaredNorm(); + heap.push({ eval_sum(quadrics, v1, v2, p) + len2 * 1e-8, v1, v2, version[size_t(v1)], + version[size_t(v2)], p }); + }; + + { + QuantizedPointMap seed_seen(1.0, std::min(face_count * 3, size_t(1) << 22)); + for (size_t f = 0; f < face_count; ++f) { + if (faces[f * 3] < 0) + continue; + for (int e = 0; e < 3; ++e) { + const int va = faces[f * 3 + size_t(e)]; + const int vb = faces[f * 3 + size_t((e + 1) % 3)]; + if (!locked_vert.empty() && (locked_vert[size_t(va)] || locked_vert[size_t(vb)])) + continue; + seed_seen.get_or_set_key(std::min(va, vb), std::max(va, vb), 0, 1); + if (seed_seen.inserted()) + push_edge(va, vb); + } + } + } + + // 0 means a stale entry, 1 a boundary edge, 2 or more safe. + const auto shared_face_count = [&](int v1, int v2) { + int count = 0; + for (int s = vf_head[size_t(v1)]; s >= 0; s = slot_next[size_t(s)]) { + const int f = slot_face[size_t(s)]; + if (faces[size_t(f) * 3] < 0) + continue; + for (int k = 0; k < 3; ++k) + if (faces[size_t(f) * 3 + size_t(k)] == v2) { + if (++count >= 2) + return 2; + break; + } + } + return count; + }; + + // Safe only when the sole common neighbours of the endpoints are the apexes of the faces the edge + // already shares; any other would pile a third triangle onto an edge after the collapse. + const auto has_link_violation = [&](int v1, int v2, uint32_t ep) { + for (int s = vf_head[size_t(v1)]; s >= 0; s = slot_next[size_t(s)]) { + const int f = slot_face[size_t(s)]; + if (faces[size_t(f) * 3] < 0) + continue; + for (int k = 0; k < 3; ++k) + if (const int x = faces[size_t(f) * 3 + size_t(k)]; x != v1) + lk_stamp[size_t(x)] = ep; + } + int shared = 0; + for (int s = vf_head[size_t(v1)]; s >= 0; s = slot_next[size_t(s)]) { + const int f = slot_face[size_t(s)]; + if (faces[size_t(f) * 3] < 0) + continue; + const int a = faces[size_t(f) * 3], b = faces[size_t(f) * 3 + 1], c = faces[size_t(f) * 3 + 2]; + if (a == v2 || b == v2 || c == v2) { + ++shared; + const int apex = (a != v1 && a != v2) ? a : (b != v1 && b != v2) ? b : c; + lk_stamp[size_t(apex)] = ep + 1; // a legal shared-face apex + } + } + if (shared > 2) + return true; // already non-manifold + for (int s = vf_head[size_t(v2)]; s >= 0; s = slot_next[size_t(s)]) { + const int f = slot_face[size_t(s)]; + if (faces[size_t(f) * 3] < 0) + continue; + for (int k = 0; k < 3; ++k) { + const int x = faces[size_t(f) * 3 + size_t(k)]; + if (x != v2 && x != v1 && lk_stamp[size_t(x)] == ep) + return true; + } + } + return false; + }; + + // Squared-dot, so no square root or division. Faces containing the other endpoint are the ones + // being removed, so they are skipped. + const auto check_flipped = [&](int vc, int vo, const Vec3d &np) { + for (int s = vf_head[size_t(vc)]; s >= 0; s = slot_next[size_t(s)]) { + const size_t f = size_t(slot_face[size_t(s)]); + if (faces[f * 3] < 0) + continue; + const int fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2]; + if (fa == vo || fb == vo || fc == vo) + continue; + const Vec3d oa = pos[size_t(fa)], ob = pos[size_t(fb)], oc = pos[size_t(fc)]; + const Vec3d on = (ob - oa).cross(oc - oa); + const Vec3d na = (fa == vc) ? np : oa; + const Vec3d nb = (fb == vc) ? np : ob; + const Vec3d nc = (fc == vc) ? np : oc; + const Vec3d nn = (nb - na).cross(nc - na); + const double raw = on.dot(nn); + if (raw < 0.0) + return true; + if (raw * raw < DECIMATE_FLIP_DOT * DECIMATE_FLIP_DOT * on.squaredNorm() * nn.squaredNorm()) + return true; + } + return false; + }; + + const size_t init_faces = active_faces; + const size_t to_remove = std::max(1, init_faces > target_triangles + ? init_faces - target_triangles : init_faces); + const double harvest_ceil = harvest_tol * harvest_tol; + bool reached_target = locked_over_budget; + double last_progress = 0.0; + + while (!heap.empty()) { + if (active_faces <= target_triangles) { + if (!harvest_flat) + break; + reached_target = true; + } + + const HeapEntry top = heap.top(); + heap.pop(); + // The popped entry is the cheapest left, so exceeding the tolerance ends the run. + if (reached_target && top.cost > harvest_ceil) + break; + + const int v1 = top.v1, v2 = top.v2; + if (!active[size_t(v1)] || !active[size_t(v2)]) + continue; + if (version[size_t(v1)] != top.ver1 || version[size_t(v2)] != top.ver2) + continue; + if (shared_face_count(v1, v2) < 2) + continue; + lk_epoch += 2; // +2 so ep and ep+1 cannot collide with the next call + if (has_link_violation(v1, v2, lk_epoch)) + continue; + if (check_flipped(v1, v2, top.p) || check_flipped(v2, v1, top.p)) + continue; + + // v1 survives at the new position, v2 goes. + pos[size_t(v1)] = top.p; + quadrics[size_t(v1)] += quadrics[size_t(v2)]; + ++version[size_t(v1)]; + + for (int s = vf_head[size_t(v2)]; s >= 0;) { + const size_t f = size_t(slot_face[size_t(s)]); + const int s_next = slot_next[size_t(s)]; // read before the list is modified + if (faces[f * 3] >= 0) { + for (int k = 0; k < 3; ++k) + if (faces[f * 3 + size_t(k)] == v2) { + faces[f * 3 + size_t(k)] = v1; + break; + } + const int fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2]; + if (fa == fb || fb == fc || fa == fc) { + for (int k = 0; k < 3; ++k) + if (const int sk = face_slot[f * 3 + size_t(k)]; sk >= 0) { + unlink_slot(sk); + face_slot[f * 3 + size_t(k)] = -1; + } + faces[f * 3] = faces[f * 3 + 1] = faces[f * 3 + 2] = -1; + --active_faces; + } else + move_slot(s, v1); + } + s = s_next; + } + active[size_t(v2)] = 0; + + ++epoch; + for (int sv = vf_head[size_t(v1)]; sv >= 0; sv = slot_next[size_t(sv)]) { + const size_t f = size_t(slot_face[size_t(sv)]); + if (faces[f * 3] < 0) + continue; + for (int k = 0; k < 3; ++k) { + const int nb = faces[f * 3 + size_t(k)]; + if (nb == v1 || nb_stamp[size_t(nb)] == epoch) + continue; + nb_stamp[size_t(nb)] = epoch; + // v1 is never locked - a locked edge never entered the heap. + if (active[size_t(nb)] && (locked_vert.empty() || !locked_vert[size_t(nb)])) + push_edge(v1, nb); + } + } + + if (on_progress) { + const double p = std::min(1.0, double(init_faces - active_faces) / double(to_remove)); + if (p - last_progress > 0.005) { + last_progress = p; + if (!on_progress(p)) + break; + } + } + } + + // Rebuild from the surviving faces, with per-face normals. + TriSoup &out = result.geometry; + for (size_t f = 0; f < face_count; ++f) { + if (faces[f * 3] < 0) + continue; + const Vec3f a = pos[size_t(faces[f * 3])].cast(); + const Vec3f b = pos[size_t(faces[f * 3 + 1])].cast(); + const Vec3f c = pos[size_t(faces[f * 3 + 2])].cast(); + const Vec3f nrm = (b - a).cross(c - a).normalized(); + out.pos.insert(out.pos.end(), { a, b, c }); + out.nrm.insert(out.nrm.end(), { nrm, nrm, nrm }); + } + return result; +} + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeDecimate.hpp b/src/libslic3r/TextureBake/TextureBakeDecimate.hpp new file mode 100644 index 0000000000..8876ab00df --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeDecimate.hpp @@ -0,0 +1,56 @@ +#pragma once + +// Quadric error metric decimation (Garland & Heckbert), with two additions that matter on a +// displaced mesh. +// +// Crease quadrics: an interior edge sharper than the threshold gets penalty planes at both endpoints, +// perpendicular to each adjacent face and through the edge, weighted so such edges collapse last or +// not at all. A texture's hard step keeps its geometry while the flat ground around it reduces. +// +// Flat-face harvesting: the loop keeps going past the triangle target while each collapse's error +// stays under an absolute bound, so flat faces that cost nothing to remove are not left behind. + +#include +#include +#include + +#include "TextureBakeIndex.hpp" + +namespace Slic3r { +namespace TextureBake { + +// Reject a collapse deviating more than about 78 degrees from the old face normal. +static constexpr double DECIMATE_FLIP_DOT = 0.2; +// Edges sharper than 60 degrees are treated as creases. +static constexpr double DECIMATE_CREASE_COS = 0.5; +// Quadric penalty weight for a crease plane. +static constexpr double DECIMATE_CREASE_WEIGHT = 1e4; + +// Upper bound in mm on the deviation a harvested collapse may introduce; the real one is smaller, +// since the cost sums squared distances over all incident faces. +// +// Absolute, not relative to the cost at which the target was crossed. A relative band fails in the +// case with the most to shed: when the target is reached with a large flat surplus left, the crossing +// cost is essentially zero, so the band is too and nothing is harvested. +static constexpr double DECIMATE_DEFAULT_HARVEST_TOL = 0.005; + +// Returns false to cancel. +using DecimateProgressFn = std::function; + +struct DecimateResult +{ + TriSoup geometry; + // The locked faces alone met the target, so it was unreachable without touching preserved + // geometry. + bool locked_over_budget = false; +}; + +// `locked_faces`: one entry per input triangle; a vertex touching one may neither move nor be +// removed, which also pins the ring between the two regions. +DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool harvest_flat = true, + double harvest_tol = DECIMATE_DEFAULT_HARVEST_TOL, + const std::vector &locked_faces = {}, + const DecimateProgressFn &on_progress = {}); + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeDisplace.cpp b/src/libslic3r/TextureBake/TextureBakeDisplace.cpp new file mode 100644 index 0000000000..ef417a7bef --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeDisplace.cpp @@ -0,0 +1,268 @@ +#include "TextureBakeDisplace.hpp" + +#include +#include +#include + +namespace Slic3r { +namespace TextureBake { + +TriSoup apply_displacement(const TriSoup &geometry, const HeightSampleFn &sample, + const DisplaceSettings &settings, const DisplaceBounds &bounds, + const DisplaceProgressFn &on_progress) +{ + TriSoup out; + const size_t count = geometry.pos.size(); + if (count == 0 || !sample) + return geometry; + + out.pos.resize(count); + out.nrm.resize(count); + + // Everything below is keyed by this id, which is what makes one vector per position expressible. + const bool need_id_positions = settings.boundary_falloff > 0.f; + QuantizedPointMap dedup(WELD_GRID_GEOMETRY, std::min(count, size_t(1) << 22)); + std::vector vertex_id(count); + std::vector id_pos; + int next_id = 0; + for (size_t i = 0; i < count; ++i) { + const int id = dedup.get_or_set(geometry.pos[i], next_id); + if (dedup.inserted()) { + ++next_id; + if (need_id_positions) + id_pos.push_back(geometry.pos[i]); + } + vertex_id[i] = id; + } + const size_t unique_count = size_t(next_id); + + // Pass 1: area-weighted smooth normals per position, plus what masking and falloff need. + std::vector smooth_nrm(unique_count, Vec3d::Zero()); + std::vector masked_area(unique_count, 0.0), total_area(unique_count, 0.0); + const bool have_weights = !geometry.exclude_weight.empty(); + std::vector user_excluded_face(have_weights ? count / 3 : 0, 0); + std::vector excluded_pos(have_weights ? unique_count : 0, 0); + + for (size_t t = 0; t + 2 < count; t += 3) { + const Vec3d a = geometry.pos[t].cast(); + const Vec3d face_n = (geometry.pos[t + 1].cast() - a).cross(geometry.pos[t + 2].cast() - a); + const double face_area = face_n.norm(); // twice the triangle area, so weighting is natural + const double nz = face_area > 1e-12 ? face_n.z() / face_area : 0.0; + const double face_angle = std::acos(std::min(1.0, std::abs(nz))) * (180.0 / M_PI); + const bool angle_masked = + nz < 0.0 ? (settings.bottom_angle_limit > 0.f && face_angle <= settings.bottom_angle_limit) + : (settings.top_angle_limit > 0.f && face_angle <= settings.top_angle_limit); + + // Thresholded high, not at a half: merging by maximum leaves a face bordering an excluded one + // with two corners at 1.0, averaging about 0.67, which a half threshold would misread. + bool user_excluded = false; + if (have_weights) { + const float avg = (geometry.exclude_weight[t] + geometry.exclude_weight[t + 1] + + geometry.exclude_weight[t + 2]) / 3.f; + user_excluded = avg > 0.99f; + if (user_excluded) + user_excluded_face[t / 3] = 1; + } + + for (int v = 0; v < 3; ++v) { + const size_t vid = size_t(vertex_id[t + size_t(v)]); + if (user_excluded && have_weights) + excluded_pos[vid] = 1; + // Subdivision split vertices at sharp edges, so these are smooth across soft edges and + // sharp across hard ones - no faceting on round surfaces, no rounding of corners. + smooth_nrm[vid] += geometry.nrm[t + size_t(v)].cast() * face_area; + if (angle_masked) + masked_area[vid] += face_area; + total_area[vid] += face_area; + } + } + + // The pre-normalisation magnitude over the total area says how much the neighbouring faces agree: + // near 1 they do, near 0 they cancelled, meaning a knife edge with no usable surface direction. + std::vector reliability(unique_count, 0.0); + for (size_t id = 0; id < unique_count; ++id) { + const double len = smooth_nrm[id].norm(); + reliability[id] = (len > 0.0 && total_area[id] > 0.0) ? len / total_area[id] : 0.0; + smooth_nrm[id] = (len > 0.0) ? Vec3d(smooth_nrm[id] / len) : Vec3d(0.0, 0.0, 1.0); + } + + // Pass 1.5: the smoothed blend normal - see the header for why it is separate. + std::vector blend_nrm = smooth_nrm; + if (settings.blend_normal_smoothing > 0 && unique_count > 0) { + // CSR adjacency over the welded graph, deliberately a multigraph: duplicates weight a pair by + // how often it shares an edge, so a well-connected surface couples more strongly. + std::vector degree(unique_count, 0); + const auto add_degree = [&](int a, int b) { + if (a != b) { ++degree[size_t(a)]; ++degree[size_t(b)]; } + }; + for (size_t t = 0; t + 2 < count; t += 3) { + const int a = vertex_id[t], b = vertex_id[t + 1], c = vertex_id[t + 2]; + add_degree(a, b); add_degree(b, c); add_degree(c, a); + } + std::vector csr_start(unique_count + 1, 0); + for (size_t id = 0; id < unique_count; ++id) + csr_start[id + 1] = csr_start[id] + degree[id]; + std::vector neighbors(csr_start[unique_count]); + std::vector cursor(unique_count, 0); + const auto add_edge = [&](int a, int b) { + if (a == b) + return; + neighbors[csr_start[size_t(a)] + cursor[size_t(a)]++] = uint32_t(b); + neighbors[csr_start[size_t(b)] + cursor[size_t(b)]++] = uint32_t(a); + }; + for (size_t t = 0; t + 2 < count; t += 3) { + const int a = vertex_id[t], b = vertex_id[t + 1], c = vertex_id[t + 2]; + add_edge(a, b); add_edge(b, c); add_edge(c, a); + } + + std::vector cur = smooth_nrm, nxt(unique_count, Vec3d::Zero()); + for (int iter = 0; iter < settings.blend_normal_smoothing; ++iter) { + for (size_t id = 0; id < unique_count; ++id) { + const uint32_t s = csr_start[id], e = csr_start[id + 1]; + if (e == s) { + nxt[id] = cur[id]; + continue; + } + Vec3d sum = Vec3d::Zero(); + for (uint32_t k = s; k < e; ++k) + sum += cur[neighbors[k]]; + sum /= double(e - s); + const double len = sum.norm(); + // Cancelling neighbours mean a knife edge; keep what we had. + nxt[id] = (len > 1e-12) ? Vec3d(sum / len) : cur[id]; + } + cur.swap(nxt); + } + blend_nrm = std::move(cur); + } + + // A boundary position borders both masked and unmasked faces, or sits on the exclusion seam. + // Every other position gets its distance to the nearest one, ramped to 1 at the falloff distance. + std::vector falloff; + if (settings.boundary_falloff > 0.f && unique_count > 0) { + std::vector boundary; + for (size_t id = 0; id < unique_count; ++id) { + const double frac = total_area[id] > 0.0 ? masked_area[id] / total_area[id] : 0.0; + const bool on_excl = !excluded_pos.empty() && excluded_pos[id] != 0; + if (on_excl || (frac > 0.0 && frac < 1.0)) + boundary.push_back(id_pos[id]); + } + falloff.assign(unique_count, 1.0); + if (!boundary.empty()) { + // A uniform grid: the query is nearest-point only, so a tree costs more than it saves. + Vec3f lo = boundary.front(), hi = boundary.front(); + for (const Vec3f &p : boundary) { + lo = lo.cwiseMin(p); + hi = hi.cwiseMax(p); + } + const Vec3f span = (hi - lo).cwiseMax(Vec3f(1e-6f, 1e-6f, 1e-6f)); + const int res = std::clamp(int(std::ceil(std::cbrt(double(boundary.size())) * 2.0)), 4, 128); + const Vec3f cell = span / float(res); + const float cell_min = cell.minCoeff(); + const auto cell_of = [&](const Vec3f &p) { + Vec3i32 c; + for (int k = 0; k < 3; ++k) + c[k] = std::clamp(int((p[k] - lo[k]) / span[k] * float(res)), 0, res - 1); + return c; + }; + const auto cell_index = [&](int x, int y, int z) { + return size_t(z) * size_t(res) * size_t(res) + size_t(y) * size_t(res) + size_t(x); + }; + std::vector> grid(size_t(res) * size_t(res) * size_t(res)); + for (size_t i = 0; i < boundary.size(); ++i) { + const Vec3i32 c = cell_of(boundary[i]); + grid[cell_index(c.x(), c.y(), c.z())].push_back(int(i)); + } + + const double radius = double(settings.boundary_falloff); + for (size_t id = 0; id < unique_count; ++id) { + const Vec3f &p = id_pos[id]; + const Vec3i32 c = cell_of(p); + double best = std::numeric_limits::max(); + // Anything in shell r is at least (r - 1) cells away, so once the best found is within + // that bound nothing closer can be hiding further out. + for (int r = 0; r < res; ++r) { + for (int dz = -r; dz <= r; ++dz) + for (int dy = -r; dy <= r; ++dy) + for (int dx = -r; dx <= r; ++dx) { + // The shell only; its interior was covered by a smaller r. + if (r > 0 && std::abs(dx) != r && std::abs(dy) != r && std::abs(dz) != r) + continue; + const int qx = c.x() + dx, qy = c.y() + dy, qz = c.z() + dz; + if (qx < 0 || qy < 0 || qz < 0 || qx >= res || qy >= res || qz >= res) + continue; + for (const int bi : grid[cell_index(qx, qy, qz)]) + best = std::min(best, double((boundary[size_t(bi)] - p).norm())); + } + if (best <= double(r) * double(cell_min)) + break; + } + falloff[id] = (best == std::numeric_limits::max() || radius <= 0.0) + ? 1.0 + : std::clamp(best / radius, 0.0, 1.0); + } + } + } + + // Pass 2: one sample per unique position. + std::vector grey(unique_count, 0.0); + std::vector grey_set(unique_count, 0); + for (size_t i = 0; i < count; ++i) { + const size_t vid = size_t(vertex_id[i]); + if (grey_set[vid]) + continue; + grey_set[vid] = 1; + grey[vid] = double(sample(geometry.pos[i], smooth_nrm[vid].cast(), + blend_nrm[vid].cast())); + } + + // Pass 3: move every copy of a position by the identical vector. + for (size_t i = 0; i < count; ++i) { + const Vec3f &p = geometry.pos[i]; + const size_t vid = size_t(vertex_id[i]); + + // Only angle masking uses the per-position blend, so an excluded face never dims its + // neighbours through a shared vertex. + const bool face_excluded = !user_excluded_face.empty() && user_excluded_face[i / 3] != 0; + // Pinned where an included face shares a position with an excluded one, sealing the boundary. + const bool sealed_boundary = + !face_excluded && !excluded_pos.empty() && excluded_pos[vid] != 0; + const double masked_frac = total_area[vid] > 0.0 ? masked_area[vid] / total_area[vid] : 0.0; + const double centered = settings.symmetric ? (grey[vid] - 0.5) : grey[vid]; + const double ramp = falloff.empty() ? 1.0 : falloff[vid]; + const double disp = (face_excluded || sealed_boundary) + ? 0.0 + : ramp * (1.0 - masked_frac) * centered * double(settings.amplitude); + + Vec3d moved = p.cast() + smooth_nrm[vid] * disp; + + // Stop a partly masked vertex poking through the surface it borders. + if (masked_frac > 0.0) { + if (settings.bottom_angle_limit > 0.f && moved.z() < double(p.z())) moved.z() = double(p.z()); + if (settings.top_angle_limit > 0.f && moved.z() > double(p.z())) moved.z() = double(p.z()); + } + if (settings.no_downward_z && moved.z() < double(p.z())) + moved.z() = double(p.z()); + // A vertex starting on the bottom plane stays there: otherwise a downward-facing face pulls + // *up* where the sample is below mid-grey, leaving bed-contact vertices at differing heights. + if (settings.no_downward_z && double(p.z()) <= double(bounds.min.z()) + 1e-5) + moved.z() = double(p.z()); + + out.pos[i] = moved.cast(); + + if (on_progress && (i % 5000) == 0 && !on_progress(double(i) / double(count))) + return geometry; // cancelled: hand back the input untouched + } + + // Per-face, not averaged across shared positions: averaging can flip an excluded face's normal + // when its neighbours moved outward. + for (size_t t = 0; t + 2 < count; t += 3) { + const Vec3f n = (out.pos[t + 1] - out.pos[t]).cross(out.pos[t + 2] - out.pos[t]).normalized(); + out.nrm[t] = out.nrm[t + 1] = out.nrm[t + 2] = n; + } + out.exclude_weight = geometry.exclude_weight; + return out; +} + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeDisplace.hpp b/src/libslic3r/TextureBake/TextureBakeDisplace.hpp new file mode 100644 index 0000000000..eee6ca0626 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeDisplace.hpp @@ -0,0 +1,68 @@ +#pragma once + +// Displacement along surface normals. +// +// The mesh is non-indexed, so at a shared edge two triangles hold the same position with different +// face normals; displacing each copy along its own normal sends them to different points and opens a +// crack. So one smooth (area-weighted) normal per unique position drives both the sample lookup and +// the displacement direction, every copy moves by the same vector, and the result is watertight by +// construction. Displaced normals are then smooth at hard edges, but the geometry is still faceted, +// so printed edges stay sharp. + +#include +#include +#include + +#include "TextureBakeIndex.hpp" + +namespace Slic3r { +namespace TextureBake { + +// Height at a point, called once per unique welded position. `smooth_normal` is the vector the +// displacement will move along; `blend_normal` is that after smoothing, for projection blend weights. +using HeightSampleFn = std::function; + +struct DisplaceSettings +{ + // Displacement height in mm, applied to the sampled value. + float amplitude = 0.4f; + + // Sample around a mid-grey rest level rather than displacing outward only. + bool symmetric = false; + + // Faces flatter than these (degrees from horizontal) are held back, leaving bed-contact and top + // surfaces alone. 0 disables that side. + float bottom_angle_limit = 5.f; + float top_angle_limit = 0.f; + + // Never move a vertex below its original Z, so no new overhang. The sideways component is kept. + bool no_downward_z = false; + + // Distance in mm over which displacement ramps up from a mask boundary. 0 leaves a hard edge. + float boundary_falloff = 0.f; + + // Laplacian iterations on the blend normal only - the displacement direction must stay the exact + // smooth normal or copies of a position move differently and the mesh cracks. Inside a blend band + // the weight gradient is largest, so a few degrees of vertex-to-vertex jitter multiplies the + // difference between two unrelated height samples into visible seam noise. A no-op on an + // already-smooth surface. + int blend_normal_smoothing = 32; +}; + +// Model extents; only the minimum Z is read, for the bottom-plane clamp. +struct DisplaceBounds +{ + Vec3f min = Vec3f::Zero(); + Vec3f max = Vec3f::Zero(); +}; + +// Returns false to cancel. +using DisplaceProgressFn = std::function; + +TriSoup apply_displacement(const TriSoup &geometry, const HeightSampleFn &sample, + const DisplaceSettings &settings, const DisplaceBounds &bounds, + const DisplaceProgressFn &on_progress = {}); + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeIndex.cpp b/src/libslic3r/TextureBake/TextureBakeIndex.cpp new file mode 100644 index 0000000000..ac7234774b --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeIndex.cpp @@ -0,0 +1,25 @@ +#include "TextureBakeIndex.hpp" + +#include + +namespace Slic3r { +namespace TextureBake { + +WeldResult weld_vertices(const std::vector &positions, double quant) +{ + WeldResult out; + QuantizedPointMap map(quant, std::min(positions.size(), size_t(1) << 22)); + out.vertex_id.resize(positions.size()); + int next_id = 0; + for (size_t i = 0; i < positions.size(); ++i) { + const int id = map.get_or_set(positions[i], next_id); + if (map.inserted()) + ++next_id; + out.vertex_id[i] = id; + } + out.unique_count = next_id; + return out; +} + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeIndex.hpp b/src/libslic3r/TextureBake/TextureBakeIndex.hpp new file mode 100644 index 0000000000..74ef068174 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeIndex.hpp @@ -0,0 +1,164 @@ +#pragma once + +// Vertex welding for the texture bake pipeline. The pipeline works on non-indexed triangle soup, so +// a shared point exists once per incident triangle with float noise between the copies; welding maps +// each quantised position to one integer id. +// +// The three grids below are deliberately not unified - changing one at a call site changes +// watertightness. 100 um matches the precision files are written with; 10 um keeps small fillet +// vertices distinct (they merge at 100 um, giving needle artifacts after displacement) while still +// absorbing float noise; 1 um is what collapse positioning needs. + +#include +#include +#include + +#include "../Point.hpp" + +namespace Slic3r { +namespace TextureBake { + +static constexpr double WELD_GRID_EXPORT = 1e4; // 100 um +static constexpr double WELD_GRID_GEOMETRY = 1e5; // 10 um +static constexpr double WELD_GRID_DECIMATION = 1e6; // 1 um + +// Round half toward positive infinity. Quantised coordinates hit exact halves often enough that the +// tie rule matters. +inline int64_t grid_round(double v) { return int64_t(std::floor(v + 0.5)); } + +// Open-addressing table over flat arrays: no allocation per lookup, exact integer key comparison. +// Values must be non-negative; -1 is the empty sentinel and what get() returns on a miss. +class QuantizedPointMap +{ +public: + explicit QuantizedPointMap(double quant, size_t expected = 256) : m_quant(quant) + { + size_t cap = 16; + const size_t target = std::max(16, size_t(std::ceil(double(expected) / 0.6))); + while (cap < target) + cap *= 2; + alloc(cap); + } + + size_t size() const { return m_size; } + // Whether the last get_or_set() inserted rather than found. + bool inserted() const { return m_inserted; } + + int get(float x, float y, float z) + { + return m_val[slot(grid_round(double(x) * m_quant), grid_round(double(y) * m_quant), + grid_round(double(z) * m_quant))]; + } + int get(const Vec3f &p) { return get(p.x(), p.y(), p.z()); } + + // The value already stored for this position's grid cell; if there is none, store `value` and + // return it. inserted() then says which of the two happened. + int get_or_set(float x, float y, float z, int value) + { + const int64_t qx = grid_round(double(x) * m_quant); + const int64_t qy = grid_round(double(y) * m_quant); + const int64_t qz = grid_round(double(z) * m_quant); + const size_t i = slot(qx, qy, qz); + if (m_val[i] != -1) { + m_inserted = false; + return m_val[i]; + } + m_qx[i] = qx; m_qy[i] = qy; m_qz[i] = qz; + m_val[i] = value; + m_inserted = true; + if (++m_size > size_t(double(m_cap) * 0.7)) + grow(); + return value; + } + int get_or_set(const Vec3f &p, int value) { return get_or_set(p.x(), p.y(), p.z(), value); } + + // The same table as a set of integer tuples (edge marking, midpoint cache). Quantisation is + // bypassed: routing ids through the float overloads loses precision above 2^24. + int get_key(int64_t a, int64_t b, int64_t c) { return m_val[slot(a, b, c)]; } + int get_or_set_key(int64_t a, int64_t b, int64_t c, int value) + { + const size_t i = slot(a, b, c); + if (m_val[i] != -1) { + m_inserted = false; + return m_val[i]; + } + m_qx[i] = a; m_qy[i] = b; m_qz[i] = c; + m_val[i] = value; + m_inserted = true; + if (++m_size > size_t(double(m_cap) * 0.7)) + grow(); + return value; + } + +private: + void alloc(size_t cap) + { + m_cap = cap; + m_mask = cap - 1; + m_qx.assign(cap, 0); + m_qy.assign(cap, 0); + m_qz.assign(cap, 0); + m_val.assign(cap, -1); + } + + size_t slot(int64_t qx, int64_t qy, int64_t qz) const + { + uint32_t h = uint32_t(int32_t(qx) * int32_t(0x9E3779B1)) ^ + uint32_t(int32_t(qy) * int32_t(0x85EBCA77)) ^ + uint32_t(int32_t(qz) * int32_t(0xC2B2AE3D)); + h ^= h >> 15; + size_t i = size_t(h) & m_mask; + // Equality is checked against the stored 64-bit keys, so truncating to 32 bits for the hash + // costs collisions at worst, never a wrong answer. + while (m_val[i] != -1) { + if (m_qx[i] == qx && m_qy[i] == qy && m_qz[i] == qz) + return i; + i = (i + 1) & m_mask; + } + return i; + } + + void grow() + { + std::vector oqx = std::move(m_qx), oqy = std::move(m_qy), oqz = std::move(m_qz); + std::vector oval = std::move(m_val); + const size_t ocap = m_cap; + alloc(ocap * 2); + for (size_t i = 0; i < ocap; ++i) { + if (oval[i] == -1) + continue; + const size_t s = slot(oqx[i], oqy[i], oqz[i]); + m_qx[s] = oqx[i]; m_qy[s] = oqy[i]; m_qz[s] = oqz[i]; + m_val[s] = oval[i]; + } + } + + double m_quant; + size_t m_cap = 0, m_mask = 0, m_size = 0; + bool m_inserted = false; + std::vector m_qx, m_qy, m_qz; + std::vector m_val; +}; + +// Three consecutive entries per triangle. The indexers turn this into shared vertices where a stage +// needs adjacency. +struct TriSoup +{ + std::vector pos; + std::vector nrm; // parallel to pos + std::vector exclude_weight; // parallel to pos; empty when nothing is excluded + + size_t triangle_count() const { return pos.size() / 3; } + bool empty() const { return pos.empty(); } +}; + +// Assign each vertex the sequential id of its quantised position, first occurrence winning. +struct WeldResult +{ + std::vector vertex_id; + int unique_count = 0; +}; +WeldResult weld_vertices(const std::vector &positions, double quant); + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeMesh.cpp b/src/libslic3r/TextureBake/TextureBakeMesh.cpp new file mode 100644 index 0000000000..6f6d72218d --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeMesh.cpp @@ -0,0 +1,62 @@ +#include "TextureBakeMesh.hpp" + +#include + +namespace Slic3r { +namespace TextureBake { + +TriSoup to_soup(const indexed_triangle_set &its, const std::vector &face_excluded) +{ + TriSoup out; + const size_t n = its.indices.size(); + out.pos.resize(n * 3); + out.nrm.resize(n * 3); + const bool have_excl = face_excluded.size() == n; + if (have_excl) + out.exclude_weight.resize(n * 3); + + for (size_t t = 0; t < n; ++t) { + const stl_triangle_vertex_indices &tri = its.indices[t]; + const Vec3f a = its.vertices[size_t(tri[0])]; + const Vec3f b = its.vertices[size_t(tri[1])]; + const Vec3f c = its.vertices[size_t(tri[2])]; + Vec3f nrm = (b - a).cross(c - a); + const float len = nrm.norm(); + nrm = (len > 0.f) ? Vec3f(nrm / len) : Vec3f(0.f, 0.f, 1.f); + out.pos[t * 3] = a; + out.pos[t * 3 + 1] = b; + out.pos[t * 3 + 2] = c; + // Per-face on purpose: the accurate indexer derives smooth normals and splits at sharp edges + // itself, so averaged ones would pre-empt that. + out.nrm[t * 3] = out.nrm[t * 3 + 1] = out.nrm[t * 3 + 2] = nrm; + if (have_excl) { + const float w = face_excluded[t] ? 1.f : 0.f; + out.exclude_weight[t * 3] = out.exclude_weight[t * 3 + 1] = out.exclude_weight[t * 3 + 2] = w; + } + } + return out; +} + +indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup) +{ + indexed_triangle_set out; + const size_t n = soup.pos.size(); + out.indices.reserve(n / 3); + QuantizedPointMap map(WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22)); + std::vector id(n); + for (size_t i = 0; i < n; ++i) { + id[i] = map.get_or_set(soup.pos[i], int(out.vertices.size())); + if (map.inserted()) + out.vertices.push_back(soup.pos[i]); + } + for (size_t t = 0; t + 2 < n; t += 3) { + // Welded-together corners carry no area. + if (id[t] == id[t + 1] || id[t + 1] == id[t + 2] || id[t] == id[t + 2]) + continue; + out.indices.emplace_back(id[t], id[t + 1], id[t + 2]); + } + return out; +} + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeMesh.hpp b/src/libslic3r/TextureBake/TextureBakeMesh.hpp new file mode 100644 index 0000000000..b4e3ced5fe --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeMesh.hpp @@ -0,0 +1,19 @@ +#pragma once + +// Conversion between the pipeline's triangle soup and the indexed mesh used elsewhere. The pipeline +// stays on soup because each stage welds on its own grid, and those differences are load-bearing. + +#include "TextureBakeIndex.hpp" +#include "../TriangleMesh.hpp" + +namespace Slic3r { +namespace TextureBake { + +// `face_excluded`: one entry per input triangle, becoming the soup's per-corner exclusion weight. +TriSoup to_soup(const indexed_triangle_set &its, const std::vector &face_excluded = {}); + +// Welds at the geometry grid. +indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup); + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakePipeline.cpp b/src/libslic3r/TextureBake/TextureBakePipeline.cpp new file mode 100644 index 0000000000..a86fda0e0d --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakePipeline.cpp @@ -0,0 +1,216 @@ +#include "TextureBakePipeline.hpp" + +#include +#include + +namespace Slic3r { +namespace TextureBake { + +void clamp_below_bottom(TriSoup &geometry, float bottom_z) +{ + for (size_t t = 0; t + 2 < geometry.pos.size(); t += 3) { + bool dirty = false; + for (int k = 0; k < 3; ++k) + if (geometry.pos[t + size_t(k)].z() < bottom_z) { + geometry.pos[t + size_t(k)].z() = bottom_z; + dirty = true; + } + if (!dirty) + continue; + Vec3f n = (geometry.pos[t + 1] - geometry.pos[t]).cross(geometry.pos[t + 2] - geometry.pos[t]); + const float len = n.norm(); + n = (len > 0.f) ? Vec3f(n / len) : Vec3f(0.f, 0.f, 1.f); + geometry.nrm[t] = geometry.nrm[t + 1] = geometry.nrm[t + 2] = n; + } +} + +size_t snap_bottom_to_flat(TriSoup &geometry, float bottom_z, double tol) +{ + const size_t vert_count = geometry.pos.size(); + const size_t tri_count = vert_count / 3; + if (tri_count == 0 || tol <= 0.0) + return 0; + + // Weld at the finest grid: by this point copies of one position are bit-identical, because every + // earlier stage moved them by the same vector. + QuantizedPointMap weld(WELD_GRID_DECIMATION, std::min(vert_count, size_t(1) << 22)); + std::vector vid(vert_count); + int unique = 0; + for (size_t i = 0; i < vert_count; ++i) { + vid[i] = weld.get_or_set(geometry.pos[i], unique); + if (weld.inserted()) + ++unique; + } + // Incident corners per position, CSR style. + std::vector start(size_t(unique) + 1, 0); + for (size_t i = 0; i < vert_count; ++i) + ++start[size_t(vid[i]) + 1]; + for (size_t id = 0; id < size_t(unique); ++id) + start[id + 1] += start[id]; + std::vector inc(vert_count), cursor(size_t(unique), 0); + for (size_t i = 0; i < vert_count; ++i) + inc[start[size_t(vid[i])] + cursor[size_t(vid[i])]++] = uint32_t(i); + + const double fold_cos = std::cos(75.0 * M_PI / 180.0); + std::vector dirty_tri(tri_count, 0); + + for (size_t id = 0; id < size_t(unique); ++id) { + const float z = geometry.pos[inc[start[id]]].z(); + if (z == bottom_z || std::abs(double(z) - double(bottom_z)) > tol) + continue; + + // Simulate the move: every incident triangle must keep positive area and must not fold. + bool ok = true; + for (uint32_t k = start[id]; k < start[id + 1] && ok; ++k) { + const size_t t = size_t(inc[k]) / 3; + Vec3f p[3]; + for (int v = 0; v < 3; ++v) { + p[v] = geometry.pos[t * 3 + size_t(v)]; + if (vid[t * 3 + size_t(v)] == int(id)) + p[v].z() = bottom_z; + } + const Vec3d on = (geometry.pos[t * 3 + 1] - geometry.pos[t * 3]) + .cross(geometry.pos[t * 3 + 2] - geometry.pos[t * 3]).cast(); + const Vec3d nn = (p[1] - p[0]).cross(p[2] - p[0]).cast(); + const double o2 = on.squaredNorm(), n2 = nn.squaredNorm(); + if (n2 < 1e-20) { ok = false; break; } // would collapse to zero area + if (o2 < 1e-20) continue; // already degenerate, cannot judge a rotation + const double dot = on.dot(nn); + if (dot < 0.0 || dot * dot < fold_cos * fold_cos * o2 * n2) + ok = false; + } + if (!ok) + continue; + + for (uint32_t k = start[id]; k < start[id + 1]; ++k) { + geometry.pos[inc[k]].z() = bottom_z; + dirty_tri[size_t(inc[k]) / 3] = 1; + } + } + + size_t dirty = 0; + for (size_t t = 0; t < tri_count; ++t) { + if (!dirty_tri[t]) + continue; + ++dirty; + Vec3f n = (geometry.pos[t * 3 + 1] - geometry.pos[t * 3]) + .cross(geometry.pos[t * 3 + 2] - geometry.pos[t * 3]); + const float len = n.norm(); + n = (len > 0.f) ? Vec3f(n / len) : Vec3f(0.f, 0.f, 1.f); + geometry.nrm[t * 3] = geometry.nrm[t * 3 + 1] = geometry.nrm[t * 3 + 2] = n; + } + return dirty; +} + +PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, + const PipelineSettings &settings, const DisplaceBounds &bounds, + PipelineMode mode, const std::vector &face_excluded, + const PipelineProgressFn &on_progress) +{ + PipelineResult result; + const auto report = [&](const char *stage, double f) { + return !on_progress || on_progress(stage, f); + }; + + if (input.empty() || !sample) { + result.geometry = input; + return result; + } + + // 1. Refine to the target edge length. + SubdivideResult sub = subdivide( + input, settings.refine_length, face_excluded, /* fast */ false, settings.safety_cap, + [&](double f, size_t, double) { return report("subdivide", f); }); + result.safety_cap_hit = sub.safety_cap_hit; + if (!report("subdivide", 1.0)) { + result.canceled = true; + return result; + } + + // 2. Dissolve the slivers refinement inherited, then recover the edges that lengthened. + if (settings.regularize) { + RegularizeOptions ropts = settings.regularize_opts; + ropts.preserve_excluded = settings.preserve_untextured; + RegularizeResult reg = regularize_mesh(sub.geometry, sub.face_parent_id, + settings.refine_length, ropts); + result.collapse_count = reg.collapse_count; + if (!report("regularize", 1.0)) { + result.canceled = true; + return result; + } + if (reg.collapse_count > 0) { + // Excluded faces are carried on the soup itself, so the flag is re-derived rather than + // indexed across the collapse. + std::vector excl; + if (!reg.geometry.exclude_weight.empty()) { + excl.assign(reg.geometry.triangle_count(), 0); + for (size_t t = 0; t < excl.size(); ++t) + excl[t] = reg.geometry.exclude_weight[t * 3] > 0.99f ? 1 : 0; + } + sub = subdivide(reg.geometry, settings.refine_length * settings.regularize_second_pass_mul, + excl, false, settings.safety_cap, + [&](double f, size_t, double) { return report("re-subdivide", f); }); + result.safety_cap_hit = result.safety_cap_hit || sub.safety_cap_hit; + // The second pass renumbers faces, so the parent map has to be composed through it. + std::vector composed(sub.face_parent_id.size()); + for (size_t i = 0; i < composed.size(); ++i) { + const int mid = sub.face_parent_id[i]; + composed[i] = (mid >= 0 && size_t(mid) < reg.face_parent_id.size()) + ? reg.face_parent_id[size_t(mid)] : -1; + } + sub.face_parent_id = std::move(composed); + } else { + sub.geometry = std::move(reg.geometry); + sub.face_parent_id = std::move(reg.face_parent_id); + } + } + + // 3. Displace. + TriSoup displaced = apply_displacement(sub.geometry, sample, settings.displace, bounds, + [&](double f) { return report("displace", f); }); + if (!report("displace", 1.0)) { + result.canceled = true; + return result; + } + + // 4. Decimate - export only. A bake needs the face-parent map, which a collapse destroys. + std::vector parent = std::move(sub.face_parent_id); + if (mode == PipelineMode::Export) { + const bool needs_decimation = displaced.triangle_count() > settings.max_triangles; + if (needs_decimation || settings.harvest_flat) { + std::vector locked; + if (settings.preserve_untextured && !displaced.exclude_weight.empty()) { + locked.assign(displaced.triangle_count(), 0); + for (size_t t = 0; t < locked.size(); ++t) + locked[t] = displaced.exclude_weight[t * 3] > 0.99f ? 1 : 0; + } + DecimateResult dec = decimate(displaced, settings.max_triangles, settings.harvest_flat, + settings.harvest_tol, locked, + [&](double f) { return report("decimate", f); }); + result.locked_over_budget = dec.locked_over_budget; + displaced = std::move(dec.geometry); + parent.clear(); // no longer meaningful + } + if (!report("decimate", 1.0)) { + result.canceled = true; + return result; + } + } + + // 5. Flatten the bed-contact surface. + if (settings.displace.bottom_angle_limit > 0.f) + clamp_below_bottom(displaced, bounds.min.z()); + if (settings.bottom_snap_tol > 0.0) + snap_bottom_to_flat(displaced, bounds.min.z(), settings.bottom_snap_tol); + + // 6. Close the T-junctions decimation left behind. Only meaningful when it ran. + if (mode == PipelineMode::Export && parent.empty()) + displaced = resolve_t_junctions(displaced); + + result.geometry = std::move(displaced); + result.face_parent_id = std::move(parent); + return result; +} + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakePipeline.hpp b/src/libslic3r/TextureBake/TextureBakePipeline.hpp new file mode 100644 index 0000000000..a9a7db1957 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakePipeline.hpp @@ -0,0 +1,98 @@ +#pragma once + +// The bake pipeline: +// +// subdivide -> [regularize -> re-subdivide] -> displace -> [decimate] +// -> bottom clamp -> bottom snap -> [resolve T-junctions] +// +// Regularization sits between two subdivisions on purpose: it dissolves the slivers refinement +// inherited, which lengthens some edges past the target, and the second pass brings those back. +// Before any subdivision it would have nothing to work on, since the slivers come from refining a +// needle; after a single pass it would leave the mesh coarser than asked for. +// +// Decimation and repair are export-only - decimation drops the output-to-input face mapping a bake +// needs to carry per-face data forward. + +#include +#include +#include + +#include "TextureBakeDecimate.hpp" +#include "TextureBakeDisplace.hpp" +#include "TextureBakeIndex.hpp" +#include "TextureBakeRegularize.hpp" +#include "TextureBakeRepair.hpp" +#include "TextureBakeSubdivide.hpp" + +namespace Slic3r { +namespace TextureBake { + +enum class PipelineMode +{ + // Keeps the face-parent mapping; skips decimation and repair. + Bake, + // The full sequence, including decimation and repair. + Export, +}; + +struct PipelineSettings +{ + // Target edge length for the refinement, in mm. + double refine_length = 1.0; + + // Sliver removal between the two subdivision passes. + bool regularize = true; + RegularizeOptions regularize_opts; + // Slightly above the first pass, so it recovers the edges regularization lengthened instead of + // re-refining what it just merged. + double regularize_second_pass_mul = 1.1; + + DisplaceSettings displace; + + // Export mode only. + size_t max_triangles = 750'000; + bool harvest_flat = true; + double harvest_tol = DECIMATE_DEFAULT_HARVEST_TOL; + // Lock the untextured region against both regularization and decimation. + bool preserve_untextured = true; + + // Snap vertices within this of the bottom plane onto it. 0 disables. + double bottom_snap_tol = 0.1; + + int safety_cap = SUBDIVIDE_SAFETY_CAP; +}; + +// Stage name and a fraction within it. Returning false cancels the run. +using PipelineProgressFn = std::function; + +struct PipelineResult +{ + TriSoup geometry; + // Output face -> input face. Empty in Export mode, where decimation invalidates it. + std::vector face_parent_id; + bool safety_cap_hit = false; + bool locked_over_budget = false; + size_t collapse_count = 0; + bool canceled = false; +}; + +PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, + const PipelineSettings &settings, const DisplaceBounds &bounds, + PipelineMode mode, const std::vector &face_excluded = {}, + const PipelineProgressFn &on_progress = {}); + +// Snap anything that ended below the model's original bottom back up to it. +void clamp_below_bottom(TriSoup &geometry, float bottom_z); + +// Flatten the bed-contact surface by snapping positions within `tol` of the bottom plane onto it. +// +// Gated, not unconditional: an unconditional band snap also flattens the undersides of texture bumps +// near the base, folding them coplanar into the bottom face. Folded faces overlap the plate, so edges +// there pick up four incident faces - non-manifold edges and phantom shells on re-import. All copies +// of a position move together, and the move is rejected if any incident triangle would go degenerate +// or rotate more than about 75 degrees. A real bed-contact sliver rotates by a fraction of a degree +// and still snaps. Returns how many triangles moved. +size_t snap_bottom_to_flat(TriSoup &geometry, float bottom_z, double tol = 0.1); + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeRegularize.cpp b/src/libslic3r/TextureBake/TextureBakeRegularize.cpp new file mode 100644 index 0000000000..4539e38723 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeRegularize.cpp @@ -0,0 +1,380 @@ +#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 diff --git a/src/libslic3r/TextureBake/TextureBakeRegularize.hpp b/src/libslic3r/TextureBake/TextureBakeRegularize.hpp new file mode 100644 index 0000000000..350666783a --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeRegularize.hpp @@ -0,0 +1,73 @@ +#pragma once + +// Sliver removal by short-edge collapse. +// +// Subdivision turns tessellation needles into chains of slivers that are within the edge-length +// budget but still poor triangles. A sliver's three vertices land on three unrelated texels, so the +// relief picks up noise that is an artifact of the tessellation rather than of the image. +// +// A candidate's edge is collapsed to its midpoint only if it passes three gates: no affected +// triangle may exceed the target edge times a slack factor; every affected triangle must keep its +// face normal within a bound of its *original* direction (which is what stops curved surfaces being +// flattened); and the link condition must hold, or the result would be non-manifold. Boundary and +// non-manifold edges are skipped outright. Rounds repeat until one achieves nothing. + +#include +#include + +#include "TextureBakeIndex.hpp" + +namespace Slic3r { +namespace TextureBake { + +struct RegularizeOptions +{ + // Candidate threshold. Set to catch real slivers - chains measure in the hundreds - without + // sweeping up moderate fillet triangles, which sit between 2 and 5. + double aspect_threshold = 5.0; + + // The base tier is loose on purpose: non-sliver boundary collapses must keep succeeding, since + // those give a chain the room to dissolve. A tight base leaves chains worse than before. The + // aggressive tier applies when at least one wing is an extreme sliver. + double slack = 3.0; + double aggressive_slack = 8.0; + + // Thinness above which a wing counts as extreme: longest edge over shortest altitude. + double extreme_sliver_aspect = 8.0; + + // Measured against each triangle's normal from before any collapse ran, so rounds of small + // allowed drift cannot compound into corner damage. Asymmetric two-tier: the loose bound needs + // *both* wings extreme, which matches a needle chain on a curved face but not a sliver beside a + // fillet, so fillets keep the tight bound. + double max_normal_delta_cos = 0.965925826289; // cos(15 degrees) + double aggressive_normal_delta_cos = 0.906307787037; // cos(25 degrees) + + // Vertices on edges sharper than this are frozen, so hard features keep every original vertex. + double sharp_edge_cos = 0.866025403784; // cos(30 degrees) + + int maxrounds = 8; + + // Freeze excluded faces entirely, so untextured geometry is never modified. + bool preserve_excluded = false; +}; + +// Which gate blocked a collapse - the only practical way to tell why a region failed to merge. +struct RegularizeRejectStats +{ + size_t frozen = 0, wing_count = 0, link_condition = 0, edge_cap = 0, normal_change = 0, + degenerate = 0, folded_apex = 0; +}; + +struct RegularizeResult +{ + TriSoup geometry; + std::vector face_parent_id; + size_t collapse_count = 0; + RegularizeRejectStats reject_stats; +}; + +RegularizeResult regularize_mesh(const TriSoup &geometry, const std::vector &face_parent_id, + double max_edge_length, const RegularizeOptions &opts = {}); + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeRepair.cpp b/src/libslic3r/TextureBake/TextureBakeRepair.cpp new file mode 100644 index 0000000000..438e0ea25d --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeRepair.cpp @@ -0,0 +1,215 @@ +#include "TextureBakeRepair.hpp" + +#include +#include +#include +#include +#include + +namespace Slic3r { +namespace TextureBake { + +namespace { + +inline uint64_t edge_key(int a, int b) +{ + const uint32_t lo = uint32_t(std::min(a, b)), hi = uint32_t(std::max(a, b)); + return (uint64_t(lo) << 32) | uint64_t(hi); +} + +// On the export grid a squared cross product is either 0 (collinear) or at least about 1e-16, the +// smallest real triangle being one grid unit per leg, so this separates the two cleanly. +constexpr double DEGENERATE_AREA_SQ = 1e-18; + +} // namespace + +EdgeDefects count_edge_defects(const TriSoup &geometry, double quant) +{ + EdgeDefects out; + const size_t n = geometry.pos.size(); + out.triangles = n / 3; + QuantizedPointMap vmap(quant, std::min(n, size_t(1) << 22)); + std::vector id(n); + int next = 0; + for (size_t i = 0; i < n; ++i) { + id[i] = vmap.get_or_set(geometry.pos[i], next); + if (vmap.inserted()) + ++next; + } + std::unordered_map counts; + for (size_t t = 0; t + 2 < n; t += 3) { + const int a = id[t], b = id[t + 1], c = id[t + 2]; + if (a == b || b == c || a == c) + continue; + const int tri[3] = { a, b, c }; + for (int e = 0; e < 3; ++e) + ++counts[edge_key(tri[e], tri[(e + 1) % 3])]; + } + for (const auto &[key, c] : counts) { + (void) key; + if (c == 1) ++out.open; + else if (c > 2) ++out.non_manifold; + } + return out; +} + +size_t count_area_slivers(const TriSoup &geometry) +{ + size_t n = 0; + for (size_t t = 0; t + 2 < geometry.pos.size(); t += 3) { + const Vec3d u = (geometry.pos[t + 1] - geometry.pos[t]).cast(); + const Vec3d v = (geometry.pos[t + 2] - geometry.pos[t]).cast(); + // The threshold a slicer applies: area below 1e-12 mm^2. + if (u.cross(v).squaredNorm() < 1e-24) + ++n; + } + return n; +} + +TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts) +{ + const size_t n_tri = geometry.triangle_count(); + const double on_tol2 = opts.on_seg_tol * opts.on_seg_tol; + const double Q = opts.weld_quant; + + // Snapped, not just welded: keeping unrounded coordinates lets a thin triangle pass the + // degeneracy test here and then collapse to collinear once the file is written, punching the very + // hole this pass prevents. Snapping makes the check see what will be written. + QuantizedPointMap vmap(Q, std::min(n_tri * 3, size_t(1) << 22)); + std::vector vert; + std::vector vid(n_tri * 3); + for (size_t i = 0; i < n_tri * 3; ++i) { + const Vec3f &p = geometry.pos[i]; + const int id = vmap.get_or_set(p, int(vert.size())); + if (vmap.inserted()) + vert.emplace_back(double(grid_round(double(p.x()) * Q)) / Q, + double(grid_round(double(p.y()) * Q)) / Q, + double(grid_round(double(p.z()) * Q)) / Q); + vid[i] = id; + } + + // Dropped: faces whose corners welded together, and needles - distinct but collinear on this + // grid. A needle reads as watertight yet is deleted downstream, and dropping it leaves exactly + // the on-edge-vertex topology the pass below closes. + std::vector> faces; + faces.reserve(n_tri); + for (size_t t = 0; t < n_tri; ++t) { + const int a = vid[t * 3], b = vid[t * 3 + 1], c = vid[t * 3 + 2]; + if (a == b || b == c || a == c) + continue; + const Vec3d u = vert[size_t(b)] - vert[size_t(a)]; + const Vec3d w = vert[size_t(c)] - vert[size_t(a)]; + if (u.cross(w).squaredNorm() < DEGENERATE_AREA_SQ) + continue; + faces.push_back({ a, b, c }); + } + + for (int iter = 0; iter < opts.max_iters; ++iter) { + std::unordered_map e_count; + for (const auto &f : faces) + for (int e = 0; e < 3; ++e) + ++e_count[edge_key(f[size_t(e)], f[size_t((e + 1) % 3)])]; + + std::unordered_set bverts; + for (const auto &[key, c] : e_count) { + if (c != 1) + continue; + bverts.insert(int(uint32_t(key >> 32))); + bverts.insert(int(uint32_t(key & 0xFFFFFFFFu))); + } + if (bverts.empty()) + break; + const std::vector bv(bverts.begin(), bverts.end()); + + struct Split { int a, b; std::vector mids; }; + std::unordered_map splits; + for (size_t fi = 0; fi < faces.size(); ++fi) { + const auto &f = faces[fi]; + for (int e = 0; e < 3; ++e) { + const int a = f[size_t(e)], b = f[size_t((e + 1) % 3)]; + if (e_count[edge_key(a, b)] != 1) + continue; // only a boundary edge carries an unresolved T-junction + const Vec3d A = vert[size_t(a)]; + const Vec3d ev = vert[size_t(b)] - A; + const double elen2 = ev.squaredNorm(); + if (elen2 < 1e-20) + continue; + std::vector> found; + for (const int c : bv) { + if (c == a || c == b) + continue; + const Vec3d cv = vert[size_t(c)] - A; + const double tp = cv.dot(ev) / elen2; + if (tp <= 1e-4 || tp >= 1.0 - 1e-4) + continue; // strictly between the ends + if ((cv - ev * tp).squaredNorm() < on_tol2) + found.emplace_back(tp, c); + } + if (!found.empty()) { + std::sort(found.begin(), found.end(), + [](const auto &x, const auto &y) { return x.first < y.first; }); + Split sp{ a, b, {} }; + for (const auto &m : found) + sp.mids.push_back(m.second); + splits.emplace(fi, std::move(sp)); + break; // one site per face per pass; iteration handles cascades + } + } + } + if (splits.empty()) + break; + + std::vector> next; + next.reserve(faces.size() + splits.size() * 2); + for (size_t fi = 0; fi < faces.size(); ++fi) { + const auto it = splits.find(fi); + if (it == splits.end()) { + next.push_back(faces[fi]); + continue; + } + const auto &f = faces[fi]; + const auto &sp = it->second; + const int apex = (f[0] != sp.a && f[0] != sp.b) ? f[0] + : (f[1] != sp.a && f[1] != sp.b) ? f[1] + : f[2]; + // Walk the base the way the face already traverses it, so the winding survives. + bool dir_ab = false; + for (int e = 0; e < 3; ++e) + if (f[size_t(e)] == sp.a && f[size_t((e + 1) % 3)] == sp.b) { + dir_ab = true; + break; + } + std::vector seq; + if (dir_ab) { + seq.push_back(sp.a); + seq.insert(seq.end(), sp.mids.begin(), sp.mids.end()); + seq.push_back(sp.b); + } else { + seq.push_back(sp.b); + seq.insert(seq.end(), sp.mids.rbegin(), sp.mids.rend()); + seq.push_back(sp.a); + } + for (size_t s = 0; s + 1 < seq.size(); ++s) + next.push_back({ seq[s], seq[s + 1], apex }); + } + faces.swap(next); + } + + TriSoup out; + out.pos.reserve(faces.size() * 3); + out.nrm.reserve(faces.size() * 3); + for (const auto &f : faces) { + const Vec3f a = vert[size_t(f[0])].cast(); + const Vec3f b = vert[size_t(f[1])].cast(); + const Vec3f c = vert[size_t(f[2])].cast(); + Vec3f nrm = (b - a).cross(c - a); + const float len = nrm.norm(); + nrm = (len > 0.f) ? Vec3f(nrm / len) : Vec3f(0.f, 0.f, 1.f); + out.pos.insert(out.pos.end(), { a, b, c }); + out.nrm.insert(out.nrm.end(), { nrm, nrm, nrm }); + } + return out; +} + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeRepair.hpp b/src/libslic3r/TextureBake/TextureBakeRepair.hpp new file mode 100644 index 0000000000..35e9f50b06 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeRepair.hpp @@ -0,0 +1,48 @@ +#pragma once + +// T-junction resolution and edge-defect accounting. +// +// Decimation can collapse a long edge whose interior still carries neighbouring triangles' vertices. +// Those then sit *on* an edge rather than at an end: watertight vertex-for-vertex, but the edge has +// one incident face on one side, which a slicer reads as an open boundary. This splits the offending +// face into a fan so every on-edge vertex becomes a real corner. + +#include +#include + +#include "TextureBakeIndex.hpp" + +namespace Slic3r { +namespace TextureBake { + +struct EdgeDefects +{ + size_t open = 0, non_manifold = 0, triangles = 0; +}; + +// Welds at the export grid first: counting on the un-snapped mesh reports defects the file does not +// have and misses ones it does. +EdgeDefects count_edge_defects(const TriSoup &geometry, double quant = WELD_GRID_EXPORT); + +// Triangles a slicer would drop as degenerate. Each one, removed, punches a hole - so a non-zero +// count means watertight only on paper. +size_t count_area_slivers(const TriSoup &geometry); + +struct RepairOptions +{ + // Coordinates are snapped onto this grid, matching the precision files are written with. + double weld_quant = WELD_GRID_EXPORT; + + // How far off an edge a vertex may sit and still count as on it. Well above the harvest + // tolerance, since harvesting leaves a region flat only to within that, making a collapsed edge a + // chord the on-edge vertices deviate from by about as much. Still far below the weld grid. + double on_seg_tol = 0.02; + + // Splitting one face can expose another behind it, so the pass cascades. + int max_iters = 16; +}; + +TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts = {}); + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakeSubdivide.cpp b/src/libslic3r/TextureBake/TextureBakeSubdivide.cpp new file mode 100644 index 0000000000..7a0082e257 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeSubdivide.cpp @@ -0,0 +1,414 @@ +#include "TextureBakeSubdivide.hpp" + +#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(); + + QuantizedPointMap mid_cache(1.0, 1 << 16); + QuantizedPointMap split_edges(1.0, 1 << 16); + + // 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. + size_t predicted = 0; + 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 int n = int(is_marked(a, b)) + int(is_marked(b, c)) + int(is_marked(c, a)); + predicted += (n == 0) ? 1 : size_t(n + 1); + } + 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 = is_marked(a, b), s_bc = is_marked(b, c), s_ca = is_marked(c, a); + 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); + for (size_t t = 0; t + 2 < n; t += 3) { + const Vec3d a = geometry.pos[t].cast(); + const Vec3d b = geometry.pos[t + 1].cast(); + const Vec3d c = geometry.pos[t + 2].cast(); + const Vec3d r = (b - a).cross(c - 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); + + for (size_t t = 0; t < tri_count; ++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; + + std::vector current_indices = indexed.indices; + std::vector current_excluded = face_excluded; + const size_t initial_tris = indexed.indices.size() / 3; + 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. + double max_edge_sq = 0.0; + for (size_t t = 0; t + 2 < current_indices.size(); t += 3) { + const int a = current_indices[t], b = current_indices[t + 1], c = current_indices[t + 2]; + max_edge_sq = std::max({ max_edge_sq, edge_len_sq(indexed.verts, a, b), + edge_len_sq(indexed.verts, b, c), + edge_len_sq(indexed.verts, c, a) }); + } + 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 diff --git a/src/libslic3r/TextureBake/TextureBakeSubdivide.hpp b/src/libslic3r/TextureBake/TextureBakeSubdivide.hpp new file mode 100644 index 0000000000..30ad56bf00 --- /dev/null +++ b/src/libslic3r/TextureBake/TextureBakeSubdivide.hpp @@ -0,0 +1,83 @@ +#pragma once + +// Adaptive subdivision to a target edge length, by global marked-edge (red-green) refinement rather +// than longest-edge bisection. Marking is global, so two triangles sharing an edge always agree and +// the result is crack-free by construction. A triangle is rebuilt from its marked-edge count: 0 +// keeps, 1 bisects, 2 fans into three, 3 does the regular 1->4 split. +// +// The 1->4 case is what keeps the tessellation regular - its children are similar to the parent. An +// irregular one shows up after displacement as adjacent triangles tilting alternately, i.e. noise. + +#include +#include + +#include "TextureBakeIndex.hpp" + +namespace Slic3r { +namespace TextureBake { + +// Memory guard for the stages downstream. At roughly 145 bytes per triangle this is about 2.9 GB. +static constexpr int SUBDIVIDE_SAFETY_CAP = 16'000'000; + +// Vertices at one position stay separate when their faces disagree by more than this: a cube keeps +// hard edges, a cylinder keeps averaged ones. +static constexpr double SUBDIVIDE_SHARP_ANGLE_DEG = 30.0; + +// A depth bound, not a work bound: the loop stops as soon as a pass changes nothing. +static constexpr int SUBDIVIDE_MAX_ITERATIONS = 12; + +// Built by the indexers, appended to by the passes. Double precision so repeated midpointing does +// not drift. +struct VertStore +{ + std::vector pos; + std::vector nrm; + std::vector wgt; // exclusion weights; empty when the caller supplied none + std::vector canon; // canonical position ids; empty in fast mode + + size_t count() const { return pos.size(); } + int push(const Vec3d &p, const Vec3d &n) + { + const int idx = int(pos.size()); + pos.push_back(p); + nrm.push_back(n); + return idx; + } +}; + +struct IndexedMesh +{ + VertStore verts; + std::vector indices; // 3 per triangle + QuantizedPointMap pos_canon_map{ WELD_GRID_GEOMETRY, 256 }; + bool has_canon = false; +}; + +// Fraction, triangle count, longest remaining edge. Returning false cancels; what comes back is +// still watertight, because passes apply whole or not at all. +using SubdivideProgressFn = std::function; + +struct SubdivideResult +{ + TriSoup geometry; + // Output triangle -> input triangle it descends from, so per-face data survives with no remap. + std::vector face_parent_id; + bool safety_cap_hit = false; +}; + +// `face_excluded`: one entry per input triangle; non-zero means its interior is never refined. Its +// edges still split when an included neighbour marks them, so no T-junction appears at the boundary. +// `fast` selects the cheap position-only indexer for previews. +SubdivideResult subdivide(const TriSoup &geometry, double max_edge_length, + const std::vector &face_excluded = {}, bool fast = false, + int safety_cap = SUBDIVIDE_SAFETY_CAP, + const SubdivideProgressFn &on_progress = {}); + +// Displacement needs the same welding and sharp-edge clustering. +IndexedMesh to_indexed(const TriSoup &geometry); +IndexedMesh to_indexed_fast(const TriSoup &geometry); +TriSoup to_non_indexed(const VertStore &verts, const std::vector &indices, + const std::vector &face_excluded); + +} // namespace TextureBake +} // namespace Slic3r diff --git a/src/libslic3r/TextureDisplacement.cpp b/src/libslic3r/TextureDisplacement.cpp index 74485c7919..4fcf00b462 100644 --- a/src/libslic3r/TextureDisplacement.cpp +++ b/src/libslic3r/TextureDisplacement.cpp @@ -15,11 +15,14 @@ #include #include +#include #include "MeshBoolean.hpp" #include "Model.hpp" #include "PNGReadWrite.hpp" #include "TriangleSelector.hpp" +#include "TextureBake/TextureBakeMesh.hpp" +#include "TextureBake/TextureBakePipeline.hpp" namespace Slic3r { @@ -1361,6 +1364,83 @@ void despeckle_triangle_colors(const indexed_triangle_set &mesh, std::vector &layers, + const TextureDisplacementFacetsData &facets_data, + const TextureDisplacementOptions &options, + const DisplacementProgressFn &progress) +{ + HeightFieldSampler combined = make_combined_displacement_sampler(mesh, layers, facets_data); + if (!combined) + return mesh; // nothing decodable to displace with + + // Unpainted triangles are excluded, keeping them out of refinement and pinned thereafter. + std::vector excluded(mesh.indices.size(), 1); + { + const TriangleMesh selector_mesh(mesh); + TriangleSelector selector(selector_mesh); + bool dirty = false; + for (const TriangleSelector::TriangleSplittingData &data : facets_data) { + if (data.triangles_to_split.empty()) + continue; + selector.deserialize(data, dirty); + dirty = true; + std::vector piece_src; + const indexed_triangle_set patch = + selector.get_facets_strict(EnforcerBlockerType::ENFORCER, &piece_src); + for (const int src : piece_src) + if (src >= 0 && size_t(src) < excluded.size()) + excluded[size_t(src)] = 0; + } + } + if (std::all_of(excluded.begin(), excluded.end(), [](uint8_t e) { return e != 0; })) + return mesh; // nothing painted + + TextureBake::PipelineSettings settings; + settings.refine_length = std::max(0.01f, options.v2_refine_mm); + settings.regularize = options.v2_regularize; + settings.max_triangles = size_t(std::max(0, options.v2_max_triangles_k)) * 1000; + settings.preserve_untextured = true; + // The sampler already returns millimetres, so the displacement stage must not scale it again. + settings.displace.amplitude = 1.f; + settings.displace.symmetric = false; + // The paint decides what moves here, so the angle limits stay off. + settings.displace.bottom_angle_limit = 0.f; + settings.displace.top_angle_limit = 0.f; + + TextureBake::DisplaceBounds bounds; + bounds.min = bounds.max = mesh.vertices.empty() ? Vec3f::Zero() : mesh.vertices.front(); + for (const Vec3f &v : mesh.vertices) { + bounds.min = bounds.min.cwiseMin(v); + bounds.max = bounds.max.cwiseMax(v); + } + + const auto sample = [&combined](const Vec3f &pos, const Vec3f &smooth_normal, const Vec3f &) { + // The smooth normal: the direction the vertex actually moves along. + return combined(pos, smooth_normal); + }; + + // 0 means no simplification, i.e. Bake mode. + const TextureBake::PipelineMode mode = settings.max_triangles > 0 ? TextureBake::PipelineMode::Export + : TextureBake::PipelineMode::Bake; + TextureBake::PipelineResult result = TextureBake::run_pipeline( + TextureBake::to_soup(mesh, excluded), sample, settings, bounds, mode, excluded, + [&progress](const char *, double f) { + return !progress || progress(std::clamp(int(f * 100.0), 0, 99)); + }); + if (result.canceled || result.geometry.empty()) + return {}; + + indexed_triangle_set out = TextureBake::to_indexed_triangle_set(result.geometry); + return out.indices.empty() ? mesh : out; +} + +} // namespace + indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh, const std::vector &layers, const TextureDisplacementFacetsData &facets_data, @@ -1384,6 +1464,9 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set if (mesh.vertices.empty() || mesh.indices.empty()) return mesh; + if (options.pipeline_v2) + return build_texture_displacement_v2(mesh, layers, facets_data, options, progress); + // Layers are combined in slot order, like stacked layers in an image editor: each one folds its // own displacement into the running total via its blend mode (see TextureBlendMode). std::vector ordered_layers; @@ -2073,26 +2156,38 @@ indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh, // instead of with the refined region, and what forced the tiny pass budget that stopped // refinement short. { - struct EdgeRec { int he[2]; int count; }; // he = encoded half-edge (triangle * 3 + local edge) - std::unordered_map edges; - edges.reserve(tris.size() * 2); - for (int ti = 0; ti < int(tris.size()); ++ti) - for (int e = 0; e < 3; ++e) { - EdgeRec &r = edges.try_emplace(edge_key(tris[ti].v[e], tris[ti].v[(e + 1) % 3]), - EdgeRec{ { -1, -1 }, 0 }) - .first->second; - if (r.count < 2) - r.he[r.count] = ti * 3 + e; - ++r.count; - } - for (int ti = 0; ti < int(tris.size()); ++ti) - for (int e = 0; e < 3; ++e) { - const EdgeRec &r = edges.at(edge_key(tris[ti].v[e], tris[ti].v[(e + 1) % 3])); - if (r.count > 2) - tris[ti].nb[e] = NB_NONMANIFOLD; - else if (r.count == 2) - tris[ti].nb[e] = (r.he[0] == ti * 3 + e ? r.he[1] : r.he[0]) / 3; + // Sort the half-edges by their edge key and walk the equal runs, rather than hashing every one + // of them twice into an unordered_map. Same result, but the two expensive parts - forming the + // keys and ordering them - both parallelise, where a shared hash map cannot. The map also cost + // a second full pass of lookups purely to read back what the first pass had just inserted. + std::vector> he(tris.size() * 3); // (edge key, triangle * 3 + local edge) + tbb::parallel_for(tbb::blocked_range(0, tris.size()), + [&](const tbb::blocked_range &range) { + for (size_t ti = range.begin(); ti < range.end(); ++ti) + for (int e = 0; e < 3; ++e) + he[ti * 3 + size_t(e)] = { + edge_key(tris[ti].v[e], tris[ti].v[(e + 1) % 3]), int(ti) * 3 + e + }; + }); + tbb::parallel_sort(he.begin(), he.end()); + // Runs of equal key are the half-edges of one edge: one is a boundary, two are neighbours, + // more is non-manifold. Serial, but it is a single linear pass over an already ordered array. + for (size_t i = 0; i < he.size();) { + size_t j = i + 1; + while (j < he.size() && he[j].first == he[i].first) + ++j; + const size_t count = j - i; + if (count == 2) { + const int a = he[i].second, b = he[i + 1].second; + tris[size_t(a / 3)].nb[a % 3] = b / 3; + tris[size_t(b / 3)].nb[b % 3] = a / 3; + } else if (count > 2) { + for (size_t k = i; k < j; ++k) + tris[size_t(he[k].second / 3)].nb[he[k].second % 3] = NB_NONMANIFOLD; } + // count == 1 keeps the NB_BOUNDARY it was initialised with. + i = j; + } } // Feature mode: per-vertex surface normal, and the sampled displacement height at each vertex. @@ -2152,6 +2247,40 @@ indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh, return vcolor[v]; }; + // Sample the input mesh's own vertices up front, in parallel, for the region that is going to be + // refined. A sampler call is a texture fetch plus the projection's trigonometry per layer, and it + // is by far the most expensive thing here - but taken one at a time from inside the refinement + // loop it is also strictly serial. Every one of these vertices is read by the very first scoring + // pass anyway, so doing them together costs nothing extra and hands the work to every core. + // + // Only the region, and only the *initial* vertices: the laziness this replaces exists so that a + // small painted patch on a big model does not pay for the whole model (see height_of()), and that + // still holds. Midpoints created later stay lazy, because they do not exist yet. + if (feature_mode || color_mode) { + std::vector wanted(verts.size(), 0); + for (const Tri &t : tris) + if (refine_region[t.src] != 0) + for (int i = 0; i < 3; ++i) + wanted[size_t(t.v[i])] = 1; + // Each index is touched by exactly one iteration, so the lazy caches can be filled without + // synchronisation - and every value is the one height_of()/color_of() would have produced. + tbb::parallel_for(tbb::blocked_range(0, verts.size()), + [&](const tbb::blocked_range &range) { + for (size_t v = range.begin(); v < range.end(); ++v) { + if (!wanted[v]) + continue; + if (feature_mode) { + vheight[v] = sampler(verts[v], vnormal[v]); + vheight_valid[v] = 1; + } + if (color_mode) { + vcolor[v] = color(verts[v], vnormal[v]); + vcolor_valid[v] = 1; + } + } + }); + } + auto elen_sq = [&](int a, int b) -> float { return (verts[a] - verts[b]).squaredNorm(); }; // The one edge of a triangle taken as its "longest": greatest squared length, exact ties broken by @@ -2388,9 +2517,22 @@ indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh, // re-scored on pop and dropped or re-pushed. The ordering is a budget-allocation heuristic only: // neither correctness nor conformality depends on it. std::priority_queue> queue; - for (int ti = 0; ti < int(tris.size()); ++ti) - if (const float p = priority(ti); p > 1.f) - queue.emplace(p, ti); + { + // Scoring the starting mesh means a detail_error() per triangle - four more sampler calls each + // - so it is worth spreading, even though the refinement that follows cannot be. Each entry is + // written by one iteration only, and the caches those calls fill (tri_err, tri_color_split) are + // likewise per triangle, so there is nothing shared to guard. The heap is then built from the + // finished array in index order, which is exactly the order the serial loop pushed in. + std::vector initial(tris.size(), 0.f); + tbb::parallel_for(tbb::blocked_range(0, tris.size()), + [&](const tbb::blocked_range &range) { + for (size_t ti = range.begin(); ti < range.end(); ++ti) + initial[ti] = priority(int(ti)); + }); + for (int ti = 0; ti < int(tris.size()); ++ti) + if (initial[size_t(ti)] > 1.f) + queue.emplace(initial[size_t(ti)], ti); + } // Every iteration either drops one satisfied triangle from the queue or performs exactly one // bisection, and bisections are capped by the triangle budget, so this always terminates. diff --git a/src/libslic3r/TextureDisplacement.hpp b/src/libslic3r/TextureDisplacement.hpp index 7abf1d656c..c187c3be48 100644 --- a/src/libslic3r/TextureDisplacement.hpp +++ b/src/libslic3r/TextureDisplacement.hpp @@ -169,7 +169,7 @@ struct TextureDisplacementLayer std::shared_ptr> image_data; float depth_mm = 0.4f; // maximum displacement along the surface normal, in mm - float tiling_scale = 10.f; // size of one texture tile, in mm + float tiling_scale = 12.5f; // size of one texture tile, in mm float rotation_deg = 0.f; Vec2f offset = Vec2f::Zero(); bool invert = false; @@ -365,6 +365,15 @@ struct TextureDisplacementOptions // deliberately (which is a blunter version of the per-layer edge-smoothing falloff). bool smooth_skip_border = true; + // Alternative bake pipeline, for side-by-side comparison. The path above is topology-preserving + // and needs the mesh prepared first; this one refines, removes slivers, displaces and optionally + // simplifies in one run. Off by default, and it produces no colour - it rebuilds the topology, so + // the per-facet assignment has nothing stable to attach to. + bool pipeline_v2 = false; + float v2_refine_mm = 0.3f; + bool v2_regularize = false; + int v2_max_triangles_k = 750; // 0 skips simplification, which is worth comparing on its own + // Colour, all of which belongs to the stack rather than to any one layer: it is about how the // printer will realise the colours, not about which image they came from. @@ -381,7 +390,8 @@ struct TextureDisplacementOptions { int mix_mode = int(color_mix_mode); ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border, - color_mix_enabled, mix_mode, color_despeckle); + pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k, color_mix_enabled, mix_mode, + color_despeckle); color_mix_mode = ColorMixMode(mix_mode); } }; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp index b0bfefddbd..8de71180f0 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp @@ -3317,7 +3317,9 @@ bool GLGizmoTextureDisplacement::collect_paint_region( ++vstart[size_t(its.indices[i][k]) + 1]; for (size_t v = 0; v < nvert; ++v) vstart[v + 1] += vstart[v]; - std::vector vtri(size_t(vstart[nvert]), 0); + // static_cast, not size_t(...): the latter parses as a parameter declaration (see the note + // above the identical prefix sum on `part`). + std::vector vtri(static_cast(vstart[nvert]), 0); { std::vector fill(vstart.begin(), vstart.begin() + nvert); for (size_t i = 0; i < ntri; ++i) @@ -4095,6 +4097,13 @@ void GLGizmoTextureDisplacement::bake_standard() } apply_standard_mode_presets(mv); // belt and braces: never bake with values the panel is not showing + // It refines as part of the bake, so preparing first would refine a second time at another + // target. + if (mv->texture_displacement_options.pipeline_v2) { + bake(); + return; + } + // The whole recipe in one go. Either stage having nothing to do is normal, not a failure - a mesh // that is already even needs no remesh, one that is already fine enough for the texture needs no // subdivision - so no "nothing changed" message here: it goes straight on to the displacement. @@ -5364,6 +5373,48 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float } m_imgui->disabled_end(); + // Next to Bake and shown in both modes: the two pipelines have to be switchable on their own. + if (mv != nullptr) { + TextureDisplacementOptions &opts = mv->texture_displacement_options; + ImGui::Separator(); + m_preview_params_dirty |= ImGui::Checkbox(_u8L("Experimental bake pipeline").c_str(), &opts.pipeline_v2); + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Bake with the alternative pipeline: it refines, cleans up sliver triangles, " + "displaces and simplifies in one run, instead of moving the vertices the mesh " + "already has. Nothing needs preparing first - Subdivide and Remesh are ignored. " + "It does not produce colours yet, because it rebuilds the topology."), + m_imgui->scaled(20.f)); + if (opts.pipeline_v2) { + ImGui::PushItemWidth(m_imgui->scaled(8.4f)); + if (m_imgui->slider_float(std::string(_u8L("Edge length (mm)")) + "##v2edge", &opts.v2_refine_mm, + 0.02f, 2.f, "%.1f", ImGuiLogSlider)) { + opts.v2_refine_mm = std::clamp(opts.v2_refine_mm, 0.02f, 2.f); + m_preview_params_dirty = true; + } + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Triangle size the painted area is refined to before displacement. This is " + "what decides how much of the texture the mesh can carry."), + m_imgui->scaled(20.f)); + if (ImGui::SliderInt((_u8L("Triangle budget (k)") + "##v2budget").c_str(), &opts.v2_max_triangles_k, + 0, 4000)) { + opts.v2_max_triangles_k = std::clamp(opts.v2_max_triangles_k, 0, 4000); + m_preview_params_dirty = true; + } + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Triangles to simplify down to after displacement, in thousands. 0 turns " + "simplification off, which is worth comparing on its own."), + m_imgui->scaled(20.f)); + ImGui::PopItemWidth(); + m_preview_params_dirty |= ImGui::Checkbox(_u8L("Clean up slivers").c_str(), &opts.v2_regularize); + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Collapse the thin triangles refinement inherits from the model's own " + "tessellation, before displacement samples them. A sliver's three corners " + "land on three unrelated parts of the texture, which is what makes the " + "relief look jagged."), + m_imgui->scaled(20.f)); + } + } + ImGui::SameLine(); m_imgui->disabled_begin(m_bake_in_progress || m_prepare_in_progress || mv == nullptr || !mv->is_texture_displacement_painted()); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp index 93bbc61117..05d2fa20eb 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp @@ -511,9 +511,9 @@ private: // The default used to be 1500 (i.e. +1.5 M triangles), which is what made Standard mode's Bake // take minutes: every stage after the subdivision - the displacement itself, the convex hull, the // GLModel upload, and the re-slice changed_object() triggers - then runs on a mesh two orders of - // magnitude denser than the input. 300k is still far finer than any FDM nozzle resolves at the + // magnitude denser than the input. 750k is still far finer than any FDM nozzle resolves at the // 0.02 mm detail tolerance Standard uses, and the slider goes to 2000 for anyone who wants more. - int m_subdivide_budget_k = 300; + int m_subdivide_budget_k = 750; void subdivide_model_adaptive(); // Fills `region` (per current-mesh triangle, a REFINE_* bitmask) from the union of every layer's // painted area plus the band straddling its edge. If `paint` is non-null, also fills the per-layer @@ -572,7 +572,6 @@ private: GLModel m_paint_overlay_glmodel; // Set on every paint event, cleared when the overlay is rebuilt in render_painter_gizmo(). Kept // separate from m_bump_preview_dirty so a stroke refreshes only the small painted patch per frame, - // not the bump mesh (which also carries every *unpainted* triangle of the volume). bool m_paint_overlay_dirty = false; void rebuild_paint_overlay(); void render_paint_overlay();