mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-30 22:32:07 +00:00
Add adaptive subdivision
This commit is contained in:
@@ -188,24 +188,43 @@ directly** - clamping the *coordinate* into range (what an earlier version did)
|
||||
border row/column of pixels outward to infinity in every direction, which is a real bug that was
|
||||
reported and fixed (visually: streaky lines radiating out from the painted patch).
|
||||
|
||||
### Subdivision (`subdivide_mesh_uniform()`)
|
||||
### Subdivision — two modes
|
||||
|
||||
Deliberately **whole-mesh and uniform**, not limited to the painted patch. A patch-only /
|
||||
adaptive subdivision would create a classic T-junction/cracking problem where the denser
|
||||
(subdivided) and sparser (untouched) regions meet - the fine side has edge midpoints the coarse
|
||||
side doesn't know about, producing a real (non-manifold-looking) crack in the baked geometry. This
|
||||
was consciously scoped down from the original plan's "adaptive per-patch subdivider" idea to avoid
|
||||
that correctness risk (a subtly-cracked mesh is a much worse outcome than "not implemented yet").
|
||||
Algorithm: recursive 1-to-4 triangle split via edge midpoints, with a shared per-pass midpoint cache
|
||||
(keyed by sorted vertex-index pair) so triangles sharing an edge get the *same* new vertex - capped
|
||||
at `max_iterations` (default 6) passes to bound worst-case triangle-count explosion.
|
||||
**Uniform (`subdivide_mesh_uniform()`)** — whole-mesh, 1-to-4 split. Recursive edge-midpoint split with
|
||||
a shared per-pass midpoint cache (keyed by sorted vertex-index pair) so triangles sharing an edge get
|
||||
the *same* new vertex - capped at `max_iterations` (default 6). Whole-mesh so it never leaves a
|
||||
T-junction, at the cost of densifying everywhere. Wired as a "Subdivide steps" slider (**0–5**, 0 =
|
||||
no subdivision), Apply snaps back to 0. Drops texture-displacement paint (no remap) via the standard
|
||||
`save_painting()`/`set_mesh()`/`restore_painting()` dance; the other four channels are remapped.
|
||||
|
||||
Wired as a "Subdivide steps" slider (**0–5**, where 0 means no subdivision and previews nothing) plus
|
||||
Preview/Apply/Done in the gizmo panel. **Apply snaps the slider back to 0**. A real, committed geometry change (like
|
||||
Bake), using the same `save_painting()`/`set_mesh()`/`restore_painting()` dance `GLGizmoSimplify`
|
||||
uses: supported/seam/mmu/fuzzy-skin masks get remapped onto the new triangles, texture-displacement
|
||||
paint does not (no remap support yet) and is dropped rather than left pointing at now-meaningless
|
||||
triangle indices.
|
||||
**Adaptive (`subdivide_mesh_adaptive()`)** — refine **only the painted area**, down to a target edge
|
||||
length, by **Rivara longest-edge bisection**. This is the algorithm that was "scoped out" originally
|
||||
for fear of the T-junction/crack problem; it is safe because it is *conformal by construction*. Each
|
||||
pass bisects only **terminal** edges - an edge that is the longest edge of *every* triangle sharing it
|
||||
- which splits both those triangles along one shared midpoint at once, so a hanging node is never
|
||||
created. Only a triangle's own longest edge can be terminal, so each triangle is split by at most one
|
||||
bisection per pass. When a triangle that still 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
|
||||
grades the mesh down into the region and closes what would be cracks (pulling a thin, bounded band of
|
||||
transition triangles just outside the painted patch). Tie-broken by mesh-vertex key so both sides of
|
||||
an edge always agree on "the" longest.
|
||||
|
||||
The win: a small decal on a big model no longer quadruples the *whole* model's triangle count.
|
||||
|
||||
**It carries the paint forward**, which is what makes it usable (uniform/remesh both drop paint). Because
|
||||
the refinement is *driven by* the paint, the remap is trivial: `subdivide_mesh_adaptive()` fills an
|
||||
`out_source[new_tri] = input_tri` map (children inherit their parent), and the gizmo rebuilds each
|
||||
layer's mask on the new mesh - a new triangle is painted iff its source was fully painted in that
|
||||
layer. `collect_paint_region()` derives both the union refine-region (any vertex of a painted patch,
|
||||
i.e. patch + a one-ring, so the boundary itself refines) and the per-layer fully-painted-triangle sets
|
||||
(a `get_facets_strict(ENFORCER)` sub-triangle with all three *original* vertex indices == a whole,
|
||||
fully-painted original triangle; a partial stroke's sub-triangles always carry a split vertex). The
|
||||
other four channels still ride the normal `restore_painting()` remap. Covered by a conformality unit
|
||||
test (`every_edge_used_twice` on a partially-refined cube - an exact crack detector for a closed mesh).
|
||||
|
||||
Both share the gizmo's Preview/Apply/Done flow; the **"Only painted area (adaptive)"** checkbox picks
|
||||
the mode, and the adaptive preview follows the paint live (`rebuild_preview()` refreshes the wireframe
|
||||
while the subdivide preview is open in adaptive mode).
|
||||
|
||||
### Fast bump preview (GPU-only, no CPU meshing)
|
||||
|
||||
@@ -353,11 +372,16 @@ of. Toolbar commands the canvas can't service itself (Average scale) are forward
|
||||
part of the mesh via the existing mesh serialization path) - what does *not* survive a project
|
||||
save/reload is any *unbaked* paint stroke and texture layer definition.
|
||||
- **No remap-across-topology-change** for texture-displacement paint (`ModelObject::split()`, mesh
|
||||
boolean ops, Simplify, and now `subdivide_mesh_uniform()` all drop it via `reset_extra_facets()`).
|
||||
The other four paint channels (supported/seam/mmu/fuzzy) do get remapped in these cases.
|
||||
boolean ops, Simplify, uniform subdivide, and remesh all drop it via `reset_extra_facets()`). The
|
||||
other four paint channels (supported/seam/mmu/fuzzy) do get remapped in these cases. The lone
|
||||
exception is **adaptive subdivide**, which carries texture-displacement paint forward itself via its
|
||||
source map (see the Subdivision section) - a targeted remap that only works because the operation is
|
||||
driven by the paint.
|
||||
- **Cylindrical/Spherical axis/center are auto-picked heuristically**, not user-controllable - no
|
||||
UI to override the auto-detected wrap axis if it picks the "wrong" one for an odd shape.
|
||||
- **Fast preview covers the active layer only**
|
||||
- **Fast preview covers the active layer only** — and is now the **default** view when the gizmo
|
||||
opens (`m_use_bump_preview = true`): it is the instant, no-CPU-meshing preview, so it is the better
|
||||
first impression while painting. The exact true-displacement view is one click away in the View row.
|
||||
- **Displacement resolution is capped by the mesh's own vertex density.** Baking only ever *moves*
|
||||
existing vertices (it never inserts any), so a coarse patch cannot show fine texture detail no
|
||||
matter how high-resolution the height map is - that is what the "Subdivide model" button is for.
|
||||
@@ -368,7 +392,8 @@ of. Toolbar commands the canvas can't service itself (Average scale) are forward
|
||||
|
||||
**libslic3r (core, no GUI dependency):**
|
||||
- `src/libslic3r/TextureDisplacement.hpp/.cpp` - data model, bake algorithm, projection methods,
|
||||
tiling, subdivision. See doc comments throughout, they're kept accurate and up to date.
|
||||
tiling, subdivision (uniform + adaptive longest-edge bisection). See doc comments throughout,
|
||||
they're kept accurate and up to date.
|
||||
- `src/libslic3r/MeshBoolean.hpp/.cpp` - added `parameterize_lscm()` and `remesh_isotropic()`
|
||||
in the `cgal` sub-namespace,
|
||||
reusing the existing `CGALMesh`/`_EpicMesh`/conversion-helper infrastructure already there for
|
||||
|
||||
@@ -1346,4 +1346,148 @@ indexed_triangle_set subdivide_mesh_uniform(const indexed_triangle_set &mesh, fl
|
||||
return current;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
// 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; };
|
||||
|
||||
std::vector<Vec3f> verts = mesh.vertices;
|
||||
std::vector<Tri> tris;
|
||||
tris.reserve(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) });
|
||||
|
||||
auto emit = [&](std::vector<Tri> &t) -> indexed_triangle_set {
|
||||
indexed_triangle_set out;
|
||||
out.vertices = verts;
|
||||
out.indices.reserve(t.size());
|
||||
if (out_source) {
|
||||
out_source->clear();
|
||||
out_source->reserve(t.size());
|
||||
}
|
||||
for (const Tri &tr : t) {
|
||||
out.indices.emplace_back(tr.v[0], tr.v[1], tr.v[2]);
|
||||
if (out_source)
|
||||
out_source->push_back(tr.src);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
// 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 (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;
|
||||
|
||||
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);
|
||||
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)
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -511,6 +511,35 @@ indexed_triangle_set build_texture_displacement(const ModelVolume &volume);
|
||||
// during baking.
|
||||
indexed_triangle_set subdivide_mesh_uniform(const indexed_triangle_set &mesh, float max_edge_length_mm, int max_iterations = 6);
|
||||
|
||||
// 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).
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// `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).
|
||||
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);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_TextureDisplacement_hpp_
|
||||
|
||||
@@ -1153,6 +1153,11 @@ void GLGizmoTextureDisplacement::rebuild_preview()
|
||||
rebuild_bump_preview_mesh();
|
||||
rebuild_uvcheck_mesh();
|
||||
rebuild_seam_overlay();
|
||||
// The adaptive subdivision preview is driven by the painted area, so it has to follow the paint
|
||||
// while it is open - a plain stroke changes which triangles would be refined. The uniform preview
|
||||
// depends only on the mesh, so it is left to its own controls.
|
||||
if (m_subdivide_editing && m_subdivide_adaptive)
|
||||
rebuild_subdivide_preview();
|
||||
|
||||
const ModelVolume *mv = texture_volume();
|
||||
if (mv == nullptr || !mv->is_texture_displacement_painted()) {
|
||||
@@ -2655,6 +2660,143 @@ void GLGizmoTextureDisplacement::subdivide_model()
|
||||
m_parent.set_as_dirty();
|
||||
}
|
||||
|
||||
bool GLGizmoTextureDisplacement::collect_paint_region(
|
||||
std::vector<uint8_t> ®ion,
|
||||
std::array<std::vector<uint8_t>, TEXTURE_DISPLACEMENT_MAX_LAYERS> *painted_tri) const
|
||||
{
|
||||
const ModelVolume *mv = texture_volume();
|
||||
if (mv == nullptr)
|
||||
return false;
|
||||
const indexed_triangle_set &its = mv->mesh().its;
|
||||
const size_t ntri = its.indices.size();
|
||||
const size_t nvert = its.vertices.size();
|
||||
|
||||
region.assign(ntri, 0);
|
||||
if (painted_tri)
|
||||
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.
|
||||
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));
|
||||
}
|
||||
|
||||
bool any_paint = false;
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
const TriangleSelector::TriangleSplittingData &data = mv->texture_displacement_facet(slot).get_data();
|
||||
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);
|
||||
|
||||
std::vector<uint8_t> painted_vertex(nvert, 0);
|
||||
if (painted_tri)
|
||||
(*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) {
|
||||
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;
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::subdivide_model_adaptive()
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
ModelObject *mo = m_c->selection_info()->model_object();
|
||||
if (mv == nullptr || mo == nullptr || m_subdivide_target_mm <= 0.f)
|
||||
return;
|
||||
|
||||
update_model_object(); // flush any in-progress stroke into the committed masks first
|
||||
|
||||
std::vector<uint8_t> region;
|
||||
std::array<std::vector<uint8_t>, TEXTURE_DISPLACEMENT_MAX_LAYERS> painted_tri;
|
||||
if (!collect_paint_region(region, &painted_tri)) {
|
||||
show_error(nullptr, _u8L("Paint the area you want to subdivide first - adaptive subdivision only "
|
||||
"refines where you have painted."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Do the (potentially slow) refinement before taking the snapshot, so a no-op leaves no empty
|
||||
// undo step - mirrors remesh_model().
|
||||
std::vector<int> source;
|
||||
indexed_triangle_set refined;
|
||||
{
|
||||
wxBusyCursor wait;
|
||||
refined = subdivide_mesh_adaptive(mv->mesh().its, region, m_subdivide_target_mm, 12, &source);
|
||||
}
|
||||
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."));
|
||||
return;
|
||||
}
|
||||
|
||||
Plater *plater = wxGetApp().plater();
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Adaptive subdivide for texture displacement"), UndoRedo::SnapshotType::GizmoAction);
|
||||
|
||||
// Other paint channels ride across via the standard remap; texture-displacement paint is rebuilt
|
||||
// by hand below from the source map, which is the whole point of driving this by the paint.
|
||||
std::optional<TriangleSelector::SavedPainting> saved_painting = mv->save_painting();
|
||||
mv->set_mesh(TriangleMesh(std::move(refined))); // refined is not needed past here; source carries the paint map
|
||||
mv->set_new_unique_id();
|
||||
mv->calculate_convex_hull();
|
||||
mv->restore_painting(saved_painting); // resets extra facets (incl. texture-displacement) + remaps the rest
|
||||
|
||||
// Carry each layer's paint onto the new mesh: a new triangle is painted iff its source triangle
|
||||
// was fully painted in that layer. Children inherit their parent's source, so this is exact.
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
if (painted_tri[slot].empty())
|
||||
continue;
|
||||
TriangleSelector sel(mv->mesh());
|
||||
for (size_t i = 0; i < source.size(); ++i)
|
||||
if (painted_tri[slot][source[i]])
|
||||
sel.set_facet(int(i), EnforcerBlockerType::ENFORCER);
|
||||
mv->texture_displacement_facet(slot).set(sel);
|
||||
}
|
||||
|
||||
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
||||
const ModelObjectPtrs &objs = plater->model().objects;
|
||||
auto it = std::find(objs.begin(), objs.end(), mo);
|
||||
if (it != objs.end())
|
||||
obj_list->update_info_items(size_t(it - objs.begin()));
|
||||
}
|
||||
plater->changed_object(*mo);
|
||||
update_from_model_object(false); // reload selectors/preview against the new mesh + carried paint
|
||||
m_parent.set_as_dirty();
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::remesh_model()
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
@@ -2712,12 +2854,24 @@ void GLGizmoTextureDisplacement::rebuild_subdivide_preview()
|
||||
m_subdivide_preview_glmodel.reset();
|
||||
m_subdivide_preview_count = -1;
|
||||
const ModelVolume *mv = texture_volume();
|
||||
if (mv == nullptr || m_subdivide_count < 1)
|
||||
if (mv == nullptr)
|
||||
return;
|
||||
|
||||
// The same subdivision Apply would commit, but kept in a throwaway mesh and shown only as a
|
||||
// The same subdivision Apply would commit, kept in a throwaway mesh and shown only as a
|
||||
// wireframe - the model itself is not touched until Apply.
|
||||
const indexed_triangle_set its = subdivide_mesh_uniform(mv->mesh().its, 0.f, m_subdivide_count);
|
||||
indexed_triangle_set its;
|
||||
if (m_subdivide_adaptive) {
|
||||
if (m_subdivide_target_mm <= 0.f)
|
||||
return;
|
||||
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);
|
||||
} else {
|
||||
if (m_subdivide_count < 1)
|
||||
return;
|
||||
its = subdivide_mesh_uniform(mv->mesh().its, 0.f, m_subdivide_count);
|
||||
}
|
||||
if (its.indices.empty())
|
||||
return;
|
||||
m_subdivide_preview_count = m_subdivide_count;
|
||||
@@ -3558,20 +3712,66 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
|
||||
ImGui::Separator();
|
||||
m_imgui->text(_u8L("Not enough vertices for fine detail?"));
|
||||
ImGui::PushItemWidth(m_imgui->scaled(8.4f));
|
||||
if (ImGui::SliderInt(_u8L("Subdivide steps").c_str(), &m_subdivide_count, 0, 5)) {
|
||||
m_subdivide_count = std::clamp(m_subdivide_count, 0, 5);
|
||||
|
||||
if (ImGui::Checkbox(_u8L("Only painted area (adaptive)").c_str(), &m_subdivide_adaptive)) {
|
||||
if (m_subdivide_editing)
|
||||
rebuild_subdivide_preview(); // a count of 0 clears the preview, it doesn't compute one
|
||||
rebuild_subdivide_preview(); // switch the wireframe between the uniform and adaptive result
|
||||
m_parent.set_as_dirty();
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(_u8L("How many times to split every triangle into four. Each step roughly quadruples the "
|
||||
"triangle count, so there are enough vertices for the height texture to displace. "
|
||||
"0 means no subdivision."),
|
||||
m_imgui->tooltip(_u8L("Refine only where you have painted, down to a target edge length, instead of splitting "
|
||||
"the whole model. Keeps the triangle count down on a big part with a small decal, and - "
|
||||
"unlike whole-model subdivision - your paint is carried onto the finer mesh instead of "
|
||||
"being cleared."),
|
||||
m_imgui->scaled(20.f));
|
||||
|
||||
if (m_subdivide_adaptive) {
|
||||
if (m_subdivide_target_mm <= 0.f && mv != nullptr) {
|
||||
// Seed the target at about half the mesh's mean edge length, so the default already adds
|
||||
// a useful amount of detail rather than landing on "no change".
|
||||
const indexed_triangle_set &its = mv->mesh().its;
|
||||
double sum = 0.0; size_t cnt = 0;
|
||||
for (const stl_triangle_vertex_indices &tri : its.indices)
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
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;
|
||||
}
|
||||
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))
|
||||
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->scaled(20.f));
|
||||
} else {
|
||||
ImGui::PushItemWidth(m_imgui->scaled(8.4f));
|
||||
if (ImGui::SliderInt(_u8L("Subdivide steps").c_str(), &m_subdivide_count, 0, 5)) {
|
||||
m_subdivide_count = std::clamp(m_subdivide_count, 0, 5);
|
||||
if (m_subdivide_editing)
|
||||
rebuild_subdivide_preview(); // a count of 0 clears the preview, it doesn't compute one
|
||||
m_parent.set_as_dirty();
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(_u8L("How many times to split every triangle into four. Each step roughly quadruples the "
|
||||
"triangle count, so there are enough vertices for the height texture to displace. "
|
||||
"0 means no subdivision."),
|
||||
m_imgui->scaled(20.f));
|
||||
}
|
||||
|
||||
// Apply is a no-op when there is nothing to commit: 0 uniform passes, or an adaptive target that
|
||||
// is not set (the preview covers the painted-area check itself).
|
||||
const bool subdivide_ready = m_subdivide_adaptive ? (m_subdivide_target_mm > 0.f) : (m_subdivide_count >= 1);
|
||||
if (!m_subdivide_editing) {
|
||||
if (m_imgui->button(_u8L("Preview subdivision"))) {
|
||||
m_subdivide_editing = true;
|
||||
@@ -3583,22 +3783,28 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
"commit it, or Done to leave the model as it is."),
|
||||
m_imgui->scaled(20.f));
|
||||
} else {
|
||||
m_imgui->disabled_begin(m_subdivide_count < 1);
|
||||
m_imgui->disabled_begin(!subdivide_ready);
|
||||
if (m_imgui->button(_u8L("Apply"))) {
|
||||
subdivide_model(); // commits m_subdivide_count passes (takes its own snapshot)
|
||||
// Back to 0 rather than staying at the count just applied: the mesh is now up to 4^N
|
||||
// times denser, so re-previewing the same N passes on top of it is both pointless (the
|
||||
// density asked for is already committed) and by far the slowest thing this panel does.
|
||||
// rebuild_subdivide_preview() at 0 just drops the wireframe.
|
||||
m_subdivide_count = 0;
|
||||
if (m_subdivide_adaptive) {
|
||||
subdivide_model_adaptive(); // refines only the painted area, carrying the paint forward
|
||||
} else {
|
||||
subdivide_model(); // commits m_subdivide_count passes (takes its own snapshot)
|
||||
// Back to 0 rather than staying at the count just applied: the mesh is now up to 4^N
|
||||
// times denser, so re-previewing the same N passes on top of it is both pointless (the
|
||||
// density asked for is already committed) and by far the slowest thing this panel does.
|
||||
m_subdivide_count = 0;
|
||||
}
|
||||
rebuild_subdivide_preview();
|
||||
m_parent.set_as_dirty();
|
||||
}
|
||||
m_imgui->disabled_end();
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(_u8L("Replaces the model's geometry with the subdivided mesh and clears any not-yet-baked "
|
||||
"paint on it (already-baked bumps are unaffected)."),
|
||||
m_imgui->scaled(20.f));
|
||||
m_imgui->tooltip(m_subdivide_adaptive ?
|
||||
_u8L("Refines the painted area to the target edge length and carries your paint onto "
|
||||
"the finer mesh. The rest of the model is left as it is.") :
|
||||
_u8L("Replaces the model's geometry with the subdivided mesh and clears any not-yet-baked "
|
||||
"paint on it (already-baked bumps are unaffected)."),
|
||||
m_imgui->scaled(20.f));
|
||||
ImGui::SameLine();
|
||||
if (m_imgui->button(_u8L("Done"))) {
|
||||
m_subdivide_editing = false;
|
||||
|
||||
@@ -307,6 +307,20 @@ private:
|
||||
void rebuild_subdivide_preview();
|
||||
void render_subdivide_preview();
|
||||
|
||||
// Adaptive subdivision: refine only the painted area, down to a target edge length, via
|
||||
// conformal longest-edge bisection (subdivide_mesh_adaptive()). Unlike the count-based uniform
|
||||
// path it does not touch the unpainted rest of the model, and - because it is driven by the paint
|
||||
// - it can carry that paint forward across the topology change (children of a painted triangle
|
||||
// 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
|
||||
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
|
||||
// forward. Returns false when nothing is painted at all. Shared by the preview and the commit.
|
||||
bool collect_paint_region(std::vector<uint8_t> ®ion,
|
||||
std::array<std::vector<uint8_t>, TEXTURE_DISPLACEMENT_MAX_LAYERS> *painted_tri) const;
|
||||
|
||||
// Isotropic remeshing (CGAL) to even out wildly varying triangle sizes so displacement has a
|
||||
// consistent density to work with. Target edge length in mm; 0 means "not yet initialised", filled
|
||||
// with the mesh's mean edge length the first time the control is shown. Like subdivide, it replaces
|
||||
@@ -326,8 +340,11 @@ private:
|
||||
// mouse button driving the drag hasn't been released yet - see on_render_input_window().
|
||||
bool m_preview_params_dirty = false;
|
||||
|
||||
// See rebuild_bump_preview_mesh()/render_bump_preview_mesh().
|
||||
bool m_use_bump_preview = false;
|
||||
// See rebuild_bump_preview_mesh()/render_bump_preview_mesh(). On by default: it is the cheap,
|
||||
// instant-updating preview, so it is the better first impression while painting. The true-
|
||||
// displacement view (a background CPU remesh) is one click away in the View row when the user
|
||||
// wants an exact look at what Bake will produce.
|
||||
bool m_use_bump_preview = true;
|
||||
// Set from the UV editor's per-move island edits instead of rebuilding the (potentially large) bump
|
||||
// mesh synchronously inside that mouse handler - doing the rebuild there stalled both the UV pane
|
||||
// and the 3D view. The rebuild is instead coalesced to once per 3D frame (render_painter_gizmo).
|
||||
|
||||
@@ -233,3 +233,91 @@ TEST_CASE("TextureDisplacement: boundary vertices shared with unpainted triangle
|
||||
CHECK_FALSE(still_at_original_position(fan.vertices[2])); // B: interior, must have moved
|
||||
CHECK_FALSE(still_at_original_position(fan.vertices[3])); // C: interior, must have moved
|
||||
}
|
||||
|
||||
// Every undirected edge of a closed manifold mesh is shared by exactly two triangles. A T-junction
|
||||
// (a hanging node where a refined region meets a coarse one) breaks that: the coarse side spans an
|
||||
// edge that the fine side has replaced with two half-edges, so those three edges each show up an
|
||||
// odd number of times. Counting edge uses is therefore an exact crack detector for a closed mesh.
|
||||
static bool every_edge_used_twice(const indexed_triangle_set &its)
|
||||
{
|
||||
std::map<std::pair<int, int>, int> uses;
|
||||
for (const auto &t : its.indices)
|
||||
for (int e = 0; e < 3; ++e) {
|
||||
int a = t[e], b = t[(e + 1) % 3];
|
||||
if (a > b)
|
||||
std::swap(a, b);
|
||||
++uses[{ a, b }];
|
||||
}
|
||||
for (const auto &[edge, n] : uses)
|
||||
if (n != 2)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST_CASE("TextureDisplacement: adaptive subdivision is conformal and region-restricted", "[TextureDisplacement]")
|
||||
{
|
||||
const indexed_triangle_set cube = its_make_cube(10., 10., 10.);
|
||||
REQUIRE(every_edge_used_twice(cube)); // sanity: the input really is a closed manifold
|
||||
|
||||
auto longest_edge = [](const indexed_triangle_set &its, const stl_triangle_vertex_indices &t) {
|
||||
float m = 0.f;
|
||||
for (int e = 0; e < 3; ++e)
|
||||
m = std::max(m, (its.vertices[t[e]] - its.vertices[t[(e + 1) % 3]]).norm());
|
||||
return m;
|
||||
};
|
||||
|
||||
SECTION("whole-mesh region refines everywhere and stays conformal")
|
||||
{
|
||||
std::vector<uint8_t> region(cube.indices.size(), 1);
|
||||
std::vector<int> source;
|
||||
const indexed_triangle_set out = subdivide_mesh_adaptive(cube, region, 3.f, 12, &source);
|
||||
|
||||
CHECK(out.indices.size() > cube.indices.size()); // it actually refined
|
||||
CHECK(every_edge_used_twice(out)); // ... without opening a single crack
|
||||
|
||||
REQUIRE(source.size() == out.indices.size());
|
||||
for (int s : source)
|
||||
CHECK((s >= 0 && s < int(cube.indices.size()))); // every child names a real parent
|
||||
}
|
||||
|
||||
SECTION("a partial region refines only there, and the boundary is still crack-free")
|
||||
{
|
||||
// Refine only the triangles whose centroid is in the upper (z > 5) half of the cube.
|
||||
std::vector<uint8_t> region(cube.indices.size(), 0);
|
||||
size_t region_count = 0;
|
||||
for (size_t i = 0; i < cube.indices.size(); ++i) {
|
||||
const auto &t = cube.indices[i];
|
||||
const float cz = (cube.vertices[t[0]].z() + cube.vertices[t[1]].z() + cube.vertices[t[2]].z()) / 3.f;
|
||||
if (cz > 5.f) {
|
||||
region[i] = 1;
|
||||
++region_count;
|
||||
}
|
||||
}
|
||||
REQUIRE(region_count > 0);
|
||||
|
||||
std::vector<int> source;
|
||||
const indexed_triangle_set out = subdivide_mesh_adaptive(cube, region, 2.f, 12, &source);
|
||||
|
||||
CHECK(out.indices.size() > cube.indices.size());
|
||||
CHECK(every_edge_used_twice(out)); // the refined/coarse seam has no T-junction
|
||||
|
||||
// The region's own triangles came down in size; count how big the largest region-sourced
|
||||
// output triangle is versus the largest region-sourced input triangle.
|
||||
float max_in = 0.f, max_out = 0.f;
|
||||
for (size_t i = 0; i < cube.indices.size(); ++i)
|
||||
if (region[i])
|
||||
max_in = std::max(max_in, longest_edge(cube, cube.indices[i]));
|
||||
for (size_t i = 0; i < out.indices.size(); ++i)
|
||||
if (region[source[i]])
|
||||
max_out = std::max(max_out, longest_edge(out, out.indices[i]));
|
||||
CHECK(max_out < max_in); // refinement genuinely happened inside the region
|
||||
}
|
||||
|
||||
SECTION("an empty region is a no-op")
|
||||
{
|
||||
std::vector<uint8_t> region(cube.indices.size(), 0);
|
||||
const indexed_triangle_set out = subdivide_mesh_adaptive(cube, region, 1.f, 12, nullptr);
|
||||
CHECK(out.indices.size() == cube.indices.size());
|
||||
CHECK(out.vertices.size() == cube.vertices.size());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user