diff --git a/src/libslic3r/TextureBake/TextureBakePipeline.cpp b/src/libslic3r/TextureBake/TextureBakePipeline.cpp index f59ed51163..37d0577b77 100644 --- a/src/libslic3r/TextureBake/TextureBakePipeline.cpp +++ b/src/libslic3r/TextureBake/TextureBakePipeline.cpp @@ -280,15 +280,12 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, } // 4. Decimate - export only. A bake needs the face-parent map, which a collapse destroys. - std::vector parent = std::move(sub.face_parent_id); + std::vector parent = std::move(sub.face_parent_id); + const size_t displaced_before_decimate = displaced.triangle_count(); if (mode == PipelineMode::Export) { - // Only when the mesh is actually over budget. Harvesting flat faces on a mesh that already fits - // cost several times the decimation itself and degraded the relief it was handed; it now only - // runs as part of a decimation that has to happen anyway. The repair pass below keys off the - // same decision (it runs only when decimation did), so an under-budget bake skips both. - const bool needs_decimation = displaced.triangle_count() > settings.max_triangles; - if (needs_decimation) { - std::vector locked; + std::vector locked; + size_t preserved = 0; + { if (settings.preserve_untextured && !displaced.exclude_weight.empty()) { locked.assign(displaced.triangle_count(), 0); // The corner average, as the displacement stage judges it: after the flip stage's @@ -304,7 +301,21 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, for (size_t t = 0; t < locked.size() && t < soft_excluded.size(); ++t) if (soft_excluded[t]) locked[t] = 0; + 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 + // 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; + const bool over_budget = displaced.triangle_count() > target; + // Only when the mesh is actually over budget: the decimation pass also welds the soup and is + // followed by the T-junction repair, and putting an under-budget bake through both changed the + // sliced result by a fifth even with the collapse tolerance at zero, i.e. with nothing + // collapsed. Harvesting flat faces on a mesh that already fits needs that path to leave the + // geometry alone first. + if (over_budget) { // Colour per face on the fine mesh, so colour boundaries become creases the collapse // respects. Excluded (unpainted) faces take no colour. std::vector face_color; @@ -322,14 +333,20 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, } }); } - DecimateResult dec = decimate(displaced, settings.max_triangles, settings.harvest_flat, + const size_t before = displaced.triangle_count(); + DecimateResult dec = decimate(displaced, target, settings.harvest_flat, settings.harvest_tol, locked, [&](double f) { return report("decimate", f); }, face_color); result.locked_over_budget = dec.locked_over_budget; displaced = std::move(dec.geometry); lap("decimate", displaced, "over budget, simplified"); + BOOST_LOG_TRIVIAL(info) << "TextureBake decimate: " << before << " -> " << displaced.triangle_count() + << " (budget " << target << ")"; parent.clear(); // no longer meaningful } + result.triangles_refined = displaced_before_decimate; + result.triangles_budget = target; + result.budget_limited = over_budget; if (!report("decimate", 1.0)) { result.canceled = true; return result; diff --git a/src/libslic3r/TextureBake/TextureBakePipeline.hpp b/src/libslic3r/TextureBake/TextureBakePipeline.hpp index d56a0d09dc..22cc962141 100644 --- a/src/libslic3r/TextureBake/TextureBakePipeline.hpp +++ b/src/libslic3r/TextureBake/TextureBakePipeline.hpp @@ -74,9 +74,11 @@ struct PipelineSettings std::function painted; // 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. 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 max_triangles - an under-budget mesh is never decimated. + // the displaced mesh is over the budget - an under-budget mesh is never decimated. bool harvest_flat = true; double harvest_tol = DECIMATE_DEFAULT_HARVEST_TOL; // Lock the untextured region against both regularization and decimation. @@ -107,6 +109,12 @@ struct PipelineResult bool locked_over_budget = false; size_t collapse_count = 0; bool canceled = false; + // What the refinement produced, before decimation, and the count it had to fit into (the budget + // plus the preserved geometry). budget_limited says the refined mesh did not fit: the result + // carries less of the texture than the resolution asked for, which is what a caller warns about. + size_t triangles_refined = 0; + size_t triangles_budget = 0; + bool budget_limited = false; }; // `debug`, when given and enabled, receives the mesh after every stage that ran - which is the only @@ -122,7 +130,7 @@ void clamp_below_bottom(TriSoup &geometry, float bottom_z); // Flatten the bed-contact surface by snapping positions within `tol` of the bottom plane onto it. // -// Gated, not unconditional: an unconditional band snap also flattens the undersides of texture bumps +// Gated, not unconditional: an unconditional band snap also flattens the undersides of texture relief // near the base, folding them coplanar into the bottom face. Folded faces overlap the plate, so edges // there pick up four incident faces - non-manifold edges and phantom shells on re-import. All copies // of a position move together, and the move is rejected if any incident triangle would go degenerate diff --git a/src/libslic3r/TextureDisplacement.cpp b/src/libslic3r/TextureDisplacement.cpp index d24e334fa8..ad3bf3f267 100644 --- a/src/libslic3r/TextureDisplacement.cpp +++ b/src/libslic3r/TextureDisplacement.cpp @@ -2095,7 +2095,8 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set const DisplacementProgressFn &progress, const TextureColorRequest *color, bool flip_normals, - BakeStageRecorder *debug) + BakeStageRecorder *debug, + TextureBakeStats *stats) { HeightFieldSampler combined = make_combined_displacement_sampler(mesh, layers, facets_data); if (!combined) @@ -2282,6 +2283,12 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set if (result.canceled || result.geometry.empty()) return {}; + if (stats != nullptr) { + stats->triangles_refined = result.triangles_refined; + stats->triangles_out = result.geometry.triangle_count(); + stats->triangles_budget = result.triangles_budget; + stats->budget_limited = result.budget_limited; + } indexed_triangle_set out = TextureBake::to_indexed_triangle_set(result.geometry); if (out.indices.empty()) return mesh; @@ -2365,8 +2372,11 @@ static indexed_triangle_set build_texture_displacement_in_place( const DisplacementProgressFn &progress, const TextureColorRequest *color, bool flip_normals, - BakeStageRecorder *debug) + BakeStageRecorder *debug, + TextureBakeStats *stats) { + // The classic path moves the vertices the mesh already has, so there is no budget to report on. + (void) stats; // 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); }; @@ -2400,7 +2410,7 @@ static indexed_triangle_set build_texture_displacement_in_place( if (options.pipeline_v2) return build_texture_displacement_v2(mesh, layers, facets_data, options, progress, color, flip_normals, - debug); + debug, stats); // Layers are combined in slot order, like stacked layers in an image editor: each one folds its // own displacement into the running total via its blend mode (see TextureBlendMode). @@ -2813,7 +2823,8 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set const DisplacementProgressFn &progress, const TextureColorRequest *color, const Transform3d &volume_to_world, - BakeStageRecorder *debug) + BakeStageRecorder *debug, + TextureBakeStats *stats) { // An untransformed volume on an untransformed instance is by far the common case, and the round // trip costs two matrix multiplies per vertex on a mesh that can carry millions of them - so take @@ -2823,7 +2834,7 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set const Transform3d frame = texture_displacement_bake_frame(volume_to_world); if (frame.matrix().isApprox(Transform3d::Identity().matrix())) return build_texture_displacement_in_place(base_mesh, layers, facets_data, options, progress, color, - false, debug); + false, debug, stats); const Transform3d to_local = frame.inverse(); // A mirroring placement leaves the positions correct but every winding-derived normal pointing @@ -2838,7 +2849,7 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set const size_t debug_mark = (debug != nullptr) ? debug->mark() : 0; indexed_triangle_set out = build_texture_displacement_in_place(world, layers, facets_data, options, - progress, color, mirrored, debug); + progress, color, mirrored, debug, stats); // Everything the bake recorded is in world millimetres, like `out` itself. The debug view draws in // the volume's local frame, so the stages are brought back the same way the result is. if (debug != nullptr) diff --git a/src/libslic3r/TextureDisplacement.hpp b/src/libslic3r/TextureDisplacement.hpp index d6b3a41548..bb39bc3266 100644 --- a/src/libslic3r/TextureDisplacement.hpp +++ b/src/libslic3r/TextureDisplacement.hpp @@ -820,6 +820,21 @@ void merge_small_color_regions(const indexed_triangle_set &mesh, std::vector &layers, const TextureDisplacementFacetsData &facets_data, @@ -829,7 +844,9 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set const Transform3d &volume_to_world = Transform3d::Identity(), // When given and enabled, receives the mesh after each // stage, already brought back into `base_mesh`'s frame. - BakeStageRecorder *debug = nullptr); + BakeStageRecorder *debug = nullptr, + // When given, receives what the bake spent. + TextureBakeStats *stats = nullptr); // `volume`'s mesh coordinates -> world millimetres: its first instance's transform times its own. // The mesh is shared by every instance, so a multi-instance object can only be baked for one of diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp index c2a19ba03a..62b23fde62 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp @@ -4114,6 +4114,44 @@ TextureDisplacementFacetsData GLGizmoTextureDisplacement::masks_after_subdivisio return out; } +double GLGizmoTextureDisplacement::painted_area_mm2(const ModelVolume &mv) +{ + // Keyed on the paint generation, which every stroke and every layer edit raises, so a panel that + // asks for this on every frame walks the mesh only when the answer can have changed. + const std::string key = std::to_string(mv.id().id) + ":" + std::to_string(mv.mesh().facets_count()) + ":" + + std::to_string(m_preview_generation->load()); + if (key == m_painted_area_key) + return m_painted_area_mm2; + + const TriangleMesh &mesh = mv.mesh(); + std::vector region; + double area = 0.; + if (collect_paint_region(mesh, facets_data_of(mv), region, nullptr)) { + // In the frame the bake refines in, so a scaled instance is measured at the size it prints at. + const Transform3d frame = texture_displacement_bake_frame(texture_displacement_volume_to_world(mv)); + const indexed_triangle_set &its = mesh.its; + for (size_t t = 0; t < its.indices.size() && t < region.size(); ++t) { + if ((region[t] & REFINE_PAINTED) == 0) + continue; + const stl_triangle_vertex_indices &tri = its.indices[t]; + const Vec3d a = frame * its.vertices[size_t(tri[0])].cast(); + const Vec3d b = frame * its.vertices[size_t(tri[1])].cast(); + const Vec3d c = frame * its.vertices[size_t(tri[2])].cast(); + area += 0.5 * (b - a).cross(c - a).norm(); + } + } + m_painted_area_mm2 = area; + m_painted_area_key = key; + return area; +} + +size_t GLGizmoTextureDisplacement::estimated_refined_triangles(const ModelVolume &mv, float edge_mm) +{ + if (edge_mm <= 0.f) + return 0; + return size_t(4.0 * painted_area_mm2(mv) / (double(edge_mm) * double(edge_mm))); +} + const V2Resolution &GLGizmoTextureDisplacement::v2_recommendation(const ModelVolume &mv) { // Rebuilt only when something it depends on changes: the surface area scan is O(triangles) and the @@ -6768,6 +6806,26 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float hover_tip(_u8L("Triangle edge length the painted area is refined to before displacement. " "Smaller carries finer texture detail and costs more triangles; the budget " "caps the result.")); + + // What this resolution costs over what is painted, against what the budget allows. Refining + // past the budget is not an error - the bake simplifies back down to it - but the result + // then carries less of the texture than the resolution asks for, and the only sign of that + // used to be a mesh that came out coarser than expected. Shown before the bake, so the + // answer is to change a number rather than to wait out a bake and redo it. + const float edge_now = auto_res ? rec.edge_mm : opts.v2_refine_mm; + const int budget_k = opts.v2_max_triangles_k < 0 ? rec.budget_k : opts.v2_max_triangles_k; + const size_t budget = size_t(std::max(0, budget_k)) * 1000; + const size_t needed = estimated_refined_triangles(*mv, edge_now); + // Only when it is clearly over: the estimate runs about 3% high where it matters and up to + // a third high at coarse resolutions, where the mesh's own triangles are already near the + // target, and a warning about a bake that would have fitted is worse than none. + if (budget > 0 && needed > budget * 5 / 4) { + const auto to_m = [](size_t n) { return double(n) / 1000000.; }; + m_imgui->warning_text(Slic3r::format(_u8L("This resolution needs about %1$.1f M triangles, " + "budget %2$.1f M - the bake will simplify back to " + "the budget and lose detail."), + to_m(needed), to_m(budget))); + } } else { m_imgui->text(_L("Triangles")); ImGui::SameLine(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp index 804c31821e..47de461f2b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp @@ -611,6 +611,18 @@ private: // is exactly what this exists to find. bool m_debug_check_topology = true; + // World-millimetre area of everything painted on `mv`, cached on the mesh and the paint generation: + // it is what the refinement has to cover, so it is what says whether a budget can pay for a + // resolution. 0 when nothing is painted. + double painted_area_mm2(const ModelVolume &mv); + double m_painted_area_mm2 = 0.; + std::string m_painted_area_key; + // Triangles the refinement needs to reach `edge_mm` over that area. Bisection converges to four of + // them to a square of the target edge - measured within 3% on a test model over a 5x range of + // resolutions - but the real count depends on the shape of the triangles it starts from, so this + // is only ever used to say a budget is clearly too small, never as a promise of what will come out. + size_t estimated_refined_triangles(const ModelVolume &mv, float edge_mm); + // The default pipeline's automatic resolution/budget for the volume, cached on what it depends on. const V2Resolution &v2_recommendation(const ModelVolume &mv); V2Resolution m_v2_rec; diff --git a/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp b/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp index 6e3976a297..0fb9cefefc 100644 --- a/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp +++ b/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp @@ -3,6 +3,7 @@ #include #include "libslic3r/Model.hpp" +#include "libslic3r/format.hpp" #include "libslic3r/TriangleSelector.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" @@ -11,6 +12,7 @@ #include "slic3r/GUI/GUI_ObjectList.hpp" #include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp" #include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/NotificationManager.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/Utils/UndoRedo.hpp" @@ -64,7 +66,7 @@ void TextureDisplacementBakeJob::process(Ctl &ctl) } return true; }, - color, m_input.volume_to_world)); + color, m_input.volume_to_world, nullptr, &m_stats)); // Always finish at 100: this is what closes the notification. Reported even on cancel, where // build_texture_displacement() returns an empty mesh and finalize() commits nothing. @@ -169,6 +171,19 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept } else { commit(); } + + // The refinement went finer than the budget could keep: the simplification had to take detail back + // out to fit, so what was baked carries less of the texture than the resolution asked for. Said + // here, with the numbers, because it is the only place that knows them - and not as an error + // dialog: the result is a usable mesh, just not the one the settings described. + if (m_stats.budget_limited) { + const auto millions = [](size_t n) { return double(n) / 1000000.; }; + wxGetApp().notification_manager()->push_notification( + NotificationType::CustomNotification, NotificationManager::NotificationLevel::WarningNotificationLevel, + Slic3r::format(_u8L("The triangle budget limited the detail: this resolution needs %1$.1f M triangles, " + "the budget kept %2$.1f M. Raise Budget or use a coarser Resolution for the full detail."), + millions(m_stats.triangles_refined), millions(m_stats.triangles_out))); + } } void queue_texture_displacement_bake(const ModelVolume &volume, const TextureColorSettings &color, diff --git a/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.hpp b/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.hpp index 753dd20742..187df3d0e7 100644 --- a/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.hpp +++ b/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.hpp @@ -50,6 +50,8 @@ public: private: TextureDisplacementBakeInput m_input; TriangleMesh m_result; + // What the bake spent, for the message it leaves behind when the budget capped the detail. + TextureBakeStats m_stats; // Per triangle of m_result: the filament to print it in, as an EnforcerBlockerType value // (0 = leave alone). Empty unless a layer asked for colour. See TextureColorRequest. std::vector m_triangle_color;