diff --git a/src/libslic3r/TextureBake/TextureBakePipeline.cpp b/src/libslic3r/TextureBake/TextureBakePipeline.cpp index 26f44671c7..2b678905b1 100644 --- a/src/libslic3r/TextureBake/TextureBakePipeline.cpp +++ b/src/libslic3r/TextureBake/TextureBakePipeline.cpp @@ -149,7 +149,7 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, // 1. Refine to the target edge length. SubdivideResult sub = subdivide( input, settings.refine_length, face_excluded, /* fast */ false, settings.safety_cap, - [&](double f, size_t, double) { return report("subdivide", f); }); + [&](double f, size_t, double) { return report("subdivide", f); }, settings.paint_within); result.safety_cap_hit = sub.safety_cap_hit; lap("subdivide", sub.geometry); if (!report("subdivide", 1.0)) { @@ -180,7 +180,8 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, } sub = subdivide(reg.geometry, settings.refine_length * settings.regularize_second_pass_mul, excl, false, settings.safety_cap, - [&](double f, size_t, double) { return report("re-subdivide", f); }); + [&](double f, size_t, double) { return report("re-subdivide", f); }, + settings.paint_within); result.safety_cap_hit = result.safety_cap_hit || sub.safety_cap_hit; // The second pass renumbers faces, so the parent map has to be composed through it. std::vector composed(sub.face_parent_id.size()); @@ -199,12 +200,8 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, // 2b. Paint finer than the input triangles. The caller includes a source triangle when any part of // it is painted; now that the faces are small, ask once more per face and switch the unpainted - // ones off. They are pinned like the excluded region from here on (their own corners at weight - // 1, and the displacement's boundary sealing pins the stroke's rim on the painted side), but they - // are refined pieces of painted triangles, not original geometry, so `soft_excluded` keeps them - // out of the decimation lock below. Every stage between here and the decimation rewrites faces in - // place, so the per-face flag stays valid by index. - std::vector soft_excluded; + // ones off. They are pinned like the excluded region from here on: their own corners at weight 1, + // and the displacement's boundary sealing pins the stroke's rim on the painted side. if (settings.painted) { const size_t nf = sub.geometry.triangle_count(); const bool have_w = !sub.geometry.exclude_weight.empty(); @@ -228,7 +225,6 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, if (unpainted[t]) sub.geometry.exclude_weight[t * 3] = sub.geometry.exclude_weight[t * 3 + 1] = sub.geometry.exclude_weight[t * 3 + 2] = 1.f; - soft_excluded = std::move(unpainted); } lap("paint", sub.geometry, std::to_string(switched) + " faces switched off"); if (!report("paint", 1.0)) { @@ -294,18 +290,14 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, for (size_t t = 0; t < locked.size(); ++t) locked[t] = (displaced.exclude_weight[t * 3] + displaced.exclude_weight[t * 3 + 1] + displaced.exclude_weight[t * 3 + 2]) / 3.f > 0.99f ? 1 : 0; - // Faces the paint test switched off carry weight 1 too, but are refined pieces of - // painted triangles rather than original geometry: locking them would keep a partly - // painted source triangle at full refinement. Face indices survived relocate, flip - // and displace unchanged, so the flag still lines up. - for (size_t t = 0; t < locked.size() && t < soft_excluded.size(); ++t) - if (soft_excluded[t]) - locked[t] = 0; + // That includes the faces the paint test switched off: the harvest would re-triangulate + // them into long slivers, which a later bake painted there would refine instead of the + // grid the graded refinement left. preserved = size_t(std::count(locked.begin(), locked.end(), uint8_t(1))); } } // The budget is what this bake may spend on what it refines. Geometry it only preserves - the - // relief of an earlier bake, which this one does not paint - is counted on top of it: charged + // unpainted surface, and on it the relief of an earlier bake - is counted on top of it: charged // against the same budget, a second bake over a fresh area had to evict the first one's // triangles to fit, so every bake after the first came out coarser than the one before. const size_t target = settings.max_triangles + preserved; diff --git a/src/libslic3r/TextureBake/TextureBakePipeline.hpp b/src/libslic3r/TextureBake/TextureBakePipeline.hpp index 482cd36311..e230cf0d49 100644 --- a/src/libslic3r/TextureBake/TextureBakePipeline.hpp +++ b/src/libslic3r/TextureBake/TextureBakePipeline.hpp @@ -73,9 +73,16 @@ struct PipelineSettings // once, so it must be safe to call concurrently. std::function painted; + // Optional. Whether the paint comes within a radius of a point, which grades the refinement (see + // subdivide()): only the paint and its surroundings reach refine_length, and the unpainted surface + // further out stays in whole pieces of its own grid. That is what a later bake painted there + // refines, so it meets the same grid a first bake does. Called from several threads at once. + SubdivideWithinFn paint_within; + // Export mode only. - // What this bake may spend on what it refines. Geometry it only preserves (see preserve_untextured) - // is counted on top of it, so an earlier bake's relief does not have to be evicted to fit this one. + // What this bake may spend on what it refines. Geometry it only preserves (see preserve_untextured), + // the unpainted surface and an earlier bake's relief on it, is counted on top of it, so that relief + // does not have to be evicted to fit this one. size_t max_triangles = 750'000; // Keep removing zero-cost flat faces past the target. Only applies when decimation runs, i.e. when // the displaced mesh is over the budget - an under-budget mesh is never decimated. diff --git a/src/libslic3r/TextureBake/TextureBakeSubdivide.cpp b/src/libslic3r/TextureBake/TextureBakeSubdivide.cpp index 8e2a8c82d8..6b882b1fc8 100644 --- a/src/libslic3r/TextureBake/TextureBakeSubdivide.cpp +++ b/src/libslic3r/TextureBake/TextureBakeSubdivide.cpp @@ -68,7 +68,8 @@ struct PassResult // 2. Rebuild, allocating once at the now-known size. PassResult subdivide_pass(VertStore &verts, const std::vector &indices, double max_edge_length, int safety_cap, const std::vector &face_excluded, - QuantizedPointMap *pos_canon_map, const std::vector &face_parent_id) + QuantizedPointMap *pos_canon_map, const std::vector &face_parent_id, + const SubdivideWithinFn &within) { PassResult out; const double max_sq = max_edge_length * max_edge_length; @@ -94,15 +95,55 @@ PassResult subdivide_pass(VertStore &verts, const std::vector &indices, dou return (u < v ? split_edges.get_key(u, v, 0) : split_edges.get_key(v, u, 0)) != -1; }; - // Step 1. An excluded triangle marks none of its own edges, so its interior never refines; its - // boundary edges are still marked by an included neighbour, and it follows that split. - for (size_t t = 0; t < tri_count; ++t) { - if (!face_excluded.empty() && face_excluded[t]) - continue; + // Graded, an edge splits while it is longer than max_edge_length + SUBDIVIDE_GRADE * (d - len/2), + // d its midpoint's distance from the region; less half the length bounds the edge's own distance + // from below, so the test errs toward splitting. Solved for d, that is the region coming within the + // radius below. It reads the positions alone, so two triangles sharing the edge agree. + const auto too_long = [&](int a, int b) { + const double len_sq = edge_len_sq(verts, a, b); + if (len_sq <= max_sq) + return false; + if (!within) + return true; + const double len = std::sqrt(len_sq); + return within((verts.pos[size_t(a)] + verts.pos[size_t(b)]) * 0.5, + 0.5 * len + (len - max_edge_length) / SUBDIVIDE_GRADE); + }; + + // Step 1. An excluded triangle marks none of its own edges while left alone, so its interior never + // refines; its boundary edges are still marked by an included neighbour, and ungraded it follows + // that split. Graded, one the refinement reaches marks its own too-long edges too, and so on + // outward. An earlier bake's relief meets the paint along edges already at the target length, so + // it is never reached and stays as it was. + std::vector want(tri_count, 0); + tbb::parallel_for(tbb::blocked_range(0, tri_count), [&](const tbb::blocked_range &range) { + for (size_t t = range.begin(); t < range.end(); ++t) { + const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2]; + want[t] = uint8_t(too_long(a, b)) | uint8_t(too_long(b, c) << 1) | uint8_t(too_long(c, a) << 2); + } + }); + const auto mark_wanted = [&](size_t t) { const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2]; - if (edge_len_sq(verts, a, b) > max_sq) mark_edge(a, b); - if (edge_len_sq(verts, b, c) > max_sq) mark_edge(b, c); - if (edge_len_sq(verts, c, a) > max_sq) mark_edge(c, a); + if (want[t] & 1) mark_edge(a, b); + if (want[t] & 2) mark_edge(b, c); + if (want[t] & 4) mark_edge(c, a); + }; + std::vector unreached; + for (size_t t = 0; t < tri_count; ++t) + if (face_excluded.empty() || !face_excluded[t]) + mark_wanted(t); + else if (within && want[t] != 0) + unreached.push_back(t); + // Marks only accumulate, so the order the reached triangles are taken in does not matter. + for (size_t n = 0; n != unreached.size();) { + n = unreached.size(); + unreached.erase(std::remove_if(unreached.begin(), unreached.end(), [&](size_t t) { + const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2]; + const bool reached = is_marked(a, b) || is_marked(b, c) || is_marked(c, a); + if (reached) + mark_wanted(t); + return reached; + }), unreached.end()); } if (split_edges.size() == 0) { out.indices = indices; @@ -376,7 +417,7 @@ TriSoup to_non_indexed(const VertStore &verts, const std::vector &indices, SubdivideResult subdivide(const TriSoup &geometry, double max_edge_length, const std::vector &face_excluded, bool fast, int safety_cap, - const SubdivideProgressFn &on_progress) + const SubdivideProgressFn &on_progress, const SubdivideWithinFn &within) { SubdivideResult result; if (geometry.empty() || max_edge_length <= 0.0) { @@ -401,7 +442,7 @@ SubdivideResult subdivide(const TriSoup &geometry, double max_edge_length, } PassResult pass = subdivide_pass(indexed.verts, current_indices, max_edge_length, safety_cap, - current_excluded, canon_map, current_parent); + current_excluded, canon_map, current_parent, within); current_indices = std::move(pass.indices); if (!pass.face_excluded.empty()) current_excluded = std::move(pass.face_excluded); diff --git a/src/libslic3r/TextureBake/TextureBakeSubdivide.hpp b/src/libslic3r/TextureBake/TextureBakeSubdivide.hpp index 30ad56bf00..743c74eb49 100644 --- a/src/libslic3r/TextureBake/TextureBakeSubdivide.hpp +++ b/src/libslic3r/TextureBake/TextureBakeSubdivide.hpp @@ -26,6 +26,9 @@ static constexpr double SUBDIVIDE_SHARP_ANGLE_DEG = 30.0; // A depth bound, not a work bound: the loop stops as soon as a pass changes nothing. static constexpr int SUBDIVIDE_MAX_ITERATIONS = 12; +// Graded refinement: millimetres of edge length allowed per millimetre of distance from the region. +static constexpr double SUBDIVIDE_GRADE = 1.0; + // Built by the indexers, appended to by the passes. Double precision so repeated midpointing does // not drift. struct VertStore @@ -56,6 +59,9 @@ struct IndexedMesh // Fraction, triangle count, longest remaining edge. Returning false cancels; what comes back is // still watertight, because passes apply whole or not at all. using SubdivideProgressFn = std::function; +// Whether the region being refined comes within `radius` mm of `point`. Called from several threads +// at once. +using SubdivideWithinFn = std::function; struct SubdivideResult { @@ -65,13 +71,20 @@ struct SubdivideResult bool safety_cap_hit = false; }; -// `face_excluded`: one entry per input triangle; non-zero means its interior is never refined. Its -// edges still split when an included neighbour marks them, so no T-junction appears at the boundary. +// `face_excluded`: one entry per input triangle; non-zero means its interior is not refined unless the +// graded refinement reaches it (see `within`). Its edges still split when an included neighbour marks +// them, so no T-junction appears at the boundary. // `fast` selects the cheap position-only indexer for previews. +// `within`, when given, grades the refinement: an edge splits only while it is longer than +// max_edge_length plus SUBDIVIDE_GRADE times its distance from the region. Away from the region the +// triangles are then left as whole pieces of the input's own 1->4 grid rather than refined throughout. +// An excluded triangle the refinement reaches - one of its edges splits - splits the same graded way, +// instead of fanning from its far corner to follow the edge it shares with an included one. SubdivideResult subdivide(const TriSoup &geometry, double max_edge_length, const std::vector &face_excluded = {}, bool fast = false, int safety_cap = SUBDIVIDE_SAFETY_CAP, - const SubdivideProgressFn &on_progress = {}); + const SubdivideProgressFn &on_progress = {}, + const SubdivideWithinFn &within = {}); // Displacement needs the same welding and sharp-edge clustering. IndexedMesh to_indexed(const TriSoup &geometry); diff --git a/src/libslic3r/TextureDisplacement.cpp b/src/libslic3r/TextureDisplacement.cpp index ea52ac64ee..862c6c9044 100644 --- a/src/libslic3r/TextureDisplacement.cpp +++ b/src/libslic3r/TextureDisplacement.cpp @@ -2227,6 +2227,15 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set if (pieces_area < included_area * (1.0 - 1e-4)) settings.painted = painted_at; } + // Graded wherever some of the surface is unpainted. A yes/no to a radius, not a distance: most + // queries are far from the paint, and those the tree turns down at its root. + const bool all_painted = std::none_of(excluded.begin(), excluded.end(), [](uint8_t e) { return e != 0; }); + if (settings.painted || !all_painted) + settings.paint_within = [&painted_pieces, &painted_tree](const Vec3d &p, double radius) { + float r2 = float(radius * radius); + return AABBTreeIndirect::is_any_triangle_in_radius(painted_pieces.vertices, painted_pieces.indices, + painted_tree, Vec3f(p.cast()), r2); + }; TextureBake::DisplaceBounds bounds; bounds.min = bounds.max = mesh.vertices.empty() ? Vec3f::Zero() : mesh.vertices.front(); @@ -2310,8 +2319,7 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set std::vector out_color(out.indices.size(), 0); const ColorFieldSampler &sampler = color_sampler; if (sampler) { - const bool all_painted = std::none_of(excluded.begin(), excluded.end(), [](uint8_t e) { return e != 0; }); - float max_depth = 0.f; + float max_depth = 0.f; for (const TextureDisplacementLayer &layer : layers) max_depth = std::max(max_depth, std::abs(layer.depth_mm)); const float relief_tol = max_depth + paint_tol; diff --git a/tests/libslic3r/test_texture_displacement.cpp b/tests/libslic3r/test_texture_displacement.cpp index 95384dae46..04713d3fd9 100644 --- a/tests/libslic3r/test_texture_displacement.cpp +++ b/tests/libslic3r/test_texture_displacement.cpp @@ -4,12 +4,14 @@ #include #include +#include #include #include #include #include #include +#include "libslic3r/AABBMesh.hpp" #include "libslic3r/TextureDisplacement.hpp" #include "libslic3r/TextureBake/TextureBakeDecimate.hpp" #include "libslic3r/TextureBake/TextureBakeFlip.hpp" @@ -156,6 +158,25 @@ static TriangleSelector::TriangleSplittingData paint_whole_mesh(const indexed_tr return selector.serialize(); } +// Paints a disc of the top face of `mesh` (the plane z = `top`) the way the gizmo's brush does: the +// selector splits the triangles under it, so the stroke need not follow the mesh. +static TriangleSelector::TriangleSplittingData paint_top_disc(const indexed_triangle_set &mesh, float top, + const Vec2f ¢re, float radius) +{ + const TriangleMesh tm(mesh); + const Vec3f at(centre.x(), centre.y(), top); + int start = -1; + Vec3d foot; + AABBMesh(tm).squared_distance(at.cast(), start, foot); // the brush starts on the face under it + TriangleSelector selector(tm); + selector.select_patch(start, + TriangleSelector::SinglePointCursor::cursor_factory( + at, at + Vec3f(0.f, 0.f, 80.f), radius, TriangleSelector::CursorType::SPHERE, + Transform3d::Identity(), TriangleSelector::ClippingPlane()), + EnforcerBlockerType::ENFORCER, Transform3d::Identity(), /* triangle_splitting */ true); + return selector.serialize(); +} + // Regression test for the bug this feature shipped with: with two layers painted over the same // area, the second one was silently dropped (its paint mask was remapped onto the mesh the first // layer had already displaced, which routinely produced an empty bitstream). Every layer is now @@ -1889,27 +1910,9 @@ TEST_CASE("TextureDisplacement: a brush stroke smaller than a triangle displaces // A plain 12-triangle cube. The selector splits the top triangle under a 3 mm spherical brush, // so the painted pieces are far smaller than the triangle. The one-run pipeline used to include // the whole source triangle: the entire top face rose. - const indexed_triangle_set cube = its_make_cube(20.f, 20.f, 20.f); - const TriangleMesh mesh(cube); - int top = -1; - for (size_t t = 0; t < cube.indices.size() && top < 0; ++t) { - const auto &f = cube.indices[t]; - if (cube.vertices[size_t(f[0])].z() > 19.9f && cube.vertices[size_t(f[1])].z() > 19.9f && - cube.vertices[size_t(f[2])].z() > 19.9f) - // The triangle that contains the face centre: the brush starts there. - for (int k = 0; k < 3; ++k) - if ((cube.vertices[size_t(f[k])] - Vec3f(10.f, 10.f, 20.f)).norm() < 15.f) - top = int(t); - } - REQUIRE(top >= 0); - TriangleSelector selector(mesh); - selector.select_patch(top, - TriangleSelector::SinglePointCursor::cursor_factory( - Vec3f(10.f, 10.f, 20.f), Vec3f(10.f, 10.f, 100.f), 3.f, TriangleSelector::CursorType::SPHERE, - Transform3d::Identity(), TriangleSelector::ClippingPlane()), - EnforcerBlockerType::ENFORCER, Transform3d::Identity(), /* triangle_splitting */ true); + const indexed_triangle_set cube = its_make_cube(20.f, 20.f, 20.f); TextureDisplacementFacetsData facets; - facets[0] = selector.serialize(); + facets[0] = paint_top_disc(cube, 20.f, Vec2f(10.f, 10.f), 3.f); REQUIRE(TriangleSelector::has_facets(facets[0], EnforcerBlockerType::ENFORCER)); TextureDisplacementLayer layer; @@ -2054,3 +2057,58 @@ TEST_CASE("Cancelling while flat faces are harvested past the budget stops the d CHECK(canceled <= target); CHECK(canceled > full); } + +TEST_CASE("A second bake beside a first comes out as fine as a single bake", "[TextureDisplacement]") +{ + // A stroke in each of the cube's two top triangles, baked one after the other. The first bake used + // to leave the unpainted ground fanned into slivers from a far corner, or re-triangulated by the + // flat harvest into long triangles, and the second bake refined those shapes rather than the + // cube's own grid: its stroke came out many times as dense, with walls off the texture's lines. + const indexed_triangle_set cube = its_make_cube(20.f, 20.f, 20.f); + const Vec2f first_stroke(5.f, 15.f), second_stroke(15.f, 5.f); + constexpr float radius = 3.f; + + TextureDisplacementLayer layer; + layer.slot = 0; + layer.image_data = make_checkerboard_png(16, 16); + layer.tiling_scale = 5.f; + layer.depth_mm = 0.5f; + TextureDisplacementOptions options; + options.pipeline_v2 = true; + options.v2_refine_mm = 0.5f; + options.v2_max_triangles_k = 750; // export: the flat harvest runs + const auto bake = [&](const indexed_triangle_set &mesh, const Vec2f &stroke) { + TextureDisplacementFacetsData facets; + facets[0] = paint_top_disc(mesh, 20.f, stroke, radius); + return build_texture_displacement(mesh, { layer }, facets, options); + }; + // Aspect ratios of the top-face triangles whose centroid's distance from `centre` passes `keep`. + const auto top_aspects = [](const indexed_triangle_set &mesh, const Vec2f ¢re, auto keep) { + std::vector out; + for (const stl_triangle_vertex_indices &t : mesh.indices) { + const Vec3f &a = mesh.vertices[size_t(t[0])], &b = mesh.vertices[size_t(t[1])], &c = mesh.vertices[size_t(t[2])]; + const Vec3f g = (a + b + c) / 3.f; + if (g.z() > 19.9f && keep((Vec2f(g.x(), g.y()) - centre).norm())) + out.push_back(triangle_aspect(a, b, c)); + } + return out; + }; + const auto in_stroke = [&](float d) { return d < radius; }; + + const indexed_triangle_set first = bake(cube, first_stroke); + REQUIRE(first.indices.size() > cube.indices.size()); + // What the first bake leaves around its stroke is ground the second one refines: whole pieces of + // the cube's grid, not slivers. + const std::vector ground = top_aspects(first, first_stroke, [&](float d) { return d > radius + 1.f; }); + REQUIRE(!ground.empty()); + const float worst = *std::max_element(ground.begin(), ground.end()); + INFO("worst aspect ratio on the ground the first bake left: " << worst); + CHECK(worst < 6.f); + + const size_t single = top_aspects(bake(cube, second_stroke), second_stroke, in_stroke).size(); + const size_t second = top_aspects(bake(first, second_stroke), second_stroke, in_stroke).size(); + INFO("second stroke baked alone: " << single << " triangles, after the first: " << second); + REQUIRE(single > 0); + CHECK(second <= single * 5 / 4); +} +