mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 10:51:22 +00:00
Displace the painted patch border and add post-process smoothing
This commit is contained in:
@@ -938,6 +938,11 @@ public:
|
||||
// see build_texture_displacement()); it only reflects UI insertion order.
|
||||
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.
|
||||
// Used for replacing mesh without losing painting data.
|
||||
// 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_6(other.texture_displacement_facets_6), texture_displacement_facets_7(other.texture_displacement_facets_7),
|
||||
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)
|
||||
{
|
||||
assert(this->id().valid());
|
||||
@@ -1288,7 +1294,7 @@ private:
|
||||
cereal::load_by_value(ar, f);
|
||||
mesh_changed |= tf != f.timestamp();
|
||||
}
|
||||
ar(texture_displacement_layers);
|
||||
ar(texture_displacement_layers, texture_displacement_options);
|
||||
cereal::load_by_value(ar, config);
|
||||
cereal::load(ar, text_configuration);
|
||||
cereal::load(ar, emboss_shape);
|
||||
@@ -1312,7 +1318,7 @@ private:
|
||||
cereal::save_by_value(ar, fuzzy_skin_facets);
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++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(ar, text_configuration);
|
||||
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,
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementFacetsData &facets_data)
|
||||
const TextureDisplacementFacetsData &facets_data,
|
||||
const TextureDisplacementOptions &options)
|
||||
{
|
||||
indexed_triangle_set mesh = base_mesh;
|
||||
// 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
|
||||
// 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).
|
||||
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<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;
|
||||
|
||||
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.
|
||||
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
|
||||
// to both the painted patch and the untouched surface, so displacing it would tear the two
|
||||
// apart. Pinning it is what keeps the result seamless with no remeshing at the seam.
|
||||
// A vertex used by even one *unpainted* triangle sits on this layer's boundary. It is still
|
||||
// needed either way - the edge-smoothing falloff measures distance from it - but whether it is
|
||||
// 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);
|
||||
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;
|
||||
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
|
||||
// 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];
|
||||
// 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.
|
||||
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;
|
||||
visited[vi] = true;
|
||||
|
||||
@@ -1277,6 +1325,19 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
if (displaced[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;
|
||||
}
|
||||
|
||||
@@ -1286,7 +1347,60 @@ indexed_triangle_set build_texture_displacement(const ModelVolume &volume)
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
// be evaluated from a background bake Job as well as from GUI-side preview code.
|
||||
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
|
||||
// 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
|
||||
// displacement is pinned to zero, so the patch never tears away from the surrounding surface. Only
|
||||
// vertices used exclusively by painted triangles move.
|
||||
// A vertex used by even one *unpainted* triangle of a layer's mask sits on that layer's boundary.
|
||||
// Whether it moves is TextureDisplacementOptions::displace_border; see that field for why displacing
|
||||
// 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
|
||||
// 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.
|
||||
// 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.
|
||||
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 TextureDisplacementFacetsData &facets_data);
|
||||
const TextureDisplacementFacetsData &facets_data,
|
||||
const TextureDisplacementOptions &options = {});
|
||||
|
||||
// Convenience overload for main-thread callers: extracts the mesh/layers/paint data from `volume`
|
||||
// and forwards to the overload above.
|
||||
// Convenience overload for main-thread callers: extracts the mesh/layers/paint data/options from
|
||||
// `volume` and forwards to the overload above.
|
||||
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
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user