Simplify flat faces even when a bake fits its budget

Nothing removed the even mesh over a texture's flat parts unless the
budget was lowered until simplification had to run. Collapses stay under
a thirty-second of the resolution: on the test map 387584 -> 369876
triangles, with the print unchanged (0.02% of the extrusion).

An earlier attempt was reverted for a 21% heavier result. That was the
colour despeckle switching itself off, not the collapsing: it keyed off
the face parent map, which any collapse empties.
This commit is contained in:
ExPikaPaka
2026-09-23 12:19:58 +02:00
parent 8ce05c03ec
commit 887fbfc397
3 changed files with 28 additions and 10 deletions
@@ -310,12 +310,13 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
// 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) {
// Flat faces are harvested whether or not the budget bites. Refinement is driven by the target
// edge length alone, so it leaves as fine a mesh over the flat parts of a texture as over its
// detail, and nothing else removes those: under its budget a bake kept every redundant triangle
// unless the budget was lowered until decimation had to run. Only collapses costing less than
// harvest_tol are taken, so this does not reach the relief.
const bool harvest_only = !over_budget && settings.harvest_flat && displaced.triangle_count() > 0;
if (over_budget || harvest_only) {
// 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;
@@ -333,20 +334,23 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
}
});
}
// Harvesting alone is asked for by handing it the count it already has: nothing is then
// over the target, so the loop only ever pops collapses under the tolerance.
const size_t before = displaced.triangle_count();
DecimateResult dec = decimate(displaced, target, settings.harvest_flat,
DecimateResult dec = decimate(displaced, over_budget ? target : before, 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");
lap("decimate", displaced, over_budget ? "over budget, simplified" : "flat faces harvested");
BOOST_LOG_TRIVIAL(info) << "TextureBake decimate: " << before << " -> " << displaced.triangle_count()
<< " (budget " << target << ")";
<< (over_budget ? " (budget " : " (flat harvest, budget ") << target << ")";
parent.clear(); // no longer meaningful
}
result.triangles_refined = displaced_before_decimate;
result.triangles_budget = target;
result.budget_limited = over_budget;
result.simplified = over_budget;
if (!report("decimate", 1.0)) {
result.canceled = true;
return result;
@@ -115,6 +115,12 @@ struct PipelineResult
size_t triangles_refined = 0;
size_t triangles_budget = 0;
bool budget_limited = false;
// The budget coarsened the mesh, so its triangles are no longer the fine, even ones the refinement
// laid down - they can be as large as the features on them, which a caller that post-processes per
// triangle (the colour despeckle) has to know. Harvesting flat faces does not count: what it
// removes carried nothing. Not the same question as "did a collapse run", which is why
// face_parent_id being empty is no longer used to answer it.
bool simplified = false;
};
// `debug`, when given and enabled, receives the mesh after every stage that ran - which is the only
+9 -1
View File
@@ -2178,6 +2178,12 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
BOOST_LOG_TRIVIAL(info) << "TextureBake resolution: " << settings.refine_length << " mm"
<< (auto_edge ? " (auto)" : "") << ", budget " << settings.max_triangles / 1000 << " k"
<< (auto_budget ? " (auto)" : "");
// How far a flat-face harvest may move the surface, tied to the resolution: what a bake refined to
// 1.4 mm edges can merge flat is not what one refined to 0.1 mm may touch. A thirty-second of the
// edge is well under a layer either way - at the default resolution it sheds about 5% of the
// triangles on a relief that covers the whole surface, and far more of one with flat ground in it -
// and the floor keeps a very fine resolution from spending the pass for nothing.
settings.harvest_tol = std::clamp(settings.refine_length / 32.0, 0.005, 0.05);
settings.preserve_untextured = true;
// Always on: relief driven under the plate is unprintable whichever pipeline produced it, so this
// is no longer a choice the user has to make. Only geometry that ends up below the model's own
@@ -2335,7 +2341,9 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
// noise. A simplified mesh is neither: its triangles are as large as the colour regions
// themselves and already end on the colour boundaries, so a majority vote among three
// neighbours would repaint whole features. Bake mode (no simplification) keeps it.
const bool simplified = result.face_parent_id.empty();
// Not "did a collapse run": a flat-face harvest leaves the mesh as fine as it was, and
// reading it as simplified switched the despeckle off for every bake that harvested.
const bool simplified = result.simplified;
despeckle_triangle_colors(out, palette, simplified ? 0 : color->despeckle_passes);
merge_small_color_regions(out, palette, color->min_color_region_mm2);
for (size_t i = 0; i < out.indices.size(); ++i) {