mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-31 14:52:06 +00:00
Rewrite adaptive subdivision to refine worst-first against a triangle budget
This commit is contained in:
@@ -197,34 +197,92 @@ T-junction, at the cost of densifying everywhere. Wired as a "Subdivide steps" s
|
|||||||
no subdivision), Apply snaps back to 0. Drops texture-displacement paint (no remap) via the standard
|
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.
|
`save_painting()`/`set_mesh()`/`restore_painting()` dance; the other four channels are remapped.
|
||||||
|
|
||||||
**Adaptive (`subdivide_mesh_adaptive()`)** — refine **only the painted area**, down to a target edge
|
**Adaptive (`subdivide_mesh_adaptive()`)** — refine **only the painted area**, by **Rivara longest-edge
|
||||||
length, by **Rivara longest-edge bisection**. This is the algorithm that was "scoped out" originally
|
bisection**. This is the algorithm that was "scoped out" originally for fear of the T-junction/crack
|
||||||
for fear of the T-junction/crack problem; it is safe because it is *conformal by construction*. Each
|
problem; it is safe because it is *conformal by construction*. Only **terminal** edges are ever bisected
|
||||||
pass bisects only **terminal** edges - an edge that is the longest edge of *every* triangle sharing it
|
- an edge that is the longest edge of *every* triangle sharing it - which splits both those triangles
|
||||||
- which splits both those triangles along one shared midpoint at once, so a hanging node is never
|
along one shared midpoint at once, so a hanging node is never created. The edge to split for a triangle
|
||||||
created. Only a triangle's own longest edge can be terminal, so each triangle is split by at most one
|
that wants refining is found by **longest-edge propagation (LEPP)**: walk to the longest edge of
|
||||||
bisection per pass. When a triangle that still needs refining has a longest edge that is not yet
|
ever-longer-edged neighbours until a terminal one is reached, and bisect that. Edge length strictly
|
||||||
terminal, the neighbour across it has a strictly longer edge and is refined first; that propagation
|
increases along the path (ties broken by mesh-vertex key, which both sides of an edge compute
|
||||||
grades the mesh down into the region and closes what would be cracks (pulling a thin, bounded band of
|
identically), so the walk cannot cycle, and Rivara's result is that repeating it refines the original
|
||||||
transition triangles just outside the painted patch). Tie-broken by mesh-vertex key so both sides of
|
triangle in a bounded number of bisections. The transition triangles it pulls in just outside the
|
||||||
an edge always agree on "the" longest.
|
painted patch are the graded band that makes the size change conformal.
|
||||||
|
|
||||||
The win: a small decal on a big model no longer quadruples the *whole* model's triangle count.
|
The win: a small decal on a big model no longer quadruples the *whole* model's triangle count.
|
||||||
|
|
||||||
|
**Run to completion, worst-first, against a triangle budget.** The refinement loop is not a fixed number
|
||||||
|
of sweeps: it holds every triangle that is over its criteria in a max-heap keyed by *how many times over*
|
||||||
|
it is, pops the worst, walks its LEPP, bisects, and re-scores. Edge adjacency (`nb[e]`, the triangle
|
||||||
|
across each edge) is built **once** and maintained incrementally through each bisection, so the cost
|
||||||
|
scales with the refined region rather than with the whole model. `max_triangles` is the only bound;
|
||||||
|
stopping on it leaves a perfectly valid, still-conformal mesh that spent its budget on the largest errors.
|
||||||
|
|
||||||
|
This shape replaced a first version that ran a fixed 12 sweeps, each rebuilding a whole-mesh edge map and
|
||||||
|
bisecting one terminal edge per active triangle. Two failure modes came out of that, and they are worth
|
||||||
|
remembering because they look like separate bugs and are not: the sweeps were consumed grading the
|
||||||
|
*coarse surroundings* (whose edges are the longest, so they win every terminal-edge contest), which both
|
||||||
|
**stopped refinement of the painted patch far short** of the requested detail and left the band outside
|
||||||
|
it looking wildly over-refined relative to the patch itself.
|
||||||
|
|
||||||
**It carries the paint forward**, which is what makes it usable (uniform/remesh both drop paint). Because
|
**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
|
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
|
`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'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,
|
layer. `collect_paint_region()` derives both:
|
||||||
i.e. patch + a one-ring, so the boundary itself refines) and the per-layer fully-painted-triangle sets
|
- the union refine-region: **exactly** the original triangles the brush touched, read straight off
|
||||||
(a `get_facets_strict(ENFORCER)` sub-triangle with all three *original* vertex indices == a whole,
|
`TriangleSplittingData::triangles_to_split` (`serialize()` records an entry per original triangle that
|
||||||
fully-painted original triangle; a partial stroke's sub-triangles always carry a split vertex). The
|
is either split - i.e. partially painted, the patch boundary - or carries a non-default state). No
|
||||||
other four channels still ride the normal `restore_painting()` remap. Covered by a conformality unit
|
dilation. An earlier version marked every triangle sharing a *vertex* with the patch, which drags in a
|
||||||
test (`every_edge_used_twice` on a partially-refined cube - an exact crack detector for a closed mesh).
|
whole fan of huge unpainted neighbours and then refines *those* down to the resolution floor, since the
|
||||||
|
height field the detail test samples is not restricted to the painted area. The conformal closure
|
||||||
|
already grades the size change outward on its own; it does not need help.
|
||||||
|
- 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),
|
||||||
|
plus tests that the target edge length is actually *reached* and that the budget caps the result without
|
||||||
|
opening a crack.
|
||||||
|
|
||||||
Both share the gizmo's Preview/Apply/Done flow; the **"Only painted area (adaptive)"** checkbox picks
|
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
|
the mode, and the adaptive preview follows the paint live (`rebuild_preview()` refreshes the wireframe
|
||||||
while the subdivide preview is open in adaptive mode).
|
while the subdivide preview is open in adaptive mode). The panel shows the previewed triangle count.
|
||||||
|
|
||||||
|
**Feature-adaptive (follow texture detail).** A sub-mode of adaptive (the **"Follow texture detail"**
|
||||||
|
checkbox) that puts triangles where the *displaced surface actually bends*, not evenly. The insight:
|
||||||
|
a flat region or a linear **ramp** needs no extra vertices (linear interpolation is exact for a ramp);
|
||||||
|
what needs them is **curvature** - the *second* derivative, not the gradient. So the extra predicate is a
|
||||||
|
**chord-error** test: sample the combined displacement at the triangle's three edge midpoints *and its
|
||||||
|
centroid* (sampling the interior is what catches a bump sitting inside a triangle, the blind spot of an
|
||||||
|
edge-only test) and take the largest departure from the flat triangle's barycentric interpolation. Refine
|
||||||
|
while that exceeds `chord_tolerance_mm` ("Detail (mm)"). Zero chord error on a ramp ⇒ untouched; high on
|
||||||
|
a bump/ridge/noise ⇒ refined until captured. Same conformal machinery, so still crack-free. The
|
||||||
|
per-triangle error is cached and recomputed only for the children of a split.
|
||||||
|
|
||||||
|
Four knobs bracket it, and all four matter:
|
||||||
|
- **"Max edge (mm)"** (`target_edge_length_mm`) is a **baseline that applies in feature mode too**.
|
||||||
|
Without it the chord test aliases: a big triangle over a fine pattern can sample four points that all
|
||||||
|
land at similar heights, report no error, and stall before refinement ever starts. The baseline
|
||||||
|
guarantees a sampling density fine enough for the curvature test to see the texture at all.
|
||||||
|
- **"Detail (mm)"** is the chord tolerance above.
|
||||||
|
- **"Min edge (mm)"** is a hard floor under both, and is what guarantees termination across a sharp
|
||||||
|
texture *step*, where the error never falls however fine the mesh gets.
|
||||||
|
- **"Added triangles (k)"** is the budget, passed as `max_triangles` (the model's own triangle count plus
|
||||||
|
the slider, so the control still means something on an already-dense model).
|
||||||
|
|
||||||
|
The height field is `make_combined_displacement_sampler()` - it mirrors `build_texture_displacement()`'s
|
||||||
|
per-layer setup (decode, patch centroid, cylinder axis, blend order, "lowest layer folds additively")
|
||||||
|
but evaluated per point. Two deliberate simplifications, both erring toward *more* detail (safe -
|
||||||
|
over-refinement is never a crack): every sampleable layer is sampled at every point (no per-point paint
|
||||||
|
test), and edge-smoothing falloff is ignored. Note the first one is *why* the refine region must not be
|
||||||
|
dilated - outside the paint the sampler still reports full relief. **LSCM layers are skipped** (no
|
||||||
|
per-point UV); a purely LSCM stack yields a null sampler and the code falls back to the length baseline
|
||||||
|
alone. Per-vertex heights are sampled lazily, so a small patch on a huge model never pays for the rest of
|
||||||
|
it. Covered by unit tests: a Gaussian bump refines densely at its center and leaves flat corners coarse,
|
||||||
|
a linear ramp produces *zero* extra triangles (the case a gradient criterion would over-refine), and a
|
||||||
|
flat field still honours the max-edge baseline.
|
||||||
|
|
||||||
### Fast bump preview (GPU-only, no CPU meshing)
|
### Fast bump preview (GPU-only, no CPU meshing)
|
||||||
|
|
||||||
@@ -384,9 +442,10 @@ of. Toolbar commands the canvas can't service itself (Average scale) are forward
|
|||||||
first impression while painting. The exact true-displacement view is one click away in the View row.
|
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*
|
- **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
|
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.
|
matter how high-resolution the height map is - that is what the subdivision controls are for
|
||||||
Since the rewrite the bake is topology-preserving, so this is now a hard, explicit property rather
|
(uniform, adaptive, or feature-adaptive; see the Subdivision section). Since the rewrite the bake is
|
||||||
than something partly papered over by the old per-layer re-meshing.
|
topology-preserving, so this is now a hard, explicit property rather than something partly papered
|
||||||
|
over by the old per-layer re-meshing.
|
||||||
|
|
||||||
## File map
|
## File map
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
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 subdivide_mesh_uniform(const indexed_triangle_set &mesh, float max_edge_length_mm, int max_iterations)
|
||||||
{
|
{
|
||||||
indexed_triangle_set current = mesh;
|
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,
|
indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
|
||||||
const std::vector<uint8_t> &refine_region,
|
const std::vector<uint8_t> &refine_region,
|
||||||
float target_edge_length_mm, int max_iterations,
|
float target_edge_length_mm, int max_triangles,
|
||||||
std::vector<int> *out_source)
|
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
|
// Neighbour slots that are not a triangle index.
|
||||||
// the caller can remap per-triangle data (paint masks) for free.
|
constexpr int NB_BOUNDARY = -1; // open edge: terminal on its own, bisected from this side alone
|
||||||
struct Tri { int v[3]; int src; };
|
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<Vec3f> verts = mesh.vertices;
|
||||||
std::vector<Tri> tris;
|
std::vector<Tri> tris(mesh.indices.size());
|
||||||
tris.reserve(mesh.indices.size());
|
|
||||||
for (size_t i = 0; i < mesh.indices.size(); ++i)
|
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;
|
indexed_triangle_set out;
|
||||||
out.vertices = verts;
|
out.vertices = verts;
|
||||||
out.indices.reserve(t.size());
|
out.indices.reserve(tris.size());
|
||||||
if (out_source) {
|
if (out_source) {
|
||||||
out_source->clear();
|
out_source->clear();
|
||||||
out_source->reserve(t.size());
|
out_source->reserve(tris.size());
|
||||||
}
|
}
|
||||||
for (const Tri &tr : t) {
|
for (const Tri &t : tris) {
|
||||||
out.indices.emplace_back(tr.v[0], tr.v[1], tr.v[2]);
|
out.indices.emplace_back(t.v[0], t.v[1], t.v[2]);
|
||||||
if (out_source)
|
if (out_source)
|
||||||
out_source->push_back(tr.src);
|
out_source->push_back(t.src);
|
||||||
}
|
}
|
||||||
return out;
|
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
|
// 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.
|
// (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())
|
if (refine_region.size() != mesh.indices.size() || int(tris.size()) + 2 > max_triangles)
|
||||||
return emit(tris);
|
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; }))
|
if (std::none_of(refine_region.begin(), refine_region.end(), [](uint8_t v) { return v != 0; }))
|
||||||
return emit(tris); // nothing flagged: no-op
|
return emit(); // 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 {
|
auto edge_key = [](int a, int b) -> uint64_t {
|
||||||
if (a > b)
|
if (a > b)
|
||||||
std::swap(a, b);
|
std::swap(a, b);
|
||||||
return (uint64_t(uint32_t(a)) << 32) | uint32_t(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 adjacency, built once here and then maintained incrementally by bisect() below. Rebuilding
|
||||||
// edge -> the (up to two) triangles sharing it. A third triangle on an edge means a
|
// it per refinement pass is what made the previous version's cost scale with the whole model
|
||||||
// non-manifold input; it is left in the second slot's place and simply not treated as
|
// instead of with the refined region, and what forced the tiny pass budget that stopped
|
||||||
// terminal, so such an edge is never bisected (better a missed refinement than a torn mesh).
|
// refinement short.
|
||||||
std::unordered_map<uint64_t, std::array<int, 2>> edge_tris;
|
{
|
||||||
edge_tris.reserve(tris.size() * 3);
|
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 ti = 0; ti < int(tris.size()); ++ti)
|
||||||
for (int e = 0; e < 3; ++e) {
|
for (int e = 0; e < 3; ++e) {
|
||||||
const uint64_t k = edge_key(tris[ti].v[e], tris[ti].v[(e + 1) % 3]);
|
EdgeRec &r = edges.try_emplace(edge_key(tris[ti].v[e], tris[ti].v[(e + 1) % 3]),
|
||||||
auto it = edge_tris.find(k);
|
EdgeRec{ { -1, -1 }, 0 })
|
||||||
if (it == edge_tris.end())
|
.first->second;
|
||||||
edge_tris.emplace(k, std::array<int, 2>{ ti, -1 });
|
if (r.count < 2)
|
||||||
else if (it->second[1] == -1)
|
r.he[r.count] = ti * 3 + e;
|
||||||
it->second[1] = ti;
|
++r.count;
|
||||||
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)
|
for (int ti = 0; ti < int(tris.size()); ++ti)
|
||||||
tri_longest[ti] = longest_key(tris[ti]);
|
for (int e = 0; e < 3; ++e) {
|
||||||
|
const EdgeRec &r = edges.at(edge_key(tris[ti].v[e], tris[ti].v[(e + 1) % 3]));
|
||||||
// Which edges to bisect this pass: terminal (the longest edge of every triangle on it) AND
|
if (r.count > 2)
|
||||||
// long enough AND wanted by the region (either side in it). Terminal-ness is exactly what
|
tris[ti].nb[e] = NB_NONMANIFOLD;
|
||||||
// guarantees both sides split together, so no hanging node is ever produced.
|
else if (r.count == 2)
|
||||||
std::unordered_set<uint64_t> to_bisect;
|
tris[ti].nb[e] = (r.he[0] == ti * 3 + e ? r.he[1] : r.he[0]) / 3;
|
||||||
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);
|
// 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
|
} // namespace Slic3r
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#define slic3r_TextureDisplacement_hpp_
|
#define slic3r_TextureDisplacement_hpp_
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -493,6 +494,25 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
|||||||
// and forwards to the overload above.
|
// and forwards to the overload above.
|
||||||
indexed_triangle_set build_texture_displacement(const ModelVolume &volume);
|
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
|
// 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
|
// 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,
|
// 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.
|
// 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
|
// Unlike subdivide_mesh_uniform() this only densifies where asked - `refine_region` (indexed by
|
||||||
// source is flagged in `refine_region` and its longest edge exceeds target_edge_length_mm - so a
|
// input-triangle index; empty or all-false => no-op) flags the triangles allowed to drive refinement
|
||||||
// small painted patch on a large model does not quadruple the whole model's triangle count. It is
|
// - so a small painted patch on a large model does not quadruple the whole model's triangle count.
|
||||||
// nonetheless *conformal*: it never leaves a T-junction/crack at the boundary between the refined
|
// It is nonetheless *conformal*: it never leaves a T-junction/crack at the boundary between the
|
||||||
// and coarse regions (the trap that made subdivide_mesh_uniform() deliberately whole-mesh).
|
// 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
|
// A triangle wants refining while it is over at least one of these, whichever applies:
|
||||||
// edge of *every* triangle sharing it. Bisecting such an edge splits both its triangles 1->2 along
|
// - length: its longest edge exceeds `target_edge_length_mm` (a *baseline* - it applies in feature
|
||||||
// the same new midpoint at once, so no hanging node is ever created. Because only a triangle's own
|
// mode too, and is what stops a coarse triangle from being declared flat merely because
|
||||||
// longest edge can be terminal, each triangle is split by at most one terminal edge per pass. When
|
// the four points the chord test samples happened to land at similar heights on a
|
||||||
// a triangle that needs refining has a longest edge that is *not* yet terminal, the neighbour across
|
// high-frequency texture: the classic aliasing stall);
|
||||||
// it has a strictly longer edge and is refined first; that propagation is what grades the mesh down
|
// - feature: (only when `sampler` is set and `chord_tolerance_mm > 0`) the *displaced* surface
|
||||||
// into the region and closes what would otherwise be cracks (Rivara, "New longest-edge algorithms").
|
// departs from the flat triangle by more than `chord_tolerance_mm`, measured as the max
|
||||||
// The transition triangles it pulls in just outside the region are a thin, bounded band.
|
// 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`
|
// Refinement runs to completion, not for a fixed number of passes: triangles are taken worst-first
|
||||||
// is non-null it is resized to the output triangle count and out_source[i] receives the input
|
// from a max-heap keyed by how many times over its criteria each one is, so a run that hits the
|
||||||
// triangle that output triangle i descends from (children inherit their parent's index), so a caller
|
// `max_triangles` budget has spent it on the largest errors rather than wherever a sweep happened to
|
||||||
// can carry per-triangle data - e.g. a paint mask - across the topology change without a geometric
|
// reach. The budget is the only bound on a pathological height field; stopping on it leaves a
|
||||||
// remap. `max_iterations` bounds the worst case; ties in "longest edge" are broken by a mesh-vertex
|
// perfectly valid, still-conformal mesh.
|
||||||
// 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).
|
// 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,
|
indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
|
||||||
const std::vector<uint8_t> &refine_region,
|
const std::vector<uint8_t> &refine_region,
|
||||||
float target_edge_length_mm, int max_iterations = 12,
|
float target_edge_length_mm, int max_triangles = 1000000,
|
||||||
std::vector<int> *out_source = nullptr);
|
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
|
} // namespace Slic3r
|
||||||
|
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ void GLGizmoTextureDisplacement::on_shutdown()
|
|||||||
m_seam_path_mode = false;
|
m_seam_path_mode = false;
|
||||||
m_seam_path_anchor = -1;
|
m_seam_path_anchor = -1;
|
||||||
m_subdivide_editing = false;
|
m_subdivide_editing = false;
|
||||||
m_subdivide_preview_count = -1;
|
m_subdivide_preview_tris = -1;
|
||||||
m_subdivide_preview_glmodel.reset();
|
m_subdivide_preview_glmodel.reset();
|
||||||
m_bump_active_chart = -1;
|
m_bump_active_chart = -1;
|
||||||
m_bump_active_vertex.clear();
|
m_bump_active_vertex.clear();
|
||||||
@@ -231,6 +231,11 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
|
|||||||
m_bump_preview_dirty = false;
|
m_bump_preview_dirty = false;
|
||||||
}
|
}
|
||||||
const bool use_bump = m_use_bump_preview && m_bump_preview_glmodel.is_initialized();
|
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) {
|
if (use_bump) {
|
||||||
m_parent.toggle_model_objects_visibility(true);
|
m_parent.toggle_model_objects_visibility(true);
|
||||||
if (ModelVolume *mv = texture_volume())
|
if (ModelVolume *mv = texture_volume())
|
||||||
@@ -244,11 +249,13 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
|
|||||||
m_c->selection_info()->get_active_instance(), mv);
|
m_c->selection_info()->get_active_instance(), mv);
|
||||||
render_preview_mesh();
|
render_preview_mesh();
|
||||||
|
|
||||||
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
|
if (show_paint_overlay) {
|
||||||
glsafe(::glPolygonOffset(-1.0f, -1.0f));
|
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
|
||||||
render_triangles(selection);
|
glsafe(::glPolygonOffset(-1.0f, -1.0f));
|
||||||
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
|
render_triangles(selection);
|
||||||
} else {
|
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
|
||||||
|
}
|
||||||
|
} else if (show_paint_overlay) {
|
||||||
render_triangles(selection);
|
render_triangles(selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -744,10 +751,9 @@ void GLGizmoTextureDisplacement::rebuild_bump_preview_mesh()
|
|||||||
if (base.vertices.size() != patch.vertices.size())
|
if (base.vertices.size() != patch.vertices.size())
|
||||||
return; // shouldn't happen: get_facets_strict() always returns the full vertex array
|
return; // shouldn't happen: get_facets_strict() always returns the full vertex array
|
||||||
|
|
||||||
std::vector<bool> is_painted(patch.vertices.size(), false);
|
// Unpainted triangles, so the surrounding surface still renders (the render path hides the real
|
||||||
for (const stl_triangle_vertex_indices &tri : patch.indices)
|
// model in bump mode). get_facets_strict() returns the same vertex array whatever state is asked.
|
||||||
for (int i = 0; i < 3; ++i)
|
const indexed_triangle_set rest = m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::NONE);
|
||||||
is_painted[tri[i]] = true;
|
|
||||||
|
|
||||||
// For LSCM we hand the shader the finished per-vertex texture uv (island placement + tiling/
|
// 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
|
// 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();
|
vertex_uv.clear();
|
||||||
|
|
||||||
GLModel::Geometry init_data;
|
GLModel::Geometry init_data;
|
||||||
// P3N3T2: normal.x carries the paint weight, tex_coord carries the precomputed uv (see the vertex
|
// P3N3T2: normal.x carries the paint weight, tex_coord the precomputed uv (see the vertex shader).
|
||||||
// shader). Reuses a standard GLModel layout rather than a bespoke vertex buffer.
|
|
||||||
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3T2 };
|
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);
|
// Per-triangle weighting via a *flat* (unshared-vertex) mesh: every corner of a painted triangle
|
||||||
// Every triangle of the whole mesh is included (not just the painted ones) so the surrounding,
|
// gets weight 1, every corner of an unpainted one weight 0. This is what a coarse mesh needs - one
|
||||||
// un-bumped surface still renders - the per-vertex weight (0 outside the patch) is what fades
|
// painted face of a raw cube has no strictly-interior vertex (all 8 are shared), so per-vertex
|
||||||
// the shader's bump effect to nothing there, exactly like the true-displacement preview fades
|
// weighting would either bleed onto the neighbours (boundary weight 1) or vanish outright (boundary
|
||||||
// to the untouched surface at its boundary.
|
// weight 0, which is what made a single face show nothing). Duplicating vertices costs no shading
|
||||||
// normal.y flags the island currently dragged in the UV editor, so the shader can move just that
|
// quality here because the bump shader takes its surface normal from screen-space derivatives of
|
||||||
// island through the island_delta uniform (see the bump shaders / on_island_edited).
|
// position (dFdx/dFdy), not from a per-vertex normal. normal.y flags the UV-editor island being
|
||||||
const bool have_active = m_bump_active_chart >= 0 && !m_bump_active_vertex.empty();
|
// dragged so the shader can move just that island via the island_delta uniform.
|
||||||
for (size_t vi = 0; vi < base.vertices.size(); ++vi) {
|
const bool have_active = m_bump_active_chart >= 0 && !m_bump_active_vertex.empty();
|
||||||
const float active = (have_active && vi < m_bump_active_vertex.size() && m_bump_active_vertex[vi]) ? 1.f : 0.f;
|
const size_t tri_total = patch.indices.size() + rest.indices.size();
|
||||||
const Vec3f weight_normal(is_painted[vi] ? 1.f : 0.f, active, 0.f);
|
init_data.reserve_vertices(tri_total * 3);
|
||||||
const Vec2f uv = m_bump_preview_uses_vertex_uv ? vertex_uv[vi] : Vec2f::Zero();
|
init_data.reserve_indices(tri_total * 3);
|
||||||
init_data.add_vertex(base.vertices[vi], weight_normal, uv);
|
unsigned vcount = 0;
|
||||||
}
|
const auto emit_triangles = [&](const indexed_triangle_set &its, float weight) {
|
||||||
for (const stl_triangle_vertex_indices &tri : base.indices)
|
for (const stl_triangle_vertex_indices &tri : its.indices) {
|
||||||
init_data.add_triangle(unsigned(tri[0]), unsigned(tri[1]), unsigned(tri[2]));
|
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));
|
m_bump_preview_glmodel.init_from(std::move(init_data));
|
||||||
// GLModel::render() unconditionally re-sets the shader's "uniform_color" from this internal
|
// 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]);
|
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) {
|
if (updated) {
|
||||||
const ModelObjectPtrs &mos = wxGetApp().model().objects;
|
const ModelObjectPtrs &mos = wxGetApp().model().objects;
|
||||||
wxGetApp().obj_list()->update_info_items(std::find(mos.begin(), mos.end(), mo) - mos.begin());
|
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)
|
for (auto &pt : *painted_tri)
|
||||||
pt.clear();
|
pt.clear();
|
||||||
|
|
||||||
// Sorted-vertex-triple -> triangle index, so a fully-painted patch sub-triangle (which comes
|
// Sorted-vertex-triple -> triangle index, so a fully-painted patch sub-triangle (which comes back
|
||||||
// back with the original mesh's own three vertex indices) can be mapped to its source triangle.
|
// with the original mesh's own three vertex indices) can be mapped to its source triangle. Only
|
||||||
// A sub-triangle produced by a *partial* brush stroke has at least one appended (split) vertex,
|
// the paint carry-forward needs it, and the live subdivide preview calls this on every slider
|
||||||
// so "all three indices are original" is exactly the test for a whole, fully-painted triangle.
|
// frame, so it is not built for the region-only path.
|
||||||
std::map<std::array<int, 3>, int> tri_by_verts;
|
std::map<std::array<int, 3>, int> tri_by_verts;
|
||||||
for (size_t i = 0; i < ntri; ++i) {
|
if (painted_tri)
|
||||||
std::array<int, 3> k{ its.indices[i][0], its.indices[i][1], its.indices[i][2] };
|
for (size_t i = 0; i < ntri; ++i) {
|
||||||
std::sort(k.begin(), k.end());
|
std::array<int, 3> k{ its.indices[i][0], its.indices[i][1], its.indices[i][2] };
|
||||||
tri_by_verts.emplace(k, int(i));
|
std::sort(k.begin(), k.end());
|
||||||
}
|
tri_by_verts.emplace(k, int(i));
|
||||||
|
}
|
||||||
|
|
||||||
bool any_paint = false;
|
bool any_paint = false;
|
||||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
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))
|
if (!TriangleSelector::has_facets(data, EnforcerBlockerType::ENFORCER))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
TriangleSelector sel(mv->mesh());
|
// The refine region is exactly the original triangles the brush touched. `triangles_to_split`
|
||||||
sel.deserialize(data, false);
|
// lists precisely those: serialize() records an entry for every original triangle that is
|
||||||
const indexed_triangle_set patch = sel.get_facets_strict(EnforcerBlockerType::ENFORCER);
|
// 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);
|
(*painted_tri)[slot].assign(ntri, 0);
|
||||||
|
for (const stl_triangle_vertex_indices &t : sel.get_facets_strict(EnforcerBlockerType::ENFORCER).indices) {
|
||||||
for (const stl_triangle_vertex_indices &t : patch.indices) {
|
// A sub-triangle produced by a *partial* stroke always carries at least one appended
|
||||||
bool all_original = true;
|
// (split) vertex, so "all three indices are original" is exactly the test for a whole,
|
||||||
for (int k = 0; k < 3; ++k) {
|
// fully-painted triangle - the only kind whose paint can be inherited wholesale.
|
||||||
if (size_t(t[k]) < nvert)
|
if (size_t(t[0]) >= nvert || size_t(t[1]) >= nvert || size_t(t[2]) >= nvert)
|
||||||
painted_vertex[t[k]] = 1; // marks the refine region (any coverage, plus a ring)
|
continue;
|
||||||
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::array<int, 3> k{ t[0], t[1], t[2] };
|
||||||
std::sort(k.begin(), k.end());
|
std::sort(k.begin(), k.end());
|
||||||
if (auto it = tri_by_verts.find(k); it != tri_by_verts.end())
|
if (auto it = tri_by_verts.find(k); it != tri_by_verts.end())
|
||||||
(*painted_tri)[slot][it->second] = 1;
|
(*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;
|
any_paint = true;
|
||||||
}
|
}
|
||||||
return any_paint;
|
return any_paint;
|
||||||
@@ -2749,17 +2769,36 @@ void GLGizmoTextureDisplacement::subdivide_model_adaptive()
|
|||||||
return;
|
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
|
// Do the (potentially slow) refinement before taking the snapshot, so a no-op leaves no empty
|
||||||
// undo step - mirrors remesh_model().
|
// 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;
|
std::vector<int> source;
|
||||||
indexed_triangle_set refined;
|
indexed_triangle_set refined;
|
||||||
{
|
{
|
||||||
wxBusyCursor wait;
|
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()) {
|
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 "
|
show_error(nullptr, _u8L("Nothing to subdivide - the painted area already meets the target edge "
|
||||||
"edge length."));
|
"length and detail tolerance, or the triangle budget is already used up."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2852,7 +2891,7 @@ void GLGizmoTextureDisplacement::remesh_model()
|
|||||||
void GLGizmoTextureDisplacement::rebuild_subdivide_preview()
|
void GLGizmoTextureDisplacement::rebuild_subdivide_preview()
|
||||||
{
|
{
|
||||||
m_subdivide_preview_glmodel.reset();
|
m_subdivide_preview_glmodel.reset();
|
||||||
m_subdivide_preview_count = -1;
|
m_subdivide_preview_tris = -1;
|
||||||
const ModelVolume *mv = texture_volume();
|
const ModelVolume *mv = texture_volume();
|
||||||
if (mv == nullptr)
|
if (mv == nullptr)
|
||||||
return;
|
return;
|
||||||
@@ -2866,7 +2905,20 @@ void GLGizmoTextureDisplacement::rebuild_subdivide_preview()
|
|||||||
std::vector<uint8_t> region;
|
std::vector<uint8_t> region;
|
||||||
if (!collect_paint_region(region, nullptr))
|
if (!collect_paint_region(region, nullptr))
|
||||||
return; // nothing painted yet: nothing to preview
|
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 {
|
} else {
|
||||||
if (m_subdivide_count < 1)
|
if (m_subdivide_count < 1)
|
||||||
return;
|
return;
|
||||||
@@ -2874,7 +2926,7 @@ void GLGizmoTextureDisplacement::rebuild_subdivide_preview()
|
|||||||
}
|
}
|
||||||
if (its.indices.empty())
|
if (its.indices.empty())
|
||||||
return;
|
return;
|
||||||
m_subdivide_preview_count = m_subdivide_count;
|
m_subdivide_preview_tris = int(its.indices.size());
|
||||||
|
|
||||||
GLModel::Geometry init_data;
|
GLModel::Geometry init_data;
|
||||||
init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 };
|
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
|
// 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
|
// 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.
|
// 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))) {
|
if (m_preview_params_dirty && (m_auto_update || !ImGui::IsMouseDown(ImGuiMouseButton_Left))) {
|
||||||
rebuild_preview();
|
rebuild_preview();
|
||||||
// Same edits (tile size, rotation, offset, a new texture) are what the projector window
|
// 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();
|
sum += (its.vertices[tri[i]] - its.vertices[tri[(i + 1) % 3]]).norm();
|
||||||
++cnt;
|
++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));
|
// Live preview: rebuild the wireframe as the slider moves, not only on release, so it tracks
|
||||||
// "##subdiv" keeps the visible label "Target edge (mm)" but gives it an ImGui ID distinct
|
// the value. The rebuild is bounded by the painted region, so it stays responsive.
|
||||||
// from the remesh slider below, which shows the same text - same label == same widget to
|
const auto preview_live = [this]() {
|
||||||
// ImGui, so without this the two would collide.
|
if (m_subdivide_editing)
|
||||||
if (m_imgui->slider_float(std::string(_u8L("Target edge (mm)")) + "##subdiv", &m_subdivide_target_mm,
|
rebuild_subdivide_preview();
|
||||||
0.1f, 20.f, "%.2f", ImGuiLogSlider)) {
|
m_parent.set_as_dirty();
|
||||||
if (m_subdivide_editing && !ImGui::IsMouseDown(ImGuiMouseButton_Left))
|
};
|
||||||
|
|
||||||
|
if (ImGui::Checkbox(_u8L("Follow texture detail").c_str(), &m_subdivide_feature)) {
|
||||||
|
if (m_subdivide_editing)
|
||||||
rebuild_subdivide_preview();
|
rebuild_subdivide_preview();
|
||||||
m_parent.set_as_dirty();
|
m_parent.set_as_dirty();
|
||||||
}
|
}
|
||||||
ImGui::PopItemWidth();
|
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
m_imgui->tooltip(_u8L("Triangles in the painted area are split until every edge is at or below this "
|
m_imgui->tooltip(_u8L("Put triangles only where the texture actually bends - dense over hills, ridges and "
|
||||||
"length. Smaller means finer detail and more triangles."),
|
"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));
|
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 {
|
} else {
|
||||||
ImGui::PushItemWidth(m_imgui->scaled(8.4f));
|
ImGui::PushItemWidth(m_imgui->scaled(8.4f));
|
||||||
if (ImGui::SliderInt(_u8L("Subdivide steps").c_str(), &m_subdivide_count, 0, 5)) {
|
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();
|
ImGui::SameLine();
|
||||||
if (m_imgui->button(_u8L("Done"))) {
|
if (m_imgui->button(_u8L("Done"))) {
|
||||||
m_subdivide_editing = false;
|
m_subdivide_editing = false;
|
||||||
m_subdivide_preview_count = -1;
|
m_subdivide_preview_tris = -1;
|
||||||
m_subdivide_preview_glmodel.reset();
|
m_subdivide_preview_glmodel.reset();
|
||||||
m_parent.set_as_dirty();
|
m_parent.set_as_dirty();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ private:
|
|||||||
// committed, i.e. the most expensive thing the panel can do, on every Apply.
|
// committed, i.e. the most expensive thing the panel can do, on every Apply.
|
||||||
int m_subdivide_count = 1;
|
int m_subdivide_count = 1;
|
||||||
bool m_subdivide_editing = false;
|
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;
|
GLModel m_subdivide_preview_glmodel;
|
||||||
void rebuild_subdivide_preview();
|
void rebuild_subdivide_preview();
|
||||||
void render_subdivide_preview();
|
void render_subdivide_preview();
|
||||||
@@ -314,6 +314,20 @@ private:
|
|||||||
// are painted), so the region survives the subdivision instead of being dropped.
|
// are painted), so the region survives the subdivision instead of being dropped.
|
||||||
bool m_subdivide_adaptive = false;
|
bool m_subdivide_adaptive = false;
|
||||||
float m_subdivide_target_mm = 0.f; // 0 = not yet seeded; filled from the mesh on first show
|
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();
|
void subdivide_model_adaptive();
|
||||||
// Fills `region` (per current-mesh triangle, 1 = refine) from the union of every layer's painted
|
// 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
|
// area. If `painted_tri` is non-null, also fills, per layer, the fully-painted triangles to carry
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#define NOMINMAX
|
#define NOMINMAX
|
||||||
#include <catch2/catch_all.hpp>
|
#include <catch2/catch_all.hpp>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
|
|
||||||
@@ -270,11 +271,20 @@ TEST_CASE("TextureDisplacement: adaptive subdivision is conformal and region-res
|
|||||||
{
|
{
|
||||||
std::vector<uint8_t> region(cube.indices.size(), 1);
|
std::vector<uint8_t> region(cube.indices.size(), 1);
|
||||||
std::vector<int> source;
|
std::vector<int> source;
|
||||||
const indexed_triangle_set out = subdivide_mesh_adaptive(cube, region, 3.f, 12, &source);
|
const indexed_triangle_set out = subdivide_mesh_adaptive(cube, region, 3.f, 100000, &source);
|
||||||
|
|
||||||
CHECK(out.indices.size() > cube.indices.size()); // it actually refined
|
CHECK(out.indices.size() > cube.indices.size()); // it actually refined
|
||||||
CHECK(every_edge_used_twice(out)); // ... without opening a single crack
|
CHECK(every_edge_used_twice(out)); // ... without opening a single crack
|
||||||
|
|
||||||
|
// Refinement runs to completion, not for a fixed number of passes: with the whole mesh in the
|
||||||
|
// region and budget to spare, *every* edge really does end up at or below the target. This is
|
||||||
|
// the regression that matters - an earlier version quietly stopped a long way short, having
|
||||||
|
// spent its pass budget grading the coarse surroundings.
|
||||||
|
float worst = 0.f;
|
||||||
|
for (const auto &t : out.indices)
|
||||||
|
worst = std::max(worst, longest_edge(out, t));
|
||||||
|
CHECK(worst <= 3.f);
|
||||||
|
|
||||||
REQUIRE(source.size() == out.indices.size());
|
REQUIRE(source.size() == out.indices.size());
|
||||||
for (int s : source)
|
for (int s : source)
|
||||||
CHECK((s >= 0 && s < int(cube.indices.size()))); // every child names a real parent
|
CHECK((s >= 0 && s < int(cube.indices.size()))); // every child names a real parent
|
||||||
@@ -296,28 +306,133 @@ TEST_CASE("TextureDisplacement: adaptive subdivision is conformal and region-res
|
|||||||
REQUIRE(region_count > 0);
|
REQUIRE(region_count > 0);
|
||||||
|
|
||||||
std::vector<int> source;
|
std::vector<int> source;
|
||||||
const indexed_triangle_set out = subdivide_mesh_adaptive(cube, region, 2.f, 12, &source);
|
const indexed_triangle_set out = subdivide_mesh_adaptive(cube, region, 2.f, 100000, &source);
|
||||||
|
|
||||||
CHECK(out.indices.size() > cube.indices.size());
|
CHECK(out.indices.size() > cube.indices.size());
|
||||||
CHECK(every_edge_used_twice(out)); // the refined/coarse seam has no T-junction
|
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
|
// Inside the region the target is actually met - refinement is not cut short by a pass budget.
|
||||||
// output triangle is versus the largest region-sourced input triangle.
|
// Outside it, only the graded transition band conformality requires is touched, so plenty of
|
||||||
|
// the unpainted mesh is still coarser than the target: the region was not a suggestion.
|
||||||
float max_in = 0.f, max_out = 0.f;
|
float max_in = 0.f, max_out = 0.f;
|
||||||
for (size_t i = 0; i < cube.indices.size(); ++i)
|
for (size_t i = 0; i < out.indices.size(); ++i) {
|
||||||
if (region[i])
|
float &acc = region[source[i]] ? max_in : max_out;
|
||||||
max_in = std::max(max_in, longest_edge(cube, cube.indices[i]));
|
acc = std::max(acc, longest_edge(out, out.indices[i]));
|
||||||
for (size_t i = 0; i < out.indices.size(); ++i)
|
}
|
||||||
if (region[source[i]])
|
CHECK(max_in <= 2.f);
|
||||||
max_out = std::max(max_out, longest_edge(out, out.indices[i]));
|
CHECK(max_out > 2.f);
|
||||||
CHECK(max_out < max_in); // refinement genuinely happened inside the region
|
|
||||||
|
std::vector<uint8_t> all(cube.indices.size(), 1);
|
||||||
|
const indexed_triangle_set whole = subdivide_mesh_adaptive(cube, all, 2.f, 100000);
|
||||||
|
CHECK(out.indices.size() < whole.indices.size()); // ... and it cost less than doing the lot
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("the triangle budget caps the result and still leaves a conformal mesh")
|
||||||
|
{
|
||||||
|
std::vector<uint8_t> region(cube.indices.size(), 1);
|
||||||
|
const indexed_triangle_set out = subdivide_mesh_adaptive(cube, region, 0.05f, /*max_triangles*/ 500);
|
||||||
|
CHECK(out.indices.size() <= 500);
|
||||||
|
CHECK(out.indices.size() > cube.indices.size()); // it spent the budget rather than giving up
|
||||||
|
CHECK(every_edge_used_twice(out)); // stopping on the budget is not a crack
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("an empty region is a no-op")
|
SECTION("an empty region is a no-op")
|
||||||
{
|
{
|
||||||
std::vector<uint8_t> region(cube.indices.size(), 0);
|
std::vector<uint8_t> region(cube.indices.size(), 0);
|
||||||
const indexed_triangle_set out = subdivide_mesh_adaptive(cube, region, 1.f, 12, nullptr);
|
const indexed_triangle_set out = subdivide_mesh_adaptive(cube, region, 1.f, 100000, nullptr);
|
||||||
CHECK(out.indices.size() == cube.indices.size());
|
CHECK(out.indices.size() == cube.indices.size());
|
||||||
CHECK(out.vertices.size() == cube.vertices.size());
|
CHECK(out.vertices.size() == cube.vertices.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("TextureDisplacement: feature-adaptive subdivision follows curvature, not slope", "[TextureDisplacement]")
|
||||||
|
{
|
||||||
|
// A flat sheet, tessellated into a regular grid to give the bisector something to work with.
|
||||||
|
indexed_triangle_set plane;
|
||||||
|
plane.vertices = { { 0.f, 0.f, 0.f }, { 1.f, 0.f, 0.f }, { 1.f, 1.f, 0.f }, { 0.f, 1.f, 0.f } };
|
||||||
|
plane.indices = { { 0, 1, 2 }, { 0, 2, 3 } };
|
||||||
|
const indexed_triangle_set grid = subdivide_mesh_uniform(plane, 0.15f, 5); // ~uniform grid of small triangles
|
||||||
|
REQUIRE(grid.indices.size() > 32);
|
||||||
|
|
||||||
|
const std::vector<uint8_t> region(grid.indices.size(), 1);
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
auto centroid_xy = [](const indexed_triangle_set &its, const stl_triangle_vertex_indices &t) {
|
||||||
|
return Vec2f((its.vertices[t[0]].x() + its.vertices[t[1]].x() + its.vertices[t[2]].x()) / 3.f,
|
||||||
|
(its.vertices[t[0]].y() + its.vertices[t[1]].y() + its.vertices[t[2]].y()) / 3.f);
|
||||||
|
};
|
||||||
|
|
||||||
|
SECTION("a sharp bump refines densely at its center and leaves flat corners coarse")
|
||||||
|
{
|
||||||
|
// A tight Gaussian bump at the sheet's center: strong curvature near (0.5, 0.5), flat far away.
|
||||||
|
HeightFieldSampler bump = [](const Vec3f &p, const Vec3f &) {
|
||||||
|
const float r2 = (p.x() - 0.5f) * (p.x() - 0.5f) + (p.y() - 0.5f) * (p.y() - 0.5f);
|
||||||
|
return 1.0f * std::exp(-r2 / 0.02f);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Baseline max edge 0.3 is coarser than the grid's own edges, so the baseline adds nothing
|
||||||
|
// here - this isolates the *curvature* contribution (the grid already meets the baseline).
|
||||||
|
std::vector<int> source;
|
||||||
|
const indexed_triangle_set out =
|
||||||
|
subdivide_mesh_adaptive(grid, region, /*max edge*/ 0.3f, 200000, &source, bump, /*tol*/ 0.02f,
|
||||||
|
/*min_edge*/ 0.01f);
|
||||||
|
|
||||||
|
CHECK(out.indices.size() > grid.indices.size()); // the bump forced real refinement
|
||||||
|
|
||||||
|
// The largest triangle near the bump's center must be much smaller than the largest in a flat
|
||||||
|
// corner - i.e. triangles went where the curvature is, not spread evenly.
|
||||||
|
float near_max = 0.f, far_max = 0.f;
|
||||||
|
for (const auto &t : out.indices) {
|
||||||
|
const Vec2f c = centroid_xy(out, t);
|
||||||
|
const float r = (c - Vec2f(0.5f, 0.5f)).norm();
|
||||||
|
const float len = longest_edge(out, t);
|
||||||
|
if (r < 0.1f)
|
||||||
|
near_max = std::max(near_max, len);
|
||||||
|
else if (r > 0.45f)
|
||||||
|
far_max = std::max(far_max, len);
|
||||||
|
}
|
||||||
|
REQUIRE(near_max > 0.f);
|
||||||
|
REQUIRE(far_max > 0.f);
|
||||||
|
CHECK(near_max < far_max); // finer at the hill than on the flats
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a linear ramp has zero curvature and is left untouched")
|
||||||
|
{
|
||||||
|
// Height varies, but linearly - a flat triangle represents it exactly, so the chord error is
|
||||||
|
// zero everywhere and nothing should be split. This is the case a gradient-based criterion
|
||||||
|
// would wrongly over-refine.
|
||||||
|
HeightFieldSampler ramp = [](const Vec3f &p, const Vec3f &) { return 2.0f * p.x(); };
|
||||||
|
|
||||||
|
// Same coarse baseline (0.3) that the grid already meets, so any split would be curvature-
|
||||||
|
// driven - and a ramp has none.
|
||||||
|
const indexed_triangle_set out =
|
||||||
|
subdivide_mesh_adaptive(grid, region, /*max edge*/ 0.3f, 200000, nullptr, ramp, /*tol*/ 0.02f,
|
||||||
|
/*min_edge*/ 0.01f);
|
||||||
|
|
||||||
|
CHECK(out.indices.size() == grid.indices.size()); // not one extra triangle
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("the max-edge baseline still applies in feature mode")
|
||||||
|
{
|
||||||
|
// A height field that is flat everywhere the four sample points of a coarse triangle happen to
|
||||||
|
// land, but not in between - the aliasing case where a chord test alone reports no error and
|
||||||
|
// refinement stalls before it ever starts. The baseline is what stops that: it guarantees a
|
||||||
|
// sampling density fine enough for the curvature test to see the texture at all.
|
||||||
|
HeightFieldSampler flat = [](const Vec3f &, const Vec3f &) { return 0.f; };
|
||||||
|
|
||||||
|
const indexed_triangle_set out =
|
||||||
|
subdivide_mesh_adaptive(grid, region, /*max edge*/ 0.03f, 200000, nullptr, flat, /*tol*/ 0.02f,
|
||||||
|
/*min_edge*/ 0.001f);
|
||||||
|
|
||||||
|
CHECK(out.indices.size() > grid.indices.size());
|
||||||
|
float worst = 0.f;
|
||||||
|
for (const auto &t : out.indices)
|
||||||
|
worst = std::max(worst, longest_edge(out, t));
|
||||||
|
CHECK(worst <= 0.03f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user