diff --git a/src/libslic3r/TextureBake/TextureBakeDecimate.cpp b/src/libslic3r/TextureBake/TextureBakeDecimate.cpp index c03607e99c..56551b8e84 100644 --- a/src/libslic3r/TextureBake/TextureBakeDecimate.cpp +++ b/src/libslic3r/TextureBake/TextureBakeDecimate.cpp @@ -551,9 +551,13 @@ DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool h // Rebuild from the surviving faces, with per-face normals. TriSoup &out = result.geometry; + if (!face_color.empty()) + result.face_color.reserve(active_faces); for (size_t f = 0; f < face_count; ++f) { if (faces[f * 3] < 0) continue; + if (!face_color.empty()) + result.face_color.push_back(f < face_color.size() ? face_color[f] : -1); const Vec3f a = pos[size_t(faces[f * 3])].cast(); const Vec3f b = pos[size_t(faces[f * 3 + 1])].cast(); const Vec3f c = pos[size_t(faces[f * 3 + 2])].cast(); diff --git a/src/libslic3r/TextureBake/TextureBakeDecimate.hpp b/src/libslic3r/TextureBake/TextureBakeDecimate.hpp index 8b31dbb642..273ad9af35 100644 --- a/src/libslic3r/TextureBake/TextureBakeDecimate.hpp +++ b/src/libslic3r/TextureBake/TextureBakeDecimate.hpp @@ -46,6 +46,13 @@ using DecimateProgressFn = std::function; struct DecimateResult { TriSoup geometry; + // One entry per output face, carried from the `face_color` handed in: a colour difference is a + // crease, so no collapse ever merges two faces of different colour and every survivor keeps exactly + // the colour it came with. Empty when no `face_color` was given. + // + // This is what lets the caller colour the simplified mesh by *provenance* rather than by sampling it + // again: the input colours were masked by the paint on the fine mesh, where that mask is exact. + std::vector face_color; // The locked faces alone met the target, so it was unreachable without touching preserved // geometry. bool locked_over_budget = false; diff --git a/src/libslic3r/TextureBake/TextureBakeMesh.cpp b/src/libslic3r/TextureBake/TextureBakeMesh.cpp index caf6e22d36..5c2eb8d424 100644 --- a/src/libslic3r/TextureBake/TextureBakeMesh.cpp +++ b/src/libslic3r/TextureBake/TextureBakeMesh.cpp @@ -42,7 +42,7 @@ TriSoup to_soup(const indexed_triangle_set &its, const std::vector &fac return out; } -indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup) +indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup, std::vector *face_color) { indexed_triangle_set out; const size_t n = soup.pos.size(); @@ -54,12 +54,22 @@ indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup) if (map.inserted()) out.vertices.push_back(soup.pos[i]); } + const bool track_color = face_color != nullptr && !face_color->empty(); + std::vector kept_color; + if (track_color) + kept_color.reserve(face_color->size()); for (size_t t = 0; t + 2 < n; t += 3) { // Welded-together corners carry no area. if (id[t] == id[t + 1] || id[t + 1] == id[t + 2] || id[t] == id[t + 2]) continue; out.indices.emplace_back(id[t], id[t + 1], id[t + 2]); + if (track_color) { + const size_t src = t / 3; + kept_color.push_back(src < face_color->size() ? (*face_color)[src] : -1); + } } + if (track_color) + *face_color = std::move(kept_color); return out; } diff --git a/src/libslic3r/TextureBake/TextureBakeMesh.hpp b/src/libslic3r/TextureBake/TextureBakeMesh.hpp index 860aa1445d..7b05ed2451 100644 --- a/src/libslic3r/TextureBake/TextureBakeMesh.hpp +++ b/src/libslic3r/TextureBake/TextureBakeMesh.hpp @@ -15,7 +15,10 @@ namespace TextureBake { TriSoup to_soup(const indexed_triangle_set &its, const std::vector &face_excluded = {}); // Welds at the geometry grid. -indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup); +// `face_color`, when given, is read as one entry per soup triangle and rewritten to match the output. +// Welding can leave a triangle with no area, and those are dropped here, so the two would otherwise +// fall out of step. +indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup, std::vector *face_color = nullptr); } // namespace TextureBake } // namespace Slic3r diff --git a/src/libslic3r/TextureBake/TextureBakePipeline.cpp b/src/libslic3r/TextureBake/TextureBakePipeline.cpp index b44ae837bc..d7f9d9a7b9 100644 --- a/src/libslic3r/TextureBake/TextureBakePipeline.cpp +++ b/src/libslic3r/TextureBake/TextureBakePipeline.cpp @@ -290,6 +290,34 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, return result; } + // Colour per face, taken here and carried from here on. This is the only point where the paint mask + // is exact: `exclude_weight` says which faces the paint left out, and the mesh is still the refined + // one the displacement produced. Everything downstream (the collapse, the T-junction repair) carries + // these along rather than sampling again, and the caller uses them as they are. + // + // It also gives the collapse its crease criterion: an edge between two colours is never collapsed + // across, which is what keeps a survivor's colour well defined. + if (color_sample) { + const size_t nf = displaced.triangle_count(); + result.face_color.assign(nf, -1); + const bool have_w = !displaced.exclude_weight.empty(); + tbb::parallel_for(tbb::blocked_range(0, nf), [&](const tbb::blocked_range &r) { + for (size_t t = r.begin(); t < r.end(); ++t) { + // Unpainted faces take no colour at all, which is what stops the texture appearing on + // surfaces the paint never covered. + if (have_w && (displaced.exclude_weight[t * 3] + displaced.exclude_weight[t * 3 + 1] + + displaced.exclude_weight[t * 3 + 2]) / 3.f > 0.99f) + continue; // stays FACE_UNPAINTED + const Vec3f &a = displaced.pos[t * 3], &b = displaced.pos[t * 3 + 1], &c = displaced.pos[t * 3 + 2]; + const int sampled = color_sample((a + b + c) / 3.f, displaced.nrm[t * 3]); + // Painted either way. The sampler expects a point on the base surface and these are on + // the displaced one, so off the patch by more than its tolerance it simply says "no + // colour" - which must not be confused with "not painted". + result.face_color[t] = (sampled >= 0) ? sampled : FACE_NO_COLOUR; + } + }); + } + // 4. Decimate - export only. A bake needs the face-parent map, which a collapse destroys. std::vector parent = std::move(sub.face_parent_id); const size_t displaced_before_decimate = displaced.triangle_count(); @@ -323,24 +351,8 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, // 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; + std::vector &face_color = result.face_color; 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 face_color; - if (color_sample) { - const size_t nf = displaced.triangle_count(); - face_color.assign(nf, -1); - const bool have_w = !displaced.exclude_weight.empty(); - tbb::parallel_for(tbb::blocked_range(0, nf), [&](const tbb::blocked_range &r) { - for (size_t t = r.begin(); t < r.end(); ++t) { - if (have_w && (displaced.exclude_weight[t * 3] + displaced.exclude_weight[t * 3 + 1] + - displaced.exclude_weight[t * 3 + 2]) / 3.f > 0.99f) - continue; - const Vec3f &a = displaced.pos[t * 3], &b = displaced.pos[t * 3 + 1], &c = displaced.pos[t * 3 + 2]; - face_color[t] = color_sample((a + b + c) / 3.f, displaced.nrm[t * 3]); - } - }); - } // 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(); @@ -350,6 +362,7 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, result.locked_over_budget = dec.locked_over_budget; result.budget_limited = result.simplified = dec.target_cost_detail; displaced = std::move(dec.geometry); + face_color = std::move(dec.face_color); lap("decimate", displaced, over_budget ? "over budget, simplified" : "flat faces harvested"); BOOST_LOG_TRIVIAL(info) << "TextureBake decimate: " << before << " -> " << displaced.triangle_count() << (over_budget ? " (budget " : " (flat harvest, budget ") << target << ")"; @@ -377,7 +390,7 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample, // 6. Close the T-junctions decimation left behind. Only meaningful when it ran. if (mode == PipelineMode::Export && parent.empty()) { - displaced = resolve_t_junctions(displaced); + displaced = resolve_t_junctions(displaced, {}, &result.face_color); lap("repair", displaced); } diff --git a/src/libslic3r/TextureBake/TextureBakePipeline.hpp b/src/libslic3r/TextureBake/TextureBakePipeline.hpp index 43429ce22f..fc1723a74c 100644 --- a/src/libslic3r/TextureBake/TextureBakePipeline.hpp +++ b/src/libslic3r/TextureBake/TextureBakePipeline.hpp @@ -109,9 +109,27 @@ using PipelineProgressFn = std::function; +// Sentinels for PipelineResult::face_color. +static constexpr int FACE_UNPAINTED = -1; // the paint did not cover this face's origin +static constexpr int FACE_NO_COLOUR = -2; // painted, but the sampler returned nothing at this point + struct PipelineResult { TriSoup geometry; + // One entry per output face, carried through decimation, the T-junction repair and the weld. + // FACE_UNPAINTED means the paint never covered the geometry this face came from; anything else means + // it did, and is the palette index `color_sample` returned there (FACE_NO_COLOUR when it returned + // none). The distinction matters: the sampler answers for points on the *base* surface, and these + // are sampled on the displaced one, so a painted face can easily come back without a colour. Only + // the painted/unpainted split is reliable here, and that is what a caller should use it for. + // + // Empty unless the caller gave a `color_sample`. + // + // A caller that needs per-face colour must use this rather than sampling the result again. The + // result is displaced geometry: a point on it is no longer where its base surface was, so matching + // it back by proximity colours whatever base surface happens to be nearest - which on a part thinner + // than the relief depth is the *opposite* face, picking up the texture meant for the painted one. + std::vector face_color; // Output face -> input face. Empty in Export mode, where decimation invalidates it. std::vector face_parent_id; bool safety_cap_hit = false; diff --git a/src/libslic3r/TextureBake/TextureBakeRepair.cpp b/src/libslic3r/TextureBake/TextureBakeRepair.cpp index 49f427d461..877379b574 100644 --- a/src/libslic3r/TextureBake/TextureBakeRepair.cpp +++ b/src/libslic3r/TextureBake/TextureBakeRepair.cpp @@ -72,7 +72,8 @@ size_t count_area_slivers(const TriSoup &geometry) return n; } -TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts) +TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts, + std::vector *face_color) { const size_t n_tri = geometry.triangle_count(); const double on_tol2 = opts.on_seg_tol * opts.on_seg_tol; @@ -98,7 +99,12 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts) // grid. A needle reads as watertight yet is deleted downstream, and dropping it leaves exactly // the on-edge-vertex topology the pass below closes. std::vector> faces; + // Parallel to `faces` throughout, so a split or a dropped degenerate keeps the two in step. + const bool track_color = face_color != nullptr && !face_color->empty(); + std::vector colors; faces.reserve(n_tri); + if (track_color) + colors.reserve(n_tri); for (size_t t = 0; t < n_tri; ++t) { const int a = vid[t * 3], b = vid[t * 3 + 1], c = vid[t * 3 + 2]; if (a == b || b == c || a == c) @@ -108,6 +114,8 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts) if (u.cross(w).squaredNorm() < DEGENERATE_AREA_SQ) continue; faces.push_back({ a, b, c }); + if (track_color) + colors.push_back(t < face_color->size() ? (*face_color)[t] : -1); } for (int iter = 0; iter < opts.max_iters; ++iter) { @@ -166,11 +174,16 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts) break; std::vector> next; + std::vector next_colors; next.reserve(faces.size() + splits.size() * 2); + if (track_color) + next_colors.reserve(next.capacity()); for (size_t fi = 0; fi < faces.size(); ++fi) { const auto it = splits.find(fi); if (it == splits.end()) { next.push_back(faces[fi]); + if (track_color) + next_colors.push_back(colors[fi]); continue; } const auto &f = faces[fi]; @@ -195,11 +208,18 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts) seq.insert(seq.end(), sp.mids.rbegin(), sp.mids.rend()); seq.push_back(sp.a); } - for (size_t s = 0; s + 1 < seq.size(); ++s) + for (size_t s = 0; s + 1 < seq.size(); ++s) { next.push_back({ seq[s], seq[s + 1], apex }); + if (track_color) + next_colors.push_back(colors[fi]); // every piece of a split face keeps its colour + } } faces.swap(next); + if (track_color) + colors.swap(next_colors); } + if (track_color) + *face_color = std::move(colors); TriSoup out; out.pos.reserve(faces.size() * 3); diff --git a/src/libslic3r/TextureBake/TextureBakeRepair.hpp b/src/libslic3r/TextureBake/TextureBakeRepair.hpp index 07d5323059..ed949dacff 100644 --- a/src/libslic3r/TextureBake/TextureBakeRepair.hpp +++ b/src/libslic3r/TextureBake/TextureBakeRepair.hpp @@ -43,7 +43,12 @@ struct RepairOptions int max_iters = 16; }; -TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts = {}); +// `face_color`, when given, is read as one entry per input face and rewritten to match the output: a +// face split to close a T-junction hands its colour to every piece, and a degenerate face dropped on +// the way takes its entry with it. Without this the caller would have no way to keep a per-face colour +// across this pass, which changes the triangle count. +TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts = {}, + std::vector *face_color = nullptr); } // namespace TextureBake } // namespace Slic3r diff --git a/src/libslic3r/TextureDisplacement.cpp b/src/libslic3r/TextureDisplacement.cpp index fdd23d0a08..2adf1dc125 100644 --- a/src/libslic3r/TextureDisplacement.cpp +++ b/src/libslic3r/TextureDisplacement.cpp @@ -2313,7 +2313,7 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set stats->triangles_budget = result.triangles_budget; stats->budget_limited = result.budget_limited; } - indexed_triangle_set out = TextureBake::to_indexed_triangle_set(result.geometry); + indexed_triangle_set out = TextureBake::to_indexed_triangle_set(result.geometry, &result.face_color); if (out.indices.empty()) return mesh; if (flip_normals) @@ -2334,6 +2334,8 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set max_depth = std::max(max_depth, std::abs(layer.depth_mm)); const float relief_tol = max_depth + paint_tol; std::vector palette(out.indices.size(), -1); + const std::vector &face_mask = result.face_color; + const bool have_face_mask = face_mask.size() == out.indices.size(); tbb::parallel_for(tbb::blocked_range(0, out.indices.size()), [&](const tbb::blocked_range &r) { for (size_t i = r.begin(); i < r.end(); ++i) { const stl_triangle_vertex_indices &t = out.indices[i]; @@ -2349,8 +2351,19 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set // reason; this path was the inconsistent one. Vec3f foot = centroid, base_n = Vec3f::UnitZ(); const float d2 = painted_closest(centroid, &foot, &base_n); - if (!all_painted && d2 >= relief_tol * relief_tol) + // Which faces may be coloured comes from the pipeline, which recorded it on the + // refined mesh where the paint mask is exact, and carried it through the collapse, + // the T-junction repair and the weld. Proximity cannot answer this: a displaced face + // is no longer where its base was, so on a part thinner than the relief depth the + // nearest painted surface to the *opposite* face is the painted one, and the texture + // appeared there too. Only the position to sample at still comes from the base + // surface, for the projection reason above. + if (have_face_mask) { + if (face_mask[i] == TextureBake::FACE_UNPAINTED) + continue; + } else if (!all_painted && d2 >= relief_tol * relief_tol) { continue; + } palette[i] = sampler(foot, base_n); } });