mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-02 07:42:14 +00:00
Displace the painted patch border and add post-process smoothing
This commit is contained in:
@@ -35,16 +35,21 @@ semantics).
|
|||||||
|
|
||||||
### Bake algorithm (`libslic3r/TextureDisplacement.cpp`)
|
### Bake algorithm (`libslic3r/TextureDisplacement.cpp`)
|
||||||
|
|
||||||
`build_texture_displacement(base_mesh, layers, facets_data)` is **accumulate-then-displace, and
|
`build_texture_displacement(base_mesh, layers, facets_data, options)` is **accumulate-then-displace,
|
||||||
topology-preserving**: the returned mesh has exactly the input's vertices and triangles, in the same
|
and topology-preserving**: the returned mesh has exactly the input's vertices and triangles, in the
|
||||||
order - only the positions of displaced vertices differ.
|
same order - only the positions of displaced vertices differ.
|
||||||
|
|
||||||
1. `its_compactify_vertices()` on a copy of the input. In practice a no-op (it only drops
|
1. `its_compactify_vertices()` on a copy of the input. In practice a no-op (it only drops
|
||||||
*unreferenced* vertices, and preserves the order and indices of the rest). It is there to
|
*unreferenced* vertices, and preserves the order and indices of the rest). It is there to
|
||||||
guarantee the index alignment step 3 depends on.
|
guarantee the index alignment step 3 depends on.
|
||||||
2. Area-weighted vertex normals of the **undisplaced** mesh, computed once. Every layer both
|
2. Area-weighted vertex normals of the **undisplaced** mesh, computed once. Every layer both
|
||||||
projects and displaces along these, so a vertex covered by several layers moves along one single
|
projects and displaces along these, so a vertex covered by several layers moves along one single
|
||||||
well-defined direction.
|
well-defined direction. Where the paint does *not* cover every triangle around a vertex, the normal
|
||||||
|
is recomputed from the painted triangles alone (the union over all layers, so it stays one direction
|
||||||
|
per vertex). On the rim of a fully painted top face the whole-mesh normal is the 45 degrees bisector
|
||||||
|
it shares with the side wall, and displacing along that flares the rim outwards instead of raising
|
||||||
|
it. Interior vertices are unaffected - all their triangles are painted, so the two coincide. Paint
|
||||||
|
coverage per original triangle comes straight off `TriangleSplittingData::triangles_to_split`.
|
||||||
3. For each layer in slot order: deserialize its stored paint mask into a `TriangleSelector` against
|
3. For each layer in slot order: deserialize its stored paint mask into a `TriangleSelector` against
|
||||||
the **base mesh** (never against a previous layer's output), then
|
the **base mesh** (never against a previous layer's output), then
|
||||||
`selector.get_facets_strict(ENFORCER)` → the painted patch. Two facts are exploited:
|
`selector.get_facets_strict(ENFORCER)` → the painted patch. Two facts are exploited:
|
||||||
@@ -57,16 +62,55 @@ order - only the positions of displaced vertices differ.
|
|||||||
referenced ones in order. Combined with step 1, **selector vertex index `i` is our vertex `i`**.
|
referenced ones in order. Combined with step 1, **selector vertex index `i` is our vertex `i`**.
|
||||||
Split vertices live past the end of our array and are simply skipped - they sit on the paint
|
Split vertices live past the end of our array and are simply skipped - they sit on the paint
|
||||||
boundary anyway (splitting only happens at partial coverage), so they would be pinned regardless.
|
boundary anyway (splitting only happens at partial coverage), so they would be pinned regardless.
|
||||||
4. A vertex used by at least one **unpainted** triangle is a boundary vertex - pinned, never
|
4. A vertex used by at least one **unpainted** triangle is a border vertex. Whether it moves is
|
||||||
displaced (its final position is ambiguous, it belongs to both regions). Only vertices used
|
`TextureDisplacementOptions::displace_border`, and it **does by default**. The original design
|
||||||
exclusively by painted triangles get displaced. This is what keeps bakes seamless with zero
|
pinned it, justified as stopping the patch tearing away from the surrounding surface - which stopped
|
||||||
remeshing/hole-filling at the seam.
|
being true the moment the bake became topology-preserving. There is no seam to tear: a border vertex
|
||||||
|
is *one* vertex shared by both regions, and moving it simply tilts the unpainted triangles that use
|
||||||
|
it. What pinning actually does is clamp the outermost ring of relief to zero, so on a fully painted
|
||||||
|
face the pattern collapses into a ring of steep ramps right at the edge - the reported "it doesn't
|
||||||
|
extrude at the border" artifact. Pinning is kept as an option for the case where the relief must not
|
||||||
|
spill past the paint at all. Either way the border still drives the `edge_smoothing` falloff.
|
||||||
5. Per interior vertex: sample the height texture (`sample_layer_height()`, see Projection methods)
|
5. Per interior vertex: sample the height texture (`sample_layer_height()`, see Projection methods)
|
||||||
and fold `height * depth_mm * (invert ? -1 : 1)` into that vertex's running total via the layer's
|
and fold `height * depth_mm * (invert ? -1 : 1)` into that vertex's running total via the layer's
|
||||||
`TextureBlendMode` (see Blend modes). A `visited` set makes each layer fold in exactly **once**
|
`TextureBlendMode` (see Blend modes). A `visited` set makes each layer fold in exactly **once**
|
||||||
per vertex, no matter how many of the patch's triangles share it - otherwise a Multiply/Subtract
|
per vertex, no matter how many of the patch's triangles share it - otherwise a Multiply/Subtract
|
||||||
layer would apply two or three times over depending on local triangle fan-out.
|
layer would apply two or three times over depending on local triangle fan-out.
|
||||||
6. Finally, move each touched vertex along its (step 2) normal by its accumulated total.
|
6. Finally, move each touched vertex along its (step 2) normal by its accumulated total.
|
||||||
|
7. Optionally (`TextureDisplacementOptions::smooth_*`) relax the result - see Post-process smoothing.
|
||||||
|
|
||||||
|
### Post-process smoothing
|
||||||
|
|
||||||
|
`smooth_mesh_vertices(mesh, movable, strength, iterations)` - plain Laplacian relaxation, run after all
|
||||||
|
layers have been folded in, restricted to the vertices flagged in `movable`. Each pass moves a movable
|
||||||
|
vertex a `strength` fraction of the way to the average of its one-ring, read from a **snapshot** of the
|
||||||
|
previous pass so the result does not depend on vertex order (a Gauss-Seidel sweep would smooth several
|
||||||
|
times as hard at the end of the array as at the start). Neighbours come from a CSR-style adjacency built
|
||||||
|
once per call.
|
||||||
|
|
||||||
|
Its job is to round off the hard steps a bitmap height map leaves behind, which is a different knob from
|
||||||
|
`TextureDisplacementLayer::smoothing` - that blurs the *height map* before it is ever sampled, this
|
||||||
|
relaxes the *geometry* afterwards. Topology-preserving, like the bake.
|
||||||
|
|
||||||
|
Two ways in, sharing one set of settings on the volume:
|
||||||
|
- The **"Smooth result"** checkbox + "Smoothing (%)" / "Passes" ride along with Preview and Bake.
|
||||||
|
`movable` is exactly the set of vertices the displacement moved, so the untouched part of the model
|
||||||
|
keeps its exact geometry and the ring just outside the displaced set anchors the relaxation (the
|
||||||
|
relief cannot creep outward).
|
||||||
|
- **"Smooth baked mesh now"** (`GLGizmoTextureDisplacement::smooth_model()`) applies the same settings to
|
||||||
|
the volume's *committed* geometry, for relief that is already baked in. `movable` there is the painted
|
||||||
|
triangles' vertices. Because smoothing never touches the triangle list, this is the one geometry
|
||||||
|
operation in the gizmo that keeps **every** paint channel verbatim - it saves and restores the eight
|
||||||
|
texture-displacement masks around `set_mesh()` rather than remapping or dropping them.
|
||||||
|
|
||||||
|
**"Ignore outer ring"** (`smooth_skip_border`, **on by default**) drops the patch's own outermost ring of
|
||||||
|
vertices from `movable`. That ring's neighbours *outside* the paint never move, so relaxing it drags the
|
||||||
|
rim of the relief back down toward the flat surface and the pattern comes out half-melted exactly where
|
||||||
|
it meets the edge - crisp everywhere else, which is what makes it look like a bug rather than a setting.
|
||||||
|
Held out, the border keeps the full depth the texture asked for and only the interior relaxes. Turning it
|
||||||
|
off softens the outer edge deliberately (a blunter version of the per-layer edge-smoothing falloff).
|
||||||
|
Note this is the *smoothing* rim, a separate question from whether that rim is displaced at all
|
||||||
|
(`displace_border`, step 4 above) - the two are independent and both default to "keep the border sharp".
|
||||||
|
|
||||||
### Blend modes
|
### Blend modes
|
||||||
|
|
||||||
@@ -451,8 +495,9 @@ of. Toolbar commands the canvas can't service itself (Average scale) are forward
|
|||||||
|
|
||||||
**libslic3r (core, no GUI dependency):**
|
**libslic3r (core, no GUI dependency):**
|
||||||
- `src/libslic3r/TextureDisplacement.hpp/.cpp` - data model, bake algorithm, projection methods,
|
- `src/libslic3r/TextureDisplacement.hpp/.cpp` - data model, bake algorithm, projection methods,
|
||||||
tiling, subdivision (uniform + adaptive longest-edge bisection). See doc comments throughout,
|
tiling, subdivision (uniform + adaptive longest-edge bisection), post-process smoothing
|
||||||
they're kept accurate and up to date.
|
(`smooth_mesh_vertices()`), and `TextureDisplacementOptions` (the whole-stack settings). See doc
|
||||||
|
comments throughout, they're kept accurate and up to date.
|
||||||
- `src/libslic3r/MeshBoolean.hpp/.cpp` - added `parameterize_lscm()` and `remesh_isotropic()`
|
- `src/libslic3r/MeshBoolean.hpp/.cpp` - added `parameterize_lscm()` and `remesh_isotropic()`
|
||||||
in the `cgal` sub-namespace,
|
in the `cgal` sub-namespace,
|
||||||
reusing the existing `CGALMesh`/`_EpicMesh`/conversion-helper infrastructure already there for
|
reusing the existing `CGALMesh`/`_EpicMesh`/conversion-helper infrastructure already there for
|
||||||
@@ -461,7 +506,8 @@ of. Toolbar commands the canvas can't service itself (Average scale) are forward
|
|||||||
LSCM_parameterizer_3, parameterize}.h`. No new dependency - CGAL 5.6.3 is already vendored and
|
LSCM_parameterizer_3, parameterize}.h`. No new dependency - CGAL 5.6.3 is already vendored and
|
||||||
the `Surface_mesh_parameterization` package headers were already present, just unused before now.
|
the `Surface_mesh_parameterization` package headers were already present, just unused before now.
|
||||||
- `src/libslic3r/Model.hpp/.cpp` - the 8 named `FacetsAnnotation` fields + accessor,
|
- `src/libslic3r/Model.hpp/.cpp` - the 8 named `FacetsAnnotation` fields + accessor,
|
||||||
`texture_displacement_layers`, and all the mirrored touch points (see Data model above).
|
`texture_displacement_layers`, `texture_displacement_options`, and all the mirrored touch points
|
||||||
|
(see Data model above).
|
||||||
|
|
||||||
**GUI:**
|
**GUI:**
|
||||||
- `src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp/.cpp` - the gizmo. Panel controls: dock/
|
- `src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp/.cpp` - the gizmo. Panel controls: dock/
|
||||||
|
|||||||
@@ -938,6 +938,11 @@ public:
|
|||||||
// see build_texture_displacement()); it only reflects UI insertion order.
|
// see build_texture_displacement()); it only reflects UI insertion order.
|
||||||
std::vector<TextureDisplacementLayer> texture_displacement_layers;
|
std::vector<TextureDisplacementLayer> texture_displacement_layers;
|
||||||
|
|
||||||
|
// Whole-stack displacement settings (border handling, post-process smoothing) - see
|
||||||
|
// TextureDisplacementOptions. They live beside the layers rather than on one of them because
|
||||||
|
// they are not a property of any single layer.
|
||||||
|
TextureDisplacementOptions texture_displacement_options;
|
||||||
|
|
||||||
// Save painting data before reset_extra_facets() discards it.
|
// Save painting data before reset_extra_facets() discards it.
|
||||||
// Used for replacing mesh without losing painting data.
|
// Used for replacing mesh without losing painting data.
|
||||||
// Only for model parts (not modifiers/connectors).
|
// Only for model parts (not modifiers/connectors).
|
||||||
@@ -1190,6 +1195,7 @@ private:
|
|||||||
texture_displacement_facets_4(other.texture_displacement_facets_4), texture_displacement_facets_5(other.texture_displacement_facets_5),
|
texture_displacement_facets_4(other.texture_displacement_facets_4), texture_displacement_facets_5(other.texture_displacement_facets_5),
|
||||||
texture_displacement_facets_6(other.texture_displacement_facets_6), texture_displacement_facets_7(other.texture_displacement_facets_7),
|
texture_displacement_facets_6(other.texture_displacement_facets_6), texture_displacement_facets_7(other.texture_displacement_facets_7),
|
||||||
texture_displacement_layers(other.texture_displacement_layers),
|
texture_displacement_layers(other.texture_displacement_layers),
|
||||||
|
texture_displacement_options(other.texture_displacement_options),
|
||||||
cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape)
|
cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape)
|
||||||
{
|
{
|
||||||
assert(this->id().valid());
|
assert(this->id().valid());
|
||||||
@@ -1288,7 +1294,7 @@ private:
|
|||||||
cereal::load_by_value(ar, f);
|
cereal::load_by_value(ar, f);
|
||||||
mesh_changed |= tf != f.timestamp();
|
mesh_changed |= tf != f.timestamp();
|
||||||
}
|
}
|
||||||
ar(texture_displacement_layers);
|
ar(texture_displacement_layers, texture_displacement_options);
|
||||||
cereal::load_by_value(ar, config);
|
cereal::load_by_value(ar, config);
|
||||||
cereal::load(ar, text_configuration);
|
cereal::load(ar, text_configuration);
|
||||||
cereal::load(ar, emboss_shape);
|
cereal::load(ar, emboss_shape);
|
||||||
@@ -1312,7 +1318,7 @@ private:
|
|||||||
cereal::save_by_value(ar, fuzzy_skin_facets);
|
cereal::save_by_value(ar, fuzzy_skin_facets);
|
||||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||||
cereal::save_by_value(ar, texture_displacement_facet(i));
|
cereal::save_by_value(ar, texture_displacement_facet(i));
|
||||||
ar(texture_displacement_layers);
|
ar(texture_displacement_layers, texture_displacement_options);
|
||||||
cereal::save_by_value(ar, config);
|
cereal::save_by_value(ar, config);
|
||||||
cereal::save(ar, text_configuration);
|
cereal::save(ar, text_configuration);
|
||||||
cereal::save(ar, emboss_shape);
|
cereal::save(ar, emboss_shape);
|
||||||
|
|||||||
@@ -1117,7 +1117,8 @@ std::vector<float> patch_boundary_distance(const indexed_triangle_set &patch, co
|
|||||||
|
|
||||||
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
|
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
|
||||||
const std::vector<TextureDisplacementLayer> &layers,
|
const std::vector<TextureDisplacementLayer> &layers,
|
||||||
const TextureDisplacementFacetsData &facets_data)
|
const TextureDisplacementFacetsData &facets_data,
|
||||||
|
const TextureDisplacementOptions &options)
|
||||||
{
|
{
|
||||||
indexed_triangle_set mesh = base_mesh;
|
indexed_triangle_set mesh = base_mesh;
|
||||||
// TriangleSelector's vertex array starts with the mesh's own vertices (any extra ones, created
|
// TriangleSelector's vertex array starts with the mesh's own vertices (any extra ones, created
|
||||||
@@ -1144,10 +1145,51 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
|||||||
// rather than against whatever the previous layer left behind. That is what lets all the layers
|
// rather than against whatever the previous layer left behind. That is what lets all the layers
|
||||||
// be evaluated independently and merged per vertex, instead of having to re-mesh and remap the
|
// be evaluated independently and merged per vertex, instead of having to re-mesh and remap the
|
||||||
// paint masks between them (see the header for why that earlier design was dropped).
|
// paint masks between them (see the header for why that earlier design was dropped).
|
||||||
const std::vector<Vec3f> vertex_normals = texture_displacement_vertex_normals(mesh);
|
std::vector<Vec3f> vertex_normals = texture_displacement_vertex_normals(mesh);
|
||||||
|
|
||||||
|
// ... with one correction, applied where the painted area does not cover every triangle around a
|
||||||
|
// vertex: there the direction to move in is the normal of the *painted* surface, not of the whole
|
||||||
|
// mesh. On the rim of a fully painted top face the whole-mesh normal is the 45 degrees bisector
|
||||||
|
// between the face and the side wall it meets, so displacing along it flares the rim outwards
|
||||||
|
// instead of raising it. Taken over the union of every layer's paint (triangles_to_split is
|
||||||
|
// exactly the set of original triangles a layer's brush touched - serialize() records an entry for
|
||||||
|
// each one that is split or carries a non-default state), so a vertex still has one single
|
||||||
|
// direction however many layers cover it. Interior vertices are unaffected: all their triangles
|
||||||
|
// are painted, so the two normals coincide.
|
||||||
|
{
|
||||||
|
std::vector<uint8_t> painted_face(mesh.indices.size(), 0);
|
||||||
|
bool any_paint = false;
|
||||||
|
for (const TextureDisplacementLayer *layer : ordered_layers)
|
||||||
|
for (const TriangleSelector::TriangleBitStreamMapping &m : facets_data[size_t(layer->slot)].triangles_to_split)
|
||||||
|
if (size_t(m.triangle_idx) < mesh.indices.size()) {
|
||||||
|
painted_face[size_t(m.triangle_idx)] = 1;
|
||||||
|
any_paint = true;
|
||||||
|
}
|
||||||
|
if (any_paint) {
|
||||||
|
std::vector<Vec3f> painted_normals(mesh.vertices.size(), Vec3f::Zero());
|
||||||
|
for (size_t i = 0; i < mesh.indices.size(); ++i) {
|
||||||
|
if (!painted_face[i])
|
||||||
|
continue;
|
||||||
|
const stl_triangle_vertex_indices &t = mesh.indices[i];
|
||||||
|
const Vec3f fn = (mesh.vertices[t[1]] - mesh.vertices[t[0]]).cross(mesh.vertices[t[2]] - mesh.vertices[t[0]]);
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
painted_normals[size_t(t[k])] += fn; // area-weighted, same convention as the full normals
|
||||||
|
}
|
||||||
|
for (size_t v = 0; v < vertex_normals.size(); ++v)
|
||||||
|
if (const float l = painted_normals[v].norm(); l > 1e-8f)
|
||||||
|
vertex_normals[v] = painted_normals[v] / l;
|
||||||
|
// else: no painted triangle touches this vertex, so it will not be displaced anyway -
|
||||||
|
// leave the whole-mesh normal in place rather than zeroing it.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<float> displacement(mesh.vertices.size(), 0.f);
|
std::vector<float> displacement(mesh.vertices.size(), 0.f);
|
||||||
std::vector<bool> displaced(mesh.vertices.size(), false);
|
std::vector<bool> displaced(mesh.vertices.size(), false);
|
||||||
|
// Union, over every layer, of that layer's patch border - the vertices the post-process smoothing
|
||||||
|
// holds when TextureDisplacementOptions::smooth_skip_border is set. A vertex on any patch's edge
|
||||||
|
// counts, which is the conservative choice: hold it rather than let one layer's smoothing melt the
|
||||||
|
// rim another layer put there.
|
||||||
|
std::vector<bool> on_patch_border(mesh.vertices.size(), false);
|
||||||
bool any_displacement = false;
|
bool any_displacement = false;
|
||||||
|
|
||||||
const TriangleMesh selector_mesh(mesh);
|
const TriangleMesh selector_mesh(mesh);
|
||||||
@@ -1171,13 +1213,19 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
|||||||
// compactify above, it is our own.
|
// compactify above, it is our own.
|
||||||
const indexed_triangle_set rest = selector.get_facets_strict(EnforcerBlockerType::NONE);
|
const indexed_triangle_set rest = selector.get_facets_strict(EnforcerBlockerType::NONE);
|
||||||
|
|
||||||
// A vertex used by even one *unpainted* triangle sits on this layer's boundary: it belongs
|
// A vertex used by even one *unpainted* triangle sits on this layer's boundary. It is still
|
||||||
// to both the painted patch and the untouched surface, so displacing it would tear the two
|
// needed either way - the edge-smoothing falloff measures distance from it - but whether it is
|
||||||
// apart. Pinning it is what keeps the result seamless with no remeshing at the seam.
|
// held flat is now the user's call (TextureDisplacementOptions::displace_border), because
|
||||||
|
// nothing can tear: the bake is topology-preserving, so a border vertex is one vertex shared
|
||||||
|
// by both regions and moving it just tilts the unpainted triangles that use it.
|
||||||
std::vector<bool> is_boundary(patch.vertices.size(), false);
|
std::vector<bool> is_boundary(patch.vertices.size(), false);
|
||||||
for (const stl_triangle_vertex_indices &tri : rest.indices)
|
for (const stl_triangle_vertex_indices &tri : rest.indices)
|
||||||
for (int i = 0; i < 3; ++i)
|
for (int i = 0; i < 3; ++i) {
|
||||||
is_boundary[tri[i]] = true;
|
is_boundary[tri[i]] = true;
|
||||||
|
if (tri[i] < int(mesh.vertices.size()))
|
||||||
|
on_patch_border[size_t(tri[i])] = true;
|
||||||
|
}
|
||||||
|
const bool pin_boundary = !options.displace_border;
|
||||||
|
|
||||||
// Only the Cylindrical/Spherical methods need these; Triplanar blends each vertex's own
|
// Only the Cylindrical/Spherical methods need these; Triplanar blends each vertex's own
|
||||||
// normal and LSCM solves the patch globally.
|
// normal and LSCM solves the patch globally.
|
||||||
@@ -1246,7 +1294,7 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
|||||||
const int vi = tri[i];
|
const int vi = tri[i];
|
||||||
// Split vertices the brush introduced live past the end of our own vertex array;
|
// Split vertices the brush introduced live past the end of our own vertex array;
|
||||||
// they carry no displacement of their own and are not part of the output mesh.
|
// they carry no displacement of their own and are not part of the output mesh.
|
||||||
if (vi >= int(mesh.vertices.size()) || is_boundary[vi] || visited[vi])
|
if (vi >= int(mesh.vertices.size()) || (pin_boundary && is_boundary[vi]) || visited[vi])
|
||||||
continue;
|
continue;
|
||||||
visited[vi] = true;
|
visited[vi] = true;
|
||||||
|
|
||||||
@@ -1277,6 +1325,19 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
|||||||
if (displaced[vi])
|
if (displaced[vi])
|
||||||
mesh.vertices[vi] += vertex_normals[vi] * displacement[vi];
|
mesh.vertices[vi] += vertex_normals[vi] * displacement[vi];
|
||||||
|
|
||||||
|
// Post-process relaxation of what the height maps left behind, restricted to the vertices that
|
||||||
|
// actually moved - the untouched part of the model keeps its exact geometry, and the ring of
|
||||||
|
// vertices just outside the displaced set stays put and anchors the smoothing so the relief does
|
||||||
|
// not creep outward. `smooth_skip_border` additionally holds the patch's own outermost ring, whose
|
||||||
|
// neighbours are those pinned outsiders: relaxing it would drag the rim of the relief back down and
|
||||||
|
// leave the pattern looking half-melted right where it meets the edge.
|
||||||
|
if (options.smooth_enabled && options.smooth_strength > 0.f && options.smooth_iterations > 0) {
|
||||||
|
std::vector<uint8_t> movable(mesh.vertices.size(), 0);
|
||||||
|
for (size_t vi = 0; vi < mesh.vertices.size(); ++vi)
|
||||||
|
movable[vi] = (displaced[vi] && !(options.smooth_skip_border && on_patch_border[vi])) ? 1 : 0;
|
||||||
|
smooth_mesh_vertices(mesh, movable, options.smooth_strength, options.smooth_iterations);
|
||||||
|
}
|
||||||
|
|
||||||
return mesh;
|
return mesh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1286,7 +1347,60 @@ indexed_triangle_set build_texture_displacement(const ModelVolume &volume)
|
|||||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||||
facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
|
facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
|
||||||
|
|
||||||
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,
|
||||||
|
volume.texture_displacement_options);
|
||||||
|
}
|
||||||
|
|
||||||
|
void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t> &movable, float strength,
|
||||||
|
int iterations)
|
||||||
|
{
|
||||||
|
if (iterations <= 0 || mesh.vertices.empty() || movable.size() != mesh.vertices.size())
|
||||||
|
return;
|
||||||
|
strength = std::clamp(strength, 0.f, 1.f);
|
||||||
|
if (strength <= 0.f)
|
||||||
|
return;
|
||||||
|
if (std::none_of(movable.begin(), movable.end(), [](uint8_t m) { return m != 0; }))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// One-ring neighbours as a CSR-style pair of arrays: counted, prefix-summed, then filled. A
|
||||||
|
// triangle contributes each of its edges to both endpoints, so a shared edge is listed once per
|
||||||
|
// incident triangle - the duplicates are harmless here, they just weight an interior edge the same
|
||||||
|
// way from both sides, and dropping them would cost a sort per vertex for no visible difference.
|
||||||
|
const size_t nv = mesh.vertices.size();
|
||||||
|
std::vector<int> start(nv + 1, 0);
|
||||||
|
for (const stl_triangle_vertex_indices &t : mesh.indices)
|
||||||
|
for (int e = 0; e < 3; ++e) {
|
||||||
|
++start[size_t(t[e]) + 1];
|
||||||
|
++start[size_t(t[(e + 1) % 3]) + 1];
|
||||||
|
}
|
||||||
|
for (size_t v = 0; v < nv; ++v)
|
||||||
|
start[v + 1] += start[v];
|
||||||
|
const size_t total_refs = size_t(start[nv]);
|
||||||
|
std::vector<int> nbr(total_refs, 0);
|
||||||
|
std::vector<int> fill(start.begin(), start.begin() + nv);
|
||||||
|
for (const stl_triangle_vertex_indices &t : mesh.indices)
|
||||||
|
for (int e = 0; e < 3; ++e) {
|
||||||
|
const int a = t[e], b = t[(e + 1) % 3];
|
||||||
|
nbr[size_t(fill[size_t(a)]++)] = b;
|
||||||
|
nbr[size_t(fill[size_t(b)]++)] = a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read every pass from a snapshot of the previous one, so the result does not depend on the order
|
||||||
|
// vertices happen to be visited in (a Gauss-Seidel sweep would smooth several times as hard at the
|
||||||
|
// end of the array as at the start).
|
||||||
|
std::vector<Vec3f> prev;
|
||||||
|
for (int it = 0; it < iterations; ++it) {
|
||||||
|
prev = mesh.vertices;
|
||||||
|
for (size_t v = 0; v < nv; ++v) {
|
||||||
|
if (!movable[v] || start[v] == start[v + 1])
|
||||||
|
continue;
|
||||||
|
Vec3f sum = Vec3f::Zero();
|
||||||
|
for (int k = start[v]; k < start[v + 1]; ++k)
|
||||||
|
sum += prev[size_t(nbr[size_t(k)])];
|
||||||
|
const Vec3f avg = sum / float(start[v + 1] - start[v]);
|
||||||
|
mesh.vertices[v] = prev[v] + (avg - prev[v]) * strength;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh,
|
HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh,
|
||||||
|
|||||||
@@ -295,6 +295,47 @@ struct TextureDisplacementLayer
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Settings that apply to the whole layer stack rather than to one layer, held per ModelVolume next
|
||||||
|
// to texture_displacement_layers and consumed by build_texture_displacement().
|
||||||
|
struct TextureDisplacementOptions
|
||||||
|
{
|
||||||
|
// Whether the painted patch's *border* vertices - the ones also used by unpainted triangles -
|
||||||
|
// are displaced along with the rest, or pinned flat.
|
||||||
|
//
|
||||||
|
// Pinning them was originally justified as keeping the patch from tearing away from the
|
||||||
|
// surrounding surface. That reasoning no longer applies: since the bake became
|
||||||
|
// topology-preserving it only ever *moves* the input's own vertices, so a border vertex is one
|
||||||
|
// vertex shared by both regions and moving it simply tilts the unpainted triangles that use it -
|
||||||
|
// nothing can come apart. What pinning actually does is clamp the outermost ring of the relief to
|
||||||
|
// zero, which on a fully painted face collapses the pattern into a ring of steep ramps right at
|
||||||
|
// the edge (the "it doesn't extrude at the border" artifact). Displacing it is the default;
|
||||||
|
// pinning is kept for the case where the relief must not spill past the paint at all.
|
||||||
|
bool displace_border = true;
|
||||||
|
|
||||||
|
// Optional Laplacian relaxation of the displaced surface, run after all layers have been folded
|
||||||
|
// in - a post-process, not a texture filter (TextureDisplacementLayer::smoothing blurs the height
|
||||||
|
// map instead, before it is ever sampled). Rounds off the hard steps a bitmap height map leaves
|
||||||
|
// behind. Restricted to vertices the displacement actually moved, so the rest of the model keeps
|
||||||
|
// its exact geometry. `smooth_strength` in [0, 1] is how far each pass moves a vertex toward the
|
||||||
|
// average of its neighbours.
|
||||||
|
bool smooth_enabled = false;
|
||||||
|
float smooth_strength = 0.3f;
|
||||||
|
int smooth_iterations = 2;
|
||||||
|
|
||||||
|
// Hold the painted patch's outermost ring of vertices out of the smoothing. Those vertices sit
|
||||||
|
// next to unpainted ones that are pinned by definition, so relaxing them drags the rim of the
|
||||||
|
// relief back down toward the undisplaced surface - the pattern looks half-melted exactly where it
|
||||||
|
// meets the edge, however crisp the rest of it is. Excluding them keeps the border extruded at
|
||||||
|
// full depth and smooths only the interior. On by default; turn it off to soften the outer edge
|
||||||
|
// deliberately (which is a blunter version of the per-layer edge-smoothing falloff).
|
||||||
|
bool smooth_skip_border = true;
|
||||||
|
|
||||||
|
template<class Archive> void serialize(Archive &ar)
|
||||||
|
{
|
||||||
|
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Decoded 8-bit grayscale height sample, independent of any GUI/OpenGL texture object so it can
|
// Decoded 8-bit grayscale height sample, independent of any GUI/OpenGL texture object so it can
|
||||||
// be evaluated from a background bake Job as well as from GUI-side preview code.
|
// be evaluated from a background bake Job as well as from GUI-side preview code.
|
||||||
struct DecodedHeightTexture
|
struct DecodedHeightTexture
|
||||||
@@ -473,9 +514,14 @@ using TextureDisplacementFacetsData = std::array<TriangleSelector::TriangleSplit
|
|||||||
// welding, one pass over the mesh), and - because the output keeps the input's exact vertex
|
// welding, one pass over the mesh), and - because the output keeps the input's exact vertex
|
||||||
// indexing - lets the GUI overlay a preview on the base mesh without any index translation.
|
// indexing - lets the GUI overlay a preview on the base mesh without any index translation.
|
||||||
//
|
//
|
||||||
// A vertex used by even one *unpainted* triangle of a layer's mask is that layer's boundary: its
|
// A vertex used by even one *unpainted* triangle of a layer's mask sits on that layer's boundary.
|
||||||
// displacement is pinned to zero, so the patch never tears away from the surrounding surface. Only
|
// Whether it moves is TextureDisplacementOptions::displace_border; see that field for why displacing
|
||||||
// vertices used exclusively by painted triangles move.
|
// it is safe (and the default). Either way the *direction* every vertex moves in is the area-weighted
|
||||||
|
// normal of the triangles that are painted in at least one layer - not of the whole mesh - so a
|
||||||
|
// border vertex travels along the painted surface's own normal instead of a blend with whatever
|
||||||
|
// unpainted geometry meets it there. Without that, the rim of a fully painted face would displace
|
||||||
|
// along the 45 degrees bisector it shares with the side wall and flare outwards. Interior vertices
|
||||||
|
// have every incident triangle painted, so for them the two are the same normal.
|
||||||
//
|
//
|
||||||
// Takes plain copied data rather than a ModelVolume reference so it is safe to call from a
|
// Takes plain copied data rather than a ModelVolume reference so it is safe to call from a
|
||||||
// background thread (e.g. a bake Job's process() method) on a snapshot captured on the main
|
// background thread (e.g. a bake Job's process() method) on a snapshot captured on the main
|
||||||
@@ -486,14 +532,28 @@ using TextureDisplacementFacetsData = std::array<TriangleSelector::TriangleSplit
|
|||||||
// mesh-boolean ops) the way TriangleSelector::remap_painting() does for the other paint channels.
|
// mesh-boolean ops) the way TriangleSelector::remap_painting() does for the other paint channels.
|
||||||
// Such operations will silently drop any unbaked texture-displacement paint on the affected
|
// Such operations will silently drop any unbaked texture-displacement paint on the affected
|
||||||
// volume. This is an explicit extension point for a later phase, not an oversight.
|
// volume. This is an explicit extension point for a later phase, not an oversight.
|
||||||
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
|
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
|
||||||
const std::vector<TextureDisplacementLayer> &layers,
|
const std::vector<TextureDisplacementLayer> &layers,
|
||||||
const TextureDisplacementFacetsData &facets_data);
|
const TextureDisplacementFacetsData &facets_data,
|
||||||
|
const TextureDisplacementOptions &options = {});
|
||||||
|
|
||||||
// Convenience overload for main-thread callers: extracts the mesh/layers/paint data from `volume`
|
// Convenience overload for main-thread callers: extracts the mesh/layers/paint data/options from
|
||||||
// and forwards to the overload above.
|
// `volume` and forwards to the overload above.
|
||||||
indexed_triangle_set build_texture_displacement(const ModelVolume &volume);
|
indexed_triangle_set build_texture_displacement(const ModelVolume &volume);
|
||||||
|
|
||||||
|
// Laplacian relaxation of `mesh` in place, restricted to the vertices flagged in `movable` (sized to
|
||||||
|
// the mesh's vertex count; anything else is held exactly where it is and still acts as an anchor for
|
||||||
|
// its neighbours). Each of `iterations` passes moves a movable vertex a `strength` fraction of the
|
||||||
|
// way to the average of the vertices it shares an edge with, computed from the positions at the
|
||||||
|
// start of that pass so the result does not depend on vertex order.
|
||||||
|
//
|
||||||
|
// Topology-preserving like the bake itself, so it composes with it: this is what "smooth the relief
|
||||||
|
// after displacing it" runs, and it is also safe to run standalone on an already baked mesh.
|
||||||
|
// `strength` is clamped to [0, 1]; 0 iterations, an empty/mis-sized `movable`, or an all-false one
|
||||||
|
// leave the mesh untouched.
|
||||||
|
void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t> &movable, float strength,
|
||||||
|
int iterations);
|
||||||
|
|
||||||
// Returns a scalar height (in mm - a displacement magnitude) at a surface point, given that point's
|
// 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
|
// 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
|
// where the displaced surface has *curvature* worth spending triangles on. Called serially from the
|
||||||
|
|||||||
@@ -1192,6 +1192,7 @@ void GLGizmoTextureDisplacement::rebuild_preview()
|
|||||||
TextureDisplacementPreviewInput input;
|
TextureDisplacementPreviewInput input;
|
||||||
input.base_mesh = mv->mesh().its;
|
input.base_mesh = mv->mesh().its;
|
||||||
input.layers = mv->texture_displacement_layers;
|
input.layers = mv->texture_displacement_layers;
|
||||||
|
input.options = mv->texture_displacement_options;
|
||||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||||
input.facets_data[size_t(i)] = mv->texture_displacement_facet(i).get_data();
|
input.facets_data[size_t(i)] = mv->texture_displacement_facet(i).get_data();
|
||||||
|
|
||||||
@@ -2836,6 +2837,90 @@ void GLGizmoTextureDisplacement::subdivide_model_adaptive()
|
|||||||
m_parent.set_as_dirty();
|
m_parent.set_as_dirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GLGizmoTextureDisplacement::smooth_model()
|
||||||
|
{
|
||||||
|
ModelVolume *mv = texture_volume();
|
||||||
|
ModelObject *mo = m_c->selection_info()->model_object();
|
||||||
|
if (mv == nullptr || mo == nullptr)
|
||||||
|
return;
|
||||||
|
const TextureDisplacementOptions &opts = mv->texture_displacement_options;
|
||||||
|
if (opts.smooth_strength <= 0.f || opts.smooth_iterations <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
update_model_object(); // flush any in-progress stroke, so the painted region below is current
|
||||||
|
|
||||||
|
// Movable = the vertices of the painted triangles, so the first ring of purely unpainted vertices
|
||||||
|
// outside them stays put and anchors the result. Restricted rather than whole-model on purpose:
|
||||||
|
// this runs on geometry that has already been baked, and relaxing the whole thing would quietly
|
||||||
|
// round off every unrelated feature on the part. With "Ignore outer ring" the patch's own rim is
|
||||||
|
// held as well - see TextureDisplacementOptions::smooth_skip_border.
|
||||||
|
const indexed_triangle_set &its = mv->mesh().its;
|
||||||
|
std::vector<uint8_t> painted_face(its.indices.size(), 0);
|
||||||
|
bool any = false;
|
||||||
|
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
|
||||||
|
for (const TriangleSelector::TriangleBitStreamMapping &m : mv->texture_displacement_facet(slot).get_data().triangles_to_split)
|
||||||
|
if (size_t(m.triangle_idx) < its.indices.size()) {
|
||||||
|
painted_face[size_t(m.triangle_idx)] = 1;
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
if (!any) {
|
||||||
|
show_error(nullptr, _u8L("Paint the area you want to smooth first - smoothing only touches the "
|
||||||
|
"painted part of the model."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::vector<uint8_t> movable(its.vertices.size(), 0);
|
||||||
|
for (size_t i = 0; i < its.indices.size(); ++i)
|
||||||
|
if (painted_face[i])
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
movable[size_t(its.indices[i][k])] = 1;
|
||||||
|
if (opts.smooth_skip_border)
|
||||||
|
for (size_t i = 0; i < its.indices.size(); ++i)
|
||||||
|
if (!painted_face[i])
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
movable[size_t(its.indices[i][k])] = 0; // also used by unpainted geometry -> the rim
|
||||||
|
if (std::none_of(movable.begin(), movable.end(), [](uint8_t m) { return m != 0; })) {
|
||||||
|
// Every painted vertex is on the patch's rim, so "Ignore outer ring" leaves nothing to move.
|
||||||
|
show_error(nullptr, _u8L("Nothing to smooth - the painted area is only one triangle deep, so with "
|
||||||
|
"\"Ignore outer ring\" on there are no interior vertices to relax."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
indexed_triangle_set smoothed = its;
|
||||||
|
{
|
||||||
|
wxBusyCursor wait;
|
||||||
|
smooth_mesh_vertices(smoothed, movable, opts.smooth_strength, opts.smooth_iterations);
|
||||||
|
}
|
||||||
|
|
||||||
|
Plater *plater = wxGetApp().plater();
|
||||||
|
Plater::TakeSnapshot snapshot(plater, _u8L("Smooth texture displacement"), UndoRedo::SnapshotType::GizmoAction);
|
||||||
|
|
||||||
|
// No save/restore-painting dance here, unlike subdivide and remesh: smoothing only moves vertices,
|
||||||
|
// it does not touch the triangle list, so every paint channel still refers to exactly the triangles
|
||||||
|
// it did before. set_mesh() clears the extra facets, so this saves and puts back the
|
||||||
|
// texture-displacement masks verbatim - no remap needed, and none of them is lost.
|
||||||
|
std::optional<TriangleSelector::SavedPainting> saved_painting = mv->save_painting();
|
||||||
|
std::array<TriangleSelector::TriangleSplittingData, TEXTURE_DISPLACEMENT_MAX_LAYERS> saved_texture;
|
||||||
|
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||||
|
saved_texture[size_t(i)] = mv->texture_displacement_facet(i).get_data();
|
||||||
|
|
||||||
|
mv->set_mesh(TriangleMesh(std::move(smoothed)));
|
||||||
|
mv->set_new_unique_id();
|
||||||
|
mv->calculate_convex_hull();
|
||||||
|
mv->restore_painting(saved_painting);
|
||||||
|
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||||
|
mv->texture_displacement_facet(i).set_data(std::move(saved_texture[size_t(i)]));
|
||||||
|
|
||||||
|
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
||||||
|
const ModelObjectPtrs &objs = plater->model().objects;
|
||||||
|
auto it = std::find(objs.begin(), objs.end(), mo);
|
||||||
|
if (it != objs.end())
|
||||||
|
obj_list->update_info_items(size_t(it - objs.begin()));
|
||||||
|
}
|
||||||
|
plater->changed_object(*mo);
|
||||||
|
update_from_model_object(false);
|
||||||
|
m_parent.set_as_dirty();
|
||||||
|
}
|
||||||
|
|
||||||
void GLGizmoTextureDisplacement::remesh_model()
|
void GLGizmoTextureDisplacement::remesh_model()
|
||||||
{
|
{
|
||||||
ModelVolume *mv = texture_volume();
|
ModelVolume *mv = texture_volume();
|
||||||
@@ -3769,6 +3854,63 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
}
|
}
|
||||||
// (The "Add layer" button now lives next to the "Texture layers" heading, as an icon.)
|
// (The "Add layer" button now lives next to the "Texture layers" heading, as an icon.)
|
||||||
|
|
||||||
|
// Settings for the whole stack rather than one layer, so they sit outside the layer list.
|
||||||
|
if (mv != nullptr) {
|
||||||
|
TextureDisplacementOptions &opts = mv->texture_displacement_options;
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
m_preview_params_dirty |= ImGui::Checkbox(_u8L("Displace up to the border").c_str(), &opts.displace_border);
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
m_imgui->tooltip(_u8L("Move the outermost ring of painted vertices as well, so the relief runs all the "
|
||||||
|
"way to the edge of the painted area. Turn this off to hold that ring flat, which "
|
||||||
|
"keeps the displacement strictly inside the paint but flattens the pattern right "
|
||||||
|
"at the border."),
|
||||||
|
m_imgui->scaled(20.f));
|
||||||
|
|
||||||
|
m_preview_params_dirty |= ImGui::Checkbox(_u8L("Smooth result").c_str(), &opts.smooth_enabled);
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
m_imgui->tooltip(_u8L("Relax the displaced surface after the textures have been applied, to round off "
|
||||||
|
"the hard steps a bitmap height map leaves behind. Only the vertices the "
|
||||||
|
"displacement moved are touched. This is geometry smoothing - the per-layer "
|
||||||
|
"\"Blur\" slider instead softens the height map before it is sampled."),
|
||||||
|
m_imgui->scaled(20.f));
|
||||||
|
if (opts.smooth_enabled) {
|
||||||
|
ImGui::PushItemWidth(m_imgui->scaled(8.4f));
|
||||||
|
float percent = opts.smooth_strength * 100.f;
|
||||||
|
if (m_imgui->slider_float(std::string(_u8L("Smoothing (%)")) + "##dispsmooth", &percent, 1.f, 100.f, "%.0f", 1.f)) {
|
||||||
|
opts.smooth_strength = std::clamp(percent / 100.f, 0.01f, 1.f);
|
||||||
|
m_preview_params_dirty = true;
|
||||||
|
}
|
||||||
|
if (ImGui::SliderInt((_u8L("Passes") + "##dispsmoothit").c_str(), &opts.smooth_iterations, 1, 10)) {
|
||||||
|
opts.smooth_iterations = std::clamp(opts.smooth_iterations, 1, 10);
|
||||||
|
m_preview_params_dirty = true;
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
m_imgui->tooltip(_u8L("How many relaxation passes to run. More passes reach further across the "
|
||||||
|
"surface; strength controls how much each one moves a vertex."),
|
||||||
|
m_imgui->scaled(20.f));
|
||||||
|
ImGui::PopItemWidth();
|
||||||
|
|
||||||
|
m_preview_params_dirty |= ImGui::Checkbox(_u8L("Ignore outer ring").c_str(), &opts.smooth_skip_border);
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
m_imgui->tooltip(_u8L("Leave the outermost ring of painted vertices out of the smoothing. Their "
|
||||||
|
"neighbours outside the paint never move, so relaxing them drags the relief "
|
||||||
|
"back down and the pattern comes out half-melted right at the edge. Turn this "
|
||||||
|
"off only if you want that edge softened on purpose."),
|
||||||
|
m_imgui->scaled(20.f));
|
||||||
|
|
||||||
|
// The settings above ride along with Preview/Bake. This button is for geometry that has
|
||||||
|
// *already* been baked, where there is no displacement left to fold the smoothing into.
|
||||||
|
if (m_imgui->button(_u8L("Smooth baked mesh now")))
|
||||||
|
smooth_model();
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
m_imgui->tooltip(_u8L("Apply the smoothing above to the model's real geometry right now, using the "
|
||||||
|
"painted area to decide what to touch. For relief that is already baked in - "
|
||||||
|
"unbaked displacement is smoothed by Bake itself."),
|
||||||
|
m_imgui->scaled(20.f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
m_imgui->text(_u8L("Not enough vertices for fine detail?"));
|
m_imgui->text(_u8L("Not enough vertices for fine detail?"));
|
||||||
|
|
||||||
|
|||||||
@@ -335,6 +335,13 @@ private:
|
|||||||
bool collect_paint_region(std::vector<uint8_t> ®ion,
|
bool collect_paint_region(std::vector<uint8_t> ®ion,
|
||||||
std::array<std::vector<uint8_t>, TEXTURE_DISPLACEMENT_MAX_LAYERS> *painted_tri) const;
|
std::array<std::vector<uint8_t>, TEXTURE_DISPLACEMENT_MAX_LAYERS> *painted_tri) const;
|
||||||
|
|
||||||
|
// Runs the volume's TextureDisplacementOptions smoothing over the *already committed* geometry,
|
||||||
|
// restricted to the painted area. The same settings are folded into Preview/Bake automatically;
|
||||||
|
// this is the escape hatch for relief that has already been baked in, where there is no
|
||||||
|
// displacement pass left to attach them to. Topology-preserving, so unlike subdivide and remesh it
|
||||||
|
// keeps every paint channel - including texture displacement - exactly as it was.
|
||||||
|
void smooth_model();
|
||||||
|
|
||||||
// Isotropic remeshing (CGAL) to even out wildly varying triangle sizes so displacement has a
|
// Isotropic remeshing (CGAL) to even out wildly varying triangle sizes so displacement has a
|
||||||
// consistent density to work with. Target edge length in mm; 0 means "not yet initialised", filled
|
// consistent density to work with. Target edge length in mm; 0 means "not yet initialised", filled
|
||||||
// with the mesh's mean edge length the first time the control is shown. Like subdivide, it replaces
|
// with the mesh's mean edge length the first time the control is shown. Like subdivide, it replaces
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
|
|||||||
|
|
||||||
// Only ever touches m_input (captured by value before this job was queued) and local state -
|
// Only ever touches m_input (captured by value before this job was queued) and local state -
|
||||||
// never the live Model - so this is safe to run concurrently with the UI thread.
|
// never the live Model - so this is safe to run concurrently with the UI thread.
|
||||||
m_result = TriangleMesh(build_texture_displacement(m_input.base_mesh, m_input.layers, m_input.facets_data));
|
m_result = TriangleMesh(build_texture_displacement(m_input.base_mesh, m_input.layers, m_input.facets_data,
|
||||||
|
m_input.options));
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &eptr)
|
void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &eptr)
|
||||||
@@ -78,6 +79,7 @@ void queue_texture_displacement_bake(const ModelVolume &volume, std::function<vo
|
|||||||
input.volume_id = volume.id();
|
input.volume_id = volume.id();
|
||||||
input.base_mesh = volume.mesh().its;
|
input.base_mesh = volume.mesh().its;
|
||||||
input.layers = volume.texture_displacement_layers;
|
input.layers = volume.texture_displacement_layers;
|
||||||
|
input.options = volume.texture_displacement_options;
|
||||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||||
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
|
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ struct TextureDisplacementBakeInput
|
|||||||
indexed_triangle_set base_mesh;
|
indexed_triangle_set base_mesh;
|
||||||
std::vector<TextureDisplacementLayer> layers;
|
std::vector<TextureDisplacementLayer> layers;
|
||||||
TextureDisplacementFacetsData facets_data;
|
TextureDisplacementFacetsData facets_data;
|
||||||
|
TextureDisplacementOptions options;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bakes a volume's painted texture-displacement layers into real mesh geometry in the background,
|
// Bakes a volume's painted texture-displacement layers into real mesh geometry in the background,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ void TextureDisplacementPreviewJob::process(Ctl &ctl)
|
|||||||
|
|
||||||
// Only ever touches m_input (captured by value before this job was queued) and local state -
|
// Only ever touches m_input (captured by value before this job was queued) and local state -
|
||||||
// never the live Model - so this is safe to run concurrently with the UI thread.
|
// never the live Model - so this is safe to run concurrently with the UI thread.
|
||||||
m_result = build_texture_displacement(m_input.base_mesh, m_input.layers, m_input.facets_data);
|
m_result = build_texture_displacement(m_input.base_mesh, m_input.layers, m_input.facets_data, m_input.options);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr)
|
void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ struct TextureDisplacementPreviewInput
|
|||||||
indexed_triangle_set base_mesh;
|
indexed_triangle_set base_mesh;
|
||||||
std::vector<TextureDisplacementLayer> layers;
|
std::vector<TextureDisplacementLayer> layers;
|
||||||
TextureDisplacementFacetsData facets_data;
|
TextureDisplacementFacetsData facets_data;
|
||||||
|
TextureDisplacementOptions options;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Computes the true (unbaked) displaced-mesh preview in the background. With several painted
|
// Computes the true (unbaked) displaced-mesh preview in the background. With several painted
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
#define NOMINMAX
|
#define NOMINMAX
|
||||||
#include <catch2/catch_all.hpp>
|
#include <catch2/catch_all.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <limits>
|
||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
|
|
||||||
#include "libslic3r/TextureDisplacement.hpp"
|
#include "libslic3r/TextureDisplacement.hpp"
|
||||||
@@ -35,6 +37,30 @@ static std::shared_ptr<std::vector<unsigned char>> make_flat_gray_png(uint8_t va
|
|||||||
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
|
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A hard-edged black/white checkerboard, the worst case for a height map: every texel boundary is a
|
||||||
|
// step, which is precisely the relief the post-process smoothing exists to round off.
|
||||||
|
static std::shared_ptr<std::vector<unsigned char>> make_checkerboard_png(size_t w = 16, size_t h = 16)
|
||||||
|
{
|
||||||
|
std::vector<uint8_t> pixels(w * h);
|
||||||
|
for (size_t y = 0; y < h; ++y)
|
||||||
|
for (size_t x = 0; x < w; ++x)
|
||||||
|
pixels[y * w + x] = ((x / 2 + y / 2) % 2) ? 255 : 0;
|
||||||
|
const boost::filesystem::path tmp_path = boost::filesystem::temp_directory_path()
|
||||||
|
/ boost::filesystem::unique_path("texdisp_test_%%%%%%%%.png");
|
||||||
|
REQUIRE(Slic3r::png::write_gray_to_file(tmp_path.string(), w, h, pixels));
|
||||||
|
|
||||||
|
std::vector<unsigned char> bytes;
|
||||||
|
{
|
||||||
|
std::ifstream ifs(tmp_path.string(), std::ios::binary);
|
||||||
|
bytes.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
boost::system::error_code ec;
|
||||||
|
boost::filesystem::remove(tmp_path, ec);
|
||||||
|
|
||||||
|
REQUIRE_FALSE(bytes.empty());
|
||||||
|
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("TextureDisplacement: decode_height_texture round-trips an 8-bit grayscale PNG", "[TextureDisplacement]")
|
TEST_CASE("TextureDisplacement: decode_height_texture round-trips an 8-bit grayscale PNG", "[TextureDisplacement]")
|
||||||
{
|
{
|
||||||
TextureDisplacementLayer layer;
|
TextureDisplacementLayer layer;
|
||||||
@@ -188,15 +214,15 @@ TEST_CASE("TextureDisplacement: the lowest layer ignores its blend mode", "[Text
|
|||||||
CHECK_THAT((result.vertices[i] - cube.vertices[i]).norm(), WithinAbs(2.0f, 1e-3f));
|
CHECK_THAT((result.vertices[i] - cube.vertices[i]).norm(), WithinAbs(2.0f, 1e-3f));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("TextureDisplacement: boundary vertices shared with unpainted triangles are pinned", "[TextureDisplacement]")
|
TEST_CASE("TextureDisplacement: the patch border is displaced by default and pinned on request", "[TextureDisplacement]")
|
||||||
{
|
{
|
||||||
// A small triangle fan around a central vertex O, with 4 outer points A/B/C/D forming 4
|
// A small triangle fan around a central vertex O, with 4 outer points A/B/C/D forming 4
|
||||||
// triangles T0..T3 in the XY plane. Only T0, T1, T2 are painted, T3 is left unpainted:
|
// triangles T0..T3 in the XY plane. Only T0, T1, T2 are painted, T3 is left unpainted, so:
|
||||||
// O: touches all 4 triangles (incl. unpainted T3) -> boundary, must NOT move
|
// O: touches all 4 triangles (incl. unpainted T3) -> patch border
|
||||||
// A: touches T0 (painted) and T3 (unpainted) -> boundary, must NOT move
|
// A: touches T0 (painted) and T3 (unpainted) -> patch border
|
||||||
// D: touches T2 (painted) and T3 (unpainted) -> boundary, must NOT move
|
// D: touches T2 (painted) and T3 (unpainted) -> patch border
|
||||||
// B: touches only T0 and T1 (both painted) -> interior, SHOULD move
|
// B: touches only T0 and T1 (both painted) -> interior
|
||||||
// C: touches only T1 and T2 (both painted) -> interior, SHOULD move
|
// C: touches only T1 and T2 (both painted) -> interior
|
||||||
indexed_triangle_set fan;
|
indexed_triangle_set fan;
|
||||||
fan.vertices = { {0.f, 0.f, 0.f}, {1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}, {-1.f, 0.f, 0.f}, {0.f, -1.f, 0.f} };
|
fan.vertices = { {0.f, 0.f, 0.f}, {1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}, {-1.f, 0.f, 0.f}, {0.f, -1.f, 0.f} };
|
||||||
fan.indices = { {0, 1, 2}, {0, 2, 3}, {0, 3, 4}, {0, 4, 1} };
|
fan.indices = { {0, 1, 2}, {0, 2, 3}, {0, 3, 4}, {0, 4, 1} };
|
||||||
@@ -217,22 +243,189 @@ TEST_CASE("TextureDisplacement: boundary vertices shared with unpainted triangle
|
|||||||
layer.tiling_scale = 5.0f;
|
layer.tiling_scale = 5.0f;
|
||||||
layer.image_data = make_flat_gray_png(255);
|
layer.image_data = make_flat_gray_png(255);
|
||||||
|
|
||||||
const indexed_triangle_set result = build_texture_displacement(fan, {layer}, facets);
|
// The bake is topology-preserving, so it only ever moves fan's own vertices, in order.
|
||||||
|
auto moved = [&](const indexed_triangle_set &result, size_t i) {
|
||||||
// Find each named vertex's post-bake position by matching the original (pinned vertices keep
|
return (result.vertices[i] - fan.vertices[i]).norm() > 1e-6f;
|
||||||
// their exact original position; moved ones won't match any original position anymore).
|
|
||||||
auto still_at_original_position = [&](const Vec3f &original) {
|
|
||||||
for (const Vec3f &v : result.vertices)
|
|
||||||
if ((v - original).norm() < 1e-6f)
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
CHECK(still_at_original_position(fan.vertices[0])); // O: boundary
|
SECTION("by default the whole painted patch moves, border included")
|
||||||
CHECK(still_at_original_position(fan.vertices[1])); // A: boundary
|
{
|
||||||
CHECK(still_at_original_position(fan.vertices[4])); // D: boundary
|
const indexed_triangle_set result = build_texture_displacement(fan, {layer}, facets);
|
||||||
CHECK_FALSE(still_at_original_position(fan.vertices[2])); // B: interior, must have moved
|
REQUIRE(result.vertices.size() == fan.vertices.size());
|
||||||
CHECK_FALSE(still_at_original_position(fan.vertices[3])); // C: interior, must have moved
|
for (size_t i = 0; i < fan.vertices.size(); ++i)
|
||||||
|
CHECK(moved(result, i));
|
||||||
|
// ... straight along the painted surface's own normal (+Z here), by the full depth. Nothing
|
||||||
|
// has torn: the unpainted triangle T3 simply shares the moved vertices.
|
||||||
|
for (size_t i = 0; i < fan.vertices.size(); ++i)
|
||||||
|
CHECK_THAT(result.vertices[i].z() - fan.vertices[i].z(), WithinAbs(1.0f, 1e-3f));
|
||||||
|
CHECK(result.indices == fan.indices);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("pinning the border holds exactly the vertices an unpainted triangle also uses")
|
||||||
|
{
|
||||||
|
TextureDisplacementOptions options;
|
||||||
|
options.displace_border = false;
|
||||||
|
const indexed_triangle_set result = build_texture_displacement(fan, {layer}, facets, options);
|
||||||
|
CHECK_FALSE(moved(result, 0)); // O: border
|
||||||
|
CHECK_FALSE(moved(result, 1)); // A: border
|
||||||
|
CHECK_FALSE(moved(result, 4)); // D: border
|
||||||
|
CHECK(moved(result, 2)); // B: interior
|
||||||
|
CHECK(moved(result, 3)); // C: interior
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("TextureDisplacement: post-process smoothing relaxes only what moved", "[TextureDisplacement]")
|
||||||
|
{
|
||||||
|
// A checkerboard height map on a fine grid gives a relief full of hard steps - exactly what the
|
||||||
|
// smoothing pass is for. Measure it as the spread of the displaced heights: relaxing the surface
|
||||||
|
// must pull the extremes in without touching the mesh's topology.
|
||||||
|
indexed_triangle_set plane;
|
||||||
|
plane.vertices = { { 0.f, 0.f, 0.f }, { 20.f, 0.f, 0.f }, { 20.f, 20.f, 0.f }, { 0.f, 20.f, 0.f } };
|
||||||
|
plane.indices = { { 0, 1, 2 }, { 0, 2, 3 } };
|
||||||
|
const indexed_triangle_set grid = subdivide_mesh_uniform(plane, 1.f, 6);
|
||||||
|
REQUIRE(grid.indices.size() > 256);
|
||||||
|
|
||||||
|
// Paint only the central square, so the patch has a real border to test against.
|
||||||
|
std::vector<uint8_t> painted(grid.indices.size(), 0);
|
||||||
|
const TriangleMesh grid_mesh(grid);
|
||||||
|
TriangleSelector selector(grid_mesh);
|
||||||
|
for (int i = 0; i < int(grid.indices.size()); ++i) {
|
||||||
|
const auto &t = grid.indices[i];
|
||||||
|
float cx = 0.f, cy = 0.f;
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
cx += grid.vertices[t[k]].x() / 3.f;
|
||||||
|
cy += grid.vertices[t[k]].y() / 3.f;
|
||||||
|
}
|
||||||
|
if (cx > 5.f && cx < 15.f && cy > 5.f && cy < 15.f) {
|
||||||
|
selector.set_facet(i, EnforcerBlockerType::ENFORCER);
|
||||||
|
painted[size_t(i)] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
REQUIRE(std::count(painted.begin(), painted.end(), uint8_t(1)) > 32);
|
||||||
|
TextureDisplacementFacetsData facets{};
|
||||||
|
facets[0] = selector.serialize();
|
||||||
|
|
||||||
|
TextureDisplacementLayer layer;
|
||||||
|
layer.slot = 0;
|
||||||
|
layer.depth_mm = 2.0f;
|
||||||
|
layer.tiling_scale = 6.0f;
|
||||||
|
layer.image_data = make_checkerboard_png();
|
||||||
|
|
||||||
|
auto z_spread = [](const indexed_triangle_set &its) {
|
||||||
|
float lo = its.vertices.front().z(), hi = lo;
|
||||||
|
for (const Vec3f &v : its.vertices) {
|
||||||
|
lo = std::min(lo, v.z());
|
||||||
|
hi = std::max(hi, v.z());
|
||||||
|
}
|
||||||
|
return hi - lo;
|
||||||
|
};
|
||||||
|
|
||||||
|
const indexed_triangle_set raw = build_texture_displacement(grid, { layer }, facets);
|
||||||
|
REQUIRE(z_spread(raw) > 1.f); // the checkerboard really did produce relief to smooth
|
||||||
|
|
||||||
|
TextureDisplacementOptions options;
|
||||||
|
options.smooth_enabled = true;
|
||||||
|
options.smooth_strength = 0.5f;
|
||||||
|
options.smooth_iterations = 4;
|
||||||
|
|
||||||
|
SECTION("it rounds off the steps without touching the topology")
|
||||||
|
{
|
||||||
|
const indexed_triangle_set smoothed = build_texture_displacement(grid, { layer }, facets, options);
|
||||||
|
CHECK(z_spread(smoothed) < z_spread(raw));
|
||||||
|
CHECK(smoothed.indices == raw.indices);
|
||||||
|
CHECK(smoothed.vertices.size() == raw.vertices.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("\"ignore outer ring\" leaves the patch rim at the depth the texture asked for")
|
||||||
|
{
|
||||||
|
// The rim: vertices shared by a painted and an unpainted triangle. Their neighbours outside
|
||||||
|
// the paint never move, so relaxing them drags the relief back down toward the flat surface -
|
||||||
|
// which is what makes the pattern look half-melted right at the edge.
|
||||||
|
std::vector<uint8_t> rim(grid.vertices.size(), 0), inside(grid.vertices.size(), 0);
|
||||||
|
for (size_t i = 0; i < grid.indices.size(); ++i)
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
(painted[i] ? inside : rim)[size_t(grid.indices[i][k])] = 1;
|
||||||
|
size_t rim_count = 0;
|
||||||
|
for (size_t v = 0; v < rim.size(); ++v) {
|
||||||
|
rim[v] = (rim[v] && inside[v]) ? 1 : 0;
|
||||||
|
rim_count += rim[v];
|
||||||
|
}
|
||||||
|
REQUIRE(rim_count > 8);
|
||||||
|
|
||||||
|
// The tallest point of the rim. Laplacian relaxation is a convex average, so no vertex can ever
|
||||||
|
// exceed the mesh's current maximum - and a rim vertex sitting at that maximum has at least one
|
||||||
|
// neighbour outside the paint at zero, so it must come down. That makes this a strict, not a
|
||||||
|
// statistical, comparison.
|
||||||
|
auto rim_peak = [&](const indexed_triangle_set &its) {
|
||||||
|
float peak = 0.f;
|
||||||
|
for (size_t v = 0; v < rim.size(); ++v)
|
||||||
|
if (rim[v])
|
||||||
|
peak = std::max(peak, its.vertices[v].z());
|
||||||
|
return peak;
|
||||||
|
};
|
||||||
|
|
||||||
|
options.smooth_skip_border = true;
|
||||||
|
const indexed_triangle_set kept = build_texture_displacement(grid, { layer }, facets, options);
|
||||||
|
options.smooth_skip_border = false;
|
||||||
|
const indexed_triangle_set relaxed = build_texture_displacement(grid, { layer }, facets, options);
|
||||||
|
|
||||||
|
// Held out of the smoothing, the rim is bit-for-bit the un-smoothed displacement.
|
||||||
|
for (size_t v = 0; v < rim.size(); ++v)
|
||||||
|
if (rim[v])
|
||||||
|
CHECK_THAT(kept.vertices[v].z(), WithinAbs(raw.vertices[v].z(), 1e-6f));
|
||||||
|
CHECK(rim_peak(relaxed) < rim_peak(kept)); // ... and melted back down without it
|
||||||
|
// ... while the interior really was smoothed - this is not just "smoothing turned off".
|
||||||
|
CHECK_FALSE(kept.vertices == raw.vertices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("TextureDisplacement: smooth_mesh_vertices holds everything outside its mask", "[TextureDisplacement]")
|
||||||
|
{
|
||||||
|
// A single spike on a flat sheet: relaxing it must pull the spike down and leave every vertex
|
||||||
|
// that is not flagged movable at exactly the coordinates it started at.
|
||||||
|
indexed_triangle_set plane;
|
||||||
|
plane.vertices = { { 0.f, 0.f, 0.f }, { 8.f, 0.f, 0.f }, { 8.f, 8.f, 0.f }, { 0.f, 8.f, 0.f } };
|
||||||
|
plane.indices = { { 0, 1, 2 }, { 0, 2, 3 } };
|
||||||
|
indexed_triangle_set grid = subdivide_mesh_uniform(plane, 1.f, 4);
|
||||||
|
|
||||||
|
// Raise one interior vertex, and let only it and its immediate neighbours move.
|
||||||
|
size_t spike = 0;
|
||||||
|
float best = std::numeric_limits<float>::max();
|
||||||
|
for (size_t i = 0; i < grid.vertices.size(); ++i)
|
||||||
|
if (const float d = (grid.vertices[i] - Vec3f(4.f, 4.f, 0.f)).norm(); d < best) {
|
||||||
|
best = d;
|
||||||
|
spike = i;
|
||||||
|
}
|
||||||
|
grid.vertices[spike].z() = 5.f;
|
||||||
|
|
||||||
|
std::vector<uint8_t> movable(grid.vertices.size(), 0);
|
||||||
|
movable[spike] = 1;
|
||||||
|
for (const auto &t : grid.indices)
|
||||||
|
for (int e = 0; e < 3; ++e)
|
||||||
|
if (size_t(t[e]) == spike)
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
movable[size_t(t[k])] = 1;
|
||||||
|
|
||||||
|
const indexed_triangle_set before = grid;
|
||||||
|
smooth_mesh_vertices(grid, movable, 0.5f, 3);
|
||||||
|
|
||||||
|
CHECK(grid.vertices[spike].z() < before.vertices[spike].z()); // the spike came down
|
||||||
|
CHECK(grid.vertices[spike].z() > 0.f); // but was not flattened outright
|
||||||
|
CHECK(grid.indices == before.indices); // topology untouched
|
||||||
|
for (size_t i = 0; i < grid.vertices.size(); ++i)
|
||||||
|
if (!movable[i])
|
||||||
|
CHECK_THAT((grid.vertices[i] - before.vertices[i]).norm(), WithinAbs(0.f, 1e-9f));
|
||||||
|
|
||||||
|
// Guard rails: each of these must leave the mesh byte-identical.
|
||||||
|
for (const auto &noop : { std::make_pair(0.f, 3), std::make_pair(0.5f, 0) }) {
|
||||||
|
indexed_triangle_set copy = before;
|
||||||
|
smooth_mesh_vertices(copy, movable, noop.first, noop.second);
|
||||||
|
CHECK(copy.vertices == before.vertices);
|
||||||
|
}
|
||||||
|
indexed_triangle_set copy = before;
|
||||||
|
smooth_mesh_vertices(copy, std::vector<uint8_t>(3, 1), 0.5f, 3); // mis-sized mask
|
||||||
|
CHECK(copy.vertices == before.vertices);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every undirected edge of a closed manifold mesh is shared by exactly two triangles. A T-junction
|
// Every undirected edge of a closed manifold mesh is shared by exactly two triangles. A T-junction
|
||||||
|
|||||||
Reference in New Issue
Block a user