mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
Improve adaptive subdivision at border & fix some visual bugs
This commit is contained in:
@@ -356,6 +356,25 @@ std::optional<std::vector<Vec2f>> parameterize_lscm(const indexed_triangle_set &
|
||||
if (border == halfedge_descriptor())
|
||||
return std::nullopt; // no boundary at all -- a closed patch, which isn't a disk either
|
||||
|
||||
// ...and exactly one boundary loop. One connected component is not enough on its own: a patch with
|
||||
// a hole in it (paint a ring, or erase the middle of a stroke) is a single component with two
|
||||
// loops, and LSCM will happily "parameterize" it into an overlapping, folded-over chart rather
|
||||
// than fail. Walk the border halfedges and check every one of them belongs to the longest loop.
|
||||
{
|
||||
std::size_t border_halfedges = 0;
|
||||
for (halfedge_descriptor h : halfedges(cgal_mesh))
|
||||
if (is_border(h, cgal_mesh))
|
||||
++border_halfedges;
|
||||
std::size_t loop_length = 0;
|
||||
halfedge_descriptor h = border;
|
||||
do {
|
||||
++loop_length;
|
||||
h = next(h, cgal_mesh);
|
||||
} while (h != border && loop_length <= border_halfedges);
|
||||
if (loop_length != border_halfedges)
|
||||
return std::nullopt; // more than one boundary loop -- not a topological disk
|
||||
}
|
||||
|
||||
using Point_2 = EpicKernel::Point_2;
|
||||
using UV_pmap = _EpicMesh::Property_map<vertex_descriptor, Point_2>;
|
||||
UV_pmap uv_map = cgal_mesh.add_property_map<vertex_descriptor, Point_2>("h:uv", Point_2(0, 0)).first;
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <tbb/blocked_range.h>
|
||||
#include <tbb/parallel_for.h>
|
||||
|
||||
#include "MeshBoolean.hpp"
|
||||
#include "Model.hpp"
|
||||
#include "PNGReadWrite.hpp"
|
||||
@@ -897,7 +900,7 @@ std::vector<Vec2f> compute_lscm_uvs(const indexed_triangle_set &patch, const Tex
|
||||
return per_vertex;
|
||||
}
|
||||
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer)
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect)
|
||||
{
|
||||
const float scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f;
|
||||
const Vec2f scaled = planar * scale;
|
||||
@@ -905,7 +908,20 @@ Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &la
|
||||
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
||||
const float cs = std::cos(rad);
|
||||
const float sn = std::sin(rad);
|
||||
const Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
|
||||
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
|
||||
|
||||
// Non-square textures. Without this the [0,1] square of uv covers the whole image whatever its
|
||||
// proportions, so a 2:1 image is squeezed into a square tile and every feature in it comes out
|
||||
// half as wide as it should be. `tiling_scale` is the tile's size along u; the tile is
|
||||
// `tiling_scale * height / width` mm along v, which is exactly what keeps texels square - so
|
||||
// dividing v by that extent is the same as multiplying it by width / height. A square texture has
|
||||
// aspect 1 and is untouched, which is why this changes nothing for the shipped library.
|
||||
//
|
||||
// Applied after the rotation, not before: scaling one axis of an already-rotated coordinate is a
|
||||
// shear, and doing it the other way round would make "Rotation" skew the pattern instead of
|
||||
// turning it.
|
||||
if (aspect > 0.f && aspect != 1.f)
|
||||
rotated.y() *= aspect;
|
||||
|
||||
return rotated + layer.offset;
|
||||
}
|
||||
@@ -954,8 +970,12 @@ float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisp
|
||||
if (texture.empty())
|
||||
return 0.f;
|
||||
|
||||
// width / height of the height map, so a non-square image keeps its proportions (see
|
||||
// apply_uv_transform()). Every projection except the projective "from view" one funnels through
|
||||
// here, so this one line is what makes them all aspect-correct.
|
||||
const float aspect = (texture.height > 0) ? float(texture.width) / float(texture.height) : 1.f;
|
||||
auto sample_at = [&](const Vec2f &planar) {
|
||||
return texture.sample(apply_uv_transform(planar, layer), layer.tile_enabled, layer.tile_method);
|
||||
return texture.sample(apply_uv_transform(planar, layer, aspect), layer.tile_enabled, layer.tile_method);
|
||||
};
|
||||
|
||||
// Precomputed per-patch LSCM solve wins over the layer's own method (see the header): the
|
||||
@@ -1118,8 +1138,13 @@ 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 TextureDisplacementOptions &options)
|
||||
const TextureDisplacementOptions &options,
|
||||
const DisplacementProgressFn &progress)
|
||||
{
|
||||
// Returns true to keep going. An aborted run returns {} (see the header): an empty mesh is the
|
||||
// one result no caller can mistake for a finished bake and commit onto the volume.
|
||||
const auto report = [&progress](int percent) { return !progress || progress(percent); };
|
||||
|
||||
indexed_triangle_set mesh = base_mesh;
|
||||
// TriangleSelector's vertex array starts with the mesh's own vertices (any extra ones, created
|
||||
// where a brush stroke split a triangle, are appended after them), and get_facets_strict()
|
||||
@@ -1183,8 +1208,14 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
}
|
||||
}
|
||||
|
||||
if (!report(5))
|
||||
return {};
|
||||
|
||||
std::vector<float> displacement(mesh.vertices.size(), 0.f);
|
||||
std::vector<bool> displaced(mesh.vertices.size(), false);
|
||||
// uint8_t rather than std::vector<bool>: the sampling loop below writes these from several
|
||||
// threads at once, and vector<bool>'s bit packing makes writes to *distinct* elements a data
|
||||
// race on the shared word.
|
||||
std::vector<uint8_t> displaced(mesh.vertices.size(), 0);
|
||||
// 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
|
||||
@@ -1193,7 +1224,21 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
bool any_displacement = false;
|
||||
|
||||
const TriangleMesh selector_mesh(mesh);
|
||||
// One selector for the whole stack, re-deserialized per layer. Its constructor computes
|
||||
// its_face_neighbors() and its_face_normals() over the *entire* mesh, which on a subdivided model
|
||||
// is by far the most expensive thing here - building a fresh one per layer paid that cost up to
|
||||
// eight times over. reset() (what deserialize(..., true) calls) only rebuilds the vertex/triangle
|
||||
// arrays; the neighbour and face-normal tables are immutable members and survive it.
|
||||
TriangleSelector selector(selector_mesh);
|
||||
bool selector_dirty = false;
|
||||
|
||||
const int layer_count = std::max(int(ordered_layers.size()), 1);
|
||||
int layer_index = 0;
|
||||
for (const TextureDisplacementLayer *layer : ordered_layers) {
|
||||
// Progress spans 5..65% across the layers; the apply and smoothing passes take it from there.
|
||||
if (!report(5 + (60 * layer_index++) / layer_count))
|
||||
return {};
|
||||
|
||||
const TriangleSelector::TriangleSplittingData &data = facets_data[size_t(layer->slot)];
|
||||
if (data.triangles_to_split.empty())
|
||||
continue;
|
||||
@@ -1202,8 +1247,9 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
if (height.empty())
|
||||
continue;
|
||||
|
||||
TriangleSelector selector(selector_mesh);
|
||||
selector.deserialize(data, false);
|
||||
// needs_reset only from the second layer on: the selector is already pristine on the first.
|
||||
selector.deserialize(data, selector_dirty);
|
||||
selector_dirty = true;
|
||||
|
||||
const indexed_triangle_set patch = selector.get_facets_strict(EnforcerBlockerType::ENFORCER);
|
||||
if (patch.indices.empty())
|
||||
@@ -1287,8 +1333,13 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
const float sign = layer->invert ? -1.f : 1.f;
|
||||
// A vertex may be reached by several of the patch's triangles; each must fold into the
|
||||
// running total exactly once, or a Multiply/Subtract layer would apply two or three times
|
||||
// over depending on how many painted triangles happen to share the vertex.
|
||||
std::vector<bool> visited(patch.vertices.size(), false);
|
||||
// over depending on how many painted triangles happen to share the vertex. Collecting the
|
||||
// unique list up front (cheap, one pass) is also what lets the expensive part - the texture
|
||||
// sampling, which is three bilinear fetches plus three pow()s per vertex for triplanar - run
|
||||
// in parallel below, instead of serially inside the triangle walk.
|
||||
std::vector<int> layer_vertices;
|
||||
std::vector<char> visited(patch.vertices.size(), 0);
|
||||
layer_vertices.reserve(patch.vertices.size());
|
||||
for (const stl_triangle_vertex_indices &tri : patch.indices)
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const int vi = tri[i];
|
||||
@@ -1296,28 +1347,53 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
// they carry no displacement of their own and are not part of the output mesh.
|
||||
if (vi >= int(mesh.vertices.size()) || (pin_boundary && is_boundary[vi]) || visited[vi])
|
||||
continue;
|
||||
visited[vi] = true;
|
||||
visited[vi] = 1;
|
||||
layer_vertices.push_back(vi);
|
||||
}
|
||||
if (layer_vertices.empty())
|
||||
continue;
|
||||
|
||||
const Vec2f *lscm_uv = lscm_uvs.empty() ? nullptr : &lscm_uvs[size_t(vi)];
|
||||
std::vector<float> sampled(layer_vertices.size(), 0.f);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, layer_vertices.size()),
|
||||
[&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t k = range.begin(); k < range.end(); ++k) {
|
||||
const size_t vi = size_t(layer_vertices[k]);
|
||||
const Vec2f *lscm_uv = lscm_uvs.empty() ? nullptr : &lscm_uvs[vi];
|
||||
const float h = sample_layer_height(height, *layer, mesh.vertices[vi], vertex_normals[vi],
|
||||
patch_centroid, patch_axis, lscm_uv);
|
||||
|
||||
// midlevel is the height that means "stay put", so anything below it displaces
|
||||
// *inwards* - see TextureDisplacementLayer::midlevel. At the default of 0 this is
|
||||
// exactly the old outward-only behaviour.
|
||||
const float edge_w = edge_weight.empty() ? 1.f : edge_weight[size_t(vi)];
|
||||
const float signed_height = (h - layer->midlevel) * layer->depth_mm * sign * edge_w;
|
||||
displacement[size_t(vi)] = blend_displacement(displacement[size_t(vi)], signed_height,
|
||||
displaced[size_t(vi)] ? layer->blend_mode : TextureBlendMode::Add);
|
||||
// The first layer to reach a vertex has nothing underneath it to blend with, so it
|
||||
// always starts the total off additively - a Multiply/Divide against an implicit
|
||||
// zero base would otherwise annihilate (or blow up) it, which is never what the
|
||||
// user means by putting a mask on the bottom of the stack.
|
||||
displaced[size_t(vi)] = true;
|
||||
any_displacement = true;
|
||||
sampled[k] = (h - layer->midlevel) * layer->depth_mm * sign;
|
||||
}
|
||||
});
|
||||
|
||||
for (size_t k = 0; k < layer_vertices.size(); ++k) {
|
||||
const size_t vi = size_t(layer_vertices[k]);
|
||||
// The first layer to reach a vertex has nothing underneath it to blend with, so it
|
||||
// always starts the total off additively - a Multiply/Divide against an implicit
|
||||
// zero base would otherwise annihilate (or blow up) it, which is never what the
|
||||
// user means by putting a mask on the bottom of the stack.
|
||||
const float accumulated = displacement[vi];
|
||||
const float blended = blend_displacement(accumulated, sampled[k],
|
||||
displaced[vi] ? layer->blend_mode : TextureBlendMode::Add);
|
||||
// Edge smoothing fades this layer's *effect*, not its input. Scaling the input instead is
|
||||
// only correct for Add/Subtract, whose neutral value is 0: on a Multiply layer a faded
|
||||
// input approaches 0, which annihilates everything beneath it at the rim rather than
|
||||
// leaving it alone, and on a Divide layer it approaches the 0.05 divisor floor, which
|
||||
// amplifies the relief underneath by up to 20x exactly where it was meant to fade out.
|
||||
// Interpolating the blended result back toward the accumulated total is the neutral
|
||||
// element for every mode at once, and reduces to the old formula exactly for Add.
|
||||
const float edge_w = edge_weight.empty() ? 1.f : edge_weight[vi];
|
||||
displacement[vi] = accumulated + (blended - accumulated) * edge_w;
|
||||
displaced[vi] = 1;
|
||||
}
|
||||
any_displacement = true;
|
||||
}
|
||||
|
||||
if (!report(65))
|
||||
return {};
|
||||
|
||||
if (!any_displacement)
|
||||
return mesh;
|
||||
|
||||
@@ -1332,12 +1408,22 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
// 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) {
|
||||
if (!report(70))
|
||||
return {};
|
||||
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);
|
||||
// The pass hook only *stops* the relaxation early; the report(99) below is what turns a
|
||||
// cancellation into an empty (uncommittable) result, since a cancelled run keeps reporting
|
||||
// cancelled.
|
||||
smooth_mesh_vertices(mesh, movable, options.smooth_strength, options.smooth_iterations,
|
||||
progress ? DisplacementProgressFn([&report, it = options.smooth_iterations](int pass) {
|
||||
return report(70 + (29 * (pass + 1)) / std::max(it, 1));
|
||||
}) : DisplacementProgressFn{});
|
||||
}
|
||||
|
||||
if (!report(99))
|
||||
return {};
|
||||
return mesh;
|
||||
}
|
||||
|
||||
@@ -1352,7 +1438,7 @@ indexed_triangle_set build_texture_displacement(const ModelVolume &volume)
|
||||
}
|
||||
|
||||
void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t> &movable, float strength,
|
||||
int iterations)
|
||||
int iterations, const DisplacementProgressFn &on_pass)
|
||||
{
|
||||
if (iterations <= 0 || mesh.vertices.empty() || movable.size() != mesh.vertices.size())
|
||||
return;
|
||||
@@ -1391,15 +1477,21 @@ void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t>
|
||||
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;
|
||||
}
|
||||
// Each vertex reads only from `prev` and writes only its own slot, so the sweep parallelises
|
||||
// with no synchronisation at all.
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, nv), [&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t v = range.begin(); v < range.end(); ++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;
|
||||
}
|
||||
});
|
||||
if (on_pass && !on_pass(it))
|
||||
return; // cancelled: leave the passes done so far in place, the caller decides what to do
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1552,7 +1644,8 @@ indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
|
||||
const std::vector<uint8_t> &refine_region,
|
||||
float target_edge_length_mm, int max_triangles,
|
||||
std::vector<int> *out_source, const HeightFieldSampler &sampler,
|
||||
float chord_tolerance_mm, float min_edge_length_mm)
|
||||
float chord_tolerance_mm, float min_edge_length_mm,
|
||||
float border_edge_length_mm)
|
||||
{
|
||||
// Neighbour slots that are not a triangle index.
|
||||
constexpr int NB_BOUNDARY = -1; // open edge: terminal on its own, bisected from this side alone
|
||||
@@ -1590,12 +1683,13 @@ indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
|
||||
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;
|
||||
const float border_sq = border_edge_length_mm > 0.f ? border_edge_length_mm * border_edge_length_mm : 0.f;
|
||||
|
||||
// refine_region is indexed by input-triangle index, and every triangle's src stays in that range
|
||||
// (children inherit their parent's src), so a wrong size would be an out-of-bounds read. Guard it.
|
||||
if (refine_region.size() != mesh.indices.size() || int(tris.size()) + 2 > max_triangles)
|
||||
return emit();
|
||||
if (!feature_mode && target_sq <= 0.f)
|
||||
if (!feature_mode && target_sq <= 0.f && border_sq <= 0.f)
|
||||
return emit(); // no criterion at all
|
||||
if (std::none_of(refine_region.begin(), refine_region.end(), [](uint8_t v) { return v != 0; }))
|
||||
return emit(); // nothing flagged: no-op
|
||||
@@ -1720,16 +1814,27 @@ indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
|
||||
// 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)
|
||||
const Tri &t = tris[ti];
|
||||
const uint8_t flags = refine_region[t.src];
|
||||
if (flags == 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);
|
||||
float p = 0.f;
|
||||
if (flags & REFINE_PAINTED) {
|
||||
p = (target_sq > 0.f) ? ll / target_sq : 0.f;
|
||||
if (feature_mode)
|
||||
p = std::max(p, detail_error(ti) / chord_tolerance_mm);
|
||||
}
|
||||
// The band straddling the paint's edge, refined by plain edge length. Deliberately *not* run
|
||||
// through detail_error(): outside the paint the sampler still reports full relief (it has no
|
||||
// per-point paint test), so the chord test there would chase texture detail on a surface the
|
||||
// bake is going to leave flat. Length alone is what this band needs - the error it is fixing
|
||||
// is the size of the triangles spanning the displacement step, not the curvature of anything.
|
||||
if ((flags & REFINE_BORDER) && border_sq > 0.f)
|
||||
p = std::max(p, ll / border_sq);
|
||||
return p;
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,14 @@ namespace Slic3r {
|
||||
|
||||
class ModelVolume;
|
||||
|
||||
// Bits of subdivide_mesh_adaptive()'s per-triangle `refine_region` mask. See that function.
|
||||
static constexpr uint8_t REFINE_PAINTED = 1;
|
||||
static constexpr uint8_t REFINE_BORDER = 2;
|
||||
|
||||
// Progress/cancellation hook for the (potentially multi-second) bake. Called with a 0..100
|
||||
// percentage; return false to abort. See build_texture_displacement().
|
||||
using DisplacementProgressFn = std::function<bool(int)>;
|
||||
|
||||
// Maximum number of simultaneous texture-displacement layers a single ModelVolume can hold.
|
||||
// Each layer owns its own paint mask (ModelVolume::texture_displacement_facet(slot)), so this
|
||||
// is also the number of independent EnforcerBlockerType selectors kept per volume.
|
||||
@@ -370,7 +378,11 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
|
||||
// LSCM's per-patch UV solve through the same scale/rotate/offset controls as every other
|
||||
// projection method, without going through project_texture_displacement_uv()'s own dispatch
|
||||
// (which only knows how to compute the *analytic* methods from a single vertex + normal).
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer);
|
||||
// `aspect` is the height map's width / height. It scales the v axis so a non-square image is not
|
||||
// squeezed into a square tile: `tiling_scale` is the tile's size along u, and the tile is
|
||||
// `tiling_scale * height / width` mm along v, which keeps texels square. 1 (the default) is the
|
||||
// square case and leaves the coordinate exactly as it always was.
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f);
|
||||
|
||||
// Applies a row-major 3x4 projective matrix (see TextureDisplacementLayer::view_project_matrix) to a
|
||||
// local-space point, writing the resulting texture uv. Returns false - and leaves `uv` untouched -
|
||||
@@ -532,10 +544,18 @@ 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.
|
||||
//
|
||||
// `progress`, when set, is called from the worker thread with a 0..100 completion percentage as the
|
||||
// bake proceeds. Returning false from it aborts the run, which then returns an *empty* mesh - never
|
||||
// a partially displaced one, so a cancelled bake can never be mistaken for a finished result and
|
||||
// committed. It exists because this is the one call in the feature that can take seconds on a
|
||||
// subdivided mesh, and without it the progress notification the Job framework puts on screen sits at
|
||||
// 0% for the whole run and offers no way to close it (its close button only appears at 100%).
|
||||
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementFacetsData &facets_data,
|
||||
const TextureDisplacementOptions &options = {});
|
||||
const TextureDisplacementOptions &options = {},
|
||||
const DisplacementProgressFn &progress = {});
|
||||
|
||||
// Convenience overload for main-thread callers: extracts the mesh/layers/paint data/options from
|
||||
// `volume` and forwards to the overload above.
|
||||
@@ -551,8 +571,10 @@ indexed_triangle_set build_texture_displacement(const ModelVolume &volume);
|
||||
// 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.
|
||||
// `on_pass`, when set, is called with the 0-based index of each completed pass; returning false stops
|
||||
// the relaxation there, leaving the passes already done in place.
|
||||
void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t> &movable, float strength,
|
||||
int iterations);
|
||||
int iterations, const DisplacementProgressFn &on_pass = {});
|
||||
|
||||
// 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
|
||||
@@ -638,12 +660,30 @@ indexed_triangle_set subdivide_mesh_uniform(const indexed_triangle_set &mesh, fl
|
||||
// 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.
|
||||
//
|
||||
// `refine_region` is a **bitmask** per input triangle, not a plain flag:
|
||||
// bit 0 (REFINE_PAINTED) - inside the painted area: refine by the length baseline and, in feature
|
||||
// mode, by the chord-error test.
|
||||
// bit 1 (REFINE_BORDER) - inside the band straddling the paint's edge: refine by
|
||||
// `border_edge_length_mm` alone.
|
||||
// A value of 1 therefore means exactly what a plain 1 always meant, and 0 still means "never touch
|
||||
// this triangle except through the conformal closure".
|
||||
//
|
||||
// The border band exists because the chord-error test is blind to the one discontinuity the bake
|
||||
// actually creates. `make_combined_displacement_sampler()` evaluates the height field everywhere,
|
||||
// with no per-point paint test, so where the paint *stops* it keeps reporting full relief - smooth
|
||||
// and low-curvature - while the baked surface steps from full displacement to zero. The test sees no
|
||||
// error there and leaves the transition at whatever density the input had, which is what turns the
|
||||
// rim of an unpainted island into a ring of large, steeply tilted triangles. Refining that band by
|
||||
// plain edge length is bounded (it is a thin ring, and a length target always terminates) and needs
|
||||
// no paint-aware sampler.
|
||||
indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
|
||||
const std::vector<uint8_t> &refine_region,
|
||||
float target_edge_length_mm, int max_triangles = 1000000,
|
||||
std::vector<int> *out_source = nullptr,
|
||||
const HeightFieldSampler &sampler = nullptr,
|
||||
float chord_tolerance_mm = 0.f, float min_edge_length_mm = 0.f);
|
||||
float chord_tolerance_mm = 0.f, float min_edge_length_mm = 0.f,
|
||||
float border_edge_length_mm = 0.f);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
|
||||
@@ -1519,9 +1519,11 @@ void TriangleSelector::get_facets(std::vector<indexed_triangle_set>& facets_per_
|
||||
}
|
||||
}
|
||||
|
||||
indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType state) const
|
||||
indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType state, std::vector<int> *out_source) const
|
||||
{
|
||||
indexed_triangle_set out;
|
||||
if (out_source)
|
||||
out_source->clear();
|
||||
|
||||
size_t num_vertices = 0;
|
||||
for (const Vertex &v : m_vertices)
|
||||
@@ -1535,8 +1537,13 @@ indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType sta
|
||||
out.vertices.emplace_back(v.v);
|
||||
}
|
||||
|
||||
for (int itriangle = 0; itriangle < m_orig_size_indices; ++ itriangle)
|
||||
for (int itriangle = 0; itriangle < m_orig_size_indices; ++ itriangle) {
|
||||
this->get_facets_strict_recursive(m_triangles[itriangle], m_neighbors[itriangle], state, out.indices);
|
||||
// Everything the recursion just appended came from this original triangle, whatever depth it
|
||||
// was split to. Recording it here keeps the recursive helpers untouched.
|
||||
if (out_source)
|
||||
out_source->resize(out.indices.size(), itriangle);
|
||||
}
|
||||
|
||||
for (auto &triangle : out.indices)
|
||||
for (int i = 0; i < 3; ++ i)
|
||||
|
||||
@@ -332,7 +332,14 @@ public:
|
||||
// Get facets at a given state. Don't triangulate T-joints.
|
||||
indexed_triangle_set get_facets(EnforcerBlockerType state) const;
|
||||
// Get facets at a given state. Triangulate T-joints.
|
||||
indexed_triangle_set get_facets_strict(EnforcerBlockerType state) const;
|
||||
// Sub-triangles in `state`, with the *whole* mesh's referenced vertex array (only .indices is
|
||||
// filtered by state, so two calls with different states share one indexing).
|
||||
//
|
||||
// `out_source`, when given, is filled parallel to the returned .indices with the index of the
|
||||
// original mesh triangle each sub-triangle came from. That is what lets a caller carry partial
|
||||
// paint - the pieces of a triangle a brush stroke only partly covered - across a refinement of
|
||||
// the same surface, instead of having to round each source triangle to wholly painted or not.
|
||||
indexed_triangle_set get_facets_strict(EnforcerBlockerType state, std::vector<int> *out_source = nullptr) const;
|
||||
// Get edges around the selected area by seed fill.
|
||||
std::vector<Vec2i32> get_seed_fill_contour() const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user