mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-24 17:26:47 +00:00
Give each bake its own budget, and warn when it is short
Relief preserved from an earlier bake is counted on top of the budget instead of eating into it: baking a second area went from ~214 k triangles to the ~1000 k it was given. When the resolution needs more triangles than the budget allows, it is said before the bake under Resolution and again afterwards.
This commit is contained in:
@@ -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<int> parent = std::move(sub.face_parent_id);
|
||||
std::vector<int> 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<uint8_t> locked;
|
||||
std::vector<uint8_t> 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<int> 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;
|
||||
|
||||
@@ -74,9 +74,11 @@ struct PipelineSettings
|
||||
std::function<bool(const Vec3f ¢roid)> 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -820,6 +820,21 @@ void merge_small_color_regions(const indexed_triangle_set &mesh, std::vector<int
|
||||
// displaced along the wrong direction: a mesh normal maps to the world normal through the inverse
|
||||
// transpose, not through the transform itself, so the relief leaned. Identity - the default - is
|
||||
// exactly the old behaviour and is what an untransformed volume gives.
|
||||
// What a bake spent, for whoever wants to report it. Only the default pipeline fills it in; the
|
||||
// classic path moves the vertices the mesh already has and has nothing to say here.
|
||||
struct TextureBakeStats
|
||||
{
|
||||
// What the refinement produced, before simplification, and what the bake committed.
|
||||
size_t triangles_refined = 0;
|
||||
size_t triangles_out = 0;
|
||||
// What the result had to fit into: the budget for what this bake refines, plus the triangles it
|
||||
// only preserves (an earlier bake's relief, which this one does not paint).
|
||||
size_t triangles_budget = 0;
|
||||
// The refined mesh did not fit, so the simplification had to take detail out of it to make it:
|
||||
// the result carries less of the texture than the chosen resolution asked for.
|
||||
bool budget_limited = false;
|
||||
};
|
||||
|
||||
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
|
||||
const std::vector<TextureDisplacementLayer> &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
|
||||
|
||||
@@ -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<uint8_t> 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<double>();
|
||||
const Vec3d b = frame * its.vertices[size_t(tri[1])].cast<double>();
|
||||
const Vec3d c = frame * its.vertices[size_t(tri[2])].cast<double>();
|
||||
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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <algorithm>
|
||||
|
||||
#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,
|
||||
|
||||
@@ -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<uint8_t> m_triangle_color;
|
||||
|
||||
Reference in New Issue
Block a user