Rewrite adaptive subdivision to refine worst-first against a triangle budget

This commit is contained in:
ExPikaPaka
2026-07-28 11:42:40 +02:00
parent 0df1424101
commit 7b98433dd7
6 changed files with 877 additions and 246 deletions
+390 -108
View File
@@ -1289,6 +1289,94 @@ indexed_triangle_set build_texture_displacement(const ModelVolume &volume)
return build_texture_displacement(volume.mesh().its, volume.texture_displacement_layers, facets_data);
}
HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data)
{
// One decoded texture + placement per sampleable layer, in blend (slot) order. Held by shared_ptr
// so the returned closure owns it for as long as the subdivider keeps calling back.
struct Prepared {
DecodedHeightTexture tex;
TextureDisplacementLayer layer; // a copy of the params (depth/tiling/rotation/offset/blend/...)
Vec3f center; // patch centroid, for Cylindrical/Spherical
Vec3f axis; // cylinder axis, for Cylindrical
};
auto prepared = std::make_shared<std::vector<Prepared>>();
if (base_mesh.indices.empty())
return nullptr;
std::vector<const TextureDisplacementLayer *> ordered;
for (const TextureDisplacementLayer &l : layers)
if (l.slot >= 0 && l.slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
ordered.push_back(&l);
std::sort(ordered.begin(), ordered.end(),
[](const TextureDisplacementLayer *a, const TextureDisplacementLayer *b) { return a->slot < b->slot; });
const std::vector<Vec3f> vertex_normals = texture_displacement_vertex_normals(base_mesh);
const TriangleMesh selector_mesh(base_mesh);
for (const TextureDisplacementLayer *layer : ordered) {
if (layer->projection_method == TextureProjectionMethod::LSCM)
continue; // no per-point UV -> not sampleable here (caller falls back to uniform for these)
const TriangleSelector::TriangleSplittingData &data = facets_data[size_t(layer->slot)];
if (data.triangles_to_split.empty())
continue;
const DecodedHeightTexture tex = decode_height_texture(*layer);
if (tex.empty())
continue;
TriangleSelector selector(selector_mesh);
selector.deserialize(data, false);
const indexed_triangle_set patch = selector.get_facets_strict(EnforcerBlockerType::ENFORCER);
if (patch.indices.empty())
continue;
// Patch centroid + cylinder axis, computed exactly as build_texture_displacement() does, so a
// Cylindrical/Spherical layer's detach criterion matches the geometry the bake will produce.
Vec3f average_normal = Vec3f::Zero();
Vec3f centroid = Vec3f::Zero();
int count = 0;
for (const stl_triangle_vertex_indices &tri : patch.indices)
for (int i = 0; i < 3; ++i) {
const int vi = tri[i];
centroid += patch.vertices[size_t(vi)];
++count;
if (vi < int(vertex_normals.size()))
average_normal += vertex_normals[size_t(vi)];
}
average_normal = (average_normal.norm() > 1e-8f) ? Vec3f(average_normal.normalized()) : Vec3f::UnitZ();
centroid = (count > 0) ? Vec3f(centroid / float(count)) : Vec3f::Zero();
Vec3f axis = Vec3f::UnitZ();
const Vec3f an = average_normal.cwiseAbs();
if (an.x() <= an.y() && an.x() <= an.z())
axis = Vec3f::UnitX();
else if (an.y() <= an.x() && an.y() <= an.z())
axis = Vec3f::UnitY();
prepared->push_back({ tex, *layer, centroid, axis });
}
if (prepared->empty())
return nullptr;
return [prepared](const Vec3f &pos, const Vec3f &normal) -> float {
float total = 0.f;
bool any = false;
for (const Prepared &p : *prepared) {
const float h = sample_layer_height(p.tex, p.layer, pos, normal, p.center, p.axis, nullptr);
const float sign = p.layer.invert ? -1.f : 1.f;
const float signed_h = (h - p.layer.midlevel) * p.layer.depth_mm * sign;
// The first (lowest) sampleable layer folds additively; the rest use their own blend mode -
// same rule build_texture_displacement() applies per vertex.
total = blend_displacement(total, signed_h, any ? p.layer.blend_mode : TextureBlendMode::Add);
any = true;
}
return total;
};
}
indexed_triangle_set subdivide_mesh_uniform(const indexed_triangle_set &mesh, float max_edge_length_mm, int max_iterations)
{
indexed_triangle_set current = mesh;
@@ -1348,146 +1436,340 @@ indexed_triangle_set subdivide_mesh_uniform(const indexed_triangle_set &mesh, fl
indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
const std::vector<uint8_t> &refine_region,
float target_edge_length_mm, int max_iterations,
std::vector<int> *out_source)
float target_edge_length_mm, int max_triangles,
std::vector<int> *out_source, const HeightFieldSampler &sampler,
float chord_tolerance_mm, float min_edge_length_mm)
{
// Each triangle carries the input-triangle index it descends from, so children inherit it and
// the caller can remap per-triangle data (paint masks) for free.
struct Tri { int v[3]; int src; };
// Neighbour slots that are not a triangle index.
constexpr int NB_BOUNDARY = -1; // open edge: terminal on its own, bisected from this side alone
constexpr int NB_NONMANIFOLD = -2; // >2 triangles on the edge: never bisected, that would tear it
// v[] and nb[] are parallel: nb[e] is the triangle across edge (v[e], v[(e+1)%3]). `src` is the
// input triangle this one descends from - children inherit it, so a caller can carry per-triangle
// data (a paint mask) across the topology change with no geometric remap.
struct Tri { int v[3]; int nb[3]; int src; };
std::vector<Vec3f> verts = mesh.vertices;
std::vector<Tri> tris;
tris.reserve(mesh.indices.size());
std::vector<Tri> tris(mesh.indices.size());
for (size_t i = 0; i < mesh.indices.size(); ++i)
tris.push_back({ { mesh.indices[i][0], mesh.indices[i][1], mesh.indices[i][2] }, int(i) });
tris[i] = { { mesh.indices[i][0], mesh.indices[i][1], mesh.indices[i][2] },
{ NB_BOUNDARY, NB_BOUNDARY, NB_BOUNDARY }, int(i) };
auto emit = [&](std::vector<Tri> &t) -> indexed_triangle_set {
auto emit = [&]() -> indexed_triangle_set {
indexed_triangle_set out;
out.vertices = verts;
out.indices.reserve(t.size());
out.indices.reserve(tris.size());
if (out_source) {
out_source->clear();
out_source->reserve(t.size());
out_source->reserve(tris.size());
}
for (const Tri &tr : t) {
out.indices.emplace_back(tr.v[0], tr.v[1], tr.v[2]);
for (const Tri &t : tris) {
out.indices.emplace_back(t.v[0], t.v[1], t.v[2]);
if (out_source)
out_source->push_back(tr.src);
out_source->push_back(t.src);
}
return out;
};
// Feature-adaptive when a sampler and a positive tolerance are supplied; otherwise refinement is
// driven by the length baseline alone.
const bool feature_mode = bool(sampler) && chord_tolerance_mm > 0.f;
const float min_floor_sq = min_edge_length_mm > 0.f ? min_edge_length_mm * min_edge_length_mm : 0.f;
const float target_sq = target_edge_length_mm > 0.f ? target_edge_length_mm * target_edge_length_mm : 0.f;
// refine_region is indexed by input-triangle index, and every triangle's src stays in that range
// (children inherit their parent's src), so a wrong size would be an out-of-bounds read. Guard it.
if (target_edge_length_mm <= 0.f || refine_region.size() != mesh.indices.size())
return emit(tris);
if (refine_region.size() != mesh.indices.size() || int(tris.size()) + 2 > max_triangles)
return emit();
if (!feature_mode && target_sq <= 0.f)
return emit(); // no criterion at all
if (std::none_of(refine_region.begin(), refine_region.end(), [](uint8_t v) { return v != 0; }))
return emit(tris); // nothing flagged: no-op
const float target_sq = target_edge_length_mm * target_edge_length_mm;
return emit(); // nothing flagged: no-op
auto edge_key = [](int a, int b) -> uint64_t {
if (a > b)
std::swap(a, b);
return (uint64_t(uint32_t(a)) << 32) | uint32_t(b);
};
auto edge_len_sq = [&](uint64_t k) -> float {
return (verts[int(k >> 32)] - verts[int(uint32_t(k))]).squaredNorm();
};
// The one edge of a triangle chosen as its "longest": greatest squared length, ties broken by the
// smaller edge key. The tie-break is by mesh-vertex indices, which both triangles sharing an edge
// compute identically - so they never disagree about whether that shared edge is "the" longest,
// which is what the conformality argument rests on.
auto longest_key = [&](const Tri &t) -> uint64_t {
uint64_t best_k = edge_key(t.v[0], t.v[1]);
float best_len = edge_len_sq(best_k);
for (int e = 1; e < 3; ++e) {
const uint64_t k = edge_key(t.v[e], t.v[(e + 1) % 3]);
const float l = edge_len_sq(k);
if (l > best_len || (l == best_len && k < best_k)) {
best_len = l;
best_k = k;
}
}
return best_k;
};
for (int iter = 0; iter < max_iterations; ++iter) {
// edge -> the (up to two) triangles sharing it. A third triangle on an edge means a
// non-manifold input; it is left in the second slot's place and simply not treated as
// terminal, so such an edge is never bisected (better a missed refinement than a torn mesh).
std::unordered_map<uint64_t, std::array<int, 2>> edge_tris;
edge_tris.reserve(tris.size() * 3);
// Edge adjacency, built once here and then maintained incrementally by bisect() below. Rebuilding
// it per refinement pass is what made the previous version's cost scale with the whole model
// 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<uint64_t, EdgeRec> edges;
edges.reserve(tris.size() * 2);
for (int ti = 0; ti < int(tris.size()); ++ti)
for (int e = 0; e < 3; ++e) {
const uint64_t k = edge_key(tris[ti].v[e], tris[ti].v[(e + 1) % 3]);
auto it = edge_tris.find(k);
if (it == edge_tris.end())
edge_tris.emplace(k, std::array<int, 2>{ ti, -1 });
else if (it->second[1] == -1)
it->second[1] = ti;
else
it->second[0] = -2; // >2 triangles: poison this edge (never terminal)
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;
}
std::vector<uint64_t> tri_longest(tris.size());
for (int ti = 0; ti < int(tris.size()); ++ti)
tri_longest[ti] = longest_key(tris[ti]);
// Which edges to bisect this pass: terminal (the longest edge of every triangle on it) AND
// long enough AND wanted by the region (either side in it). Terminal-ness is exactly what
// guarantees both sides split together, so no hanging node is ever produced.
std::unordered_set<uint64_t> to_bisect;
for (const auto &[k, slot] : edge_tris) {
const int t0 = slot[0], t1 = slot[1];
if (t0 < 0)
continue; // poisoned (non-manifold)
if (tri_longest[t0] != k)
continue;
if (t1 >= 0 && tri_longest[t1] != k)
continue;
if (edge_len_sq(k) <= target_sq)
continue;
const bool in_region = (refine_region[tris[t0].src] != 0) ||
(t1 >= 0 && refine_region[tris[t1].src] != 0);
if (in_region)
to_bisect.insert(k);
}
if (to_bisect.empty())
break;
// One midpoint per bisected edge, shared by both sides.
std::unordered_map<uint64_t, int> mid;
mid.reserve(to_bisect.size());
for (const uint64_t k : to_bisect) {
const int a = int(k >> 32), b = int(uint32_t(k));
mid.emplace(k, int(verts.size()));
verts.push_back((verts[a] + verts[b]) * 0.5f);
}
std::vector<Tri> next;
next.reserve(tris.size() + to_bisect.size());
for (const Tri &t : tris) {
// At most one of a triangle's edges can be terminal (only its own longest can be), so at
// most one is in to_bisect - find that one.
int be = -1;
for (int e = 0; e < 3; ++e)
if (to_bisect.count(edge_key(t.v[e], t.v[(e + 1) % 3])) != 0) {
be = e;
break;
}
if (be == -1) {
next.push_back(t);
continue;
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;
}
const int a = t.v[be], b = t.v[(be + 1) % 3], c = t.v[(be + 2) % 3];
const int m = mid.at(edge_key(a, b));
next.push_back({ { a, m, c }, t.src }); // both children keep the original winding a->b->c
next.push_back({ { m, b, c }, t.src });
}
tris.swap(next);
}
return emit(tris);
// Feature mode: per-vertex surface normal, and the sampled displacement height at each vertex.
// Heights are filled in lazily - on a big model only a small painted region is ever looked at, and
// a sampler call is a texture fetch (plus trig) per layer, so sampling every vertex of the whole
// mesh up front was pure waste. Both arrays grow in lockstep with `verts`.
std::vector<Vec3f> vnormal;
std::vector<float> vheight;
std::vector<uint8_t> vheight_valid;
if (feature_mode) {
vnormal.assign(verts.size(), Vec3f::Zero());
for (const Tri &t : tris) {
const Vec3f fn = (verts[t.v[1]] - verts[t.v[0]]).cross(verts[t.v[2]] - verts[t.v[0]]); // area-weighted
for (int i = 0; i < 3; ++i)
vnormal[t.v[i]] += fn;
}
for (Vec3f &n : vnormal) {
const float l = n.norm();
n = (l > 1e-12f) ? Vec3f(n / l) : Vec3f(Vec3f::UnitZ());
}
vheight.assign(verts.size(), 0.f);
vheight_valid.assign(verts.size(), 0);
}
auto height_of = [&](int v) -> float {
if (!vheight_valid[v]) {
vheight[v] = sampler(verts[v], vnormal[v]);
vheight_valid[v] = 1;
}
return vheight[v];
};
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
// the smaller (sorted) vertex-index key. Both triangles sharing an edge compute the same key for
// it, so they can never disagree about which of them is longest - the property the conformality
// argument and the LEPP walk's termination both rest on.
auto longest_local = [&](int ti) -> int {
const Tri &t = tris[ti];
int best = 0;
float bl = elen_sq(t.v[0], t.v[1]);
uint64_t bk = edge_key(t.v[0], t.v[1]);
for (int e = 1; e < 3; ++e) {
const float l = elen_sq(t.v[e], t.v[(e + 1) % 3]);
const uint64_t k = edge_key(t.v[e], t.v[(e + 1) % 3]);
if (l > bl || (l == bl && k < bk)) {
bl = l;
bk = k;
best = e;
}
}
return best;
};
// How far the *displaced* surface departs from the flat triangle, sampled across the WHOLE
// triangle - the three edge midpoints and the centroid - not just one edge midpoint. Sampling the
// interior is what catches a bump that sits inside a triangle (the blind spot of an edge-only
// test). Cached per triangle: it can only change when the triangle is split, and then both
// children are fresh entries.
std::vector<float> tri_err;
if (feature_mode)
tri_err.assign(tris.size(), -1.f);
auto detail_error = [&](int ti) -> float {
if (tri_err[ti] >= 0.f)
return tri_err[ti];
const Tri &t = tris[ti];
const Vec3f pa = verts[t.v[0]], pb = verts[t.v[1]], pc = verts[t.v[2]];
const Vec3f na = vnormal[t.v[0]], nb = vnormal[t.v[1]], nc = vnormal[t.v[2]];
const float ha = height_of(t.v[0]), hb = height_of(t.v[1]), hc = height_of(t.v[2]);
static const float BARY[4][3] = { { 0.5f, 0.5f, 0.f }, { 0.f, 0.5f, 0.5f },
{ 0.5f, 0.f, 0.5f }, { 1.f / 3, 1.f / 3, 1.f / 3 } };
float maxerr = 0.f;
for (const auto &w : BARY) {
Vec3f n = w[0] * na + w[1] * nb + w[2] * nc;
const float nl = n.norm();
n = (nl > 1e-12f) ? Vec3f(n / nl) : na;
const float actual = sampler(w[0] * pa + w[1] * pb + w[2] * pc, n);
maxerr = std::max(maxerr, std::abs(actual - (w[0] * ha + w[1] * hb + w[2] * hc)));
}
tri_err[ti] = maxerr;
return maxerr;
};
// How many times over its criteria a triangle is: <= 1 means "good enough, leave it alone", and
// the larger the value the more a split buys. Driving the heap with this is what makes a run that
// runs out of budget spend it on the worst offenders instead of wherever a sweep happened to
// reach. Triangles outside the region always score 0 - they are only ever touched by the conformal
// closure below, never refined on their own account.
auto priority = [&](int ti) -> float {
const Tri &t = tris[ti];
if (refine_region[t.src] == 0)
return 0.f;
const int le = longest_local(ti);
const float ll = elen_sq(t.v[le], t.v[(le + 1) % 3]);
if (ll <= min_floor_sq)
return 0.f; // at the resolution floor - also what stops a sharp texture step going forever
float p = (target_sq > 0.f) ? ll / target_sq : 0.f;
if (feature_mode)
p = std::max(p, detail_error(ti) / chord_tolerance_mm);
return p;
};
auto set_nb = [&](int ti, int u, int v, int val) {
if (ti < 0)
return;
Tri &t = tris[ti];
for (int e = 0; e < 3; ++e)
if ((t.v[e] == u && t.v[(e + 1) % 3] == v) || (t.v[e] == v && t.v[(e + 1) % 3] == u)) {
t.nb[e] = val;
return;
}
};
// Bisects triangle `ti` across its local edge `e`, which the caller has established is terminal.
// Both triangles on that edge are split in the one operation, around a single shared midpoint -
// which is exactly why a hanging node (and so a crack) can never appear. `ti` and the opposite
// triangle are each reused as one of their own children, so only two back-pointers in the
// surrounding mesh need repointing. Triangles whose geometry changed are left in `touched`.
std::vector<int> touched;
auto bisect = [&](int ti, int e) -> bool {
const int a = tris[ti].v[e], b = tris[ti].v[(e + 1) % 3], c = tris[ti].v[(e + 2) % 3];
const int n = tris[ti].nb[e];
// Locate the shared edge from the far side before touching anything: bailing out half way
// through would be the one way this could leave a crack.
int f = -1;
if (n >= 0) {
for (int k = 0; k < 3; ++k) {
const int u = tris[n].v[k], w = tris[n].v[(k + 1) % 3];
if ((u == a && w == b) || (u == b && w == a)) {
f = k;
break;
}
}
if (f < 0) {
tris[ti].nb[e] = NB_NONMANIFOLD; // inconsistent adjacency: refuse to split across it
return false;
}
}
const int m = int(verts.size());
verts.push_back(0.5f * (verts[a] + verts[b]));
if (feature_mode) {
const Vec3f mn = vnormal[a] + vnormal[b];
const float ml = mn.norm();
vnormal.push_back(ml > 1e-12f ? Vec3f(mn / ml) : vnormal[a]);
vheight.push_back(0.f);
vheight_valid.push_back(0);
}
// Near side: ti becomes (a, m, c), the new triangle is (m, b, c). Both keep the original
// a->b->c winding.
const int nb_bc = tris[ti].nb[(e + 1) % 3];
const int nb_ca = tris[ti].nb[(e + 2) % 3];
const int src = tris[ti].src;
const int t2 = int(tris.size());
tris.push_back(Tri{ { m, b, c }, { NB_BOUNDARY, nb_bc, ti }, src });
{
Tri &t1 = tris[ti];
t1.v[0] = a; t1.v[1] = m; t1.v[2] = c;
t1.nb[0] = NB_BOUNDARY; t1.nb[1] = t2; t1.nb[2] = nb_ca;
}
set_nb(nb_bc, b, c, t2); // that outer neighbour borders the second child now, not ti
if (feature_mode) {
tri_err.push_back(-1.f);
tri_err[ti] = -1.f;
}
touched.assign({ ti, t2 });
if (n < 0) {
return true; // boundary edge: nothing on the far side to split
}
// Far side, same shape: n becomes (p, m, d), the new triangle is (m, q, d).
const int p = tris[n].v[f], q = tris[n].v[(f + 1) % 3], d = tris[n].v[(f + 2) % 3];
const int nb_qd = tris[n].nb[(f + 1) % 3];
const int nb_dp = tris[n].nb[(f + 2) % 3];
const int nsrc = tris[n].src;
const int n2 = int(tris.size());
tris.push_back(Tri{ { m, q, d }, { NB_BOUNDARY, nb_qd, n }, nsrc });
{
Tri &n1 = tris[n];
n1.v[0] = p; n1.v[1] = m; n1.v[2] = d;
n1.nb[0] = NB_BOUNDARY; n1.nb[1] = n2; n1.nb[2] = nb_dp;
}
set_nb(nb_qd, q, d, n2);
if (feature_mode) {
tri_err.push_back(-1.f);
tri_err[n] = -1.f;
}
// Stitch the two sides back together: whichever far child holds `a` borders the near child
// that holds `a`. (Which one that is depends on how n happens to be wound.)
const int side_a = (p == a) ? n : n2;
const int side_b = (p == a) ? n2 : n;
tris[ti].nb[0] = side_a; // near child (a, m, c), edge (a, m)
tris[t2].nb[0] = side_b; // near child (m, b, c), edge (m, b)
set_nb(side_a, a, m, ti);
set_nb(side_b, b, m, t2);
touched.assign({ ti, t2, n, n2 });
return true;
};
// Worst-first. Entries go stale as their triangle is split; a stale entry is harmless - it is
// 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<std::pair<float, int>> queue;
for (int ti = 0; ti < int(tris.size()); ++ti)
if (const float p = priority(ti); p > 1.f)
queue.emplace(p, 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.
while (!queue.empty() && int(tris.size()) + 2 <= max_triangles) {
const int ti = queue.top().second;
queue.pop();
if (priority(ti) <= 1.f)
continue; // stale: already refined past its criteria
// Longest-Edge Propagation Path: step to the neighbour across the current longest edge for as
// long as that neighbour has a strictly longer one, then bisect the terminal edge we land on.
// Length strictly increases along the path, so it cannot cycle.
int cur = ti, split_edge = -1;
for (size_t guard = 0; guard <= tris.size(); ++guard) {
const int le = longest_local(cur);
const int nbr = tris[cur].nb[le];
if (nbr == NB_NONMANIFOLD)
break; // cannot split across it without tearing the mesh: give up on this path
if (nbr == NB_BOUNDARY) {
split_edge = le; // boundary longest edge -> terminal
break;
}
const int nle = longest_local(nbr);
if (edge_key(tris[nbr].v[nle], tris[nbr].v[(nle + 1) % 3]) ==
edge_key(tris[cur].v[le], tris[cur].v[(le + 1) % 3])) {
split_edge = le; // mutual longest edge -> terminal
break;
}
cur = nbr;
}
if (split_edge < 0 || !bisect(cur, split_edge))
continue; // ti sits in a non-manifold neighbourhood and cannot be refined safely
for (const int t : touched)
if (const float p = priority(t); p > 1.f)
queue.emplace(p, t);
// The bisection may have been a step on the way to ti rather than ti itself, in which case ti
// is not in `touched` and has to go back on the heap to be walked again.
if (cur != ti)
if (const float p = priority(ti); p > 1.f)
queue.emplace(p, ti);
}
return emit();
}
} // namespace Slic3r
+67 -22
View File
@@ -2,6 +2,7 @@
#define slic3r_TextureDisplacement_hpp_
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>
@@ -493,6 +494,25 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
// and forwards to the overload above.
indexed_triangle_set build_texture_displacement(const ModelVolume &volume);
// Returns a scalar height (in mm - a displacement magnitude) at a surface point, given that point's
// position and interpolated normal. This is what feature-adaptive subdivision samples to decide
// where the displaced surface has *curvature* worth spending triangles on. Called serially from the
// subdivider, so it only needs to be safe on the calling thread.
using HeightFieldSampler = std::function<float(const Vec3f &pos, const Vec3f &normal)>;
// Builds a sampler of the *combined* (all-layers) displacement height in mm at an arbitrary surface
// point, for feature-adaptive subdivision. It mirrors build_texture_displacement()'s per-layer setup
// (decode, patch centroid, cylinder axis, blend order, "lowest layer folds additively") but evaluates
// per point instead of per vertex. Two deliberate simplifications, both erring toward *more* detail
// (safe - over-refinement is never a crack): every sampleable layer is evaluated at every point (no
// per-point paint-mask test, so a point sees all layers' textures, not only the ones painted there),
// and edge-smoothing's boundary falloff is ignored. LSCM layers have no per-point UV and are skipped.
// Returns a null sampler (bool false) when no layer can be sampled - the caller then falls back to
// uniform adaptive subdivision.
HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data);
// Uniformly subdivides `mesh` (every triangle recursively split into 4 via edge midpoints, using a
// shared cache so a midpoint is computed once and reused by both triangles on either side of that
// edge) until every edge is at or below max_edge_length_mm, or max_iterations passes have run,
@@ -513,32 +533,57 @@ indexed_triangle_set subdivide_mesh_uniform(const indexed_triangle_set &mesh, fl
// Adaptive subdivision by Rivara longest-edge bisection, restricted to a region.
//
// Unlike subdivide_mesh_uniform() this only densifies where asked - a triangle is refined when its
// source is flagged in `refine_region` and its longest edge exceeds target_edge_length_mm - so a
// small painted patch on a large model does not quadruple the whole model's triangle count. It is
// nonetheless *conformal*: it never leaves a T-junction/crack at the boundary between the refined
// and coarse regions (the trap that made subdivide_mesh_uniform() deliberately whole-mesh).
// Unlike subdivide_mesh_uniform() this only densifies where asked - `refine_region` (indexed by
// input-triangle index; empty or all-false => no-op) flags the triangles allowed to drive refinement
// - so a small painted patch on a large model does not quadruple the whole model's triangle count.
// It is nonetheless *conformal*: it never leaves a T-junction/crack at the boundary between the
// refined and coarse regions (the trap that made subdivide_mesh_uniform() deliberately whole-mesh).
//
// How it stays crack-free: each pass bisects only "terminal" edges - an edge that is the longest
// edge of *every* triangle sharing it. Bisecting such an edge splits both its triangles 1->2 along
// the same new midpoint at once, so no hanging node is ever created. Because only a triangle's own
// longest edge can be terminal, each triangle is split by at most one terminal edge per pass. When
// a triangle that needs refining has a longest edge that is *not* yet terminal, the neighbour across
// it has a strictly longer edge and is refined first; that propagation is what grades the mesh down
// into the region and closes what would otherwise be cracks (Rivara, "New longest-edge algorithms").
// The transition triangles it pulls in just outside the region are a thin, bounded band.
// A triangle wants refining while it is over at least one of these, whichever applies:
// - length: its longest edge exceeds `target_edge_length_mm` (a *baseline* - it applies in feature
// mode too, and is what stops a coarse triangle from being declared flat merely because
// the four points the chord test samples happened to land at similar heights on a
// high-frequency texture: the classic aliasing stall);
// - feature: (only when `sampler` is set and `chord_tolerance_mm > 0`) the *displaced* surface
// departs from the flat triangle by more than `chord_tolerance_mm`, measured as the max
// over the three edge midpoints AND the centroid of |sampled displacement - the flat
// triangle's barycentric interpolation|. Sampling the interior, not just edge midpoints,
// is what catches a bump that sits inside a triangle. This is a *curvature* test: it is
// exactly zero on a plane or a linear ramp (barycentric interpolation is exact there, so
// those stay coarse - the case a gradient criterion would over-refine) and large on a
// bump/ridge/noise.
// `min_edge_length_mm` is a hard floor under both: no triangle whose longest edge is already at or
// below it is ever refined, which is also what guarantees termination across a sharp texture step
// (where the chord error never falls below the tolerance no matter how fine the mesh gets).
//
// `refine_region` is indexed by input-triangle index (empty or all-false => no-op). If `out_source`
// is non-null it is resized to the output triangle count and out_source[i] receives the input
// triangle that output triangle i descends from (children inherit their parent's index), so a caller
// can carry per-triangle data - e.g. a paint mask - across the topology change without a geometric
// remap. `max_iterations` bounds the worst case; ties in "longest edge" are broken by a mesh-vertex
// key so both triangles on an edge always agree, at the cost of occasionally stopping one pass early
// on a pathologically tie-heavy mesh (a quality shortfall, never a crack).
// Refinement runs to completion, not for a fixed number of passes: triangles are taken worst-first
// from a max-heap keyed by how many times over its criteria each one is, so a run that hits the
// `max_triangles` budget has spent it on the largest errors rather than wherever a sweep happened to
// reach. The budget is the only bound on a pathological height field; stopping on it leaves a
// perfectly valid, still-conformal mesh.
//
// How it stays crack-free: only "terminal" edges are ever bisected - an edge that is the longest edge
// of *both* triangles sharing it (or a boundary edge that is the longest of its one triangle).
// Bisecting such an edge splits both its triangles 1->2 around the same new midpoint, so a hanging
// node is never created. The edge to split for a triangle that wants refining is found by Rivara
// longest-edge propagation (LEPP): walk to the longest edge of ever-longer-edged neighbours until a
// terminal edge is reached, and bisect that. Edge length strictly increases along the path (ties
// broken by a mesh-vertex key, which both sides of an edge compute identically), so the walk cannot
// cycle, and Rivara's result is that repeating it refines the original triangle in a bounded number
// of bisections - closing the propagation gap a plain per-edge test leaves behind. The transition
// triangles this pulls in just outside the region are the graded band that makes the size change
// conformal; they are a bounded cost paid once, not a per-pass tax.
//
// If `out_source` is non-null it is resized to the output triangle count and out_source[i] receives
// the input triangle that output triangle i descends from (children inherit their parent's index), so
// a caller can carry per-triangle data - e.g. a paint mask - across the topology change without a
// geometric remap.
indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
const std::vector<uint8_t> &refine_region,
float target_edge_length_mm, int max_iterations = 12,
std::vector<int> *out_source = nullptr);
float target_edge_length_mm, int max_triangles = 1000000,
std::vector<int> *out_source = nullptr,
const HeightFieldSampler &sampler = nullptr,
float chord_tolerance_mm = 0.f, float min_edge_length_mm = 0.f);
} // namespace Slic3r
@@ -166,7 +166,7 @@ void GLGizmoTextureDisplacement::on_shutdown()
m_seam_path_mode = false;
m_seam_path_anchor = -1;
m_subdivide_editing = false;
m_subdivide_preview_count = -1;
m_subdivide_preview_tris = -1;
m_subdivide_preview_glmodel.reset();
m_bump_active_chart = -1;
m_bump_active_vertex.clear();
@@ -231,6 +231,11 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
m_bump_preview_dirty = false;
}
const bool use_bump = m_use_bump_preview && m_bump_preview_glmodel.is_initialized();
// In Checker/Distortion mode the UV-check overlay *is* the surface visualization the user is
// looking at, so the opaque paint-selection highlight must not be drawn on top of it - same
// reasoning as skipping it for the bump preview (see bug #12). Without this the painted area
// covers the checker/heatmap and it can't be seen.
const bool show_paint_overlay = m_uv_check_mode == UVCheckMode::None;
if (use_bump) {
m_parent.toggle_model_objects_visibility(true);
if (ModelVolume *mv = texture_volume())
@@ -244,11 +249,13 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
m_c->selection_info()->get_active_instance(), mv);
render_preview_mesh();
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
glsafe(::glPolygonOffset(-1.0f, -1.0f));
render_triangles(selection);
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
} else {
if (show_paint_overlay) {
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
glsafe(::glPolygonOffset(-1.0f, -1.0f));
render_triangles(selection);
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
}
} else if (show_paint_overlay) {
render_triangles(selection);
}
@@ -744,10 +751,9 @@ void GLGizmoTextureDisplacement::rebuild_bump_preview_mesh()
if (base.vertices.size() != patch.vertices.size())
return; // shouldn't happen: get_facets_strict() always returns the full vertex array
std::vector<bool> is_painted(patch.vertices.size(), false);
for (const stl_triangle_vertex_indices &tri : patch.indices)
for (int i = 0; i < 3; ++i)
is_painted[tri[i]] = true;
// Unpainted triangles, so the surrounding surface still renders (the render path hides the real
// model in bump mode). get_facets_strict() returns the same vertex array whatever state is asked.
const indexed_triangle_set rest = m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::NONE);
// For LSCM we hand the shader the finished per-vertex texture uv (island placement + tiling/
// rotation/offset already folded in, exactly what the bake samples), because it cannot be
@@ -761,26 +767,38 @@ void GLGizmoTextureDisplacement::rebuild_bump_preview_mesh()
vertex_uv.clear();
GLModel::Geometry init_data;
// P3N3T2: normal.x carries the paint weight, tex_coord carries the precomputed uv (see the vertex
// shader). Reuses a standard GLModel layout rather than a bespoke vertex buffer.
// P3N3T2: normal.x carries the paint weight, tex_coord the precomputed uv (see the vertex shader).
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3T2 };
init_data.reserve_vertices(base.vertices.size());
init_data.reserve_indices(base.indices.size() * 3);
// Every triangle of the whole mesh is included (not just the painted ones) so the surrounding,
// un-bumped surface still renders - the per-vertex weight (0 outside the patch) is what fades
// the shader's bump effect to nothing there, exactly like the true-displacement preview fades
// to the untouched surface at its boundary.
// normal.y flags the island currently dragged in the UV editor, so the shader can move just that
// island through the island_delta uniform (see the bump shaders / on_island_edited).
const bool have_active = m_bump_active_chart >= 0 && !m_bump_active_vertex.empty();
for (size_t vi = 0; vi < base.vertices.size(); ++vi) {
const float active = (have_active && vi < m_bump_active_vertex.size() && m_bump_active_vertex[vi]) ? 1.f : 0.f;
const Vec3f weight_normal(is_painted[vi] ? 1.f : 0.f, active, 0.f);
const Vec2f uv = m_bump_preview_uses_vertex_uv ? vertex_uv[vi] : Vec2f::Zero();
init_data.add_vertex(base.vertices[vi], weight_normal, uv);
}
for (const stl_triangle_vertex_indices &tri : base.indices)
init_data.add_triangle(unsigned(tri[0]), unsigned(tri[1]), unsigned(tri[2]));
// Per-triangle weighting via a *flat* (unshared-vertex) mesh: every corner of a painted triangle
// gets weight 1, every corner of an unpainted one weight 0. This is what a coarse mesh needs - one
// painted face of a raw cube has no strictly-interior vertex (all 8 are shared), so per-vertex
// weighting would either bleed onto the neighbours (boundary weight 1) or vanish outright (boundary
// weight 0, which is what made a single face show nothing). Duplicating vertices costs no shading
// quality here because the bump shader takes its surface normal from screen-space derivatives of
// position (dFdx/dFdy), not from a per-vertex normal. normal.y flags the UV-editor island being
// dragged so the shader can move just that island via the island_delta uniform.
const bool have_active = m_bump_active_chart >= 0 && !m_bump_active_vertex.empty();
const size_t tri_total = patch.indices.size() + rest.indices.size();
init_data.reserve_vertices(tri_total * 3);
init_data.reserve_indices(tri_total * 3);
unsigned vcount = 0;
const auto emit_triangles = [&](const indexed_triangle_set &its, float weight) {
for (const stl_triangle_vertex_indices &tri : its.indices) {
for (int i = 0; i < 3; ++i) {
const int idx = tri[i];
const float act = (have_active && idx >= 0 && size_t(idx) < m_bump_active_vertex.size() &&
m_bump_active_vertex[size_t(idx)]) ? 1.f : 0.f;
const Vec2f uv = (weight > 0.5f && m_bump_preview_uses_vertex_uv && size_t(idx) < vertex_uv.size()) ?
vertex_uv[size_t(idx)] : Vec2f::Zero();
init_data.add_vertex(its.vertices[size_t(idx)], Vec3f(weight, act, 0.f), uv);
}
init_data.add_triangle(vcount, vcount + 1, vcount + 2);
vcount += 3;
}
};
emit_triangles(patch, 1.f); // painted -> bumped
emit_triangles(rest, 0.f); // untouched surface -> flat, so it still shows but isn't bumped
m_bump_preview_glmodel.init_from(std::move(init_data));
// GLModel::render() unconditionally re-sets the shader's "uniform_color" from this internal
@@ -2186,6 +2204,13 @@ void GLGizmoTextureDisplacement::update_model_object()
updated |= mv->texture_displacement_facet(m_active_layer_slot).set(*m_triangle_selectors[idx]);
}
// The fast (bump) preview reads the live selector, so it has to be rebuilt after any stroke that
// flushes here - not only when set() reports a change. Rebuilding it via rebuild_preview() below
// is gated on `updated`, which misses e.g. the first paint into a slot; marking it dirty makes the
// render loop (render_painter_gizmo) rebuild it next frame regardless. Without this, fast preview -
// now the default view - stayed blank until a full reload (select-whole-model / reopen).
m_bump_preview_dirty = true;
if (updated) {
const ModelObjectPtrs &mos = wxGetApp().model().objects;
wxGetApp().obj_list()->update_info_items(std::find(mos.begin(), mos.end(), mo) - mos.begin());
@@ -2676,16 +2701,17 @@ bool GLGizmoTextureDisplacement::collect_paint_region(
for (auto &pt : *painted_tri)
pt.clear();
// Sorted-vertex-triple -> triangle index, so a fully-painted patch sub-triangle (which comes
// back with the original mesh's own three vertex indices) can be mapped to its source triangle.
// A sub-triangle produced by a *partial* brush stroke has at least one appended (split) vertex,
// so "all three indices are original" is exactly the test for a whole, fully-painted triangle.
// Sorted-vertex-triple -> triangle index, so a fully-painted patch sub-triangle (which comes back
// with the original mesh's own three vertex indices) can be mapped to its source triangle. Only
// the paint carry-forward needs it, and the live subdivide preview calls this on every slider
// frame, so it is not built for the region-only path.
std::map<std::array<int, 3>, int> tri_by_verts;
for (size_t i = 0; i < ntri; ++i) {
std::array<int, 3> k{ its.indices[i][0], its.indices[i][1], its.indices[i][2] };
std::sort(k.begin(), k.end());
tri_by_verts.emplace(k, int(i));
}
if (painted_tri)
for (size_t i = 0; i < ntri; ++i) {
std::array<int, 3> k{ its.indices[i][0], its.indices[i][1], its.indices[i][2] };
std::sort(k.begin(), k.end());
tri_by_verts.emplace(k, int(i));
}
bool any_paint = false;
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
@@ -2693,40 +2719,34 @@ bool GLGizmoTextureDisplacement::collect_paint_region(
if (!TriangleSelector::has_facets(data, EnforcerBlockerType::ENFORCER))
continue;
TriangleSelector sel(mv->mesh());
sel.deserialize(data, false);
const indexed_triangle_set patch = sel.get_facets_strict(EnforcerBlockerType::ENFORCER);
// The refine region is exactly the original triangles the brush touched. `triangles_to_split`
// lists precisely those: serialize() records an entry for every original triangle that is
// either split (i.e. partially painted, which is the patch boundary) or carries a non-default
// state (fully painted). No dilation - an earlier version marked every triangle sharing a
// *vertex* with the patch, which on a coarse model pulls in a whole fan of huge unpainted
// neighbours and then refines them to the resolution floor, since the height field the detail
// test samples is not restricted to the painted area. The conformal closure inside
// subdivide_mesh_adaptive() already grades the size change outward on its own.
for (const TriangleSelector::TriangleBitStreamMapping &m : data.triangles_to_split)
if (size_t(m.triangle_idx) < ntri)
region[m.triangle_idx] = 1;
std::vector<uint8_t> painted_vertex(nvert, 0);
if (painted_tri)
if (painted_tri) {
TriangleSelector sel(mv->mesh());
sel.deserialize(data, false);
(*painted_tri)[slot].assign(ntri, 0);
for (const stl_triangle_vertex_indices &t : patch.indices) {
bool all_original = true;
for (int k = 0; k < 3; ++k) {
if (size_t(t[k]) < nvert)
painted_vertex[t[k]] = 1; // marks the refine region (any coverage, plus a ring)
else
all_original = false; // a split vertex -> this is a partial sub-triangle
}
if (all_original && painted_tri) {
for (const stl_triangle_vertex_indices &t : sel.get_facets_strict(EnforcerBlockerType::ENFORCER).indices) {
// A sub-triangle produced by a *partial* stroke always carries at least one appended
// (split) vertex, so "all three indices are original" is exactly the test for a whole,
// fully-painted triangle - the only kind whose paint can be inherited wholesale.
if (size_t(t[0]) >= nvert || size_t(t[1]) >= nvert || size_t(t[2]) >= nvert)
continue;
std::array<int, 3> k{ t[0], t[1], t[2] };
std::sort(k.begin(), k.end());
if (auto it = tri_by_verts.find(k); it != tri_by_verts.end())
(*painted_tri)[slot][it->second] = 1;
}
}
// A triangle is in the refine region if any of its vertices is painted. That deliberately
// over-includes a one-triangle ring just outside the strict patch, which is exactly the
// transition band the conformal bisection would pull in anyway - and it makes sure the patch
// boundary itself gets refined rather than staying coarse right where the relief ends.
for (size_t i = 0; i < ntri; ++i)
for (int k = 0; k < 3; ++k)
if (painted_vertex[its.indices[i][k]]) {
region[i] = 1;
break;
}
any_paint = true;
}
return any_paint;
@@ -2749,17 +2769,36 @@ void GLGizmoTextureDisplacement::subdivide_model_adaptive()
return;
}
// Feature-adaptive: sample the combined displacement so refinement follows texture curvature. A
// null sampler (only LSCM layers, or nothing decodable) falls back to the uniform target below.
HeightFieldSampler sampler;
if (m_subdivide_feature) {
TextureDisplacementFacetsData facets{};
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
facets[size_t(i)] = mv->texture_displacement_facet(i).get_data();
sampler = make_combined_displacement_sampler(mv->mesh().its, mv->texture_displacement_layers, facets);
}
// Do the (potentially slow) refinement before taking the snapshot, so a no-op leaves no empty
// undo step - mirrors remesh_model().
// "Min edge" is a feature-mode control (it is the floor the curvature test refines down to); in
// plain adaptive mode the target edge length is the only criterion, so the floor must not be
// allowed to silently override a target the user set below it.
const float tol = m_subdivide_feature ? m_subdivide_detail_mm : 0.f;
const float floor = m_subdivide_feature ? m_subdivide_min_edge_mm : 0.f;
std::vector<int> source;
indexed_triangle_set refined;
{
wxBusyCursor wait;
refined = subdivide_mesh_adaptive(mv->mesh().its, region, m_subdivide_target_mm, 12, &source);
// The budget slider is "triangles the refinement may *add*", so the model's own count is the
// baseline - otherwise the control would be meaningless (or a dead end) on a dense model.
refined = subdivide_mesh_adaptive(mv->mesh().its, region, m_subdivide_target_mm,
int(mv->mesh().its.indices.size()) + m_subdivide_budget_k * 1000,
&source, sampler, tol, floor);
}
if (refined.indices.size() == mv->mesh().its.indices.size()) {
show_error(nullptr, _u8L("Nothing to subdivide - the painted area is already at or below the target "
"edge length."));
show_error(nullptr, _u8L("Nothing to subdivide - the painted area already meets the target edge "
"length and detail tolerance, or the triangle budget is already used up."));
return;
}
@@ -2852,7 +2891,7 @@ void GLGizmoTextureDisplacement::remesh_model()
void GLGizmoTextureDisplacement::rebuild_subdivide_preview()
{
m_subdivide_preview_glmodel.reset();
m_subdivide_preview_count = -1;
m_subdivide_preview_tris = -1;
const ModelVolume *mv = texture_volume();
if (mv == nullptr)
return;
@@ -2866,7 +2905,20 @@ void GLGizmoTextureDisplacement::rebuild_subdivide_preview()
std::vector<uint8_t> region;
if (!collect_paint_region(region, nullptr))
return; // nothing painted yet: nothing to preview
its = subdivide_mesh_adaptive(mv->mesh().its, region, m_subdivide_target_mm, 12, nullptr);
HeightFieldSampler sampler;
if (m_subdivide_feature) {
TextureDisplacementFacetsData facets{};
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
facets[size_t(i)] = mv->texture_displacement_facet(i).get_data();
sampler = make_combined_displacement_sampler(mv->mesh().its, mv->texture_displacement_layers, facets);
}
// Feature mode: curvature (Detail tolerance) on top of the Max-edge baseline, down to the
// Min-edge floor. Plain adaptive: tol 0, so only the target-edge-length criterion applies.
const float tol = m_subdivide_feature ? m_subdivide_detail_mm : 0.f;
const float floor = m_subdivide_feature ? m_subdivide_min_edge_mm : 0.f;
its = subdivide_mesh_adaptive(mv->mesh().its, region, m_subdivide_target_mm,
int(mv->mesh().its.indices.size()) + m_subdivide_budget_k * 1000,
nullptr, sampler, tol, floor);
} else {
if (m_subdivide_count < 1)
return;
@@ -2874,7 +2926,7 @@ void GLGizmoTextureDisplacement::rebuild_subdivide_preview()
}
if (its.indices.empty())
return;
m_subdivide_preview_count = m_subdivide_count;
m_subdivide_preview_tris = int(its.indices.size());
GLModel::Geometry init_data;
init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 };
@@ -3701,6 +3753,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// once on release - rebuilding the preview (a real CPU mesh recompute) on every one of those
// frames is what made dragging these sliders feel slow. Only rebuild once the mouse button
// that's driving the drag is released, i.e. once per edit instead of dozens of times per drag.
// The feature-adaptive subdivision follows the *displaced* surface (it samples depth * texture),
// so depth / tile-size / rotation / invert all change where it puts triangles. The heavy
// displacement preview below only rebuilds on release, which made the subdivide wireframe look
// like it ignored those edits - so rebuild it live here (during the drag), the same cadence its
// own sliders use. Cheap: it is bounded by the painted region.
if (m_preview_params_dirty && m_subdivide_editing && m_subdivide_adaptive && m_subdivide_feature)
rebuild_subdivide_preview();
if (m_preview_params_dirty && (m_auto_update || !ImGui::IsMouseDown(ImGuiMouseButton_Left))) {
rebuild_preview();
// Same edits (tile size, rotation, offset, a new texture) are what the projector window
@@ -3736,23 +3795,80 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
sum += (its.vertices[tri[i]] - its.vertices[tri[(i + 1) % 3]]).norm();
++cnt;
}
m_subdivide_target_mm = cnt > 0 ? std::clamp(float(sum / double(cnt)) * 0.5f, 0.1f, 20.f) : 1.f;
m_subdivide_target_mm = cnt > 0 ? std::clamp(float(sum / double(cnt)) * 0.5f, 0.001f, 20.f) : 1.f;
}
ImGui::PushItemWidth(m_imgui->scaled(8.4f));
// "##subdiv" keeps the visible label "Target edge (mm)" but gives it an ImGui ID distinct
// from the remesh slider below, which shows the same text - same label == same widget to
// ImGui, so without this the two would collide.
if (m_imgui->slider_float(std::string(_u8L("Target edge (mm)")) + "##subdiv", &m_subdivide_target_mm,
0.1f, 20.f, "%.2f", ImGuiLogSlider)) {
if (m_subdivide_editing && !ImGui::IsMouseDown(ImGuiMouseButton_Left))
// Live preview: rebuild the wireframe as the slider moves, not only on release, so it tracks
// the value. The rebuild is bounded by the painted region, so it stays responsive.
const auto preview_live = [this]() {
if (m_subdivide_editing)
rebuild_subdivide_preview();
m_parent.set_as_dirty();
};
if (ImGui::Checkbox(_u8L("Follow texture detail").c_str(), &m_subdivide_feature)) {
if (m_subdivide_editing)
rebuild_subdivide_preview();
m_parent.set_as_dirty();
}
ImGui::PopItemWidth();
if (ImGui::IsItemHovered())
m_imgui->tooltip(_u8L("Triangles in the painted area are split until every edge is at or below this "
"length. Smaller means finer detail and more triangles."),
m_imgui->tooltip(_u8L("Put triangles only where the texture actually bends - dense over hills, ridges and "
"noise, sparse over flat areas and smooth slopes - instead of an even density "
"everywhere. Uses the combined displacement of all painted layers."),
m_imgui->scaled(20.f));
ImGui::PushItemWidth(m_imgui->scaled(8.4f));
// The edge-length target is a baseline in both modes. In feature mode it is what guarantees
// the curvature test can actually see the texture: left too coarse, a big triangle over a
// fine pattern can sample four points that all happen to land at similar heights, report no
// error, and stall before refinement ever starts. "##subdiv" avoids an ID clash with the
// remesh "Target edge (mm)" slider below (same label == same widget to ImGui).
if (m_imgui->slider_float(std::string(m_subdivide_feature ? _u8L("Max edge (mm)") : _u8L("Target edge (mm)")) +
"##subdiv",
&m_subdivide_target_mm, 0.001f, 20.f, "%.3f", ImGuiLogSlider))
preview_live();
if (ImGui::IsItemHovered())
m_imgui->tooltip(m_subdivide_feature ?
_u8L("Nothing in the painted area stays coarser than this, even where the texture is "
"flat. Keep it near the size of the features you want picked up - too coarse and "
"fine detail can be missed entirely.") :
_u8L("Triangles in the painted area are split until every edge is at or below this "
"length. Smaller means finer detail and more triangles."),
m_imgui->scaled(20.f));
if (m_subdivide_feature) {
// On top of the baseline: the chord-error tolerance, and the resolution floor.
if (m_imgui->slider_float(std::string(_u8L("Detail (mm)")) + "##subdivdetail", &m_subdivide_detail_mm,
0.001f, 1.f, "%.3f", ImGuiLogSlider))
preview_live();
if (ImGui::IsItemHovered())
m_imgui->tooltip(_u8L("How closely the mesh follows the texture's relief. Smaller captures finer bumps; "
"larger only chases the big features."),
m_imgui->scaled(20.f));
if (m_imgui->slider_float(std::string(_u8L("Min edge (mm)")) + "##subdivmin", &m_subdivide_min_edge_mm,
0.001f, 20.f, "%.3f", ImGuiLogSlider))
preview_live();
if (ImGui::IsItemHovered())
m_imgui->tooltip(_u8L("The finest triangle size refinement will ever produce. Smaller captures finer "
"relief; also stops runaway subdivision at a sharp texture step, where the "
"surface never becomes flat."),
m_imgui->scaled(20.f));
}
// The budget. Refinement is worst-error-first, so a run that hits it has still spent its
// triangles on the biggest deviations - raising it buys detail, it does not redistribute it.
if (ImGui::SliderInt((_u8L("Added triangles (k)") + "##subdivbudget").c_str(), &m_subdivide_budget_k, 10, 2000)) {
m_subdivide_budget_k = std::clamp(m_subdivide_budget_k, 10, 2000);
preview_live();
}
if (ImGui::IsItemHovered())
m_imgui->tooltip(_u8L("How many thousand triangles the refinement may add. It always splits the "
"worst-fitting triangle first, so a run that uses the whole budget has still spent "
"it where it shows most - raise this if the preview still looks too coarse."),
m_imgui->scaled(20.f));
ImGui::PopItemWidth();
if (m_subdivide_editing && m_subdivide_preview_tris > 0)
m_imgui->text(Slic3r::format(_u8L("Preview: %1% triangles"), m_subdivide_preview_tris));
} else {
ImGui::PushItemWidth(m_imgui->scaled(8.4f));
if (ImGui::SliderInt(_u8L("Subdivide steps").c_str(), &m_subdivide_count, 0, 5)) {
@@ -3808,7 +3924,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
ImGui::SameLine();
if (m_imgui->button(_u8L("Done"))) {
m_subdivide_editing = false;
m_subdivide_preview_count = -1;
m_subdivide_preview_tris = -1;
m_subdivide_preview_glmodel.reset();
m_parent.set_as_dirty();
}
@@ -302,7 +302,7 @@ private:
// committed, i.e. the most expensive thing the panel can do, on every Apply.
int m_subdivide_count = 1;
bool m_subdivide_editing = false;
int m_subdivide_preview_count = -1; // the count m_subdivide_preview_glmodel was built for
int m_subdivide_preview_tris = -1; // triangle count of the previewed result, shown in the panel
GLModel m_subdivide_preview_glmodel;
void rebuild_subdivide_preview();
void render_subdivide_preview();
@@ -314,6 +314,20 @@ private:
// are painted), so the region survives the subdivision instead of being dropped.
bool m_subdivide_adaptive = false;
float m_subdivide_target_mm = 0.f; // 0 = not yet seeded; filled from the mesh on first show
// Feature-adaptive sub-mode: put the triangles where the *displaced surface* bends (texture
// curvature) rather than spreading them evenly. `detail_mm` is the chord-error tolerance ("Detail"
// slider: how far the true surface may sit off the flat triangle before it is split); the target
// above stays in play as a coarse baseline ("Max edge"), and `min_edge_mm` is the hard floor
// ("Min edge"). See subdivide_mesh_adaptive().
bool m_subdivide_feature = false;
float m_subdivide_detail_mm = 0.05f;
float m_subdivide_min_edge_mm = 0.1f;
// How many thousand triangles refinement may *add* (the mesh's own count is added on before it is
// passed as subdivide_mesh_adaptive()'s absolute cap, so the control still means something on a
// dense model). Refinement is worst-error-first, so hitting the budget still yields the best mesh
// that many triangles can buy - and it is what keeps a fine "Detail" over a noisy texture from
// turning into an out-of-memory, or an unrenderable preview wireframe.
int m_subdivide_budget_k = 200;
void subdivide_model_adaptive();
// Fills `region` (per current-mesh triangle, 1 = refine) from the union of every layer's painted
// area. If `painted_tri` is non-null, also fills, per layer, the fully-painted triangles to carry