mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-28 12:29:43 +00:00
Move computation to background thread & resolve freeze
This commit is contained in:
@@ -1645,7 +1645,8 @@ indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
|
||||
float target_edge_length_mm, int max_triangles,
|
||||
std::vector<int> *out_source, const HeightFieldSampler &sampler,
|
||||
float chord_tolerance_mm, float min_edge_length_mm,
|
||||
float border_edge_length_mm)
|
||||
float border_edge_length_mm,
|
||||
const DisplacementProgressFn &progress)
|
||||
{
|
||||
// Neighbour slots that are not a triangle index.
|
||||
constexpr int NB_BOUNDARY = -1; // open edge: terminal on its own, bisected from this side alone
|
||||
@@ -1948,7 +1949,18 @@ indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
|
||||
|
||||
// Every iteration either drops one satisfied triangle from the queue or performs exactly one
|
||||
// bisection, and bisections are capped by the triangle budget, so this always terminates.
|
||||
// Progress is reported against the triangle budget, which is what the loop is bounded by. Polled
|
||||
// rather than pushed on every bisection: a refinement spends its budget in hundreds of thousands
|
||||
// of them, and every hook call wakes the UI's idle loop to repaint the notification.
|
||||
const int start_tris = int(tris.size());
|
||||
const int budget_tris = std::max(1, max_triangles - start_tris);
|
||||
int next_poll = start_tris;
|
||||
while (!queue.empty() && int(tris.size()) + 2 <= max_triangles) {
|
||||
if (progress && int(tris.size()) >= next_poll) {
|
||||
next_poll = int(tris.size()) + std::max(1024, budget_tris / 100);
|
||||
if (!progress(std::clamp((int(tris.size()) - start_tris) * 100 / budget_tris, 0, 100)))
|
||||
break; // still conformal - whole bisections only; the caller decides whether to keep it
|
||||
}
|
||||
const int ti = queue.top().second;
|
||||
queue.pop();
|
||||
if (priority(ti) <= 1.f)
|
||||
|
||||
@@ -677,13 +677,51 @@ indexed_triangle_set subdivide_mesh_uniform(const indexed_triangle_set &mesh, fl
|
||||
// rim of an unpainted island into a ring of large, steeply tilted triangles. Refining that band by
|
||||
// plain edge length is bounded (it is a thin ring, and a length target always terminates) and needs
|
||||
// no paint-aware sampler.
|
||||
//
|
||||
// `progress`, when given, is called with a 0..100 percentage of the triangle budget spent; returning
|
||||
// false stops the refinement early. What it hands back then is still a complete, conformal mesh - the
|
||||
// loop only ever finishes whole bisections - so a caller that wants to discard it has to do so itself.
|
||||
indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
|
||||
const std::vector<uint8_t> &refine_region,
|
||||
float target_edge_length_mm, int max_triangles = 1000000,
|
||||
std::vector<int> *out_source = nullptr,
|
||||
const HeightFieldSampler &sampler = nullptr,
|
||||
float chord_tolerance_mm = 0.f, float min_edge_length_mm = 0.f,
|
||||
float border_edge_length_mm = 0.f);
|
||||
float border_edge_length_mm = 0.f,
|
||||
const DisplacementProgressFn &progress = nullptr);
|
||||
|
||||
// The recipe for getting a mesh ready to receive displacement: even out the triangle density, then
|
||||
// refine it where the texture bends. Either stage is skipped when its target is <= 0. Pure data, and
|
||||
// the whole of it, so the preparation can be handed to a background job instead of running on the UI
|
||||
// thread - see GLGizmoTextureDisplacement::prepare_mesh().
|
||||
struct TextureDisplacementPrepareParams
|
||||
{
|
||||
// Isotropic remesh (CGAL). The target is clamped against the part's own surface area before it is
|
||||
// used, so a value that would produce millions of triangles cannot be asked for by accident.
|
||||
float remesh_edge_mm = 0.f;
|
||||
float remesh_sharp_deg = 0.f; // 0 = do not protect sharp edges
|
||||
|
||||
// Adaptive (Rivara) subdivision of the painted area. See subdivide_mesh_adaptive().
|
||||
float subdiv_target_mm = 0.f; // "Max edge": the length baseline, and the only criterion when
|
||||
// subdiv_feature is off
|
||||
float subdiv_detail_mm = 0.f; // "Detail": chord tolerance, feature mode only
|
||||
float subdiv_min_edge_mm = 0.f; // "Min edge": the floor under both, feature mode only
|
||||
float subdiv_border_mm = 0.f; // "Edge detail": the band straddling the paint's edge, 0 = off
|
||||
bool subdiv_feature = false; // follow texture curvature, not just edge length
|
||||
int subdiv_added_triangles = 0; // budget, *added* to the mesh's own count
|
||||
};
|
||||
|
||||
// What a preparation run produced. An empty `mesh` means there was nothing to do and the caller must
|
||||
// commit nothing - which is not a failure: a mesh that is already even needs no remesh, and one that
|
||||
// is already fine enough for the texture needs no subdivision.
|
||||
struct TextureDisplacementPrepareResult
|
||||
{
|
||||
indexed_triangle_set mesh;
|
||||
TextureDisplacementFacetsData masks;
|
||||
// The remesh landed but no layer's paint survived being carried onto it. Nothing is committed:
|
||||
// everything downstream is driven by that paint, so baking on would bake a flat mesh.
|
||||
bool paint_lost = false;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
|
||||
@@ -327,6 +327,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/Jobs/SLAImportJob.hpp
|
||||
GUI/Jobs/TextureDisplacementBakeJob.cpp
|
||||
GUI/Jobs/TextureDisplacementBakeJob.hpp
|
||||
GUI/Jobs/TextureDisplacementPrepareJob.cpp
|
||||
GUI/Jobs/TextureDisplacementPrepareJob.hpp
|
||||
GUI/Jobs/TextureDisplacementPreviewJob.cpp
|
||||
GUI/Jobs/TextureDisplacementPreviewJob.hpp
|
||||
GUI/Jobs/ThreadSafeQueue.hpp
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include "libslic3r/AABBTreeIndirect.hpp"
|
||||
#include "libslic3r/MeshBoolean.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
@@ -23,11 +24,13 @@
|
||||
#include "slic3r/GUI/TextureProjectorFrame.hpp"
|
||||
#include "slic3r/GUI/UVEditorCanvas.hpp"
|
||||
#include "slic3r/GUI/Jobs/TextureDisplacementBakeJob.hpp"
|
||||
#include "slic3r/GUI/Jobs/TextureDisplacementPrepareJob.hpp"
|
||||
#include "slic3r/GUI/Jobs/TextureDisplacementPreviewJob.hpp"
|
||||
#include "slic3r/Utils/UndoRedo.hpp"
|
||||
#include "GLGizmoUtils.hpp"
|
||||
|
||||
#include <glad/gl.h>
|
||||
#include <tbb/parallel_for.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
@@ -98,6 +101,117 @@ bool point_in_triangle_coplanar(const Vec3f &p, const std::array<Vec3f, 3> &t)
|
||||
(t[0] - t[2]).cross(p - t[2]).dot(n) >= eps;
|
||||
}
|
||||
|
||||
// Carries one texture-displacement paint mask from `src_mesh` onto `dst_mesh` - a different
|
||||
// tessellation of the same surface (an isotropic remesh, in practice).
|
||||
//
|
||||
// TriangleSelector::remap_painting(), which ModelVolume::restore_painting() uses for the four
|
||||
// standard paint channels, is not usable here. It runs one select_patch() flood fill per
|
||||
// (painted sub-triangle x overlapping target facet) pair, and select_patch() splits the *target's*
|
||||
// triangles down to a 0.02-0.05 mm edge limit along every cursor boundary, behind an
|
||||
// O(target triangles) visited-set allocation per call. A coarse import painted with a fine brush
|
||||
// carries tens of thousands of painted sub-triangles, so that is tens of millions of sub-triangles
|
||||
// created on the target - which is what made Remesh, and Standard mode's Bake (which remeshes for
|
||||
// you), appear to hang rather than finish.
|
||||
//
|
||||
// Each target triangle here asks one question instead: is the point on the source surface closest
|
||||
// to my centroid inside a painted piece? One AABB-tree query per target triangle, whole facets
|
||||
// only, no splitting - O(target log source). `src_tree` is built over `src_mesh.its` by the caller
|
||||
// and shared across the layers; `dst_to_src` brings the target's vertices into the source's frame.
|
||||
TriangleSelector::TriangleSplittingData remap_texture_paint_spatial(
|
||||
const TriangleMesh &src_mesh, const TriangleSelector::TriangleSplittingData &src_data,
|
||||
const AABBTreeIndirect::Tree3f &src_tree, const TriangleMesh &dst_mesh, const Vec3f &dst_to_src)
|
||||
{
|
||||
const indexed_triangle_set &src = src_mesh.its;
|
||||
const indexed_triangle_set &dst = dst_mesh.its;
|
||||
const size_t ntri = src.indices.size();
|
||||
if (src_data.bitstream.empty() || ntri == 0 || dst.indices.empty() || src_tree.empty())
|
||||
return {};
|
||||
|
||||
// The painted patch, split into per-source-triangle pieces - the same shape
|
||||
// collect_paint_region() builds, and for the same reason: a source triangle the brush only
|
||||
// partly covered has to answer "is this point painted" geometrically rather than be rounded to
|
||||
// painted-or-not, which leaves a ragged fringe along any curved brush boundary.
|
||||
TriangleSelector src_sel(src_mesh);
|
||||
src_sel.deserialize(src_data, false);
|
||||
std::vector<int> piece_src;
|
||||
const indexed_triangle_set patch = src_sel.get_facets_strict(EnforcerBlockerType::ENFORCER, &piece_src);
|
||||
if (patch.indices.empty())
|
||||
return {};
|
||||
|
||||
const auto tri_area2 = [](const Vec3f &a, const Vec3f &b, const Vec3f &c) {
|
||||
return (b - a).cross(c - a).norm();
|
||||
};
|
||||
std::vector<float> src_area2(ntri, 0.f), covered2(ntri, 0.f);
|
||||
for (size_t i = 0; i < ntri; ++i)
|
||||
src_area2[i] = tri_area2(src.vertices[size_t(src.indices[i][0])], src.vertices[size_t(src.indices[i][1])],
|
||||
src.vertices[size_t(src.indices[i][2])]);
|
||||
for (size_t j = 0; j < patch.indices.size() && j < piece_src.size(); ++j) {
|
||||
if (size_t(piece_src[j]) >= ntri)
|
||||
continue;
|
||||
const stl_triangle_vertex_indices &t = patch.indices[j];
|
||||
covered2[size_t(piece_src[j])] += tri_area2(patch.vertices[size_t(t[0])], patch.vertices[size_t(t[1])],
|
||||
patch.vertices[size_t(t[2])]);
|
||||
}
|
||||
std::vector<uint8_t> full(ntri, 0);
|
||||
for (size_t i = 0; i < ntri; ++i)
|
||||
full[i] = (src_area2[i] > 0.f && covered2[i] >= 0.999f * src_area2[i]) ? 1 : 0;
|
||||
|
||||
// CSR pieces, kept for partly covered sources only - a full one answers every query "painted".
|
||||
std::vector<int> part_start(ntri + 1, 0);
|
||||
for (size_t j = 0; j < patch.indices.size() && j < piece_src.size(); ++j)
|
||||
if (size_t(piece_src[j]) < ntri && !full[size_t(piece_src[j])])
|
||||
++part_start[size_t(piece_src[j]) + 1];
|
||||
for (size_t i = 0; i < ntri; ++i)
|
||||
part_start[i + 1] += part_start[i];
|
||||
std::vector<std::array<Vec3f, 3>> part;
|
||||
part.resize(size_t(part_start[ntri])); // not a constructor call: `vector<T> p(size_t(x[n]));` parses
|
||||
// as a function declaration, and every use of `part` below
|
||||
// then fails with something that does not mention the cause.
|
||||
{
|
||||
std::vector<int> fill(part_start.begin(), part_start.begin() + ntri);
|
||||
for (size_t j = 0; j < patch.indices.size() && j < piece_src.size(); ++j) {
|
||||
const size_t S = size_t(piece_src[j]);
|
||||
if (S >= ntri || full[S])
|
||||
continue;
|
||||
const stl_triangle_vertex_indices &t = patch.indices[j];
|
||||
part[size_t(fill[S]++)] = { patch.vertices[size_t(t[0])], patch.vertices[size_t(t[1])],
|
||||
patch.vertices[size_t(t[2])] };
|
||||
}
|
||||
}
|
||||
|
||||
// Classify in parallel, then write the mask serially - TriangleSelector is not thread safe.
|
||||
std::vector<uint8_t> painted(dst.indices.size(), 0);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, dst.indices.size()),
|
||||
[&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t i = range.begin(); i < range.end(); ++i) {
|
||||
const stl_triangle_vertex_indices &t = dst.indices[i];
|
||||
const Vec3f centroid = (dst.vertices[size_t(t[0])] + dst.vertices[size_t(t[1])] +
|
||||
dst.vertices[size_t(t[2])]) / 3.f + dst_to_src;
|
||||
size_t hit = 0;
|
||||
Vec3f hit_pos = Vec3f::Zero();
|
||||
if (AABBTreeIndirect::squared_distance_to_indexed_triangle_set(src.vertices, src.indices, src_tree,
|
||||
centroid, hit, hit_pos) < 0.f ||
|
||||
hit >= ntri)
|
||||
continue;
|
||||
if (full[hit]) {
|
||||
painted[i] = 1;
|
||||
continue;
|
||||
}
|
||||
for (int k = part_start[hit]; k < part_start[hit + 1]; ++k)
|
||||
if (point_in_triangle_coplanar(hit_pos, part[size_t(k)])) {
|
||||
painted[i] = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
TriangleSelector dst_sel(dst_mesh);
|
||||
for (size_t i = 0; i < painted.size(); ++i)
|
||||
if (painted[i])
|
||||
dst_sel.set_facet(int(i), EnforcerBlockerType::ENFORCER);
|
||||
return dst_sel.serialize();
|
||||
}
|
||||
|
||||
std::unique_ptr<GLTexture> upload_height_thumbnail(const DecodedHeightTexture &decoded, int max_px = THUMBNAIL_MAX_PX)
|
||||
{
|
||||
if (decoded.empty())
|
||||
@@ -2964,13 +3078,11 @@ void GLGizmoTextureDisplacement::subdivide_model()
|
||||
}
|
||||
|
||||
bool GLGizmoTextureDisplacement::collect_paint_region(
|
||||
const TriangleMesh &mesh, const TextureDisplacementFacetsData &facets,
|
||||
std::vector<uint8_t> ®ion,
|
||||
std::array<LayerPaintMap, TEXTURE_DISPLACEMENT_MAX_LAYERS> *paint) const
|
||||
std::array<LayerPaintMap, TEXTURE_DISPLACEMENT_MAX_LAYERS> *paint)
|
||||
{
|
||||
const ModelVolume *mv = texture_volume();
|
||||
if (mv == nullptr)
|
||||
return false;
|
||||
const indexed_triangle_set &its = mv->mesh().its;
|
||||
const indexed_triangle_set &its = mesh.its;
|
||||
const size_t ntri = its.indices.size();
|
||||
const size_t nvert = its.vertices.size();
|
||||
|
||||
@@ -2996,7 +3108,7 @@ bool GLGizmoTextureDisplacement::collect_paint_region(
|
||||
|
||||
bool any_paint = false;
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
const TriangleSelector::TriangleSplittingData &data = mv->texture_displacement_facet(slot).get_data();
|
||||
const TriangleSelector::TriangleSplittingData &data = facets[size_t(slot)];
|
||||
if (!TriangleSelector::has_facets(data, EnforcerBlockerType::ENFORCER))
|
||||
continue;
|
||||
|
||||
@@ -3013,7 +3125,7 @@ bool GLGizmoTextureDisplacement::collect_paint_region(
|
||||
region[m.triangle_idx] |= REFINE_PAINTED;
|
||||
|
||||
if (paint) {
|
||||
TriangleSelector sel(mv->mesh());
|
||||
TriangleSelector sel(mesh);
|
||||
sel.deserialize(data, false);
|
||||
// get_facets_strict() now reports which source triangle each sub-triangle came from, which
|
||||
// is what lets partial coverage be carried forward geometrically instead of being rounded
|
||||
@@ -3120,87 +3232,146 @@ bool GLGizmoTextureDisplacement::collect_paint_region(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GLGizmoTextureDisplacement::plan_adaptive_subdivision(const ModelVolume &mv, SubdivisionPlan &out) const
|
||||
TextureDisplacementFacetsData GLGizmoTextureDisplacement::masks_after_subdivision(
|
||||
const TriangleMesh &new_mesh, const std::vector<int> &source,
|
||||
const std::array<LayerPaintMap, TEXTURE_DISPLACEMENT_MAX_LAYERS> &paint)
|
||||
{
|
||||
if (m_subdivide_target_mm <= 0.f)
|
||||
return false;
|
||||
|
||||
std::vector<uint8_t> region;
|
||||
if (!collect_paint_region(region, &out.paint))
|
||||
return false;
|
||||
|
||||
// Feature-adaptive: sample the combined displacement so refinement follows texture curvature. A
|
||||
// null sampler (only LSCM layers, or nothing decodable) falls back to the length baseline alone.
|
||||
HeightFieldSampler sampler;
|
||||
if (m_subdivide_feature) {
|
||||
TextureDisplacementFacetsData facets{};
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
facets[size_t(i)] = mv.texture_displacement_facet(i).get_data();
|
||||
sampler = make_combined_displacement_sampler(mv.mesh().its, mv.texture_displacement_layers, facets);
|
||||
}
|
||||
|
||||
// "Min edge" is a feature-mode control (it is the floor the curvature test refines down to); in
|
||||
// plain adaptive mode the target edge length is the only criterion, so the floor must not be
|
||||
// allowed to silently override a target the user set below it.
|
||||
const float tol = m_subdivide_feature ? m_subdivide_detail_mm : 0.f;
|
||||
const float floor = m_subdivide_feature ? m_subdivide_min_edge_mm : 0.f;
|
||||
{
|
||||
wxBusyCursor wait;
|
||||
// The budget slider is "triangles the refinement may *add*", so the model's own count is the
|
||||
// baseline - otherwise the control would be meaningless (or a dead end) on a dense model.
|
||||
out.refined = subdivide_mesh_adaptive(mv.mesh().its, region, m_subdivide_target_mm,
|
||||
int(mv.mesh().its.indices.size()) + m_subdivide_budget_k * 1000,
|
||||
&out.source, sampler, tol, floor, m_subdivide_border_mm);
|
||||
}
|
||||
return out.refined.indices.size() != mv.mesh().its.indices.size();
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::apply_adaptive_subdivision(ModelVolume &mv, SubdivisionPlan &&plan)
|
||||
{
|
||||
// Other paint channels ride across via the standard remap; texture-displacement paint is rebuilt
|
||||
// below from the plan's source map, which is the whole point of driving this by the paint.
|
||||
std::optional<TriangleSelector::SavedPainting> saved_painting = mv.save_painting();
|
||||
mv.set_mesh(TriangleMesh(std::move(plan.refined)));
|
||||
mv.set_new_unique_id();
|
||||
mv.calculate_convex_hull();
|
||||
mv.restore_painting(saved_painting); // resets extra facets (incl. texture-displacement) + remaps the rest
|
||||
|
||||
// Carry each layer's paint onto the new mesh. Children inherit their parent's source triangle, and
|
||||
// subdivision only ever adds edge midpoints, so every new triangle lies inside its source and on
|
||||
// the same surface - which means the source's painted *pieces* can be queried directly by point
|
||||
// containment. That is what keeps a brush outline smooth: rounding each source to wholly painted
|
||||
// or not instead leaves a ragged fringe of isolated triangles along any curved boundary, and the
|
||||
// refined mesh then reproduces that fringe exactly rather than hiding it.
|
||||
const indexed_triangle_set &new_its = mv.mesh().its;
|
||||
// Children inherit their parent's source triangle, and subdivision only ever adds edge midpoints,
|
||||
// so every new triangle lies inside its source and on the same surface - which means the source's
|
||||
// painted *pieces* can be queried directly by point containment. That is what keeps a brush outline
|
||||
// smooth: rounding each source to wholly painted or not instead leaves a ragged fringe of isolated
|
||||
// triangles along any curved boundary, and the refined mesh then reproduces that fringe exactly
|
||||
// rather than hiding it.
|
||||
TextureDisplacementFacetsData out{};
|
||||
const indexed_triangle_set &its = new_mesh.its;
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
const LayerPaintMap &pm = plan.paint[size_t(slot)];
|
||||
const LayerPaintMap &pm = paint[size_t(slot)];
|
||||
if (pm.empty())
|
||||
continue;
|
||||
TriangleSelector sel(mv.mesh());
|
||||
for (size_t i = 0; i < plan.source.size() && i < new_its.indices.size(); ++i) {
|
||||
const size_t S = size_t(plan.source[i]);
|
||||
TriangleSelector sel(new_mesh);
|
||||
for (size_t i = 0; i < source.size() && i < its.indices.size(); ++i) {
|
||||
const size_t S = size_t(source[i]);
|
||||
if (S >= pm.full.size())
|
||||
continue;
|
||||
bool painted = pm.full[S] != 0;
|
||||
if (!painted && pm.part_start[S] != pm.part_start[S + 1]) {
|
||||
const stl_triangle_vertex_indices &t = new_its.indices[i];
|
||||
const Vec3f centroid = (new_its.vertices[size_t(t[0])] + new_its.vertices[size_t(t[1])] +
|
||||
new_its.vertices[size_t(t[2])]) / 3.f;
|
||||
const stl_triangle_vertex_indices &t = its.indices[i];
|
||||
const Vec3f centroid = (its.vertices[size_t(t[0])] + its.vertices[size_t(t[1])] +
|
||||
its.vertices[size_t(t[2])]) / 3.f;
|
||||
for (int k = pm.part_start[S]; k < pm.part_start[S + 1] && !painted; ++k)
|
||||
painted = point_in_triangle_coplanar(centroid, pm.part[size_t(k)]);
|
||||
}
|
||||
if (painted)
|
||||
sel.set_facet(int(i), EnforcerBlockerType::ENFORCER);
|
||||
}
|
||||
mv.texture_displacement_facet(slot).set(sel);
|
||||
out[size_t(slot)] = sel.serialize();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
TextureDisplacementFacetsData GLGizmoTextureDisplacement::facets_data_of(const ModelVolume &mv)
|
||||
{
|
||||
TextureDisplacementFacetsData out{};
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
out[size_t(i)] = mv.texture_displacement_facet(i).get_data();
|
||||
return out;
|
||||
}
|
||||
|
||||
TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
|
||||
const indexed_triangle_set &base, const TextureDisplacementFacetsData &masks,
|
||||
const std::vector<TextureDisplacementLayer> &layers, const TextureDisplacementPrepareParams ¶ms,
|
||||
const DisplacementProgressFn &progress)
|
||||
{
|
||||
TextureDisplacementPrepareResult out;
|
||||
const auto report = [&progress](int pct) { return !progress || progress(pct); };
|
||||
|
||||
TriangleMesh mesh(base);
|
||||
TextureDisplacementFacetsData current = masks;
|
||||
bool changed = false;
|
||||
bool had_paint = false;
|
||||
for (const TriangleSelector::TriangleSplittingData &m : masks)
|
||||
had_paint = had_paint || TriangleSelector::has_facets(m, EnforcerBlockerType::ENFORCER);
|
||||
if (!report(1))
|
||||
return out;
|
||||
|
||||
// 1. Even out the triangle density, and carry the paint onto the result. Everything downstream is
|
||||
// driven by that paint, so losing it is a hard stop rather than something to bake around - and
|
||||
// stopping here, before anything is committed, leaves the user's model exactly as it was.
|
||||
if (params.remesh_edge_mm > 0.f) {
|
||||
indexed_triangle_set remeshed;
|
||||
if (plan_remesh(mesh.its, params.remesh_edge_mm, params.remesh_sharp_deg, remeshed)) {
|
||||
TriangleMesh new_mesh(std::move(remeshed));
|
||||
const AABBTreeIndirect::Tree3f old_tree =
|
||||
AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(mesh.its.vertices, mesh.its.indices);
|
||||
TextureDisplacementFacetsData carried{};
|
||||
bool any = false;
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) {
|
||||
// Both meshes are the volume's own local frame, so there is no shift between them.
|
||||
carried[size_t(i)] =
|
||||
remap_texture_paint_spatial(mesh, current[size_t(i)], old_tree, new_mesh, Vec3f::Zero());
|
||||
any = any || !carried[size_t(i)].bitstream.empty();
|
||||
}
|
||||
mesh = std::move(new_mesh);
|
||||
current = std::move(carried);
|
||||
changed = true;
|
||||
if (had_paint && !any) {
|
||||
out.paint_lost = true;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!report(50))
|
||||
return {};
|
||||
|
||||
// 2. Refine where the texture bends - the painted area only, plus the graded band the conformal
|
||||
// closure pulls in around it.
|
||||
if (params.subdiv_target_mm > 0.f) {
|
||||
std::vector<uint8_t> region;
|
||||
std::array<LayerPaintMap, TEXTURE_DISPLACEMENT_MAX_LAYERS> paint;
|
||||
if (collect_paint_region(mesh, current, region, &paint)) {
|
||||
// Feature-adaptive: sample the combined displacement so refinement follows texture
|
||||
// curvature. A null sampler (only LSCM layers, or nothing decodable) falls back to the
|
||||
// length baseline alone.
|
||||
HeightFieldSampler sampler;
|
||||
if (params.subdiv_feature)
|
||||
sampler = make_combined_displacement_sampler(mesh.its, layers, current);
|
||||
// "Min edge" is a feature-mode control (it is the floor the curvature test refines down
|
||||
// to); in plain adaptive mode the target edge length is the only criterion, so the floor
|
||||
// must not be allowed to silently override a target the user set below it.
|
||||
const float tol = params.subdiv_feature ? params.subdiv_detail_mm : 0.f;
|
||||
const float floor = params.subdiv_feature ? params.subdiv_min_edge_mm : 0.f;
|
||||
bool aborted = false;
|
||||
std::vector<int> source;
|
||||
// The budget is "triangles the refinement may *add*", so the mesh's own count is the
|
||||
// baseline - otherwise the control would be meaningless (or a dead end) on a dense model.
|
||||
indexed_triangle_set refined =
|
||||
subdivide_mesh_adaptive(mesh.its, region, params.subdiv_target_mm,
|
||||
int(mesh.its.indices.size()) + params.subdiv_added_triangles, &source,
|
||||
sampler, tol, floor, params.subdiv_border_mm, [&](int pct) {
|
||||
aborted = !report(50 + pct / 2);
|
||||
return !aborted;
|
||||
});
|
||||
if (aborted)
|
||||
return {};
|
||||
if (refined.indices.size() != mesh.its.indices.size()) {
|
||||
mesh = TriangleMesh(std::move(refined));
|
||||
current = masks_after_subdivision(mesh, source, paint);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!report(100) || !changed)
|
||||
return out; // an empty result: nothing to commit, which is not a failure
|
||||
|
||||
out.mesh = std::move(mesh.its);
|
||||
out.masks = std::move(current);
|
||||
return out;
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::subdivide_model_adaptive()
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
ModelObject *mo = m_c->selection_info()->model_object();
|
||||
if (mv == nullptr || mo == nullptr)
|
||||
if (mv == nullptr || m_subdivide_target_mm <= 0.f)
|
||||
return;
|
||||
|
||||
update_model_object(); // flush any in-progress stroke into the committed masks first
|
||||
@@ -3211,27 +3382,16 @@ void GLGizmoTextureDisplacement::subdivide_model_adaptive()
|
||||
return;
|
||||
}
|
||||
|
||||
// Plan before the snapshot, so a no-op leaves no empty undo step - mirrors remesh_model().
|
||||
SubdivisionPlan plan;
|
||||
if (!plan_adaptive_subdivision(*mv, plan)) {
|
||||
show_error(nullptr, _u8L("Nothing to subdivide - the painted area already meets the target edge "
|
||||
"length and detail tolerance, or the triangle budget is already used up."));
|
||||
return;
|
||||
}
|
||||
|
||||
Plater *plater = wxGetApp().plater();
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Adaptive subdivide for texture displacement"), UndoRedo::SnapshotType::GizmoAction);
|
||||
apply_adaptive_subdivision(*mv, std::move(plan));
|
||||
|
||||
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
||||
const ModelObjectPtrs &objs = plater->model().objects;
|
||||
auto it = std::find(objs.begin(), objs.end(), mo);
|
||||
if (it != objs.end())
|
||||
obj_list->update_info_items(size_t(it - objs.begin()));
|
||||
}
|
||||
plater->changed_object(*mo);
|
||||
update_from_model_object(false); // reload selectors/preview against the new mesh + carried paint
|
||||
m_parent.set_as_dirty();
|
||||
TextureDisplacementPrepareParams params;
|
||||
params.subdiv_target_mm = m_subdivide_target_mm;
|
||||
params.subdiv_detail_mm = m_subdivide_detail_mm;
|
||||
params.subdiv_min_edge_mm = m_subdivide_min_edge_mm;
|
||||
params.subdiv_border_mm = m_subdivide_border_mm;
|
||||
params.subdiv_feature = m_subdivide_feature;
|
||||
params.subdiv_added_triangles = m_subdivide_budget_k * 1000;
|
||||
queue_prepare(params, _u8L("Adaptive subdivide for texture displacement"), /* then_bake */ false,
|
||||
_u8L("Nothing to subdivide - the painted area already meets the target edge length and "
|
||||
"detail tolerance, or the triangle budget is already used up."));
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::smooth_model()
|
||||
@@ -3318,49 +3478,35 @@ void GLGizmoTextureDisplacement::smooth_model()
|
||||
m_parent.set_as_dirty();
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::replace_mesh_keep_all_paint(ModelVolume &mv, TriangleMesh &&new_mesh)
|
||||
{
|
||||
const indexed_triangle_set old_its = mv.mesh().its;
|
||||
std::array<TriangleSelector::TriangleSplittingData, TEXTURE_DISPLACEMENT_MAX_LAYERS> saved_texture;
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
saved_texture[size_t(i)] = mv.texture_displacement_facet(i).get_data();
|
||||
std::optional<TriangleSelector::SavedPainting> saved_painting = mv.save_painting();
|
||||
|
||||
mv.set_mesh(std::move(new_mesh));
|
||||
mv.set_new_unique_id();
|
||||
mv.calculate_convex_hull();
|
||||
mv.restore_painting(saved_painting); // clears the extra facets, then remaps the four standard channels
|
||||
|
||||
// ... and the same spatial remap for the texture-displacement masks, which restore_painting() knows
|
||||
// nothing about. Without this a remesh would wipe the texture paint, which is fine for a standalone
|
||||
// "Remesh" click but fatal for Standard mode's pipeline: it remeshes *after* the user has painted,
|
||||
// and the subdivision and displacement that follow are both driven by that paint.
|
||||
const Transform3d to_source = Slic3r::Geometry::translation_transform(mv.mesh().get_init_shift());
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) {
|
||||
if (saved_texture[size_t(i)].bitstream.empty())
|
||||
continue;
|
||||
TriangleSelector::TriangleSplittingData remapped =
|
||||
TriangleSelector::remap_painting(old_its, saved_texture[size_t(i)], mv.mesh().its, to_source,
|
||||
{}); // no existing paint to merge with: set_mesh() just cleared it
|
||||
if (!remapped.bitstream.empty())
|
||||
mv.texture_displacement_facet(i).set_data(std::move(remapped));
|
||||
}
|
||||
}
|
||||
|
||||
bool GLGizmoTextureDisplacement::plan_remesh(const ModelVolume &mv, float target_edge_mm, float sharp_angle_deg,
|
||||
TriangleMesh &out)
|
||||
bool GLGizmoTextureDisplacement::plan_remesh(const indexed_triangle_set &src, float target_edge_mm,
|
||||
float sharp_angle_deg, indexed_triangle_set &out)
|
||||
{
|
||||
if (target_edge_mm <= 0.f)
|
||||
return false;
|
||||
|
||||
// CGAL isotropic remeshing can be slow on a big mesh, which is why this is separated from applying
|
||||
// it: the caller runs it before taking the snapshot so a failure leaves no empty undo step.
|
||||
const indexed_triangle_set &src = mv.mesh().its;
|
||||
indexed_triangle_set remeshed;
|
||||
{
|
||||
wxBusyCursor wait;
|
||||
remeshed = MeshBoolean::cgal::remesh_isotropic(src, double(target_edge_mm), 3, double(sharp_angle_deg));
|
||||
// Its cost and memory grow with the *square* of 1/target_edge, and it runs single-threaded on the
|
||||
// UI thread, so the target has to be bounded against the part's actual surface area rather than
|
||||
// taken at face value. Standard mode asks for 1 mm whatever it is handed, and the slider goes down
|
||||
// to 0.1 mm: on a 150 mm part those are ~500 k and ~50 M triangles respectively - minutes to hours
|
||||
// of frozen UI, which is indistinguishable from a hang. Raising the target instead still gives the
|
||||
// subdivider the even density it needs, and the subdivision that follows is where the detail was
|
||||
// always going to come from anyway. An equilateral triangle of edge L covers sqrt(3)/4 * L^2.
|
||||
static constexpr double REMESH_MAX_TRIANGLES = 150000.0;
|
||||
double area = 0.0;
|
||||
for (const stl_triangle_vertex_indices &tri : src.indices)
|
||||
area += 0.5 * double((src.vertices[size_t(tri[1])] - src.vertices[size_t(tri[0])])
|
||||
.cross(src.vertices[size_t(tri[2])] - src.vertices[size_t(tri[0])])
|
||||
.norm());
|
||||
if (const double budget_edge = std::sqrt(area / (0.4330127 * REMESH_MAX_TRIANGLES));
|
||||
budget_edge > double(target_edge_mm)) {
|
||||
BOOST_LOG_TRIVIAL(info) << "Texture displacement: remesh target raised from " << target_edge_mm
|
||||
<< " mm to " << budget_edge << " mm to stay within "
|
||||
<< int(REMESH_MAX_TRIANGLES) << " triangles.";
|
||||
target_edge_mm = float(budget_edge);
|
||||
}
|
||||
|
||||
indexed_triangle_set remeshed =
|
||||
MeshBoolean::cgal::remesh_isotropic(src, double(target_edge_mm), 3, double(sharp_angle_deg));
|
||||
// remesh_isotropic() signals failure by handing the input straight back, so compare against it
|
||||
// structurally. Vertex count alone is not enough: a remesh that only redistributes triangles at
|
||||
// roughly the current density legitimately lands on the same count, and treating that as failure
|
||||
@@ -3370,39 +3516,23 @@ bool GLGizmoTextureDisplacement::plan_remesh(const ModelVolume &mv, float target
|
||||
remeshed.indices == src.indices))
|
||||
return false;
|
||||
|
||||
out = TriangleMesh(std::move(remeshed));
|
||||
out = std::move(remeshed);
|
||||
return true;
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::remesh_model()
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
ModelObject *mo = m_c->selection_info()->model_object();
|
||||
if (mv == nullptr || mo == nullptr || m_remesh_target_edge_mm <= 0.f)
|
||||
if (texture_volume() == nullptr || m_remesh_target_edge_mm <= 0.f)
|
||||
return;
|
||||
|
||||
TriangleMesh remeshed;
|
||||
if (!plan_remesh(*mv, m_remesh_target_edge_mm, m_remesh_keep_sharp_edges ? m_remesh_sharp_angle_deg : 0.f,
|
||||
remeshed)) {
|
||||
show_error(nullptr, _u8L("Remeshing did not change the model. It may be non-manifold (open edges or "
|
||||
"edges shared by more than two triangles), or the target edge length may "
|
||||
"already be met."));
|
||||
return;
|
||||
}
|
||||
update_model_object(); // the paint rides across the remesh, so flush any in-progress stroke first
|
||||
|
||||
Plater *plater = wxGetApp().plater();
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Remesh model for texture displacement"), UndoRedo::SnapshotType::GizmoAction);
|
||||
replace_mesh_keep_all_paint(*mv, std::move(remeshed));
|
||||
|
||||
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
||||
const ModelObjectPtrs &objs = plater->model().objects;
|
||||
auto it = std::find(objs.begin(), objs.end(), mo);
|
||||
if (it != objs.end())
|
||||
obj_list->update_info_items(size_t(it - objs.begin()));
|
||||
}
|
||||
plater->changed_object(*mo);
|
||||
update_from_model_object(false);
|
||||
m_parent.set_as_dirty();
|
||||
TextureDisplacementPrepareParams params;
|
||||
params.remesh_edge_mm = m_remesh_target_edge_mm;
|
||||
params.remesh_sharp_deg = m_remesh_keep_sharp_edges ? m_remesh_sharp_angle_deg : 0.f;
|
||||
queue_prepare(params, _u8L("Remesh model for texture displacement"), /* then_bake */ false,
|
||||
_u8L("Remeshing did not change the model. It may be non-manifold (open edges or edges "
|
||||
"shared by more than two triangles), or the target edge length may already be met."));
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::rebuild_subdivide_preview()
|
||||
@@ -3419,16 +3549,13 @@ void GLGizmoTextureDisplacement::rebuild_subdivide_preview()
|
||||
if (m_subdivide_adaptive) {
|
||||
if (m_subdivide_target_mm <= 0.f)
|
||||
return;
|
||||
std::vector<uint8_t> region;
|
||||
if (!collect_paint_region(region, nullptr))
|
||||
const TextureDisplacementFacetsData facets = facets_data_of(*mv);
|
||||
std::vector<uint8_t> region;
|
||||
if (!collect_paint_region(mv->mesh(), facets, region, nullptr))
|
||||
return; // nothing painted yet: nothing to preview
|
||||
HeightFieldSampler sampler;
|
||||
if (m_subdivide_feature) {
|
||||
TextureDisplacementFacetsData facets{};
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
facets[size_t(i)] = mv->texture_displacement_facet(i).get_data();
|
||||
if (m_subdivide_feature)
|
||||
sampler = make_combined_displacement_sampler(mv->mesh().its, mv->texture_displacement_layers, facets);
|
||||
}
|
||||
// Feature mode: curvature (Detail tolerance) on top of the Max-edge baseline, down to the
|
||||
// Min-edge floor. Plain adaptive: tol 0, so only the target-edge-length criterion applies.
|
||||
const float tol = m_subdivide_feature ? m_subdivide_detail_mm : 0.f;
|
||||
@@ -3605,8 +3732,7 @@ bool GLGizmoTextureDisplacement::apply_standard_mode_presets(ModelVolume *mv)
|
||||
void GLGizmoTextureDisplacement::bake_standard()
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
ModelObject *mo = m_c->selection_info()->model_object();
|
||||
if (mv == nullptr || mo == nullptr || m_bake_in_progress)
|
||||
if (mv == nullptr || m_bake_in_progress || m_prepare_in_progress)
|
||||
return;
|
||||
|
||||
update_model_object(); // flush the active layer's in-progress strokes before anything reads them
|
||||
@@ -3616,63 +3742,70 @@ void GLGizmoTextureDisplacement::bake_standard()
|
||||
}
|
||||
apply_standard_mode_presets(mv); // belt and braces: never bake with values the panel is not showing
|
||||
|
||||
// Both stages are planned before the snapshot so a stage that has nothing to do is simply skipped.
|
||||
// Skipping is normal, not a failure: a mesh that is already even needs no remesh, and one that is
|
||||
// already fine enough for the texture needs no subdivision.
|
||||
TriangleMesh remeshed;
|
||||
const bool do_remesh = plan_remesh(*mv, STD_REMESH_EDGE_MM, STD_REMESH_SHARP_DEG, remeshed);
|
||||
// The whole recipe in one go. Either stage having nothing to do is normal, not a failure - a mesh
|
||||
// that is already even needs no remesh, one that is already fine enough for the texture needs no
|
||||
// subdivision - so no "nothing changed" message here: it goes straight on to the displacement.
|
||||
TextureDisplacementPrepareParams params;
|
||||
params.remesh_edge_mm = STD_REMESH_EDGE_MM;
|
||||
params.remesh_sharp_deg = STD_REMESH_SHARP_DEG;
|
||||
params.subdiv_target_mm = STD_SUBDIV_MAX_EDGE_MM;
|
||||
params.subdiv_detail_mm = STD_SUBDIV_DETAIL_MM;
|
||||
params.subdiv_min_edge_mm = STD_SUBDIV_MIN_EDGE_MM;
|
||||
params.subdiv_border_mm = STD_SUBDIV_BORDER_MM;
|
||||
params.subdiv_feature = true;
|
||||
params.subdiv_added_triangles = m_subdivide_budget_k * 1000;
|
||||
queue_prepare(params, _u8L("Bake texture displacement"), /* then_bake */ true, {});
|
||||
}
|
||||
|
||||
Plater *plater = wxGetApp().plater();
|
||||
bool paint_transfer_failed = false;
|
||||
{
|
||||
// ONE undo step for the whole pipeline. take_snapshot() records the state *before* the change,
|
||||
// so a single Undo goes all the way back to the untouched mesh - which is the only thing "undo
|
||||
// the bake" can sensibly mean when the bake is also what prepared the geometry. The background
|
||||
// displacement job is told not to add its own (see queue_texture_displacement_bake's
|
||||
// take_snapshot), because an undo step landing between the subdivision and the displacement
|
||||
// leaves a mesh with 1.5 M extra triangles and no relief on it - and baking again from there
|
||||
// subdivides that mesh a second time.
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Bake texture displacement"),
|
||||
UndoRedo::SnapshotType::GizmoAction);
|
||||
if (do_remesh)
|
||||
replace_mesh_keep_all_paint(*mv, std::move(remeshed));
|
||||
|
||||
// The remesh carries the paint across spatially, but if that remap came back empty the rest of
|
||||
// the pipeline has nothing to work from - stop here rather than silently baking a flat mesh.
|
||||
// Note this cannot just `return`: the remesh above has already replaced the volume's mesh, so
|
||||
// the scene and the gizmo's own TriangleSelectors still have to be brought back into step with
|
||||
// it below. Returning from here left the gizmo painting and raycasting against a mesh that no
|
||||
// longer existed.
|
||||
paint_transfer_failed = !mv->is_texture_displacement_painted();
|
||||
|
||||
// Planning the subdivision has to happen inside the snapshot because it reads the mesh the
|
||||
// remesh just produced. It is the expensive step, but by here we are committed anyway.
|
||||
if (!paint_transfer_failed) {
|
||||
SubdivisionPlan plan;
|
||||
if (plan_adaptive_subdivision(*mv, plan))
|
||||
apply_adaptive_subdivision(*mv, std::move(plan));
|
||||
}
|
||||
}
|
||||
|
||||
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
||||
const ModelObjectPtrs &objs = plater->model().objects;
|
||||
auto it = std::find(objs.begin(), objs.end(), mo);
|
||||
if (it != objs.end())
|
||||
obj_list->update_info_items(size_t(it - objs.begin()));
|
||||
}
|
||||
plater->changed_object(*mo);
|
||||
update_from_model_object(false); // reload selectors against the prepared mesh + carried paint
|
||||
m_parent.set_as_dirty();
|
||||
|
||||
if (paint_transfer_failed) {
|
||||
show_error(nullptr, _u8L("The painted area could not be transferred onto the remeshed model. Undo, "
|
||||
"then switch to Pro mode to prepare the mesh before painting."));
|
||||
void GLGizmoTextureDisplacement::queue_prepare(const TextureDisplacementPrepareParams ¶ms,
|
||||
const std::string &snapshot_name, bool then_bake,
|
||||
const std::string &unchanged_msg)
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
if (mv == nullptr || m_prepare_in_progress || m_bake_in_progress)
|
||||
return;
|
||||
}
|
||||
|
||||
// ... and finally the displacement itself, in the background exactly as the Pro-mode button does -
|
||||
// except that it commits into the snapshot taken above instead of pushing another one.
|
||||
bake(/* own_snapshot */ false);
|
||||
TextureDisplacementPrepareInput input;
|
||||
input.volume_id = mv->id();
|
||||
input.base_mesh = mv->mesh().its;
|
||||
input.masks = facets_data_of(*mv);
|
||||
input.layers = mv->texture_displacement_layers;
|
||||
input.params = params;
|
||||
input.snapshot_name = snapshot_name;
|
||||
|
||||
m_prepare_in_progress = true;
|
||||
queue_texture_displacement_prepare(std::move(input), [this, then_bake, unchanged_msg](
|
||||
TextureDisplacementPrepareOutcome outcome) {
|
||||
m_prepare_in_progress = false;
|
||||
// The commit replaced the volume's mesh (new id, new topology) without changing the object's id
|
||||
// or volume count, which is not something GLGizmoPainterBase::data_changed() can detect - so the
|
||||
// reload is explicit, exactly as it is after a bake. Done for every outcome: even a run that
|
||||
// committed nothing may have left the panel showing a stale triangle count.
|
||||
if (m_state == On && m_c->selection_info() && m_c->selection_info()->model_object())
|
||||
update_from_model_object(false);
|
||||
m_parent.set_as_dirty();
|
||||
|
||||
switch (outcome) {
|
||||
case TextureDisplacementPrepareOutcome::Failed:
|
||||
return; // cancelled, or the volume went away while the job ran - say nothing, do nothing
|
||||
case TextureDisplacementPrepareOutcome::PaintLost:
|
||||
show_error(nullptr, _u8L("The painted area could not be carried onto the remeshed model, so "
|
||||
"nothing was changed. Switch to Pro mode and remesh before painting."));
|
||||
return;
|
||||
case TextureDisplacementPrepareOutcome::Unchanged:
|
||||
if (!unchanged_msg.empty())
|
||||
show_error(nullptr, unchanged_msg);
|
||||
break;
|
||||
case TextureDisplacementPrepareOutcome::Committed:
|
||||
break;
|
||||
}
|
||||
// ... and then the displacement itself, in the background exactly as the Pro-mode button does.
|
||||
// It commits into the snapshot the prepare opened - but only if the prepare opened one: a run
|
||||
// that found nothing to do took none, and a bake chained onto that has to push its own or it
|
||||
// would not be undoable at all.
|
||||
if (then_bake)
|
||||
bake(/* own_snapshot */ outcome == TextureDisplacementPrepareOutcome::Unchanged);
|
||||
});
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::render_paint_cursor_hint()
|
||||
@@ -4686,7 +4819,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
"commit it, or Done to leave the model as it is."),
|
||||
m_imgui->scaled(20.f));
|
||||
} else {
|
||||
m_imgui->disabled_begin(!subdivide_ready);
|
||||
m_imgui->disabled_begin(!subdivide_ready || m_prepare_in_progress || m_bake_in_progress);
|
||||
if (m_imgui->button(_u8L("Apply"))) {
|
||||
if (m_subdivide_adaptive) {
|
||||
subdivide_model_adaptive(); // refines only the painted area, carrying the paint forward
|
||||
@@ -4752,7 +4885,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_imgui->scaled(20.f));
|
||||
}
|
||||
|
||||
m_imgui->disabled_begin(mv == nullptr);
|
||||
m_imgui->disabled_begin(mv == nullptr || m_prepare_in_progress || m_bake_in_progress);
|
||||
if (m_imgui->button(_u8L("Remesh")))
|
||||
remesh_model();
|
||||
m_imgui->disabled_end();
|
||||
@@ -4783,8 +4916,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_imgui->disabled_end();
|
||||
|
||||
ImGui::SameLine();
|
||||
m_imgui->disabled_begin(m_bake_in_progress || mv == nullptr || !mv->is_texture_displacement_painted());
|
||||
if (m_imgui->button(m_bake_in_progress ? _L("Baking...") : m_desc.at("bake"))) {
|
||||
m_imgui->disabled_begin(m_bake_in_progress || m_prepare_in_progress || mv == nullptr ||
|
||||
!mv->is_texture_displacement_painted());
|
||||
if (m_imgui->button(m_prepare_in_progress ? _L("Preparing...") :
|
||||
m_bake_in_progress ? _L("Baking...") :
|
||||
m_desc.at("bake"))) {
|
||||
// Standard mode's Bake is the whole pipeline (remesh -> refine -> displace); Pro's is only the
|
||||
// displacement, because there the user has already prepared the mesh with the controls above.
|
||||
if (pro_mode())
|
||||
|
||||
@@ -32,6 +32,24 @@ class GLGizmoTextureDisplacement : public GLGizmoPainterBase
|
||||
public:
|
||||
GLGizmoTextureDisplacement(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id);
|
||||
|
||||
// The whole of mesh preparation - remesh, carry the paint across, refine where the texture bends -
|
||||
// as one pure function over plain data: no ModelVolume, no Model, no GUI, no undo. That is what lets
|
||||
// TextureDisplacementPrepareJob run it on the job worker instead of on the UI thread, where CGAL's
|
||||
// remesher and a several-hundred-thousand-triangle refinement together freeze the window for tens
|
||||
// of seconds with nothing to look at and no way to cancel.
|
||||
//
|
||||
// `progress` is called with 0..100 and aborts the run when it returns false; an aborted run reports
|
||||
// an empty result. An empty result is also how "nothing needed doing" is reported - see
|
||||
// TextureDisplacementPrepareResult.
|
||||
static TextureDisplacementPrepareResult prepare_mesh(const indexed_triangle_set &base,
|
||||
const TextureDisplacementFacetsData &masks,
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementPrepareParams ¶ms,
|
||||
const DisplacementProgressFn &progress);
|
||||
// The volume's eight texture-displacement masks, gathered into the array every pure function here
|
||||
// (and every job input) takes.
|
||||
static TextureDisplacementFacetsData facets_data_of(const ModelVolume &mv);
|
||||
|
||||
void render_painter_gizmo() override;
|
||||
|
||||
// Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/
|
||||
@@ -88,20 +106,26 @@ private:
|
||||
// Standard mode's Bake: remesh to an even density, refine where the texture bends, then displace.
|
||||
// The order matters and is the whole reason this is one button - a height map can only move
|
||||
// existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop
|
||||
// the paint if it were not remapped across (see replace_mesh_keep_all_paint()).
|
||||
// the paint if it were not carried across (see prepare_mesh()).
|
||||
void bake_standard();
|
||||
|
||||
// Replaces `mv`'s mesh and carries *every* paint channel onto it, texture displacement included -
|
||||
// the four standard channels via ModelVolume::restore_painting(), the eight texture-displacement
|
||||
// masks via the same TriangleSelector::remap_painting() spatial remap, which restore_painting()
|
||||
// does not cover. Shared by Remesh and by Standard mode's pipeline.
|
||||
static void replace_mesh_keep_all_paint(ModelVolume &mv, TriangleMesh &&new_mesh);
|
||||
// Remesh and adaptive Subdivide are each split into a *plan* (the heavy geometry work, touching
|
||||
// nothing) and an *apply* (pure mutation). That split is what lets both the standalone buttons and
|
||||
// Standard mode's one-button pipeline run the expensive part **before** taking the undo snapshot,
|
||||
// so a run that turns out to be a no-op does not leave an empty undo step behind - and it keeps
|
||||
// snapshot ownership with the caller, which matters because the buttons want one snapshot per click
|
||||
// while the pipeline wants a single one around remesh + subdivide together.
|
||||
// Queues one prepare_mesh() run on the job worker and commits its result when it lands. Every
|
||||
// mesh-preparation button goes through here - Pro's Remesh, Pro's adaptive Subdivide and Standard's
|
||||
// Bake differ only in which stages `params` enables and in what happens afterwards:
|
||||
// - `snapshot_name` is the single undo step the commit opens. With `then_bake` it is also the step
|
||||
// the displacement job that follows commits into, rather than pushing its own - an undo landing
|
||||
// between the two would leave a mesh carrying every added triangle and no relief on it, and
|
||||
// baking again from there would prepare it a second time.
|
||||
// - `unchanged_msg`, when not empty, is shown if the run had nothing to do. Standard's Bake passes
|
||||
// nothing: a mesh that already meets the criteria is not an error there, it just goes straight
|
||||
// on to the displacement.
|
||||
void queue_prepare(const TextureDisplacementPrepareParams ¶ms, const std::string &snapshot_name,
|
||||
bool then_bake, const std::string &unchanged_msg);
|
||||
// Set from queue_prepare() until its job's result has been committed. Distinct from
|
||||
// m_bake_in_progress because Standard's Bake sets both in turn, and because every button that would
|
||||
// read or replace the mesh has to stay disabled for the whole of it.
|
||||
bool m_prepare_in_progress = false;
|
||||
|
||||
// How one layer's paint sits on the pre-subdivision mesh, precise enough to carry across the
|
||||
// refinement without rounding each source triangle to wholly painted or not.
|
||||
//
|
||||
@@ -117,17 +141,16 @@ private:
|
||||
std::vector<std::array<Vec3f, 3>> part; // painted pieces of partly covered source triangles
|
||||
bool empty() const { return full.empty(); }
|
||||
};
|
||||
struct SubdivisionPlan
|
||||
{
|
||||
indexed_triangle_set refined;
|
||||
std::vector<int> source; // new tri -> input tri
|
||||
std::array<LayerPaintMap, TEXTURE_DISPLACEMENT_MAX_LAYERS> paint; // per layer
|
||||
};
|
||||
// False means "nothing to refine" and `out` must not be used.
|
||||
bool plan_adaptive_subdivision(const ModelVolume &mv, SubdivisionPlan &out) const;
|
||||
static void apply_adaptive_subdivision(ModelVolume &mv, SubdivisionPlan &&plan);
|
||||
// False means the remesh failed or changed nothing (CGAL signals failure by handing the input back).
|
||||
static bool plan_remesh(const ModelVolume &mv, float target_edge_mm, float sharp_angle_deg, TriangleMesh &out);
|
||||
// Rebuilds every layer's mask on a subdivided mesh from `source` (new triangle -> the input triangle
|
||||
// it descends from) and the pre-subdivision coverage in `paint`.
|
||||
static TextureDisplacementFacetsData masks_after_subdivision(
|
||||
const TriangleMesh &new_mesh, const std::vector<int> &source,
|
||||
const std::array<LayerPaintMap, TEXTURE_DISPLACEMENT_MAX_LAYERS> &paint);
|
||||
// False means the remesh failed or changed nothing (CGAL signals failure by handing the input back),
|
||||
// and `out` must not be used. `target_edge_mm` is a request rather than a promise: it is clamped
|
||||
// against the part's surface area first, because CGAL's cost grows with the square of 1/target.
|
||||
static bool plan_remesh(const indexed_triangle_set &src, float target_edge_mm, float sharp_angle_deg,
|
||||
indexed_triangle_set &out);
|
||||
|
||||
// Marks every facet of every model-part volume as painted for the currently active layer -
|
||||
// "whole model" as an alternative to brushing/clicking every triangle by hand.
|
||||
@@ -417,8 +440,9 @@ private:
|
||||
// painted area plus the band straddling its edge. If `paint` is non-null, also fills the per-layer
|
||||
// coverage map the subdivision carries forward - the expensive half, skipped by the live preview,
|
||||
// which only needs the region. Returns false when nothing is painted at all.
|
||||
bool collect_paint_region(std::vector<uint8_t> ®ion,
|
||||
std::array<LayerPaintMap, TEXTURE_DISPLACEMENT_MAX_LAYERS> *paint) const;
|
||||
static bool collect_paint_region(const TriangleMesh &mesh, const TextureDisplacementFacetsData &facets,
|
||||
std::vector<uint8_t> ®ion,
|
||||
std::array<LayerPaintMap, TEXTURE_DISPLACEMENT_MAX_LAYERS> *paint);
|
||||
|
||||
// Runs the volume's TextureDisplacementOptions smoothing over the *already committed* geometry,
|
||||
// restricted to the painted area. The same settings are folded into Preview/Bake automatically;
|
||||
@@ -430,9 +454,9 @@ private:
|
||||
// Isotropic remeshing (CGAL) to even out wildly varying triangle sizes so displacement has a
|
||||
// consistent density to work with. Target edge length in mm; 0 means "not yet initialised", filled
|
||||
// with the mesh's mean edge length the first time the control is shown. Like subdivide, it replaces
|
||||
// the geometry, but unlike subdivide it keeps every paint channel: replace_mesh_keep_all_paint()
|
||||
// remaps the texture-displacement masks spatially, which is also what lets Standard mode remesh
|
||||
// *after* the user has painted.
|
||||
// the geometry, but unlike subdivide it keeps every paint channel: prepare_mesh() carries the
|
||||
// texture-displacement masks across spatially, which is also what lets Standard mode remesh *after*
|
||||
// the user has painted.
|
||||
float m_remesh_target_edge_mm = 0.f;
|
||||
// Dihedral angle above which an edge counts as a hard feature and is held fixed by the remesher.
|
||||
// Off by default would round every sharp edge off, so this is on; 0 disables the protection.
|
||||
|
||||
117
src/slic3r/GUI/Jobs/TextureDisplacementPrepareJob.cpp
Normal file
117
src/slic3r/GUI/Jobs/TextureDisplacementPrepareJob.cpp
Normal file
@@ -0,0 +1,117 @@
|
||||
#include "TextureDisplacementPrepareJob.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TriangleSelector.hpp"
|
||||
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
|
||||
#include "slic3r/Utils/UndoRedo.hpp"
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
TextureDisplacementPrepareJob::TextureDisplacementPrepareJob(
|
||||
TextureDisplacementPrepareInput &&input, std::function<void(TextureDisplacementPrepareOutcome)> on_finished)
|
||||
: m_input(std::move(input)), m_on_finished(std::move(on_finished))
|
||||
{
|
||||
}
|
||||
|
||||
void TextureDisplacementPrepareJob::process(Ctl &ctl)
|
||||
{
|
||||
const std::string status = _u8L("Preparing mesh for texture displacement");
|
||||
ctl.update_status(1, status);
|
||||
|
||||
// Only ever touches m_input (captured by value before this job was queued) and local state - never
|
||||
// the live Model - so this is safe to run concurrently with the UI thread.
|
||||
//
|
||||
// The progress hook is not just cosmetic: the framework's notification only grows a close button
|
||||
// once it reaches 100%, and below that it shows Cancel, which is wired through here. Reporting is
|
||||
// throttled to whole percentage points because every call repaints the notification and wakes the
|
||||
// idle loop.
|
||||
int last_reported = 1;
|
||||
m_result = GLGizmoTextureDisplacement::prepare_mesh(m_input.base_mesh, m_input.masks, m_input.layers,
|
||||
m_input.params,
|
||||
[&ctl, &status, &last_reported](int percent) {
|
||||
if (ctl.was_canceled())
|
||||
return false;
|
||||
if (percent > last_reported) {
|
||||
last_reported = percent;
|
||||
ctl.update_status(percent, status);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
ctl.update_status(100, status); // always finish at 100: this is what closes the notification
|
||||
}
|
||||
|
||||
void TextureDisplacementPrepareJob::finalize(bool canceled, std::exception_ptr &eptr)
|
||||
{
|
||||
TextureDisplacementPrepareOutcome outcome = TextureDisplacementPrepareOutcome::Failed;
|
||||
struct OnExit
|
||||
{
|
||||
std::function<void(TextureDisplacementPrepareOutcome)> fn;
|
||||
const TextureDisplacementPrepareOutcome *outcome;
|
||||
~OnExit() { if (fn) fn(*outcome); }
|
||||
} on_exit{m_on_finished, &outcome};
|
||||
|
||||
if (canceled || eptr)
|
||||
return;
|
||||
if (m_result.paint_lost) {
|
||||
outcome = TextureDisplacementPrepareOutcome::PaintLost;
|
||||
return;
|
||||
}
|
||||
if (m_result.mesh.indices.empty()) {
|
||||
outcome = TextureDisplacementPrepareOutcome::Unchanged;
|
||||
return;
|
||||
}
|
||||
|
||||
Plater *plater = wxGetApp().plater();
|
||||
ModelVolume *volume = get_model_volume(m_input.volume_id, plater->model().objects);
|
||||
// The lookup doubles as a staleness check: anything that replaces a volume's mesh also gives it a
|
||||
// new id (set_new_unique_id()), so a prepare computed from a mesh that has since been replaced -
|
||||
// by an undo, another bake, or a boolean - simply fails to find its volume and commits nothing.
|
||||
if (volume == nullptr)
|
||||
return;
|
||||
ModelObject *object = volume->get_object();
|
||||
if (object == nullptr)
|
||||
return;
|
||||
|
||||
{
|
||||
Plater::TakeSnapshot snapshot(plater, m_input.snapshot_name, UndoRedo::SnapshotType::GizmoAction);
|
||||
|
||||
// The four standard paint channels ride across on ModelVolume's own spatial remap. The eight
|
||||
// texture-displacement masks do not go through it - prepare_mesh() has already carried them,
|
||||
// exactly, from the source triangles the refinement records - so they are put back after
|
||||
// restore_painting(), which resets every extra facet before remapping the channels it knows.
|
||||
std::optional<TriangleSelector::SavedPainting> saved_painting = volume->save_painting();
|
||||
volume->set_mesh(TriangleMesh(std::move(m_result.mesh)));
|
||||
volume->set_new_unique_id();
|
||||
volume->calculate_convex_hull();
|
||||
volume->restore_painting(saved_painting);
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
volume->texture_displacement_facet(i).set_data(std::move(m_result.masks[size_t(i)]));
|
||||
|
||||
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
||||
const ModelObjectPtrs &objs = plater->model().objects;
|
||||
auto it = std::find(objs.begin(), objs.end(), object);
|
||||
if (it != objs.end())
|
||||
obj_list->update_info_items(size_t(it - objs.begin()));
|
||||
}
|
||||
plater->changed_object(*object);
|
||||
}
|
||||
outcome = TextureDisplacementPrepareOutcome::Committed;
|
||||
}
|
||||
|
||||
void queue_texture_displacement_prepare(TextureDisplacementPrepareInput &&input,
|
||||
std::function<void(TextureDisplacementPrepareOutcome)> on_finished)
|
||||
{
|
||||
auto &worker = wxGetApp().plater()->get_ui_job_worker();
|
||||
queue_job(worker, std::make_unique<TextureDisplacementPrepareJob>(std::move(input), std::move(on_finished)));
|
||||
}
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
72
src/slic3r/GUI/Jobs/TextureDisplacementPrepareJob.hpp
Normal file
72
src/slic3r/GUI/Jobs/TextureDisplacementPrepareJob.hpp
Normal file
@@ -0,0 +1,72 @@
|
||||
#ifndef slic3r_TextureDisplacementPrepareJob_hpp_
|
||||
#define slic3r_TextureDisplacementPrepareJob_hpp_
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Job.hpp"
|
||||
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
class ModelVolume;
|
||||
}
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
// Getting a mesh ready to receive displacement - the isotropic remesh, carrying the paint onto it, and
|
||||
// the adaptive refinement - off the UI thread.
|
||||
//
|
||||
// All three used to run inline behind a wxBusyCursor, which on any part big enough to matter meant tens
|
||||
// of seconds with the window not repainting and no way to stop it: CGAL's remesher is single threaded
|
||||
// and its cost grows with the square of 1/target_edge, and the refinement that follows spends a budget
|
||||
// of hundreds of thousands of triangles. Indistinguishable from a hang, and reported as one.
|
||||
//
|
||||
// The work itself is GLGizmoTextureDisplacement::prepare_mesh(), which is pure - it takes an
|
||||
// indexed_triangle_set and the layers' masks and returns new ones, touching no Model and no GUI - so all
|
||||
// this job adds is the worker thread, the progress notification with its Cancel button, and the commit.
|
||||
enum class TextureDisplacementPrepareOutcome
|
||||
{
|
||||
Committed, // the volume now carries the prepared mesh and the paint carried onto it
|
||||
Unchanged, // nothing needed doing - the mesh already met the criteria; the model was not touched
|
||||
PaintLost, // the remesh landed but no layer's paint survived it, so nothing was committed
|
||||
Failed, // cancelled, threw, or the volume went away while the job ran
|
||||
};
|
||||
|
||||
struct TextureDisplacementPrepareInput
|
||||
{
|
||||
ObjectID volume_id;
|
||||
indexed_triangle_set base_mesh;
|
||||
TextureDisplacementFacetsData masks;
|
||||
std::vector<TextureDisplacementLayer> layers;
|
||||
TextureDisplacementPrepareParams params;
|
||||
// The undo step the commit opens. Standard mode's Bake names it after the bake, because the
|
||||
// displacement job that follows commits into this same step rather than pushing its own.
|
||||
std::string snapshot_name;
|
||||
};
|
||||
|
||||
class TextureDisplacementPrepareJob : public Job
|
||||
{
|
||||
TextureDisplacementPrepareInput m_input;
|
||||
TextureDisplacementPrepareResult m_result;
|
||||
std::function<void(TextureDisplacementPrepareOutcome)> m_on_finished;
|
||||
|
||||
public:
|
||||
TextureDisplacementPrepareJob(TextureDisplacementPrepareInput &&input,
|
||||
std::function<void(TextureDisplacementPrepareOutcome)> on_finished);
|
||||
|
||||
void process(Ctl &ctl) override;
|
||||
void finalize(bool canceled, std::exception_ptr &eptr) override;
|
||||
};
|
||||
|
||||
// `on_finished` runs on the UI thread once the result has been committed (or found not to need
|
||||
// committing), and always runs exactly once.
|
||||
void queue_texture_displacement_prepare(TextureDisplacementPrepareInput &&input,
|
||||
std::function<void(TextureDisplacementPrepareOutcome)> on_finished);
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_TextureDisplacementPrepareJob_hpp_
|
||||
Reference in New Issue
Block a user