mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-23 17:22:56 -05:00
Bake texture displacement on every painted part of an object (#16721)
# Description Texture displacement read the layer stack and the bake target from an object's first model part, so a multibody object was displaced only there while the paint on its other parts was kept and ignored. The bake, prepare and preview jobs now each carry a vector of parts and run them in one job, under one progress bar and one undo step; remesh and subdivide cover every part too # Screenshots/Recordings/Graphs <img width="3515" height="1838" alt="image" src="https://github.com/user-attachments/assets/227cd067-4b04-476a-982e-b52c4fdcca23" /> # Other Fixes #16559 [How to Download Pull Requests Artifacts for Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
This commit is contained in:
15 files changed
+1129
-467
No files matched your search
@@ -2724,7 +2724,8 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
bool flip_normals,
|
||||
BakeStageRecorder *debug,
|
||||
TextureBakeStats *stats,
|
||||
const std::vector<Vec2f> *model_uv)
|
||||
const std::vector<Vec2f> *model_uv,
|
||||
float resting_drop)
|
||||
{
|
||||
HeightFieldSampler combined = make_combined_displacement_sampler(mesh, layers, facets_data, model_uv);
|
||||
if (!combined)
|
||||
@@ -2871,6 +2872,9 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
bounds.min = bounds.min.cwiseMin(v);
|
||||
bounds.max = bounds.max.cwiseMax(v);
|
||||
}
|
||||
// The bottom the pipeline clamps and snaps to is the plane the whole object rests on, not this
|
||||
// part's own: a part held off the plate by another keeps the relief on its underside.
|
||||
bounds.min.z() -= resting_drop;
|
||||
|
||||
const auto sample = [&combined](const Vec3f &pos, const Vec3f &, const Vec3f &blend_normal) {
|
||||
// The blend normal chooses the projection; the move itself is along the smooth normal, which
|
||||
@@ -3030,7 +3034,8 @@ static indexed_triangle_set build_texture_displacement_in_place(
|
||||
bool flip_normals,
|
||||
BakeStageRecorder *debug,
|
||||
TextureBakeStats *stats,
|
||||
const std::vector<Vec2f> *model_uv)
|
||||
const std::vector<Vec2f> *model_uv,
|
||||
float resting_drop)
|
||||
{
|
||||
// The classic path moves the vertices the mesh already has, so there is no budget to report on.
|
||||
(void) stats;
|
||||
@@ -3067,7 +3072,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, stats, model_uv);
|
||||
debug, stats, model_uv, resting_drop);
|
||||
|
||||
// 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).
|
||||
@@ -3399,10 +3404,12 @@ static indexed_triangle_set build_texture_displacement_in_place(
|
||||
if (!any_displacement)
|
||||
return mesh;
|
||||
|
||||
// The model's own resting plane, taken before anything moves - see the clamp after smoothing.
|
||||
// The plane the model rests on, taken before anything moves - see the clamp after smoothing. That
|
||||
// is this mesh's own bottom, lowered to the object's when another part of it sits lower.
|
||||
float resting_z = std::numeric_limits<float>::max();
|
||||
for (const Vec3f &v : mesh.vertices)
|
||||
resting_z = std::min(resting_z, v.z());
|
||||
resting_z -= resting_drop;
|
||||
|
||||
size_t moved_count = 0;
|
||||
for (size_t vi = 0; vi < mesh.vertices.size(); ++vi)
|
||||
@@ -3494,17 +3501,20 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
const Transform3d &volume_to_world,
|
||||
BakeStageRecorder *debug,
|
||||
TextureBakeStats *stats,
|
||||
const std::vector<Vec2f> *model_uv)
|
||||
const std::vector<Vec2f> *model_uv,
|
||||
const Vec3d *texture_anchor,
|
||||
float resting_drop)
|
||||
{
|
||||
// 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
|
||||
// the identity out of the way rather than paying for it.
|
||||
// The texture is projected in the bake frame - world orientation and scale, anchored at the
|
||||
// volume's origin (see texture_displacement_bake_frame()).
|
||||
const Transform3d frame = texture_displacement_bake_frame(volume_to_world);
|
||||
// texture's anchor (see texture_displacement_bake_frame()).
|
||||
const Transform3d frame = texture_anchor != nullptr ? texture_displacement_bake_frame(volume_to_world, *texture_anchor) :
|
||||
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, stats, model_uv);
|
||||
false, debug, stats, model_uv, resting_drop);
|
||||
|
||||
const Transform3d to_local = frame.inverse();
|
||||
// A mirroring placement leaves the positions correct but every winding-derived normal pointing
|
||||
@@ -3520,8 +3530,8 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
const size_t debug_mark = (debug != nullptr) ? debug->mark() : 0;
|
||||
// The UV channel rides along untouched: a uv is a coordinate in the texture, not on the model, so
|
||||
// the frame the mesh is measured in does not reach it.
|
||||
indexed_triangle_set out = build_texture_displacement_in_place(world, layers, facets_data, options,
|
||||
progress, color, mirrored, debug, stats, model_uv);
|
||||
indexed_triangle_set out = build_texture_displacement_in_place(world, layers, facets_data, options, progress,
|
||||
color, mirrored, debug, stats, model_uv, resting_drop);
|
||||
// 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)
|
||||
@@ -3568,13 +3578,18 @@ void texture_displacement_patch_frame(const indexed_triangle_set &patch, const s
|
||||
(an.y() <= an.x() && an.y() <= an.z()) ? Vec3f::UnitY() : Vec3f::UnitZ();
|
||||
}
|
||||
|
||||
Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world, const Vec3d &anchor)
|
||||
{
|
||||
// World orientation and scale, but the origin moved onto the model: the texture then rides with
|
||||
// the model when it is moved about the plate (and a single, untiled stamp starts on the model
|
||||
// rather than at the plate's corner), while a tile is still `tiling_scale` printed millimetres
|
||||
// whatever the instance's scale.
|
||||
return Eigen::Translation3d(-anchor) * volume_to_world;
|
||||
}
|
||||
|
||||
Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world)
|
||||
{
|
||||
// World orientation and scale, but the origin moved to where the volume's own origin sits: the
|
||||
// texture then rides with the model when it is moved about the plate (and a single, untiled stamp
|
||||
// starts on the model rather than at the plate's corner), while a tile is still `tiling_scale`
|
||||
// printed millimetres whatever the instance's scale.
|
||||
return Eigen::Translation3d(-volume_to_world.translation()) * volume_to_world;
|
||||
return texture_displacement_bake_frame(volume_to_world, volume_to_world.translation());
|
||||
}
|
||||
|
||||
Transform3d texture_displacement_volume_to_world(const ModelVolume &volume)
|
||||
|
||||
@@ -1040,14 +1040,26 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
// When given, receives what the bake spent.
|
||||
TextureBakeStats *stats = nullptr,
|
||||
// The model's own UV channel; see above.
|
||||
const std::vector<Vec2f> *model_uv = nullptr);
|
||||
const std::vector<Vec2f> *model_uv = nullptr,
|
||||
// The world point the texture is anchored at, null for
|
||||
// the volume's own origin. Every part of a multibody
|
||||
// object passes the same point, so the pattern runs on
|
||||
// across them. See texture_displacement_bake_frame().
|
||||
const Vec3d *texture_anchor = nullptr,
|
||||
// How far the object's lowest point lies below this
|
||||
// volume's own, in mm. Relief is kept above the plane
|
||||
// the whole object rests on, so a part held off the
|
||||
// plate by another keeps the relief on its underside.
|
||||
float resting_drop = 0.f);
|
||||
|
||||
// `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
|
||||
// them; the first is what the gizmo edits against. Identity when the volume has no object yet.
|
||||
Transform3d texture_displacement_volume_to_world(const ModelVolume &volume);
|
||||
// The frame the bake and the previews project the texture in: `volume_to_world` with its translation
|
||||
// removed, i.e. world orientation and scale about the volume's own origin. See build_texture_displacement().
|
||||
// The frame the bake and the previews project the texture in: world orientation and scale, with the
|
||||
// world point `anchor` as its origin. See build_texture_displacement().
|
||||
Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world, const Vec3d &anchor);
|
||||
// Anchored at the volume's own origin, which is where a single-part object's texture sits.
|
||||
Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world);
|
||||
|
||||
// Area-weighted vertex normals of `its` - the directions the bake both projects and displaces along.
|
||||
|
||||
@@ -114,6 +114,11 @@ Vec3f GLGizmoPainterBase::get_tilt_up_direction() const
|
||||
}
|
||||
|
||||
void GLGizmoPainterBase::render_triangles(const Selection& selection, const ModelVolume* skip) const
|
||||
{
|
||||
render_triangles(selection, skip == nullptr ? std::vector<const ModelVolume*>{} : std::vector<const ModelVolume*>{skip});
|
||||
}
|
||||
|
||||
void GLGizmoPainterBase::render_triangles(const Selection& selection, const std::vector<const ModelVolume*>& skip) const
|
||||
{
|
||||
auto* shader = wxGetApp().get_shader("mm_gouraud");
|
||||
if (!shader)
|
||||
@@ -135,7 +140,7 @@ void GLGizmoPainterBase::render_triangles(const Selection& selection, const Mode
|
||||
continue;
|
||||
|
||||
++mesh_id;
|
||||
if (mv == skip)
|
||||
if (std::find(skip.begin(), skip.end(), mv) != skip.end())
|
||||
continue;
|
||||
|
||||
Transform3d trafo_matrix;
|
||||
|
||||
@@ -235,6 +235,10 @@ public:
|
||||
protected:
|
||||
// Draws every model part's selector, except `skip`'s when given.
|
||||
virtual void render_triangles(const Selection& selection, const ModelVolume* skip = nullptr) const;
|
||||
// The same, skipping every part in `skip`. A gizmo can have a preview standing in for more than one
|
||||
// model part at a time - the texture-displacement gizmo previews every painted part of a multibody
|
||||
// object - and drawing those parts' selectors over it would bury the preview under a flat surface.
|
||||
void render_triangles(const Selection& selection, const std::vector<const ModelVolume*>& skip) const;
|
||||
void render_cursor();
|
||||
void render_cursor_circle();
|
||||
void render_cursor_sphere(const Transform3d& trafo) const;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "ColorSpaceConvert.hpp"
|
||||
#include "libslic3r/AABBTreeIndirect.hpp"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "libslic3r/FilamentMixer.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
@@ -26,6 +27,7 @@
|
||||
#include "slic3r/GUI/GuiColor.hpp"
|
||||
#include "slic3r/GUI/ImGuiWrapper.hpp"
|
||||
#include "slic3r/GUI/MainFrame.hpp" // wxGetApp().mainframe, as the projector window's parent
|
||||
#include "slic3r/GUI/MeshUtils.hpp"
|
||||
#include "slic3r/GUI/MsgDialog.hpp"
|
||||
#include "slic3r/GUI/OpenGLManager.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
@@ -580,6 +582,7 @@ void GLGizmoTextureDisplacement::on_shutdown()
|
||||
{
|
||||
m_parent.toggle_model_objects_visibility(true);
|
||||
m_preview_glmodel.reset();
|
||||
m_part_previews.clear();
|
||||
m_shaded_preview_glmodel.reset();
|
||||
m_paint_overlay_glmodel.reset();
|
||||
m_paint_overlay_dirty = false;
|
||||
@@ -705,8 +708,7 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
|
||||
// why those two modes looked like they did nothing. Leave it out and let the undisplaced volume show
|
||||
// through instead (toggle_model_objects_visibility below) - that one *is* coincident with the
|
||||
// overlay, which is what the offset assumes, and it is the surface whose mapping is being inspected.
|
||||
const bool use_true_preview = !use_shaded && m_uv_check_mode == UVCheckMode::None &&
|
||||
m_preview_glmodel.is_initialized();
|
||||
const bool use_true_preview = !use_shaded && m_uv_check_mode == UVCheckMode::None && has_true_preview();
|
||||
// In Checker/Distortion mode the UV-check overlay *is* the surface visualization the user is
|
||||
// looking at, so the opaque paint-selection highlight must not be drawn on top of it - same
|
||||
// reasoning as skipping it for the shaded preview (see bug #12). Without this the painted area
|
||||
@@ -717,10 +719,23 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
|
||||
// put it back. Getting this wrong leaves an invisible model, so it is decided once, here, rather
|
||||
// than per branch below.
|
||||
ModelVolume *mv = texture_volume();
|
||||
// The parts a preview is standing in for, and whose real volumes therefore have to be hidden and
|
||||
// whose selectors must not be drawn over it. The shaded view only ever shades the layer stack's own
|
||||
// part; the true-displacement view covers every painted part of a multibody object.
|
||||
std::vector<const ModelVolume *> previewed;
|
||||
if (use_shaded && mv != nullptr) {
|
||||
previewed.push_back(mv);
|
||||
} else if (use_true_preview) {
|
||||
if (mv != nullptr && m_preview_glmodel.is_initialized())
|
||||
previewed.push_back(mv);
|
||||
for (const std::unique_ptr<PartPreview> &preview : m_part_previews)
|
||||
if (const ModelVolume *part = preview_part(*preview); part != nullptr)
|
||||
previewed.push_back(part);
|
||||
}
|
||||
m_parent.toggle_model_objects_visibility(true);
|
||||
if ((use_shaded || use_true_preview) && mv != nullptr)
|
||||
for (const ModelVolume *volume : previewed)
|
||||
m_parent.toggle_model_objects_visibility(false, m_c->selection_info()->model_object(),
|
||||
m_c->selection_info()->get_active_instance(), mv);
|
||||
m_c->selection_info()->get_active_instance(), volume);
|
||||
|
||||
// Over a preview, the paint feedback - the selectors' highlight in the Normal view, the tint in both -
|
||||
// is drawn only while a mouse button is down: during a stroke, a fill click included. Between strokes
|
||||
@@ -734,14 +749,15 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
|
||||
if (use_shaded) {
|
||||
render_shaded_preview_mesh();
|
||||
// The shaded mesh is the textured volume alone, so the other model parts are still the selectors' to draw.
|
||||
render_triangles(selection, mv);
|
||||
render_triangles(selection, previewed);
|
||||
textured_selector_drawn = false;
|
||||
} else if (use_true_preview) {
|
||||
render_preview_mesh();
|
||||
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
|
||||
glsafe(::glPolygonOffset(-1.0f, -1.0f));
|
||||
// Without paint feedback, only the other model parts: render_preview_mesh() draws the textured one.
|
||||
render_triangles(selection, paint_feedback ? nullptr : mv);
|
||||
// Without paint feedback, only the parts with no preview over them: render_preview_mesh() draws
|
||||
// the rest.
|
||||
render_triangles(selection, paint_feedback ? std::vector<const ModelVolume *>{} : previewed);
|
||||
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
|
||||
textured_selector_drawn = paint_feedback;
|
||||
} else {
|
||||
@@ -1239,36 +1255,61 @@ void GLGizmoTextureDisplacement::render_seam_overlay()
|
||||
void GLGizmoTextureDisplacement::render_preview_mesh()
|
||||
{
|
||||
const ModelObject *mo = m_c->selection_info()->model_object();
|
||||
const ModelVolume *mv = texture_volume();
|
||||
if (mo == nullptr || mv == nullptr)
|
||||
if (mo == nullptr)
|
||||
return;
|
||||
|
||||
const Selection &selection = m_parent.get_selection();
|
||||
const Transform3d trafo_matrix = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix();
|
||||
|
||||
auto *shader = wxGetApp().get_shader("gouraud_light");
|
||||
if (shader == nullptr)
|
||||
return;
|
||||
|
||||
const Selection &selection = m_parent.get_selection();
|
||||
const Transform3d instance_matrix = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix();
|
||||
const Camera &camera = wxGetApp().plater()->get_camera();
|
||||
const Transform3d &view_matrix = camera.get_view_matrix();
|
||||
|
||||
shader->start_using();
|
||||
const Camera &camera = wxGetApp().plater()->get_camera();
|
||||
const Transform3d &view_matrix = camera.get_view_matrix();
|
||||
shader->set_uniform("view_model_matrix", view_matrix * trafo_matrix);
|
||||
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
|
||||
const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * trafo_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
|
||||
shader->set_uniform("view_normal_matrix", view_normal_matrix);
|
||||
if (m_preview_color_runs.empty()) {
|
||||
m_preview_glmodel.render();
|
||||
} else {
|
||||
// set_color() writes the uniform the shader reads, so one call per group is all the
|
||||
// per-triangle colour this needs.
|
||||
for (const PreviewColorRun &run : m_preview_color_runs) {
|
||||
m_preview_glmodel.set_color(run.color);
|
||||
m_preview_glmodel.render(run.range, shader);
|
||||
// Each part is drawn through its own matrix: a multibody object's parts sit in different places
|
||||
// inside the object, so one matrix for all of them would pile every preview onto the first part.
|
||||
const auto draw = [&](const ModelVolume &volume, GLModel &model, const std::vector<PreviewColorRun> &runs) {
|
||||
if (!model.is_initialized())
|
||||
return;
|
||||
const Transform3d trafo_matrix = instance_matrix * volume.get_matrix();
|
||||
shader->set_uniform("view_model_matrix", view_matrix * trafo_matrix);
|
||||
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
|
||||
const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) *
|
||||
trafo_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
|
||||
shader->set_uniform("view_normal_matrix", view_normal_matrix);
|
||||
if (runs.empty()) {
|
||||
model.render();
|
||||
} else {
|
||||
// set_color() writes the uniform the shader reads, so one call per group is all the
|
||||
// per-triangle colour this needs.
|
||||
for (const PreviewColorRun &run : runs) {
|
||||
model.set_color(run.color);
|
||||
model.render(run.range, shader);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (const ModelVolume *owner = texture_volume(); owner != nullptr)
|
||||
draw(*owner, m_preview_glmodel, m_preview_color_runs);
|
||||
for (const std::unique_ptr<PartPreview> &preview : m_part_previews)
|
||||
if (const ModelVolume *part = preview_part(*preview); part != nullptr)
|
||||
draw(*part, preview->glmodel, preview->color_runs);
|
||||
shader->stop_using();
|
||||
}
|
||||
|
||||
const ModelVolume *GLGizmoTextureDisplacement::preview_part(const PartPreview &preview) const
|
||||
{
|
||||
const ModelObject *mo = m_c->selection_info()->model_object();
|
||||
if (mo == nullptr)
|
||||
return nullptr;
|
||||
for (const ModelVolume *mv : mo->volumes)
|
||||
if (mv->id() == preview.volume_id)
|
||||
return mv;
|
||||
return nullptr; // the part was removed, or its mesh replaced, since the preview was built
|
||||
}
|
||||
|
||||
float GLGizmoTextureDisplacement::layer_texture_aspect(const TextureDisplacementLayer &layer)
|
||||
{
|
||||
// decode_height_texture() is cached on the image_data allocation, so this is a hash lookup rather
|
||||
@@ -2362,8 +2403,9 @@ void GLGizmoTextureDisplacement::rebuild_preview()
|
||||
return;
|
||||
|
||||
const ModelVolume *mv = texture_volume();
|
||||
if (mv == nullptr || !mv->is_texture_displacement_painted()) {
|
||||
if (mv == nullptr || !any_part_painted()) {
|
||||
m_preview_glmodel.reset();
|
||||
m_part_previews.clear();
|
||||
m_preview_its = indexed_triangle_set{}; // no displaced mesh; wireframe falls back to the base
|
||||
m_preview_job_pending = false;
|
||||
refresh_wireframe();
|
||||
@@ -2384,14 +2426,44 @@ void GLGizmoTextureDisplacement::rebuild_preview()
|
||||
queue_preview_job();
|
||||
}
|
||||
|
||||
indexed_triangle_set GLGizmoTextureDisplacement::group_preview_by_color(const indexed_triangle_set &its,
|
||||
const std::vector<uint8_t> &triangle_color,
|
||||
const std::vector<ColorRGBA> &entry_colors,
|
||||
std::vector<PreviewColorRun> &runs)
|
||||
{
|
||||
runs.clear();
|
||||
if (triangle_color.size() != its.indices.size() || entry_colors.empty())
|
||||
return its;
|
||||
|
||||
indexed_triangle_set sorted;
|
||||
sorted.vertices = its.vertices;
|
||||
sorted.indices.reserve(its.indices.size());
|
||||
for (int want = 0; want <= int(entry_colors.size()); ++want) {
|
||||
const size_t first = sorted.indices.size();
|
||||
for (size_t i = 0; i < its.indices.size(); ++i)
|
||||
if (int(triangle_color[i]) == want)
|
||||
sorted.indices.push_back(its.indices[i]);
|
||||
if (sorted.indices.size() == first)
|
||||
continue;
|
||||
runs.push_back({ { first * 3, sorted.indices.size() * 3 },
|
||||
want == 0 ? GLVolume::NEUTRAL_COLOR : entry_colors[size_t(want - 1)] });
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::queue_preview_job()
|
||||
{
|
||||
// A job in flight when the gizmo closes still runs its completion handler, which would otherwise
|
||||
// happily queue the follow-up run it was holding - against a gizmo nobody is looking at any more.
|
||||
if (m_state != On)
|
||||
return;
|
||||
const ModelVolume *mv = texture_volume();
|
||||
if (mv == nullptr || !mv->is_texture_displacement_painted())
|
||||
const ModelVolume *owner = texture_volume();
|
||||
if (owner == nullptr)
|
||||
return;
|
||||
// Every painted part, so the preview shows the whole of what Bake will do rather than its first
|
||||
// part - the same set bake() displaces.
|
||||
const std::vector<ModelVolume *> parts = painted_parts();
|
||||
if (parts.empty())
|
||||
return;
|
||||
|
||||
// One in flight at a time; everything requested meanwhile collapses into a single follow-up run
|
||||
@@ -2404,22 +2476,39 @@ void GLGizmoTextureDisplacement::queue_preview_job()
|
||||
const uint64_t generation = m_preview_generation->load();
|
||||
|
||||
TextureDisplacementPreviewInput input;
|
||||
input.base_mesh = mv->mesh().its;
|
||||
input.layers = mv->texture_displacement_layers;
|
||||
input.options = mv->texture_displacement_options;
|
||||
// The preview is looked at, not printed or sliced: skip the simplification and the repair that
|
||||
// follows it, which are most of the one-run pipeline's time. The relief is the same.
|
||||
if (input.options.pipeline_v2)
|
||||
input.options.v2_max_triangles_k = 0;
|
||||
input.volume_to_world = texture_displacement_volume_to_world(*mv);
|
||||
if (mv->has_texture_uv())
|
||||
input.model_uv = mv->texture_uv;
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
input.facets_data[size_t(i)] = mv->texture_displacement_facet(i).get_data();
|
||||
// Captured here rather than read in the handler: the palette is main-thread state, and the preview
|
||||
// has to be grouped against the same palette it was computed with, not whatever it is by the time
|
||||
// the result lands.
|
||||
input.color = color_settings_for(*mv);
|
||||
// the result lands. Read off the layer stack's own part, since that is where the stack lives.
|
||||
input.color = color_settings_for(*owner);
|
||||
input.volumes.reserve(parts.size());
|
||||
// Which part each result belongs to, so the handler can tell the layer stack's own preview - the one
|
||||
// the overlays and the wireframe are keyed to - from the rest.
|
||||
std::vector<ObjectID> part_ids;
|
||||
part_ids.reserve(parts.size());
|
||||
const Vec3d anchor = texture_anchor();
|
||||
const std::vector<float> drops = resting_drops(parts);
|
||||
for (size_t i = 0; i < parts.size(); ++i) {
|
||||
const ModelVolume *part = parts[i];
|
||||
TextureDisplacementPreviewVolume entry;
|
||||
entry.base_mesh = part->mesh().its;
|
||||
entry.layers = layers_for_part(*part);
|
||||
entry.options = owner->texture_displacement_options;
|
||||
// The preview is looked at, not printed or sliced: skip the simplification and the repair that
|
||||
// follows it, which are most of the one-run pipeline's time. The relief is the same.
|
||||
if (entry.options.pipeline_v2)
|
||||
entry.options.v2_max_triangles_k = 0;
|
||||
entry.volume_to_world = texture_displacement_volume_to_world(*part);
|
||||
entry.texture_anchor = anchor;
|
||||
entry.resting_drop = drops[i];
|
||||
if (part->has_texture_uv())
|
||||
entry.model_uv = part->texture_uv;
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
|
||||
entry.facets_data[size_t(slot)] = part->texture_displacement_facet(slot).get_data();
|
||||
input.volumes.push_back(std::move(entry));
|
||||
part_ids.push_back(part->id());
|
||||
}
|
||||
const ObjectID owner_id = owner->id();
|
||||
|
||||
// The result names a palette entry per triangle (index + 1), so these are the colours to draw it in:
|
||||
// a single filament's own, or for a mix the colour its slot will show once a bake creates it.
|
||||
std::vector<ColorRGBA> entry_colors;
|
||||
@@ -2430,43 +2519,52 @@ void GLGizmoTextureDisplacement::queue_preview_job()
|
||||
m_preview_job_running = true;
|
||||
auto &worker = wxGetApp().plater()->get_ui_job_worker();
|
||||
queue_job(worker, std::make_unique<TextureDisplacementPreviewJob>(std::move(input), generation, m_preview_generation,
|
||||
[this, entry_colors](TextureDisplacementPreviewResult result, uint64_t result_generation) {
|
||||
indexed_triangle_set its = std::move(result.mesh);
|
||||
[this, entry_colors, part_ids, owner_id](std::vector<TextureDisplacementPreviewResult> results,
|
||||
uint64_t result_generation) {
|
||||
m_preview_job_running = false;
|
||||
if (result_generation != m_preview_generation->load()) {
|
||||
// Superseded while this was computing (it will have aborted early and come back
|
||||
// empty). Whatever the newest state is, it still needs a run.
|
||||
m_preview_job_pending = true;
|
||||
} else if (its.indices.empty()) {
|
||||
} else if (results.empty()) {
|
||||
// Aborted or cancelled rather than finished - the handler runs on every outcome so
|
||||
// the in-flight latch above always clears. Keep whatever preview is already on screen
|
||||
// rather than blanking it; there is no new result to show, not a new empty one.
|
||||
} else {
|
||||
// A finished run carries the whole object, so it replaces the previous one outright: a
|
||||
// part that is no longer painted must lose its preview, or its flat volume would stay
|
||||
// hidden behind geometry from before.
|
||||
m_preview_glmodel.reset();
|
||||
m_preview_color_runs.clear();
|
||||
if (result.triangle_color.size() == its.indices.size() && !entry_colors.empty()) {
|
||||
indexed_triangle_set sorted;
|
||||
sorted.vertices = its.vertices;
|
||||
sorted.indices.reserve(its.indices.size());
|
||||
for (int want = 0; want <= int(entry_colors.size()); ++want) {
|
||||
const size_t first = sorted.indices.size();
|
||||
for (size_t i = 0; i < its.indices.size(); ++i)
|
||||
if (int(result.triangle_color[i]) == want)
|
||||
sorted.indices.push_back(its.indices[i]);
|
||||
if (sorted.indices.size() == first)
|
||||
continue;
|
||||
m_preview_color_runs.push_back(
|
||||
{ { first * 3, sorted.indices.size() * 3 },
|
||||
want == 0 ? GLVolume::NEUTRAL_COLOR : entry_colors[size_t(want - 1)] });
|
||||
m_preview_its = indexed_triangle_set{};
|
||||
m_part_previews.clear();
|
||||
|
||||
for (size_t i = 0; i < results.size() && i < part_ids.size(); ++i) {
|
||||
indexed_triangle_set its = std::move(results[i].mesh);
|
||||
if (its.indices.empty())
|
||||
continue; // this part's layers moved nothing; its own volume is drawn instead
|
||||
|
||||
std::vector<PreviewColorRun> runs;
|
||||
indexed_triangle_set grouped =
|
||||
group_preview_by_color(its, results[i].triangle_color, entry_colors, runs);
|
||||
|
||||
if (part_ids[i] == owner_id) {
|
||||
m_preview_color_runs = std::move(runs);
|
||||
m_preview_glmodel.init_from(grouped);
|
||||
m_preview_glmodel.set_color(GLVolume::NEUTRAL_COLOR);
|
||||
// Keep the displaced mesh so the wireframe overlay can be drawn on the raised
|
||||
// surface actually shown in the true-displacement view.
|
||||
m_preview_its = std::move(its);
|
||||
} else {
|
||||
auto preview = std::make_unique<PartPreview>();
|
||||
preview->volume_id = part_ids[i];
|
||||
preview->color_runs = std::move(runs);
|
||||
preview->glmodel.init_from(grouped);
|
||||
preview->glmodel.set_color(GLVolume::NEUTRAL_COLOR);
|
||||
m_part_previews.push_back(std::move(preview));
|
||||
}
|
||||
m_preview_glmodel.init_from(sorted);
|
||||
} else {
|
||||
m_preview_glmodel.init_from(its);
|
||||
}
|
||||
m_preview_glmodel.set_color(GLVolume::NEUTRAL_COLOR);
|
||||
// Keep the displaced mesh so the wireframe overlay can be drawn on it (the
|
||||
// true-displacement view), then refresh the wireframe from it.
|
||||
m_preview_its = std::move(its);
|
||||
|
||||
refresh_wireframe();
|
||||
// The paint tint rides the displaced surface in this view, so it follows the new mesh.
|
||||
m_paint_overlay_dirty = true;
|
||||
@@ -3702,6 +3800,82 @@ const ModelVolume* GLGizmoTextureDisplacement::texture_volume() const
|
||||
return const_cast<GLGizmoTextureDisplacement *>(this)->texture_volume();
|
||||
}
|
||||
|
||||
std::vector<ModelVolume *> GLGizmoTextureDisplacement::texture_parts()
|
||||
{
|
||||
std::vector<ModelVolume *> out;
|
||||
ModelObject *mo = m_c->selection_info()->model_object();
|
||||
if (mo == nullptr)
|
||||
return out;
|
||||
for (ModelVolume *mv : mo->volumes)
|
||||
if (mv->is_model_part())
|
||||
out.push_back(mv);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<ModelVolume *> GLGizmoTextureDisplacement::painted_parts()
|
||||
{
|
||||
std::vector<ModelVolume *> out;
|
||||
for (ModelVolume *mv : texture_parts())
|
||||
if (mv->is_texture_displacement_painted())
|
||||
out.push_back(mv);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool GLGizmoTextureDisplacement::any_part_painted() const
|
||||
{
|
||||
for (const ModelVolume *mv : const_cast<GLGizmoTextureDisplacement *>(this)->texture_parts())
|
||||
if (mv->is_texture_displacement_painted())
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<TextureDisplacementLayer> GLGizmoTextureDisplacement::layers_for_part(const ModelVolume &part) const
|
||||
{
|
||||
const ModelVolume *owner = texture_volume();
|
||||
if (owner == nullptr)
|
||||
return {};
|
||||
std::vector<TextureDisplacementLayer> out = owner->texture_displacement_layers;
|
||||
if (&part == owner)
|
||||
return out;
|
||||
for (TextureDisplacementLayer &layer : out) {
|
||||
layer.lscm_seam_edges.clear();
|
||||
layer.islands.clear();
|
||||
layer.island_groups.clear();
|
||||
layer.lscm_uv_overrides.clear();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Vec3d GLGizmoTextureDisplacement::texture_anchor() const
|
||||
{
|
||||
const ModelVolume *owner = texture_volume();
|
||||
return owner != nullptr ? Vec3d(texture_displacement_volume_to_world(*owner).translation()) : Vec3d::Zero();
|
||||
}
|
||||
|
||||
std::vector<float> GLGizmoTextureDisplacement::resting_drops(const std::vector<ModelVolume *> &parts)
|
||||
{
|
||||
const auto bottom = [](const ModelVolume &part) {
|
||||
return part.mesh().transformed_bounding_box(texture_displacement_volume_to_world(part)).min.z();
|
||||
};
|
||||
// Each part's bottom is measured the same way as the object's, so the lowest part's drop is exactly
|
||||
// 0 and a single-part object bakes against its own bottom as it always has.
|
||||
double object_bottom = std::numeric_limits<double>::max();
|
||||
for (const ModelVolume *part : texture_parts())
|
||||
object_bottom = std::min(object_bottom, bottom(*part));
|
||||
std::vector<float> out;
|
||||
out.reserve(parts.size());
|
||||
for (const ModelVolume *part : parts)
|
||||
out.push_back(float(bottom(*part) - object_bottom));
|
||||
return out;
|
||||
}
|
||||
|
||||
Transform3d GLGizmoTextureDisplacement::prepare_texture_frame(const ModelVolume &part)
|
||||
{
|
||||
if (texture_parts().size() < 2)
|
||||
return Transform3d::Identity();
|
||||
return texture_displacement_bake_frame(texture_displacement_volume_to_world(part), texture_anchor());
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::update_model_object()
|
||||
{
|
||||
store_panel_state();
|
||||
@@ -4982,7 +5156,10 @@ void GLGizmoTextureDisplacement::remove_texture_layer(int slot)
|
||||
[slot](const TextureDisplacementLayer &l) { return l.slot == slot; }),
|
||||
layers.end());
|
||||
if (slot >= 0 && slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS)) {
|
||||
mv->texture_displacement_facet(slot).reset();
|
||||
// Every model part: paint left in the slot on another part would still bake, and the next layer
|
||||
// added reuses the slot.
|
||||
for (ModelVolume *part : texture_parts())
|
||||
part->texture_displacement_facet(slot).reset();
|
||||
m_thumbnails[size_t(slot)].reset();
|
||||
m_thumbnail_source[size_t(slot)].reset();
|
||||
m_layer_expanded[size_t(slot)] = false;
|
||||
@@ -5073,25 +5250,19 @@ void GLGizmoTextureDisplacement::move_texture_layer(int slot, int to_index)
|
||||
|
||||
int GLGizmoTextureDisplacement::select_visible_faces(const std::array<float, 12> *uv_clip)
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
ModelObject *mo = m_c->selection_info()->model_object();
|
||||
const int idx = texture_volume_raycaster_index();
|
||||
if (mv == nullptr || mo == nullptr || idx < 0 || idx >= int(m_triangle_selectors.size()))
|
||||
const ModelVolume *owner = texture_volume();
|
||||
ModelObject *mo = m_c->selection_info()->model_object();
|
||||
// Every model part, at its selector's and its raycaster's index (see texture_parts()): what is in
|
||||
// view gets painted whichever part it belongs to, and one part can hide another.
|
||||
const std::vector<ModelVolume *> parts = texture_parts();
|
||||
const std::vector<const MeshRaycaster *> raycasters = m_c->raycaster()->raycasters();
|
||||
if (owner == nullptr || mo == nullptr || parts.size() > m_triangle_selectors.size() || parts.size() > raycasters.size())
|
||||
return 0;
|
||||
|
||||
const indexed_triangle_set &its = mv->mesh().its;
|
||||
if (its.indices.empty())
|
||||
return 0;
|
||||
|
||||
const Selection &selection = m_parent.get_selection();
|
||||
const Geometry::Transformation trafo(mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() *
|
||||
mv->get_matrix());
|
||||
const Transform3d &to_world = trafo.get_matrix();
|
||||
const Camera &camera = wxGetApp().plater()->get_camera();
|
||||
|
||||
// Normals transform by the inverse transpose, not by the matrix itself - with a non-uniform
|
||||
// scale the two differ, and using the wrong one flips the facing test on the scaled axes.
|
||||
const Matrix3d normal_matrix = to_world.matrix().block<3, 3>(0, 0).inverse().transpose();
|
||||
const Selection &selection = m_parent.get_selection();
|
||||
const Transform3d instance = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix();
|
||||
const Transform3d owner_to_world = instance * owner->get_matrix();
|
||||
const Camera &camera = wxGetApp().plater()->get_camera();
|
||||
|
||||
// Pass 1 (cheap): drop back-facing triangles. Under perspective the view direction varies across
|
||||
// the model, so it is taken per triangle from the eye to the centroid; under an orthographic
|
||||
@@ -5101,69 +5272,109 @@ int GLGizmoTextureDisplacement::select_visible_faces(const std::array<float, 12>
|
||||
const bool ortho = camera.get_type() == Camera::EType::Ortho;
|
||||
const Vec3d fwd = camera.get_dir_forward();
|
||||
|
||||
std::vector<Vec3f> centroids; // world coords, what get_unobscured_idxs() expects
|
||||
std::vector<unsigned> front_facing; // parallel: which facet each centroid came from
|
||||
centroids.reserve(its.indices.size() / 2);
|
||||
front_facing.reserve(its.indices.size() / 2);
|
||||
std::vector<Geometry::Transformation> trafos;
|
||||
std::vector<std::vector<Vec3f>> centroids(parts.size()); // world coords, what get_unobscured_idxs() expects
|
||||
std::vector<std::vector<unsigned>> front_facing(parts.size()); // parallel: which facet each centroid came from
|
||||
trafos.reserve(parts.size());
|
||||
for (size_t p = 0; p < parts.size(); ++p) {
|
||||
const ModelVolume &part = *parts[p];
|
||||
const indexed_triangle_set &its = part.mesh().its;
|
||||
trafos.emplace_back(instance * part.get_matrix());
|
||||
const Transform3d &to_world = trafos.back().get_matrix();
|
||||
// The projection frame maps the owner's local coordinates (see apply_projection_frame()), so
|
||||
// another part's centroids are brought into them before the clip test.
|
||||
const Transform3d to_owner = (&part == owner) ? Transform3d::Identity() : Transform3d(owner_to_world.inverse() * to_world);
|
||||
|
||||
for (size_t f = 0; f < its.indices.size(); ++f) {
|
||||
const stl_triangle_vertex_indices &tri = its.indices[f];
|
||||
const Vec3d a = its.vertices[tri[0]].cast<double>();
|
||||
const Vec3d b = its.vertices[tri[1]].cast<double>();
|
||||
const Vec3d c = its.vertices[tri[2]].cast<double>();
|
||||
const Vec3d n_world = normal_matrix * (b - a).cross(c - a);
|
||||
if (n_world.squaredNorm() < 1e-20)
|
||||
continue; // degenerate triangle: no meaningful normal, so no meaningful facing test
|
||||
const Vec3d centroid_local = (a + b + c) / 3.0;
|
||||
const Vec3d centroid_world = to_world * centroid_local;
|
||||
const Vec3d view_dir = ortho ? fwd : Vec3d(centroid_world - eye);
|
||||
if (n_world.dot(view_dir) >= 0.0)
|
||||
continue; // facing away from the camera
|
||||
// Normals transform by the inverse transpose, not by the matrix itself - with a non-uniform
|
||||
// scale the two differ, and using the wrong one flips the facing test on the scaled axes.
|
||||
const Matrix3d normal_matrix = to_world.matrix().block<3, 3>(0, 0).inverse().transpose();
|
||||
|
||||
// Clip to the projection frame before the raycast, not after: outside the frame the texture
|
||||
// samples to nothing anyway, so those facets would only be painted to no effect - and this
|
||||
// is also what keeps the ray queries proportional to the framed area instead of the model.
|
||||
if (uv_clip != nullptr) {
|
||||
Vec2f uv;
|
||||
if (!project_uv_projective(*uv_clip, centroid_local.cast<float>(), uv))
|
||||
continue; // behind the projector
|
||||
if (uv.x() < 0.f || uv.x() > 1.f || uv.y() < 0.f || uv.y() > 1.f)
|
||||
continue;
|
||||
centroids[p].reserve(its.indices.size() / 2);
|
||||
front_facing[p].reserve(its.indices.size() / 2);
|
||||
for (size_t f = 0; f < its.indices.size(); ++f) {
|
||||
const stl_triangle_vertex_indices &tri = its.indices[f];
|
||||
const Vec3d a = its.vertices[tri[0]].cast<double>();
|
||||
const Vec3d b = its.vertices[tri[1]].cast<double>();
|
||||
const Vec3d c = its.vertices[tri[2]].cast<double>();
|
||||
const Vec3d n_world = normal_matrix * (b - a).cross(c - a);
|
||||
if (n_world.squaredNorm() < 1e-20)
|
||||
continue; // degenerate triangle: no meaningful normal, so no meaningful facing test
|
||||
const Vec3d centroid_local = (a + b + c) / 3.0;
|
||||
const Vec3d centroid_world = to_world * centroid_local;
|
||||
const Vec3d view_dir = ortho ? fwd : Vec3d(centroid_world - eye);
|
||||
if (n_world.dot(view_dir) >= 0.0)
|
||||
continue; // facing away from the camera
|
||||
|
||||
// Clip to the projection frame before the raycast, not after: outside the frame the texture
|
||||
// samples to nothing anyway, so those facets would only be painted to no effect - and this
|
||||
// is also what keeps the ray queries proportional to the framed area instead of the model.
|
||||
if (uv_clip != nullptr) {
|
||||
Vec2f uv;
|
||||
if (!project_uv_projective(*uv_clip, (to_owner * centroid_local).cast<float>(), uv))
|
||||
continue; // behind the projector
|
||||
if (uv.x() < 0.f || uv.x() > 1.f || uv.y() < 0.f || uv.y() > 1.f)
|
||||
continue;
|
||||
}
|
||||
centroids[p].emplace_back(centroid_world.cast<float>());
|
||||
front_facing[p].push_back(unsigned(f));
|
||||
}
|
||||
centroids.emplace_back(centroid_world.cast<float>());
|
||||
front_facing.push_back(unsigned(f));
|
||||
}
|
||||
if (centroids.empty())
|
||||
return 0;
|
||||
|
||||
// Pass 2 (the expensive one): a real ray query per surviving centroid, so geometry in front of a
|
||||
// front-facing triangle correctly hides it - the far inner wall of a cup is front-facing but not
|
||||
// visible. This is why the whole thing is click-driven rather than live.
|
||||
std::vector<unsigned> unobscured;
|
||||
// visible. This is why the whole thing is click-driven rather than live. Each raycaster only sees
|
||||
// its own part's mesh, so a centroid has to be clear of every one of them; its own part goes first,
|
||||
// since that is what hides most of them.
|
||||
size_t visible = 0;
|
||||
{
|
||||
wxBusyCursor wait;
|
||||
unobscured = m_c->raycaster()->raycasters()[size_t(idx)]->get_unobscured_idxs(
|
||||
trafo, camera, centroids, m_c->object_clipper()->get_clipping_plane());
|
||||
const ClippingPlane *clipping_plane = m_c->object_clipper()->get_clipping_plane();
|
||||
for (size_t p = 0; p < parts.size(); ++p) {
|
||||
std::vector<Vec3f> &points = centroids[p];
|
||||
std::vector<unsigned> &facets = front_facing[p];
|
||||
const auto keep_unobscured_by = [&](size_t q) {
|
||||
const std::vector<unsigned> clear = raycasters[q]->get_unobscured_idxs(trafos[q], camera, points, clipping_plane);
|
||||
std::vector<Vec3f> kept_points;
|
||||
std::vector<unsigned> kept_facets;
|
||||
kept_points.reserve(clear.size());
|
||||
kept_facets.reserve(clear.size());
|
||||
for (unsigned i : clear) {
|
||||
kept_points.push_back(points[i]);
|
||||
kept_facets.push_back(facets[i]);
|
||||
}
|
||||
points = std::move(kept_points);
|
||||
facets = std::move(kept_facets);
|
||||
};
|
||||
if (!points.empty())
|
||||
keep_unobscured_by(p);
|
||||
for (size_t q = 0; q < parts.size() && !points.empty(); ++q)
|
||||
if (q != p)
|
||||
keep_unobscured_by(q);
|
||||
visible += facets.size();
|
||||
}
|
||||
}
|
||||
if (unobscured.empty())
|
||||
if (visible == 0)
|
||||
return 0;
|
||||
|
||||
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Select visible faces for texture displacement"),
|
||||
UndoRedo::SnapshotType::GizmoAction);
|
||||
|
||||
// Replaces the layer's paint rather than adding to it: the checkbox means "project onto what I
|
||||
// can see", so a second capture from a new angle should not leave the previous angle painted.
|
||||
// Replaces the layer's paint rather than adding to it, on every part including those nothing was
|
||||
// visible on: the checkbox means "project onto what I can see", so a second capture from a new
|
||||
// angle should not leave the previous angle painted.
|
||||
// TriangleSelectorGUI, not the TriangleSelector base: request_update_render_data() is declared on
|
||||
// the GUI subclass, so binding to the base here would drop it.
|
||||
TriangleSelectorGUI &selector = *m_triangle_selectors[size_t(idx)];
|
||||
selector.reset();
|
||||
for (unsigned i : unobscured)
|
||||
selector.set_facet(int(front_facing[i]), EnforcerBlockerType::ENFORCER);
|
||||
selector.request_update_render_data(true);
|
||||
for (size_t p = 0; p < parts.size(); ++p) {
|
||||
TriangleSelectorGUI &selector = *m_triangle_selectors[p];
|
||||
selector.reset();
|
||||
for (unsigned f : front_facing[p])
|
||||
selector.set_facet(int(f), EnforcerBlockerType::ENFORCER);
|
||||
selector.request_update_render_data(true);
|
||||
}
|
||||
|
||||
update_model_object();
|
||||
m_parent.set_as_dirty();
|
||||
return int(unobscured.size());
|
||||
return int(visible);
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::select_whole_model()
|
||||
@@ -5190,27 +5401,31 @@ void GLGizmoTextureDisplacement::select_whole_model()
|
||||
|
||||
void GLGizmoTextureDisplacement::subdivide_model()
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
ModelObject *mo = m_c->selection_info()->model_object();
|
||||
if (mv == nullptr || mo == nullptr || m_subdivide_count < 1)
|
||||
// Every model part, like the remesh: this is a plain mesh operation on the object, and a part left
|
||||
// coarse is a part no height map can raise relief on.
|
||||
const std::vector<ModelVolume *> parts = texture_parts();
|
||||
if (mo == nullptr || parts.empty() || m_subdivide_count < 1)
|
||||
return; // 0 passes means "no subdivision" - don't take a snapshot for a no-op
|
||||
|
||||
Plater *plater = wxGetApp().plater();
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Subdivide model for texture displacement"), UndoRedo::SnapshotType::GizmoAction);
|
||||
|
||||
// Same save/replace/restore-painting dance GLGizmoSimplify uses when it re-tessellates a
|
||||
// volume's mesh: supported/seam/mmu/fuzzy-skin masks get remapped onto the new triangles,
|
||||
// texture-displacement doesn't (no remap support for it yet) and is dropped instead of being
|
||||
// left referring to triangle indices that no longer mean the same thing.
|
||||
std::optional<TriangleSelector::SavedPainting> saved_painting = mv->save_painting();
|
||||
for (ModelVolume *mv : parts) {
|
||||
// Same save/replace/restore-painting dance GLGizmoSimplify uses when it re-tessellates a
|
||||
// volume's mesh: supported/seam/mmu/fuzzy-skin masks get remapped onto the new triangles,
|
||||
// texture-displacement doesn't (no remap support for it yet) and is dropped instead of being
|
||||
// left referring to triangle indices that no longer mean the same thing.
|
||||
std::optional<TriangleSelector::SavedPainting> saved_painting = mv->save_painting();
|
||||
|
||||
// max_edge_length 0 means "no triangle is ever small enough", so every non-degenerate edge is
|
||||
// split on each of the m_subdivide_count passes - i.e. a plain "subdivide the whole mesh N times".
|
||||
TriangleMesh new_mesh(subdivide_mesh_uniform(mv->mesh().its, 0.f, m_subdivide_count));
|
||||
mv->set_mesh(std::move(new_mesh));
|
||||
mv->set_new_unique_id();
|
||||
mv->calculate_convex_hull();
|
||||
mv->restore_painting(saved_painting);
|
||||
// max_edge_length 0 means "no triangle is ever small enough", so every non-degenerate edge is
|
||||
// split on each of the m_subdivide_count passes - i.e. a plain "subdivide the whole mesh N times".
|
||||
TriangleMesh new_mesh(subdivide_mesh_uniform(mv->mesh().its, 0.f, m_subdivide_count));
|
||||
mv->set_mesh(std::move(new_mesh));
|
||||
mv->set_new_unique_id();
|
||||
mv->calculate_convex_hull();
|
||||
mv->restore_painting(saved_painting);
|
||||
}
|
||||
|
||||
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
||||
const ModelObjectPtrs &objs = plater->model().objects;
|
||||
@@ -5855,13 +6070,35 @@ ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::ve
|
||||
TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
|
||||
const indexed_triangle_set &base, const TextureDisplacementFacetsData &masks,
|
||||
const std::vector<TextureDisplacementLayer> &layers, const std::vector<Vec2f> &model_uv,
|
||||
const TextureDisplacementPrepareParams ¶ms,
|
||||
const Transform3d &texture_frame, const TextureDisplacementPrepareParams ¶ms,
|
||||
const TextureColorSettings &color_settings, const DisplacementProgressFn &progress,
|
||||
BakeStageRecorder *debug)
|
||||
{
|
||||
TextureDisplacementPrepareResult out;
|
||||
const auto report = [&progress](int pct) { return !progress || progress(pct); };
|
||||
|
||||
// The samplers are built over a copy of the current mesh moved into `texture_frame`, and queried
|
||||
// with points and normals mapped across, so the stages below go on measuring the mesh itself.
|
||||
const bool framed = !texture_frame.matrix().isApprox(Transform3d::Identity().matrix());
|
||||
const Matrix3d frame_normal = texture_frame.linear().inverse().transpose();
|
||||
indexed_triangle_set framed_mesh;
|
||||
const auto mesh_to_sample = [&](const indexed_triangle_set &its) -> const indexed_triangle_set & {
|
||||
if (!framed)
|
||||
return its;
|
||||
framed_mesh = its;
|
||||
for (Vec3f &v : framed_mesh.vertices)
|
||||
v = (texture_frame * v.cast<double>()).cast<float>();
|
||||
return framed_mesh;
|
||||
};
|
||||
const auto sample_in_frame = [&](auto sampler) -> decltype(sampler) {
|
||||
if (!framed || !sampler)
|
||||
return sampler;
|
||||
return [sampler, frame = texture_frame, frame_normal](const Vec3f &pos, const Vec3f &normal) {
|
||||
return sampler((frame * pos.cast<double>()).cast<float>(),
|
||||
(frame_normal * normal.cast<double>()).normalized().cast<float>());
|
||||
};
|
||||
};
|
||||
|
||||
// Stage capture for the debug view. Timed from the end of the previous stage, so the capture -
|
||||
// a copy plus an edge scan - never lands inside the measurement it reports.
|
||||
auto stage_clock = std::chrono::steady_clock::now();
|
||||
@@ -5937,17 +6174,20 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
|
||||
// length baseline alone.
|
||||
const std::vector<Vec2f> uv_stage = uv_now();
|
||||
const std::vector<Vec2f> *uv_ptr = uv_stage.empty() ? nullptr : &uv_stage;
|
||||
const indexed_triangle_set &sampled = mesh_to_sample(mesh.its);
|
||||
HeightFieldSampler sampler;
|
||||
if (params.subdiv_feature)
|
||||
sampler = make_combined_displacement_sampler(mesh.its, layers, current, uv_ptr);
|
||||
sampler = sample_in_frame(make_combined_displacement_sampler(sampled, layers, current, uv_ptr));
|
||||
// Colour boundaries need triangles of their own - the chord test cannot see them, since
|
||||
// the height field is perfectly smooth across a change of filament.
|
||||
ColorFieldSampler color;
|
||||
// A flat-colour layer is matched against single filaments only, as the bake does, so its
|
||||
// boundaries are refined where the bake will actually change filament.
|
||||
if (params.subdiv_color_edge_mm > 0.f && !color_settings.empty())
|
||||
color = make_combined_color_sampler(mesh.its, layers, current, make_palette_quantizer(color_settings.palette),
|
||||
make_palette_quantizer(color_settings.palette_pure), uv_ptr);
|
||||
color = sample_in_frame(make_combined_color_sampler(sampled, layers, current,
|
||||
make_palette_quantizer(color_settings.palette),
|
||||
make_palette_quantizer(color_settings.palette_pure),
|
||||
uv_ptr));
|
||||
// The refinement follows perceived colour: a mix is one colour here, however the slicer
|
||||
// interleaves its filaments layer by layer.
|
||||
// "Min edge" is a feature-mode control (it is the floor the curvature test refines down
|
||||
@@ -6008,8 +6248,8 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
|
||||
cut_region[i] = (region[size_t(source[i])] & REFINE_PAINTED) ? 1 : 0;
|
||||
const std::vector<Vec2f> uv_refined = uv_now();
|
||||
const HeightFieldSampler refined_sampler =
|
||||
make_combined_displacement_sampler(mesh.its, layers, current,
|
||||
uv_refined.empty() ? nullptr : &uv_refined);
|
||||
sample_in_frame(make_combined_displacement_sampler(mesh_to_sample(mesh.its), layers, current,
|
||||
uv_refined.empty() ? nullptr : &uv_refined));
|
||||
std::vector<int> cut_source;
|
||||
size_t cuts = 0;
|
||||
indexed_triangle_set cut = cut_mesh_at_steps(mesh.its, cut_region, refined_sampler,
|
||||
@@ -6047,7 +6287,7 @@ void GLGizmoTextureDisplacement::subdivide_model_adaptive()
|
||||
|
||||
update_model_object(); // flush any in-progress stroke into the committed masks first
|
||||
|
||||
if (!mv->is_texture_displacement_painted()) {
|
||||
if (!any_part_painted()) {
|
||||
show_error(nullptr, _u8L("Paint the area you want to subdivide first - adaptive subdivision only "
|
||||
"refines where you have painted."));
|
||||
return;
|
||||
@@ -6209,7 +6449,8 @@ void GLGizmoTextureDisplacement::remesh_model()
|
||||
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."));
|
||||
"shared by more than two triangles), or the target edge length may already be met."),
|
||||
PrepareScope::AllParts);
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::rebuild_subdivide_preview()
|
||||
@@ -6360,23 +6601,35 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_height_texture(const TextureDis
|
||||
|
||||
void GLGizmoTextureDisplacement::bake(bool own_snapshot)
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
if (!mv || m_bake_in_progress)
|
||||
ModelVolume *owner = texture_volume();
|
||||
if (owner == nullptr || m_bake_in_progress)
|
||||
return;
|
||||
|
||||
// Make sure the currently active layer's in-progress edits are flushed into the model before
|
||||
// baking, otherwise the most recent, not-yet-committed strokes would be silently skipped.
|
||||
update_model_object();
|
||||
|
||||
if (!mv->is_texture_displacement_painted()) {
|
||||
// Every painted part, not only the one holding the layer stack: the brush paints across all of
|
||||
// them (see update_model_object()), so baking one would leave the user's paint on the others
|
||||
// untouched and their geometry flat.
|
||||
const std::vector<ModelVolume *> parts = painted_parts();
|
||||
if (parts.empty()) {
|
||||
show_error(nullptr, _u8L("Nothing is painted, there is nothing to bake."));
|
||||
return;
|
||||
}
|
||||
|
||||
const Vec3d anchor = texture_anchor();
|
||||
const std::vector<float> drops = resting_drops(parts);
|
||||
std::vector<TextureDisplacementBakeVolume> volumes;
|
||||
volumes.reserve(parts.size());
|
||||
for (size_t i = 0; i < parts.size(); ++i)
|
||||
volumes.push_back(texture_displacement_bake_volume(*parts[i], layers_for_part(*parts[i]),
|
||||
owner->texture_displacement_options, anchor, drops[i]));
|
||||
|
||||
m_bake_in_progress = true;
|
||||
queue_texture_displacement_bake(*mv, color_settings_for(*mv), [this]() {
|
||||
queue_texture_displacement_bake(std::move(volumes), color_settings_for(*owner), [this]() {
|
||||
m_bake_in_progress = false;
|
||||
// Baking replaces the volume's mesh (new id, new topology) without changing the object's
|
||||
// Baking replaces each baked part's mesh (new id, new topology) without changing the object's
|
||||
// id or volume count, so GLGizmoPainterBase::data_changed()'s usual change-detection never
|
||||
// notices it needs to reload - do it explicitly here, otherwise the gizmo keeps painting
|
||||
// and rendering against the stale, pre-bake TriangleSelectorPatch until the object is
|
||||
@@ -6448,7 +6701,7 @@ void GLGizmoTextureDisplacement::bake_standard()
|
||||
return;
|
||||
|
||||
update_model_object(); // flush the active layer's in-progress strokes before anything reads them
|
||||
if (!mv->is_texture_displacement_painted()) {
|
||||
if (!any_part_painted()) {
|
||||
show_error(nullptr, _u8L("Nothing is painted, there is nothing to bake."));
|
||||
return;
|
||||
}
|
||||
@@ -6520,7 +6773,8 @@ void GLGizmoTextureDisplacement::run_stage_debug()
|
||||
|
||||
m_debug_in_progress = true;
|
||||
queue_texture_displacement_debug(
|
||||
*mv, color_settings_for(*mv), params, run_prepare, m_debug_check_topology,
|
||||
*mv, color_settings_for(*mv), params, prepare_texture_frame(*mv), resting_drops({mv}).front(), run_prepare,
|
||||
m_debug_check_topology,
|
||||
[this](std::vector<BakeStageSnapshot> stages) {
|
||||
m_debug_in_progress = false;
|
||||
if (!stages.empty()) {
|
||||
@@ -6548,6 +6802,9 @@ void GLGizmoTextureDisplacement::show_debug_stage(int index)
|
||||
m_use_shaded_preview = false;
|
||||
m_preview_color_runs.clear();
|
||||
m_preview_glmodel.reset();
|
||||
// The stage is a capture of one part's run, so the other parts go back to being drawn as themselves
|
||||
// rather than keeping a live preview the captured stages say nothing about.
|
||||
m_part_previews.clear();
|
||||
|
||||
indexed_triangle_set its;
|
||||
its.vertices = stage.mesh.vertices;
|
||||
@@ -6669,23 +6926,36 @@ void GLGizmoTextureDisplacement::render_debug_stage_panel(ModelVolume *mv)
|
||||
|
||||
void GLGizmoTextureDisplacement::queue_prepare(const TextureDisplacementPrepareParams ¶ms,
|
||||
const std::string &snapshot_name, bool then_bake,
|
||||
const std::string &unchanged_msg)
|
||||
const std::string &unchanged_msg, PrepareScope scope)
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
if (mv == nullptr || m_prepare_in_progress || m_bake_in_progress)
|
||||
ModelVolume *owner = texture_volume();
|
||||
if (owner == nullptr || m_prepare_in_progress || m_bake_in_progress)
|
||||
return;
|
||||
|
||||
// Preparing only the layer stack's own part would hand the displacement a coarse mesh everywhere
|
||||
// else - a height map can only move vertices that already exist, so on a multibody object that is
|
||||
// the difference between relief and a flat face. See PrepareScope for which parts that means.
|
||||
const std::vector<ModelVolume *> parts = scope == PrepareScope::AllParts ? texture_parts() : painted_parts();
|
||||
if (parts.empty())
|
||||
return;
|
||||
|
||||
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;
|
||||
if (mv->has_texture_uv())
|
||||
input.model_uv = mv->texture_uv;
|
||||
input.volumes.reserve(parts.size());
|
||||
for (const ModelVolume *part : parts) {
|
||||
TextureDisplacementPrepareVolume entry;
|
||||
entry.volume_id = part->id();
|
||||
entry.base_mesh = part->mesh().its;
|
||||
entry.masks = facets_data_of(*part);
|
||||
entry.layers = layers_for_part(*part);
|
||||
if (part->has_texture_uv())
|
||||
entry.model_uv = part->texture_uv;
|
||||
entry.texture_frame = prepare_texture_frame(*part);
|
||||
input.volumes.push_back(std::move(entry));
|
||||
}
|
||||
if (params.subdiv_color_edge_mm > 0.f)
|
||||
input.color = color_settings_for(*mv);
|
||||
input.color = color_settings_for(*owner);
|
||||
|
||||
m_prepare_in_progress = true;
|
||||
queue_texture_displacement_prepare(std::move(input), [this, then_bake, unchanged_msg](
|
||||
@@ -8399,9 +8669,20 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
const float footer_top = ImGui::GetCursorScreenPos().y;
|
||||
ImGui::Separator();
|
||||
{
|
||||
const float x0 = ImGui::GetCursorPosX();
|
||||
const int base_k = mv != nullptr ? int((mv->mesh().facets_count() + 500) / 1000) : 0;
|
||||
const bool v2 = mv != nullptr && mv->texture_displacement_options.pipeline_v2;
|
||||
const float x0 = ImGui::GetCursorPosX();
|
||||
// Over every part the bake will act on, not just the one holding the layer stack, so the count
|
||||
// the footer quotes is the count Bake is actually working with on a multibody object. Before
|
||||
// anything is painted there is nothing to bake yet, and the whole object is the honest estimate.
|
||||
size_t base_facets = 0;
|
||||
{
|
||||
std::vector<ModelVolume *> counted = painted_parts();
|
||||
if (counted.empty())
|
||||
counted = texture_parts();
|
||||
for (const ModelVolume *part : counted)
|
||||
base_facets += part->mesh().facets_count();
|
||||
}
|
||||
const int base_k = int((base_facets + 500) / 1000);
|
||||
const bool v2 = mv != nullptr && mv->texture_displacement_options.pipeline_v2;
|
||||
|
||||
// The triangle budget is the one subdivision control Standard mode keeps: its right value depends on
|
||||
// the part rather than on the recipe (a big model, or a fine texture, simply needs more of them), and
|
||||
@@ -8534,7 +8815,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
|
||||
ImGui::SameLine(x0 + panel_w - button_w * 2 - style.ItemSpacing.x);
|
||||
|
||||
const bool can_bake = !busy && mv != nullptr && mv->is_texture_displacement_painted();
|
||||
// Read before Close below, which releases the selection any_part_painted() finds the parts through.
|
||||
const bool painted = mv != nullptr && any_part_painted();
|
||||
const bool can_bake = !busy && painted;
|
||||
const std::string bake_label = m_prepare_in_progress ? _u8L("Preparing...") :
|
||||
m_bake_in_progress ? _u8L("Baking...") :
|
||||
into_u8(m_desc.at("bake"));
|
||||
@@ -8554,7 +8837,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_imgui->pop_bold_font();
|
||||
GLGizmoUtils::pop_orca_button_style();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
panel_tooltip(mv != nullptr && !mv->is_texture_displacement_painted() ?
|
||||
panel_tooltip(mv != nullptr && !painted ?
|
||||
(m_seam_edit_mode ? _u8L("Nothing is painted yet. The UV editor's seam tool is on, so "
|
||||
"strokes on the model mark seams instead of painting - turn "
|
||||
"it off in the pane to paint.") :
|
||||
@@ -8580,8 +8863,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
// What Bake will produce, and which layers it will skip.
|
||||
if (mv != nullptr) {
|
||||
const std::string base_count = base_k > 0 ? Slic3r::format(_u8L("%1% k"), base_k) :
|
||||
std::to_string(mv->mesh().facets_count());
|
||||
const unsigned base_n = base_k > 0 ? unsigned(base_k) * 1000 : unsigned(mv->mesh().facets_count());
|
||||
std::to_string(base_facets);
|
||||
const unsigned base_n = base_k > 0 ? unsigned(base_k) * 1000 : unsigned(base_facets);
|
||||
const unsigned result_n = unsigned(base_k + m_subdivide_budget_k) * 1000;
|
||||
// TRN %1% is a triangle count, either exact or rounded to thousands such as "120 k".
|
||||
std::string note = v2 ? Slic3r::format(into_u8(_L_PLURAL("Model: %1% triangle - the bake refines to the resolution above, up to its budget",
|
||||
@@ -8591,7 +8874,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
"Bake moves existing vertices - the model stays at %1% triangles", base_n)), base_count) :
|
||||
Slic3r::format(into_u8(_L_PLURAL("Bake result up to ~%1% k triangle", "Bake result up to ~%1% k triangles", result_n)),
|
||||
base_k + m_subdivide_budget_k);
|
||||
if (mv->is_texture_displacement_painted()) {
|
||||
if (painted) {
|
||||
std::vector<std::string> skipped;
|
||||
for (const TextureDisplacementLayer &l : mv->texture_displacement_layers)
|
||||
if (!slot_painted(l.slot))
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include <cstddef>
|
||||
#include "libslic3r/TriangleSelector.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
@@ -69,6 +70,14 @@ public:
|
||||
// to the samplers so a ModelUV layer drives
|
||||
// the refinement through its own layout.
|
||||
const std::vector<Vec2f> &model_uv,
|
||||
// `base`'s coordinates -> the frame the bake
|
||||
// samples the texture in, so the refinement
|
||||
// follows the texture the bake will apply.
|
||||
// Only the sampling moves into it: the mesh,
|
||||
// and every length in `params`, stay in
|
||||
// `base`'s own coordinates. Identity samples
|
||||
// there as well.
|
||||
const Transform3d &texture_frame,
|
||||
const TextureDisplacementPrepareParams ¶ms,
|
||||
const TextureColorSettings &color_settings,
|
||||
const DisplacementProgressFn &progress,
|
||||
@@ -185,6 +194,35 @@ public:
|
||||
ColorRGBA color;
|
||||
};
|
||||
std::vector<PreviewColorRun> m_preview_color_runs;
|
||||
|
||||
// The displaced preview of each painted model part *other* than the one owning the layer stack. That
|
||||
// part keeps m_preview_glmodel and m_preview_its, which the paint overlays, the wireframe and the UV
|
||||
// and seam editors are all keyed to; the remaining parts of a multibody object only need their
|
||||
// geometry drawn, so that is all this holds. Built by the same preview job, one entry per part.
|
||||
//
|
||||
// Held behind unique_ptr because GLModel owns GPU buffers and has no move constructor: a vector of
|
||||
// them by value would copy the handles on reallocation and free them twice.
|
||||
struct PartPreview
|
||||
{
|
||||
ObjectID volume_id;
|
||||
GLModel glmodel;
|
||||
std::vector<PreviewColorRun> color_runs;
|
||||
};
|
||||
std::vector<std::unique_ptr<PartPreview>> m_part_previews;
|
||||
// The part `preview` was built for, or nullptr if it has gone away or had its mesh replaced since -
|
||||
// in which case that part's own volume is drawn instead of a preview that no longer describes it.
|
||||
const ModelVolume *preview_part(const PartPreview &preview) const;
|
||||
// Whether the true-displacement view has anything to draw. Asking m_preview_glmodel alone would
|
||||
// hide the real volumes behind an empty preview whenever only another part is painted.
|
||||
bool has_true_preview() const { return m_preview_glmodel.is_initialized() || !m_part_previews.empty(); }
|
||||
// The preview mesh as it is uploaded: when the run reported a palette entry per triangle, the index
|
||||
// buffer is reordered so that each palette colour becomes one contiguous draw range (returned in
|
||||
// `runs`); otherwise `its` is handed back unchanged and `runs` stays empty. Only the index buffer is
|
||||
// permuted, never the vertices - see PreviewColorRun.
|
||||
static indexed_triangle_set group_preview_by_color(const indexed_triangle_set &its,
|
||||
const std::vector<uint8_t> &triangle_color,
|
||||
const std::vector<ColorRGBA> &entry_colors,
|
||||
std::vector<PreviewColorRun> &runs);
|
||||
// True if any of the volume's layers would actually colour something: colour turned on, and a
|
||||
// texture that has colour to give. What decides whether a palette is captured into a job at all,
|
||||
// and so whether the colour criterion and the mmu write ever run.
|
||||
@@ -258,12 +296,46 @@ private:
|
||||
void on_shutdown() override;
|
||||
PainterGizmoType get_painter_type() const override;
|
||||
|
||||
// Phase 1 restricts the texture layer list to the first model-part volume of the current
|
||||
// object (the common single-volume case); multi-part objects only get texture layers on
|
||||
// their first part until a later phase. Returns nullptr if there is no model part.
|
||||
// The model part that owns the object's texture layer stack, its options, and the unwrap the UV and
|
||||
// seam editors work on: the first model part of the current object. Those are object-wide settings
|
||||
// kept on one part rather than copied onto all of them. Returns nullptr if there is no model part.
|
||||
//
|
||||
// This is *not* the set of parts a bake acts on - see painted_parts(). Reading the layer stack and
|
||||
// baking are different questions, and answering the second one with this is what made texture
|
||||
// displacement ignore every part of a multibody object but the first.
|
||||
ModelVolume* texture_volume();
|
||||
const ModelVolume* texture_volume() const;
|
||||
|
||||
// Every model part of the current object, in the object's own volume order - which is also the
|
||||
// order update_from_model_object() builds m_triangle_selectors in, so index i here is selector i.
|
||||
std::vector<ModelVolume *> texture_parts();
|
||||
// The parts a bake or a preview acts on: those of texture_parts() carrying paint in at least one
|
||||
// slot. Empty when nothing is painted anywhere on the object, which is what the Bake button and its
|
||||
// "nothing is painted" check test.
|
||||
std::vector<ModelVolume *> painted_parts();
|
||||
bool any_part_painted() const;
|
||||
|
||||
// The object's layer stack as it applies to `part`. The stack itself lives on texture_volume(), but
|
||||
// four of each layer's fields index *that* part's mesh and the chart ids of *its* unwrap - the LSCM
|
||||
// seam edges, the island placements, their join groups and the per-vertex uv overrides. Carried onto
|
||||
// another part they would address triangles and charts that have nothing to do with it, so they are
|
||||
// dropped and that part unwraps on its own. Everything else - the image, depth, tiling, projection
|
||||
// method, blend mode, colour - applies unchanged, since every part is baked in the one texture frame
|
||||
// anchored at texture_anchor(); that is what lets one stack texture a whole multibody object.
|
||||
std::vector<TextureDisplacementLayer> layers_for_part(const ModelVolume &part) const;
|
||||
// The world point the object's texture is anchored at: texture_volume()'s origin. Every part is
|
||||
// baked and previewed in the frame anchored there (see texture_displacement_bake_frame()), so a
|
||||
// tiled pattern runs on across the parts and a view projection lands where it was aimed on each.
|
||||
Vec3d texture_anchor() const;
|
||||
// For each of `parts`, at the same index: how far the object's lowest point lies below that part's
|
||||
// own, in mm - what its bake lowers its resting plane by, so every part is clamped against the one
|
||||
// plane the object rests on. 0 for the lowest part, and so for a single-part object.
|
||||
std::vector<float> resting_drops(const std::vector<ModelVolume *> &parts);
|
||||
// Where prepare_mesh() samples `part`'s texture: the frame its bake samples in when the object has
|
||||
// several model parts, so they are all refined for one continuous texture. A single-part object
|
||||
// keeps the identity, the frame it has always been refined in.
|
||||
Transform3d prepare_texture_frame(const ModelVolume &part);
|
||||
|
||||
void add_texture_layer();
|
||||
void remove_texture_layer(int slot);
|
||||
// Moves the layer in `slot` to position `to_index` of the slot-ordered stack (an insertion index,
|
||||
@@ -310,8 +382,14 @@ private:
|
||||
// - `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.
|
||||
// - `scope` is which of the object's model parts to prepare. Anything feeding a displacement
|
||||
// prepares exactly the parts that displacement will move; a plain Remesh is a mesh operation on
|
||||
// the object and takes all of them, so painting the rest of a multibody object afterwards has a
|
||||
// dense enough mesh to show relief on.
|
||||
enum class PrepareScope { PaintedParts, AllParts };
|
||||
void queue_prepare(const TextureDisplacementPrepareParams ¶ms, const std::string &snapshot_name,
|
||||
bool then_bake, const std::string &unchanged_msg);
|
||||
bool then_bake, const std::string &unchanged_msg,
|
||||
PrepareScope scope = PrepareScope::PaintedParts);
|
||||
// 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.
|
||||
@@ -351,14 +429,14 @@ private:
|
||||
// volume's slot among them, or -1 if it has none (no selection, or the lists disagree).
|
||||
int texture_volume_raycaster_index() const;
|
||||
|
||||
// Paints exactly the facets currently visible from the camera onto the active layer, replacing
|
||||
// whatever that layer had painted. "Visible" is two tests: the facet faces the camera, and its
|
||||
// centroid is not hidden behind other geometry (a real raycast, so a concave part's far inner
|
||||
// wall is correctly excluded). When `uv_clip` is given (the projection frame's matrix), facets
|
||||
// whose centroid falls outside the frame's uv unit square are skipped first - which both clips
|
||||
// the selection to the frame and spares the raycast for everything outside it. Costs one ray
|
||||
// query per surviving facet, so it is a one-shot action, never a per-frame one. Returns the
|
||||
// number of facets selected.
|
||||
// Paints exactly the facets currently visible from the camera onto the active layer, on every model
|
||||
// part, replacing whatever that layer had painted on each. "Visible" is two tests: the facet faces
|
||||
// the camera, and its centroid is not hidden behind other geometry of any part (a real raycast, so
|
||||
// a concave part's far inner wall is correctly excluded). When `uv_clip` is given (the projection
|
||||
// frame's matrix), facets whose centroid falls outside the frame's uv unit square are skipped first
|
||||
// - which both clips the selection to the frame and spares the raycast for everything outside it.
|
||||
// Costs one ray query per surviving facet and part, so it is a one-shot action, never a per-frame
|
||||
// one. Returns the number of facets selected.
|
||||
int select_visible_faces(const std::array<float, 12> *uv_clip = nullptr);
|
||||
|
||||
// When set, "Capture current view" also re-selects the visible faces, so the viewpoint the
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include <exception>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -50,6 +51,9 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
|
||||
// bake, which on a subdivided mesh can run for several seconds.
|
||||
// Colour, when any layer asks for it, is computed in the same pass as the displacement: both need
|
||||
// the same per-layer projection and UV work, and doing it twice would double the expensive part.
|
||||
//
|
||||
// The quantizers are built once for the whole job rather than per part: each fills a colour lookup
|
||||
// cube that costs tens of milliseconds, and the palette is the object's, not the part's.
|
||||
TextureColorRequest color_request;
|
||||
TextureColorRequest *color = nullptr;
|
||||
if (!m_input.color.empty()) {
|
||||
@@ -57,27 +61,38 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
|
||||
if (!m_input.color.palette_pure.empty())
|
||||
color_request.quantize_pure = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette_pure);
|
||||
color_request.despeckle_passes = m_input.color.despeckle_passes;
|
||||
color_request.out_triangle = &m_triangle_color;
|
||||
if (color_request.quantize)
|
||||
color = &color_request;
|
||||
}
|
||||
|
||||
// Each part gets its own slice of the 1..100 range, in order, so one progress bar covers the whole
|
||||
// bake however many parts it displaces.
|
||||
const size_t count = m_input.volumes.size();
|
||||
m_results.resize(count);
|
||||
int last_reported = 1;
|
||||
m_result = TriangleMesh(build_texture_displacement(
|
||||
m_input.base_mesh, m_input.layers, m_input.facets_data, m_input.options,
|
||||
[&ctl, &status, &last_reported](int percent) {
|
||||
if (ctl.was_canceled())
|
||||
return false;
|
||||
// The notification repaints (and wakes the idle loop) on every call, so only push a
|
||||
// message when the displayed integer percentage actually moves.
|
||||
if (percent > last_reported) {
|
||||
last_reported = percent;
|
||||
ctl.update_status(percent, status);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
color, m_input.volume_to_world, nullptr, &m_stats,
|
||||
m_input.model_uv.empty() ? nullptr : &m_input.model_uv));
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
const TextureDisplacementBakeVolume &volume = m_input.volumes[i];
|
||||
if (color != nullptr)
|
||||
color_request.out_triangle = &m_results[i].triangle_color;
|
||||
m_results[i].mesh = TriangleMesh(build_texture_displacement(
|
||||
volume.base_mesh, volume.layers, volume.facets_data, volume.options,
|
||||
[&ctl, &status, &last_reported, i, count](int percent) {
|
||||
if (ctl.was_canceled())
|
||||
return false;
|
||||
// The notification repaints (and wakes the idle loop) on every call, so only push a
|
||||
// message when the displayed integer percentage actually moves.
|
||||
const int overall = int((i * 100 + size_t(std::max(0, percent))) / count);
|
||||
if (overall > last_reported) {
|
||||
last_reported = overall;
|
||||
ctl.update_status(overall, status);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
color, volume.volume_to_world, nullptr, &m_results[i].stats,
|
||||
volume.model_uv.empty() ? nullptr : &volume.model_uv, &volume.texture_anchor, volume.resting_drop));
|
||||
if (ctl.was_canceled())
|
||||
break;
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -92,147 +107,184 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
|
||||
~OnExit() { if (fn) fn(); }
|
||||
} on_exit{m_on_finished};
|
||||
|
||||
if (canceled || eptr || m_result.empty())
|
||||
if (canceled || eptr)
|
||||
return;
|
||||
|
||||
// A bake that moved nothing - no layer could be sampled, or every sample was zero - must not be
|
||||
// committed: committing is what clears the baked layers' paint, so the user would see the painted
|
||||
// region simply vanish with no relief in its place and no idea why. Keep the paint and say so.
|
||||
{
|
||||
const indexed_triangle_set &out = m_result.its;
|
||||
bool unchanged = out.indices.size() == m_input.base_mesh.indices.size() &&
|
||||
out.vertices.size() == m_input.base_mesh.vertices.size();
|
||||
for (size_t i = 0; unchanged && i < out.vertices.size(); ++i)
|
||||
unchanged = (out.vertices[i] - m_input.base_mesh.vertices[i]).cwiseAbs().maxCoeff() < 1e-5f;
|
||||
if (unchanged) {
|
||||
show_error(nullptr, _u8L("The bake produced no displacement, so nothing was changed and the paint was kept. "
|
||||
"Check that the painted layer has a texture and a non-zero depth."));
|
||||
return;
|
||||
}
|
||||
// Judged per part, because on a multibody object only some parts may carry a layer that displaces;
|
||||
// the ones that did move are committed and the rest are left exactly as they were.
|
||||
const auto moved = [this](size_t i) {
|
||||
const TriangleMesh &result = m_results[i].mesh;
|
||||
if (result.empty())
|
||||
return false;
|
||||
const indexed_triangle_set &out = result.its;
|
||||
const indexed_triangle_set &base = m_input.volumes[i].base_mesh;
|
||||
bool unchanged = out.indices.size() == base.indices.size() && out.vertices.size() == base.vertices.size();
|
||||
for (size_t v = 0; unchanged && v < out.vertices.size(); ++v)
|
||||
unchanged = (out.vertices[v] - base.vertices[v]).cwiseAbs().maxCoeff() < 1e-5f;
|
||||
return !unchanged;
|
||||
};
|
||||
|
||||
std::vector<size_t> to_commit;
|
||||
for (size_t i = 0; i < m_results.size(); ++i)
|
||||
if (moved(i))
|
||||
to_commit.push_back(i);
|
||||
if (to_commit.empty()) {
|
||||
show_error(nullptr, _u8L("The bake produced no displacement, so nothing was changed and the paint was kept. "
|
||||
"Check that the painted layer has a texture and a non-zero depth."));
|
||||
return;
|
||||
}
|
||||
|
||||
Plater *plater = wxGetApp().plater();
|
||||
|
||||
const auto commit = [this, plater]() {
|
||||
ModelVolume *volume = get_model_volume(m_input.volume_id, plater->model().objects);
|
||||
if (volume == nullptr)
|
||||
return;
|
||||
|
||||
// The filament each palette entry prints in. This is where a mix becomes a mixed filament slot,
|
||||
// and only a mix the bake actually painted with: the preview never creates one, so the project
|
||||
// gains only the slots this result needs. Done before anything below names them - adding a slot
|
||||
// runs ModelVolume::update_extruder_count(), which clamps paint above the old filament count.
|
||||
// The filament each palette entry prints in. This is where a mix becomes a mixed filament slot,
|
||||
// and only a mix the bake actually painted with: the preview never creates one, so the project
|
||||
// gains only the slots this result needs. Decided once for the whole bake, over the entries every
|
||||
// part used between them, so the same mix cannot end up in two slots. Done before anything below
|
||||
// names them - adding a slot runs ModelVolume::update_extruder_count(), which clamps paint above
|
||||
// the old filament count.
|
||||
const auto filaments_for_palette = [this, plater, &to_commit]() {
|
||||
// palette_filaments() only cares *which* entries appear, so the union of the parts' entries
|
||||
// stands in for their concatenated per-triangle colours.
|
||||
std::vector<char> seen(m_input.color.palette.size() + 1, 0);
|
||||
std::vector<uint8_t> used;
|
||||
for (const size_t i : to_commit)
|
||||
for (const uint8_t entry : m_results[i].triangle_color)
|
||||
if (entry > 0 && size_t(entry) < seen.size() && !seen[size_t(entry)]) {
|
||||
seen[size_t(entry)] = 1;
|
||||
used.push_back(entry);
|
||||
}
|
||||
Sidebar &sidebar = plater->sidebar();
|
||||
const std::vector<int> filament_of = GLGizmoTextureDisplacement::palette_filaments(
|
||||
m_input.color.palette, m_triangle_color, [&sidebar](const PrintableColor &mix) {
|
||||
return GLGizmoTextureDisplacement::palette_filaments(
|
||||
m_input.color.palette, used, [&sidebar](const PrintableColor &mix) {
|
||||
return sidebar.ensure_mixed_filament({ unsigned(mix.a + 1), unsigned(mix.b + 1) },
|
||||
{ mix.a_percent(), 100 - mix.a_percent() });
|
||||
});
|
||||
};
|
||||
|
||||
// Whether the bake replaced the triangles or only moved the vertices it was given. The classic
|
||||
// path is topology-preserving by contract, so every per-triangle annotation on the volume still
|
||||
// means what it did; the default pipeline refines, cuts and simplifies, so none of them do.
|
||||
// Both are checked: the flag is what the bake promised, the count is what it delivered. Only
|
||||
// the triangles matter - the annotations are per facet and reference no vertex index - so a
|
||||
// vertex count that changed on its own (the bake compacts unreferenced vertices away) is not a
|
||||
// reason to remap anything.
|
||||
const bool topology_changed = m_input.options.pipeline_v2 ||
|
||||
m_result.its.indices.size() != m_input.base_mesh.indices.size();
|
||||
const auto commit = [this, plater, &to_commit, &filaments_for_palette]() {
|
||||
const std::vector<int> filament_of = filaments_for_palette();
|
||||
ModelObject *object = nullptr;
|
||||
|
||||
// Carry the volume's existing paint - the model's own colours above all - across that rebuild.
|
||||
// Without it a bake left the mmu_segmentation_facets bitstream pointing at a triangle set that
|
||||
// no longer existed, which is what scrambled the colours of an imported coloured model even
|
||||
// with colour switched off on every layer and nothing in the bake that was meant to touch them.
|
||||
//
|
||||
// transfer_facet_paint() rather than ModelVolume::restore_painting(): the latter's remap wants
|
||||
// a target triangle overlapping the source paint to within 0.01 mm, which the baked surface is
|
||||
// nowhere near - it has moved by the relief depth. Nothing to do when the topology is intact:
|
||||
// triangle i is still triangle i, so the paint is already exactly right and carrying it at all
|
||||
// could only lose detail.
|
||||
std::array<TriangleSelector::TriangleSplittingData, 4> before;
|
||||
if (topology_changed) {
|
||||
before[0] = volume->supported_facets.get_data();
|
||||
before[1] = volume->seam_facets.get_data();
|
||||
before[2] = volume->mmu_segmentation_facets.get_data();
|
||||
before[3] = volume->fuzzy_skin_facets.get_data();
|
||||
}
|
||||
for (const size_t i : to_commit) {
|
||||
ModelVolume *volume = get_model_volume(m_input.volumes[i].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, so a part replaced since this bake started - by an undo, another bake, or a
|
||||
// boolean - simply fails to find its volume and is skipped.
|
||||
if (volume == nullptr)
|
||||
continue;
|
||||
const TextureDisplacementBakeVolume &input = m_input.volumes[i];
|
||||
const std::vector<uint8_t> &triangle_color = m_results[i].triangle_color;
|
||||
|
||||
volume->set_mesh(std::move(m_result));
|
||||
volume->set_new_unique_id();
|
||||
volume->calculate_convex_hull();
|
||||
// Whether the bake replaced the triangles or only moved the vertices it was given. The classic
|
||||
// path is topology-preserving by contract, so every per-triangle annotation on the volume still
|
||||
// means what it did; the default pipeline refines, cuts and simplifies, so none of them do.
|
||||
// Both are checked: the flag is what the bake promised, the count is what it delivered. Only
|
||||
// the triangles matter - the annotations are per facet and reference no vertex index - so a
|
||||
// vertex count that changed on its own (the bake compacts unreferenced vertices away) is not a
|
||||
// reason to remap anything.
|
||||
const bool topology_changed = input.options.pipeline_v2 ||
|
||||
m_results[i].mesh.its.indices.size() != input.base_mesh.indices.size();
|
||||
|
||||
if (topology_changed) {
|
||||
const std::array<FacetsAnnotation *, 4> target{ &volume->supported_facets, &volume->seam_facets,
|
||||
&volume->mmu_segmentation_facets,
|
||||
&volume->fuzzy_skin_facets };
|
||||
for (size_t ch = 0; ch < target.size(); ++ch) {
|
||||
const std::vector<uint8_t> state =
|
||||
transfer_facet_paint(m_input.base_mesh, before[ch], volume->mesh().its);
|
||||
// Nothing was painted in this channel, or there is nothing to paint it onto: either
|
||||
// way the annotation must not keep describing the mesh that is gone.
|
||||
if (state.empty()) {
|
||||
target[ch]->reset();
|
||||
continue;
|
||||
}
|
||||
TriangleSelector selector(volume->mesh());
|
||||
bool any = false;
|
||||
for (size_t i = 0; i < state.size(); ++i)
|
||||
if (state[i] != 0) {
|
||||
selector.set_facet(int(i), EnforcerBlockerType(state[i]));
|
||||
any = true;
|
||||
// Carry the volume's existing paint - the model's own colours above all - across that rebuild.
|
||||
// Without it a bake left the mmu_segmentation_facets bitstream pointing at a triangle set that
|
||||
// no longer existed, which is what scrambled the colours of an imported coloured model even
|
||||
// with colour switched off on every layer and nothing in the bake that was meant to touch them.
|
||||
//
|
||||
// transfer_facet_paint() rather than ModelVolume::restore_painting(): the latter's remap wants
|
||||
// a target triangle overlapping the source paint to within 0.01 mm, which the baked surface is
|
||||
// nowhere near - it has moved by the relief depth. Nothing to do when the topology is intact:
|
||||
// triangle i is still triangle i, so the paint is already exactly right and carrying it at all
|
||||
// could only lose detail.
|
||||
std::array<TriangleSelector::TriangleSplittingData, 4> before;
|
||||
if (topology_changed) {
|
||||
before[0] = volume->supported_facets.get_data();
|
||||
before[1] = volume->seam_facets.get_data();
|
||||
before[2] = volume->mmu_segmentation_facets.get_data();
|
||||
before[3] = volume->fuzzy_skin_facets.get_data();
|
||||
}
|
||||
|
||||
volume->set_mesh(std::move(m_results[i].mesh));
|
||||
volume->set_new_unique_id();
|
||||
volume->calculate_convex_hull();
|
||||
|
||||
if (topology_changed) {
|
||||
const std::array<FacetsAnnotation *, 4> target{ &volume->supported_facets, &volume->seam_facets,
|
||||
&volume->mmu_segmentation_facets,
|
||||
&volume->fuzzy_skin_facets };
|
||||
for (size_t ch = 0; ch < target.size(); ++ch) {
|
||||
const std::vector<uint8_t> state =
|
||||
transfer_facet_paint(input.base_mesh, before[ch], volume->mesh().its);
|
||||
// Nothing was painted in this channel, or there is nothing to paint it onto: either
|
||||
// way the annotation must not keep describing the mesh that is gone.
|
||||
if (state.empty()) {
|
||||
target[ch]->reset();
|
||||
continue;
|
||||
}
|
||||
if (any)
|
||||
target[ch]->set(selector);
|
||||
else
|
||||
target[ch]->reset();
|
||||
TriangleSelector selector(volume->mesh());
|
||||
bool any = false;
|
||||
for (size_t t = 0; t < state.size(); ++t)
|
||||
if (state[t] != 0) {
|
||||
selector.set_facet(int(t), EnforcerBlockerType(state[t]));
|
||||
any = true;
|
||||
}
|
||||
if (any)
|
||||
target[ch]->set(selector);
|
||||
else
|
||||
target[ch]->reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Colour lands in mmu_segmentation_facets, merged *over* whatever is already painted there
|
||||
// rather than replacing it: a triangle the texture does not colour keeps its existing filament,
|
||||
// and one the user never painted at all stays at NONE, which already means "the volume's own
|
||||
// filament". That is what confines the effect to the painted area without having to invent a
|
||||
// colour for everything outside it. What was already painted is whatever the transfer above
|
||||
// left in the annotation, so this bake's colour lands on top of it rather than instead of it.
|
||||
if (!m_triangle_color.empty() && m_triangle_color.size() == volume->mesh().its.indices.size()) {
|
||||
TriangleSelector selector(volume->mesh());
|
||||
const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data();
|
||||
if (!existing.bitstream.empty())
|
||||
selector.deserialize(existing, false);
|
||||
for (size_t i = 0; i < m_triangle_color.size(); ++i) {
|
||||
const size_t entry = size_t(m_triangle_color[i]);
|
||||
if (entry > 0 && entry <= filament_of.size() && filament_of[entry - 1] >= 0)
|
||||
selector.set_facet(int(i), EnforcerBlockerType(filament_of[entry - 1] + 1));
|
||||
// Colour lands in mmu_segmentation_facets, merged *over* whatever is already painted there
|
||||
// rather than replacing it: a triangle the texture does not colour keeps its existing filament,
|
||||
// and one the user never painted at all stays at NONE, which already means "the volume's own
|
||||
// filament". That is what confines the effect to the painted area without having to invent a
|
||||
// colour for everything outside it. What was already painted is whatever the transfer above
|
||||
// left in the annotation, so this bake's colour lands on top of it rather than instead of it.
|
||||
if (!triangle_color.empty() && triangle_color.size() == volume->mesh().its.indices.size()) {
|
||||
TriangleSelector selector(volume->mesh());
|
||||
const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data();
|
||||
if (!existing.bitstream.empty())
|
||||
selector.deserialize(existing, false);
|
||||
for (size_t t = 0; t < triangle_color.size(); ++t) {
|
||||
const size_t entry = size_t(triangle_color[t]);
|
||||
if (entry > 0 && entry <= filament_of.size() && filament_of[entry - 1] >= 0)
|
||||
selector.set_facet(int(t), EnforcerBlockerType(filament_of[entry - 1] + 1));
|
||||
}
|
||||
volume->mmu_segmentation_facets.set(selector);
|
||||
}
|
||||
volume->mmu_segmentation_facets.set(selector);
|
||||
|
||||
// Clear the paint mask of every layer that was actually baked so a repeat bake (or the paint
|
||||
// overlay) doesn't act on triangles that no longer represent the same unbaked surface. The
|
||||
// texture layer definitions themselves are left untouched so the user can keep sculpting with
|
||||
// the same textures.
|
||||
//
|
||||
// A mask the bake did *not* consume only still means what it did if the topology is unchanged,
|
||||
// which is true of the classic path (it moves existing vertices) but not of the one-run
|
||||
// pipeline, which rebuilds and then simplifies the mesh. A mask left behind against the old
|
||||
// topology is exactly what makes the gizmo reload an empty selector over a non-empty mask and
|
||||
// then erase it on the next flush - see GLGizmoTextureDisplacement::update_from_model_object().
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
const auto it = std::find_if(input.layers.begin(), input.layers.end(),
|
||||
[slot](const TextureDisplacementLayer &l) { return l.slot == slot; });
|
||||
if (topology_changed || (it != input.layers.end() && !it->empty()))
|
||||
volume->texture_displacement_facet(slot).reset();
|
||||
}
|
||||
|
||||
// The model's own UV channel. Resampled onto a rebuilt mesh from the surface the bake started
|
||||
// from: a displaced vertex came from its foot point on that surface, which is the position its
|
||||
// uv belongs to - so no distance limit, the relief depth is exactly how far it has moved.
|
||||
if (!input.model_uv.empty())
|
||||
volume->texture_uv = topology_changed ? transfer_uv_channel(input.base_mesh, input.model_uv,
|
||||
volume->mesh().its) :
|
||||
input.model_uv;
|
||||
|
||||
if (object == nullptr)
|
||||
object = volume->get_object();
|
||||
}
|
||||
|
||||
// Clear the paint mask of every layer that was actually baked so a repeat bake (or the paint
|
||||
// overlay) doesn't act on triangles that no longer represent the same unbaked surface. The
|
||||
// texture layer definitions themselves are left untouched so the user can keep sculpting with
|
||||
// the same textures.
|
||||
//
|
||||
// A mask the bake did *not* consume only still means what it did if the topology is unchanged,
|
||||
// which is true of the classic path (it moves existing vertices) but not of the one-run
|
||||
// pipeline, which rebuilds and then simplifies the mesh. A mask left behind against the old
|
||||
// topology is exactly what makes the gizmo reload an empty selector over a non-empty mask and
|
||||
// then erase it on the next flush - see GLGizmoTextureDisplacement::update_from_model_object().
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
const auto it = std::find_if(m_input.layers.begin(), m_input.layers.end(),
|
||||
[slot](const TextureDisplacementLayer &l) { return l.slot == slot; });
|
||||
if (topology_changed || (it != m_input.layers.end() && !it->empty()))
|
||||
volume->texture_displacement_facet(slot).reset();
|
||||
}
|
||||
|
||||
// The model's own UV channel. Resampled onto a rebuilt mesh from the surface the bake started
|
||||
// from: a displaced vertex came from its foot point on that surface, which is the position its
|
||||
// uv belongs to - so no distance limit, the relief depth is exactly how far it has moved.
|
||||
if (!m_input.model_uv.empty())
|
||||
volume->texture_uv = topology_changed ? transfer_uv_channel(m_input.base_mesh, m_input.model_uv,
|
||||
volume->mesh().its) :
|
||||
m_input.model_uv;
|
||||
|
||||
ModelObject *object = volume->get_object();
|
||||
if (object == nullptr)
|
||||
return;
|
||||
|
||||
@@ -261,8 +313,15 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
|
||||
// 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) {
|
||||
// dialog: the result is a usable mesh, just not the one the settings described. Summed over the
|
||||
// parts that hit it, since the budget is spent per part and one message is enough.
|
||||
size_t refined = 0, budget = 0;
|
||||
for (const size_t i : to_commit)
|
||||
if (m_results[i].stats.budget_limited) {
|
||||
refined += m_results[i].stats.triangles_refined;
|
||||
budget += m_results[i].stats.triangles_budget;
|
||||
}
|
||||
if (refined > 0) {
|
||||
// Thousands under a million, so a small budget is not reported as "0.1 M".
|
||||
const auto count = [](size_t n) {
|
||||
return n >= 1000000 ? Slic3r::format("%1$.1f M", double(n) / 1000000.) :
|
||||
@@ -272,26 +331,39 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
|
||||
NotificationType::CustomNotification, NotificationManager::NotificationLevel::WarningNotificationLevel,
|
||||
Slic3r::format(_u8L("The triangle budget limited the detail: this resolution needs %1% triangles, "
|
||||
"the budget kept %2%. Raise Budget or use a coarser Resolution for the full detail."),
|
||||
count(m_stats.triangles_refined), count(m_stats.triangles_budget)));
|
||||
count(refined), count(budget)));
|
||||
}
|
||||
}
|
||||
|
||||
void queue_texture_displacement_bake(const ModelVolume &volume, const TextureColorSettings &color,
|
||||
TextureDisplacementBakeVolume texture_displacement_bake_volume(const ModelVolume &volume,
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementOptions &options,
|
||||
const Vec3d &texture_anchor,
|
||||
float resting_drop)
|
||||
{
|
||||
TextureDisplacementBakeVolume out;
|
||||
out.volume_id = volume.id();
|
||||
out.base_mesh = volume.mesh().its;
|
||||
out.layers = layers;
|
||||
out.options = options;
|
||||
out.volume_to_world = texture_displacement_volume_to_world(volume);
|
||||
out.texture_anchor = texture_anchor;
|
||||
out.resting_drop = resting_drop;
|
||||
if (volume.has_texture_uv())
|
||||
out.model_uv = volume.texture_uv;
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
out.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
|
||||
return out;
|
||||
}
|
||||
|
||||
void queue_texture_displacement_bake(std::vector<TextureDisplacementBakeVolume> volumes,
|
||||
const TextureColorSettings &color,
|
||||
std::function<void()> on_finished, bool take_snapshot)
|
||||
{
|
||||
TextureDisplacementBakeInput input;
|
||||
input.volumes = std::move(volumes);
|
||||
input.color = color;
|
||||
input.take_snapshot = take_snapshot;
|
||||
input.volume_id = volume.id();
|
||||
input.base_mesh = volume.mesh().its;
|
||||
if (volume.has_texture_uv())
|
||||
input.model_uv = volume.texture_uv;
|
||||
input.layers = volume.texture_displacement_layers;
|
||||
input.options = volume.texture_displacement_options;
|
||||
input.volume_to_world = texture_displacement_volume_to_world(volume);
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
|
||||
|
||||
|
||||
auto &worker = wxGetApp().plater()->get_ui_job_worker();
|
||||
queue_job(worker, std::make_unique<TextureDisplacementBakeJob>(std::move(input), std::move(on_finished)));
|
||||
|
||||
@@ -17,10 +17,11 @@ namespace Slic3r { class ModelVolume; }
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
// Everything process() needs, captured by value on the main thread when the job is queued so the
|
||||
// worker thread never touches the live Model concurrently with the UI (mirrors how EmbossJob's
|
||||
// DataBase is captured before process() runs).
|
||||
struct TextureDisplacementBakeInput
|
||||
// One model part's share of a bake: its own mesh, its own paint masks and UV channel, and the layer
|
||||
// stack to displace it by. Captured by value on the main thread when the job is queued so the worker
|
||||
// thread never touches the live Model concurrently with the UI (mirrors how EmbossJob's DataBase is
|
||||
// captured before process() runs).
|
||||
struct TextureDisplacementBakeVolume
|
||||
{
|
||||
ObjectID volume_id;
|
||||
indexed_triangle_set base_mesh;
|
||||
@@ -30,22 +31,38 @@ struct TextureDisplacementBakeInput
|
||||
// Mesh coordinates -> world millimetres. Captured here with everything else so the worker never
|
||||
// reaches back into the live Model for it. See build_texture_displacement().
|
||||
Transform3d volume_to_world = Transform3d::Identity();
|
||||
// Captured on the main thread. Empty unless some layer is colouring, in which case the bake also
|
||||
// writes the volume's mmu_segmentation_facets - the same per-triangle filament assignment the MMU
|
||||
// paint gizmo writes - alongside the displaced geometry.
|
||||
TextureColorSettings color;
|
||||
// The volume's own UV channel (ModelVolume::texture_uv), empty when it has none: what a ModelUV
|
||||
// The part's own UV channel (ModelVolume::texture_uv), empty when it has none: what a ModelUV
|
||||
// layer maps through, and what the commit puts back on the baked mesh.
|
||||
std::vector<Vec2f> model_uv;
|
||||
// Where the part sits in its object: the world point the object's texture is anchored at, and how
|
||||
// far the object's lowest point lies below the part's own. See build_texture_displacement().
|
||||
Vec3d texture_anchor = Vec3d::Zero();
|
||||
float resting_drop = 0.f;
|
||||
};
|
||||
|
||||
struct TextureDisplacementBakeInput
|
||||
{
|
||||
// Every model part the bake acts on, in the object's own volume order. An object made of several
|
||||
// parts carries one entry per part the paint actually reaches: the layer stack is the object's,
|
||||
// but each part is displaced through its own mesh, its own masks and its own UV channel. Baking
|
||||
// only the first part is what made texture displacement silently ignore every other body of a
|
||||
// multibody object.
|
||||
std::vector<TextureDisplacementBakeVolume> volumes;
|
||||
// Captured on the main thread, and shared by every part: the palette comes from the project's
|
||||
// filaments and from the layer stack, neither of which is per part. Empty unless some layer is
|
||||
// colouring, in which case the bake also writes each part's mmu_segmentation_facets - the same
|
||||
// per-triangle filament assignment the MMU paint gizmo writes - alongside the displaced geometry.
|
||||
TextureColorSettings color;
|
||||
// Whether this job pushes its own undo step when it commits. False when the caller has already
|
||||
// taken one that is meant to cover the displacement as well - Standard mode's Bake, which remeshes
|
||||
// and subdivides first and has to undo as a single action.
|
||||
bool take_snapshot = true;
|
||||
bool take_snapshot = true;
|
||||
};
|
||||
|
||||
// Bakes a volume's painted texture-displacement layers into real mesh geometry in the background,
|
||||
// Bakes an object's painted texture-displacement layers into real mesh geometry in the background,
|
||||
// then commits the result on the main thread - mirrors EmbossJob's UpdateJob/update_volume()
|
||||
// bake-and-commit pattern (see EmbossJob.cpp).
|
||||
// bake-and-commit pattern (see EmbossJob.cpp). Every part is displaced in the one job and committed
|
||||
// under the one undo step, so a multibody object undoes as a single action.
|
||||
class TextureDisplacementBakeJob : public Job
|
||||
{
|
||||
public:
|
||||
@@ -55,24 +72,41 @@ public:
|
||||
void finalize(bool canceled, std::exception_ptr &eptr) override;
|
||||
|
||||
private:
|
||||
// What the bake produced for one part of m_input.volumes, at the same index.
|
||||
struct VolumeResult
|
||||
{
|
||||
TriangleMesh mesh;
|
||||
// Per triangle of `mesh`: the palette entry to print it in, as its index + 1 (0 = leave
|
||||
// alone). Empty unless a layer asked for colour. See TextureColorRequest; finalize() turns
|
||||
// these into filaments.
|
||||
std::vector<uint8_t> triangle_color;
|
||||
// What the bake spent, for the message it leaves behind when the budget capped the detail.
|
||||
TextureBakeStats stats;
|
||||
};
|
||||
|
||||
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 palette entry to print it in, as its index + 1 (0 = leave alone).
|
||||
// Empty unless a layer asked for colour. See TextureColorRequest; finalize() turns these into
|
||||
// filaments.
|
||||
std::vector<uint8_t> m_triangle_color;
|
||||
std::vector<VolumeResult> m_results;
|
||||
std::function<void()> m_on_finished;
|
||||
};
|
||||
|
||||
// Captures `volume`'s current mesh/layers/paint data and queues a TextureDisplacementBakeJob on
|
||||
// the app's UI job worker. `on_finished` is always called once the job settles (success, failure,
|
||||
// or cancellation), so the caller can clear its own "bake in progress" UI state. Must be called
|
||||
// from the main thread.
|
||||
void queue_texture_displacement_bake(const ModelVolume &volume, const TextureColorSettings &color,
|
||||
// Captures each of `volumes`' current mesh/layers/paint data and queues a single
|
||||
// TextureDisplacementBakeJob on the app's UI job worker. `on_finished` is always called once the job
|
||||
// settles (success, failure, or cancellation), so the caller can clear its own "bake in progress" UI
|
||||
// state. Must be called from the main thread.
|
||||
void queue_texture_displacement_bake(std::vector<TextureDisplacementBakeVolume> volumes,
|
||||
const TextureColorSettings &color,
|
||||
std::function<void()> on_finished, bool take_snapshot = true);
|
||||
|
||||
// `volume`'s share of a bake, read off the live Model. `layers`, `options`, `texture_anchor` and
|
||||
// `resting_drop` are passed in rather than taken from `volume` because they belong to the object, not
|
||||
// to the part: every part of a multibody object is displaced by the same stack, in the same texture
|
||||
// frame and against the same plate (see GLGizmoTextureDisplacement::layers_for_part()).
|
||||
TextureDisplacementBakeVolume texture_displacement_bake_volume(const ModelVolume &volume,
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementOptions &options,
|
||||
const Vec3d &texture_anchor,
|
||||
float resting_drop);
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_TextureDisplacementBakeJob_hpp_
|
||||
@@ -60,8 +60,8 @@ void TextureDisplacementDebugJob::process(Ctl &ctl)
|
||||
// experimental pipeline refines as part of the bake and has nothing to prepare.
|
||||
if (m_input.run_prepare && !m_input.options.pipeline_v2) {
|
||||
const TextureDisplacementPrepareResult prepared =
|
||||
GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, uv, m_input.prepare_params,
|
||||
m_input.color,
|
||||
GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, uv, m_input.prepare_texture_frame,
|
||||
m_input.prepare_params, m_input.color,
|
||||
// Preparation is roughly half the run; the bake
|
||||
// takes the progress bar from there.
|
||||
[&report](int pct) { return report(1 + pct / 2); },
|
||||
@@ -86,7 +86,8 @@ void TextureDisplacementDebugJob::process(Ctl &ctl)
|
||||
build_texture_displacement(mesh, m_input.layers, masks, m_input.options,
|
||||
[&report](int pct) { return report(50 + pct / 2); },
|
||||
/* color */ nullptr, m_input.volume_to_world, &recorder, /* stats */ nullptr,
|
||||
uv.empty() ? nullptr : &uv);
|
||||
uv.empty() ? nullptr : &uv, /* texture_anchor: the volume's own origin */ nullptr,
|
||||
m_input.resting_drop);
|
||||
|
||||
m_stages = recorder.take();
|
||||
ctl.update_status(100, status); // always finish at 100: this is what closes the notification
|
||||
@@ -106,6 +107,7 @@ void TextureDisplacementDebugJob::finalize(bool canceled, std::exception_ptr &ep
|
||||
|
||||
void queue_texture_displacement_debug(const ModelVolume &volume, const TextureColorSettings &color,
|
||||
const TextureDisplacementPrepareParams &prepare_params,
|
||||
const Transform3d &prepare_texture_frame, float resting_drop,
|
||||
bool run_prepare, bool check_topology,
|
||||
std::function<void(std::vector<BakeStageSnapshot>)> on_finished)
|
||||
{
|
||||
@@ -119,6 +121,8 @@ void queue_texture_displacement_debug(const ModelVolume &volume, const TextureCo
|
||||
input.check_topology = check_topology;
|
||||
input.color = color;
|
||||
input.volume_to_world = texture_displacement_volume_to_world(volume);
|
||||
input.prepare_texture_frame = prepare_texture_frame;
|
||||
input.resting_drop = resting_drop;
|
||||
if (volume.has_texture_uv())
|
||||
input.model_uv = volume.texture_uv;
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
|
||||
@@ -44,6 +44,11 @@ struct TextureDisplacementDebugInput
|
||||
// Mesh coordinates -> world millimetres, as the bake gets it. The captured stages are brought
|
||||
// back into mesh coordinates before they are handed over, so the gizmo can draw them directly.
|
||||
Transform3d volume_to_world = Transform3d::Identity();
|
||||
// Where the preparation samples the texture, and how far below the volume's own bottom its object
|
||||
// rests - both as the bake and its preparation get them (see TextureDisplacementPrepareVolume and
|
||||
// TextureDisplacementBakeVolume).
|
||||
Transform3d prepare_texture_frame = Transform3d::Identity();
|
||||
float resting_drop = 0.f;
|
||||
// Only the quantizer is used, and only by the refinement's colour criterion - the debug run
|
||||
// produces no colour of its own.
|
||||
TextureColorSettings color;
|
||||
@@ -79,6 +84,7 @@ private:
|
||||
// caller can clear its own in-progress state. Must be called from the main thread.
|
||||
void queue_texture_displacement_debug(const ModelVolume &volume, const TextureColorSettings &color,
|
||||
const TextureDisplacementPrepareParams &prepare_params,
|
||||
const Transform3d &prepare_texture_frame, float resting_drop,
|
||||
bool run_prepare, bool check_topology,
|
||||
std::function<void(std::vector<BakeStageSnapshot>)> on_finished);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/Jobs/Worker.hpp"
|
||||
|
||||
@@ -44,18 +45,29 @@ void TextureDisplacementPrepareJob::process(Ctl &ctl)
|
||||
// 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.
|
||||
// Each part gets its own slice of the range, in order, so one bar covers them all.
|
||||
const size_t count = m_input.volumes.size();
|
||||
m_results.resize(count);
|
||||
int last_reported = 1;
|
||||
m_result = GLGizmoTextureDisplacement::prepare_mesh(m_input.base_mesh, m_input.masks, m_input.layers,
|
||||
m_input.model_uv, m_input.params, m_input.color,
|
||||
[&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;
|
||||
});
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
const TextureDisplacementPrepareVolume &volume = m_input.volumes[i];
|
||||
m_results[i] = GLGizmoTextureDisplacement::prepare_mesh(volume.base_mesh, volume.masks, volume.layers,
|
||||
volume.model_uv, volume.texture_frame,
|
||||
m_input.params, m_input.color,
|
||||
[&ctl, &status, &last_reported, i, count](int percent) {
|
||||
if (ctl.was_canceled())
|
||||
return false;
|
||||
const int overall =
|
||||
int((i * 100 + size_t(std::max(0, percent))) / count);
|
||||
if (overall > last_reported) {
|
||||
last_reported = overall;
|
||||
ctl.update_status(overall, status);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (ctl.was_canceled())
|
||||
break;
|
||||
}
|
||||
|
||||
ctl.update_status(100, status); // always finish at 100: this is what closes the notification
|
||||
}
|
||||
@@ -72,44 +84,56 @@ void TextureDisplacementPrepareJob::finalize(bool canceled, std::exception_ptr &
|
||||
|
||||
if (canceled || eptr)
|
||||
return;
|
||||
if (m_result.paint_lost) {
|
||||
outcome = TextureDisplacementPrepareOutcome::PaintLost;
|
||||
return;
|
||||
// One part losing its paint stops the whole prepare, exactly as it stops a single-part one: the
|
||||
// promise is that a prepare which cannot carry the paint changes nothing, and committing the other
|
||||
// parts would break it halfway. The message that follows tells the user to remesh by hand instead.
|
||||
for (const TextureDisplacementPrepareResult &result : m_results)
|
||||
if (result.paint_lost) {
|
||||
outcome = TextureDisplacementPrepareOutcome::PaintLost;
|
||||
return;
|
||||
}
|
||||
|
||||
Plater *plater = wxGetApp().plater();
|
||||
|
||||
// Resolved before the snapshot is opened, so a run whose parts have all gone away leaves no empty
|
||||
// undo step behind. The lookup doubles as the 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.
|
||||
std::vector<std::pair<size_t, ModelVolume *>> to_commit;
|
||||
for (size_t i = 0; i < m_results.size(); ++i) {
|
||||
if (m_results[i].mesh.indices.empty())
|
||||
continue;
|
||||
if (ModelVolume *volume = get_model_volume(m_input.volumes[i].volume_id, plater->model().objects);
|
||||
volume != nullptr && volume->get_object() != nullptr)
|
||||
to_commit.emplace_back(i, volume);
|
||||
}
|
||||
if (m_result.mesh.indices.empty()) {
|
||||
if (to_commit.empty()) {
|
||||
// Nothing needed doing anywhere, which is not an error - every mesh already met the criteria.
|
||||
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;
|
||||
|
||||
ModelObject *object = to_commit.front().second->get_object();
|
||||
{
|
||||
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)]));
|
||||
// The model's own UV channel, resampled onto the new mesh by the run. Also put back after
|
||||
// restore_painting(), which resets it along with the extra facets for the same reason: it is
|
||||
// indexed by triangle and the triangles have changed.
|
||||
volume->texture_uv = std::move(m_result.uv);
|
||||
for (const auto &[i, volume] : to_commit) {
|
||||
// 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_results[i].mesh)));
|
||||
volume->set_new_unique_id();
|
||||
volume->calculate_convex_hull();
|
||||
volume->restore_painting(saved_painting);
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
|
||||
volume->texture_displacement_facet(slot).set_data(std::move(m_results[i].masks[size_t(slot)]));
|
||||
// The model's own UV channel, resampled onto the new mesh by the run. Also put back after
|
||||
// restore_painting(), which resets it along with the extra facets for the same reason: it is
|
||||
// indexed by triangle and the triangles have changed.
|
||||
volume->texture_uv = std::move(m_results[i].uv);
|
||||
}
|
||||
|
||||
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
||||
const ModelObjectPtrs &objs = plater->model().objects;
|
||||
|
||||
@@ -31,36 +31,50 @@ namespace Slic3r::GUI {
|
||||
// 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
|
||||
Committed, // the parts now carry the prepared mesh and the paint carried onto it
|
||||
Unchanged, // nothing needed doing - the meshes already met the criteria; the model was not touched
|
||||
PaintLost, // a remesh landed but no layer's paint survived it, so nothing was committed
|
||||
Failed, // cancelled, threw, or the volumes went away while the job ran
|
||||
};
|
||||
|
||||
struct TextureDisplacementPrepareInput
|
||||
// One model part's share of a prepare. As with the bake, `layers` belongs to the object rather than to
|
||||
// the part (see GLGizmoTextureDisplacement::layers_for_part()).
|
||||
struct TextureDisplacementPrepareVolume
|
||||
{
|
||||
ObjectID volume_id;
|
||||
indexed_triangle_set base_mesh;
|
||||
TextureDisplacementFacetsData masks;
|
||||
std::vector<TextureDisplacementLayer> layers;
|
||||
TextureDisplacementPrepareParams params;
|
||||
// Captured on the main thread. Empty when no layer is colouring, in which case the refinement
|
||||
// skips the colour criterion entirely. Only the *quantizer* is used here: refinement follows
|
||||
// perceived colour, never the interleaving that realises a mix.
|
||||
TextureColorSettings color;
|
||||
// The volume's own UV channel (ModelVolume::texture_uv), empty when it has none. prepare_mesh()
|
||||
// The part's own UV channel (ModelVolume::texture_uv), empty when it has none. prepare_mesh()
|
||||
// resamples it onto the mesh it produces, and the commit puts it back - like the masks, it is
|
||||
// indexed by triangle and so cannot survive the stages on its own.
|
||||
std::vector<Vec2f> model_uv;
|
||||
// Where prepare_mesh() samples the texture: the bake frame shared by every part of a multibody
|
||||
// object, so each part is refined for the texture the bake will put on it. Identity, the part's own
|
||||
// coordinates, for a single-part object.
|
||||
Transform3d texture_frame = Transform3d::Identity();
|
||||
};
|
||||
|
||||
struct TextureDisplacementPrepareInput
|
||||
{
|
||||
// Every model part to prepare, in the object's own volume order - the same parts the bake that
|
||||
// follows will displace, so a multibody object is refined everywhere its paint reaches rather than
|
||||
// only on its first part.
|
||||
std::vector<TextureDisplacementPrepareVolume> volumes;
|
||||
TextureDisplacementPrepareParams params;
|
||||
// Captured on the main thread. Empty when no layer is colouring, in which case the refinement
|
||||
// skips the colour criterion entirely. Only the *quantizer* is used here: refinement follows
|
||||
// perceived colour, never the interleaving that realises a mix.
|
||||
TextureColorSettings color;
|
||||
// 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;
|
||||
std::string snapshot_name;
|
||||
};
|
||||
|
||||
class TextureDisplacementPrepareJob : public Job
|
||||
{
|
||||
TextureDisplacementPrepareInput m_input;
|
||||
TextureDisplacementPrepareResult m_result;
|
||||
std::vector<TextureDisplacementPrepareResult> m_results;
|
||||
std::function<void(TextureDisplacementPrepareOutcome)> m_on_finished;
|
||||
|
||||
public:
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
#include "TextureDisplacementPreviewJob.hpp"
|
||||
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include <exception>
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
TextureDisplacementPreviewJob::TextureDisplacementPreviewJob(TextureDisplacementPreviewInput &&input, uint64_t generation,
|
||||
std::shared_ptr<const std::atomic<uint64_t>> current_generation,
|
||||
std::function<void(TextureDisplacementPreviewResult, uint64_t)> on_finished)
|
||||
TextureDisplacementPreviewJob::TextureDisplacementPreviewJob(
|
||||
TextureDisplacementPreviewInput &&input, uint64_t generation,
|
||||
std::shared_ptr<const std::atomic<uint64_t>> current_generation,
|
||||
std::function<void(std::vector<TextureDisplacementPreviewResult>, uint64_t)> on_finished)
|
||||
: m_input(std::move(input)), m_generation(generation), m_current_generation(std::move(current_generation)),
|
||||
m_on_finished(std::move(on_finished))
|
||||
{
|
||||
@@ -26,6 +29,9 @@ void TextureDisplacementPreviewJob::process(Ctl &ctl)
|
||||
|
||||
// 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 quantizers are built once for the whole run: the palette is the object's, not the part's,
|
||||
// and each one fills a colour lookup cube that costs tens of milliseconds.
|
||||
TextureColorRequest color_request;
|
||||
TextureColorRequest *color = nullptr;
|
||||
if (!m_input.color.empty()) {
|
||||
@@ -33,23 +39,32 @@ void TextureDisplacementPreviewJob::process(Ctl &ctl)
|
||||
if (!m_input.color.palette_pure.empty())
|
||||
color_request.quantize_pure = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette_pure);
|
||||
color_request.despeckle_passes = m_input.color.despeckle_passes;
|
||||
color_request.out_triangle = &m_result.triangle_color;
|
||||
if (color_request.quantize)
|
||||
color = &color_request;
|
||||
}
|
||||
|
||||
m_result.mesh = build_texture_displacement(m_input.base_mesh, m_input.layers, m_input.facets_data,
|
||||
m_input.options,
|
||||
[this, &ctl](int) {
|
||||
// Bail the moment this preview stops being the current
|
||||
// one; build_texture_displacement() then returns an
|
||||
// empty mesh and finalize() drops it.
|
||||
return !ctl.was_canceled() &&
|
||||
(!m_current_generation ||
|
||||
m_current_generation->load() == m_generation);
|
||||
},
|
||||
color, m_input.volume_to_world, nullptr, nullptr,
|
||||
m_input.model_uv.empty() ? nullptr : &m_input.model_uv);
|
||||
// Bail the moment this preview stops being the current one: build_texture_displacement() then
|
||||
// returns an empty mesh, and finalize() drops the whole run rather than showing half of it.
|
||||
const auto still_wanted = [this, &ctl](int) {
|
||||
return !ctl.was_canceled() &&
|
||||
(!m_current_generation || m_current_generation->load() == m_generation);
|
||||
};
|
||||
|
||||
m_results.resize(m_input.volumes.size());
|
||||
for (size_t i = 0; i < m_input.volumes.size(); ++i) {
|
||||
const TextureDisplacementPreviewVolume &volume = m_input.volumes[i];
|
||||
if (color != nullptr)
|
||||
color_request.out_triangle = &m_results[i].triangle_color;
|
||||
m_results[i].mesh = build_texture_displacement(volume.base_mesh, volume.layers, volume.facets_data,
|
||||
volume.options, still_wanted, color, volume.volume_to_world,
|
||||
nullptr, nullptr,
|
||||
volume.model_uv.empty() ? nullptr : &volume.model_uv,
|
||||
&volume.texture_anchor, volume.resting_drop);
|
||||
if (!still_wanted(0)) {
|
||||
m_results.clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr)
|
||||
@@ -62,9 +77,9 @@ void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &
|
||||
// was ever queued again for the rest of the session. An empty result is the caller's signal that
|
||||
// nothing usable came back; it already handles that.
|
||||
if (canceled || eptr)
|
||||
m_on_finished(TextureDisplacementPreviewResult{}, m_generation);
|
||||
m_on_finished({}, m_generation);
|
||||
else
|
||||
m_on_finished(std::move(m_result), m_generation);
|
||||
m_on_finished(std::move(m_results), m_generation);
|
||||
}
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
@@ -15,9 +15,9 @@
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
// Everything process() needs, captured by value on the main thread when the job is queued -
|
||||
// mirrors TextureDisplacementBakeInput, but a preview never writes back to the Model.
|
||||
struct TextureDisplacementPreviewInput
|
||||
// One model part's share of a preview. Captured by value on the main thread when the job is queued -
|
||||
// mirrors TextureDisplacementBakeVolume, but a preview never writes back to the Model.
|
||||
struct TextureDisplacementPreviewVolume
|
||||
{
|
||||
indexed_triangle_set base_mesh;
|
||||
std::vector<TextureDisplacementLayer> layers;
|
||||
@@ -26,16 +26,26 @@ struct TextureDisplacementPreviewInput
|
||||
// Mesh coordinates -> world millimetres, so the preview is displaced in the same space the bake
|
||||
// is and the two cannot disagree. See build_texture_displacement().
|
||||
Transform3d volume_to_world = Transform3d::Identity();
|
||||
// Empty unless a layer is colouring, in which case the preview reports the palette entry per
|
||||
// triangle alongside the mesh, so the Normal view shows the colours the bake will produce.
|
||||
TextureColorSettings color;
|
||||
// The volume's own UV channel (ModelVolume::texture_uv), so a ModelUV layer previews through the
|
||||
// same layout it will bake through. Empty when the volume has none.
|
||||
// The part's own UV channel (ModelVolume::texture_uv), so a ModelUV layer previews through the
|
||||
// same layout it will bake through. Empty when the part has none.
|
||||
std::vector<Vec2f> model_uv;
|
||||
// Where the part sits in its object, as TextureDisplacementBakeVolume has it.
|
||||
Vec3d texture_anchor = Vec3d::Zero();
|
||||
float resting_drop = 0.f;
|
||||
};
|
||||
|
||||
// A preview result: the displaced mesh, and - when the input carried a palette - one palette entry per
|
||||
// triangle (its index + 1; 0 means "no colour from the texture").
|
||||
struct TextureDisplacementPreviewInput
|
||||
{
|
||||
// The same parts the bake would displace, in the object's own volume order, so the preview shows
|
||||
// the whole of what Bake will do on a multibody object rather than just its first part.
|
||||
std::vector<TextureDisplacementPreviewVolume> volumes;
|
||||
// Empty unless a layer is colouring, in which case the preview reports the palette entry per
|
||||
// triangle alongside the mesh, so the Normal view shows the colours the bake will produce.
|
||||
TextureColorSettings color;
|
||||
};
|
||||
|
||||
// A preview result for one part: the displaced mesh, and - when the input carried a palette - one
|
||||
// palette entry per triangle (its index + 1; 0 means "no colour from the texture").
|
||||
struct TextureDisplacementPreviewResult
|
||||
{
|
||||
indexed_triangle_set mesh;
|
||||
@@ -65,9 +75,12 @@ public:
|
||||
// already been superseded stops burning CPU instead of running to completion for a result that
|
||||
// will only be thrown away. That matters because the UI job worker runs one job at a time in FIFO
|
||||
// order: without it, a Bake queued behind a handful of stale previews waits for every one of them.
|
||||
//
|
||||
// `on_finished` receives one result per input part, at the same index - or an empty vector when
|
||||
// the run was superseded, cancelled or threw.
|
||||
TextureDisplacementPreviewJob(TextureDisplacementPreviewInput &&input, uint64_t generation,
|
||||
std::shared_ptr<const std::atomic<uint64_t>> current_generation,
|
||||
std::function<void(TextureDisplacementPreviewResult, uint64_t)> on_finished);
|
||||
std::function<void(std::vector<TextureDisplacementPreviewResult>, uint64_t)> on_finished);
|
||||
|
||||
void process(Ctl &ctl) override;
|
||||
void finalize(bool canceled, std::exception_ptr &eptr) override;
|
||||
@@ -76,8 +89,8 @@ private:
|
||||
TextureDisplacementPreviewInput m_input;
|
||||
uint64_t m_generation;
|
||||
std::shared_ptr<const std::atomic<uint64_t>> m_current_generation;
|
||||
TextureDisplacementPreviewResult m_result;
|
||||
std::function<void(TextureDisplacementPreviewResult, uint64_t)> m_on_finished;
|
||||
std::vector<TextureDisplacementPreviewResult> m_results;
|
||||
std::function<void(std::vector<TextureDisplacementPreviewResult>, uint64_t)> m_on_finished;
|
||||
};
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
|
||||
@@ -2424,6 +2424,89 @@ TEST_CASE("TextureDisplacement: moving the model about the plate does not move t
|
||||
CHECK(max_diff < 1e-3f);
|
||||
}
|
||||
|
||||
TEST_CASE("Parts anchored at one point carry one continuous texture", "[TextureDisplacement]")
|
||||
{
|
||||
// The same surface, once as a part whose origin is the anchor and once as a part whose own origin
|
||||
// sits `offset` away from it - the way the bodies of a multibody object are each centred on their
|
||||
// own origin. Anchored at the same point they must bake the same relief; anchored at their own
|
||||
// origins, the second is the tiled pattern shifted by `offset`.
|
||||
const indexed_triangle_set cube = subdivide_mesh_uniform(its_make_cube(10.f, 10.f, 10.f), 1.f, 6);
|
||||
// Not a multiple of the 4 mm tile, and exact in binary like the grid itself: the checkerboard's steps
|
||||
// fall right on grid vertices, so a vertex rounded across a step would bake the other side of it.
|
||||
const Vec3d offset(1.25, 2.5, 0.75);
|
||||
indexed_triangle_set shifted = cube;
|
||||
for (Vec3f &v : shifted.vertices)
|
||||
v -= offset.cast<float>();
|
||||
const Transform3d shifted_to_world = Transform3d(Eigen::Translation3d(offset));
|
||||
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = 0;
|
||||
layer.image_data = make_checkerboard_png(16, 16);
|
||||
layer.tiling_scale = 4.f;
|
||||
layer.depth_mm = 0.5f;
|
||||
TextureDisplacementFacetsData facets;
|
||||
facets[0] = paint_whole_mesh(cube);
|
||||
|
||||
const indexed_triangle_set reference = build_texture_displacement(cube, { layer }, facets, classic_options());
|
||||
const Vec3d anchor = Vec3d::Zero();
|
||||
const indexed_triangle_set anchored = build_texture_displacement(shifted, { layer }, facets, classic_options(), {},
|
||||
nullptr, shifted_to_world, nullptr, nullptr,
|
||||
nullptr, &anchor);
|
||||
const indexed_triangle_set own_origin = build_texture_displacement(shifted, { layer }, facets, classic_options(), {},
|
||||
nullptr, shifted_to_world);
|
||||
REQUIRE(anchored.vertices.size() == reference.vertices.size());
|
||||
REQUIRE(own_origin.vertices.size() == reference.vertices.size());
|
||||
|
||||
float anchored_diff = 0.f, own_origin_diff = 0.f;
|
||||
for (size_t i = 0; i < reference.vertices.size(); ++i) {
|
||||
anchored_diff = std::max(anchored_diff, (anchored.vertices[i] + offset.cast<float>() - reference.vertices[i]).norm());
|
||||
own_origin_diff = std::max(own_origin_diff, (own_origin.vertices[i] + offset.cast<float>() - reference.vertices[i]).norm());
|
||||
}
|
||||
CHECK(anchored_diff < 1e-3f);
|
||||
CHECK(own_origin_diff > 0.1f);
|
||||
}
|
||||
|
||||
TEST_CASE("A part held above the plate keeps the relief on its underside", "[TextureDisplacement]")
|
||||
{
|
||||
// A fully painted cube displaces its bottom face straight down by the full depth. On its own, that
|
||||
// relief is clamped to the cube's bottom (see "relief is never driven below the model's resting
|
||||
// plane"); as a part of an object resting `drop` below it, it is clamped only at the object's plane.
|
||||
const bool v2 = GENERATE(false, true);
|
||||
const indexed_triangle_set cube = subdivide_mesh_uniform(its_make_cube(10.f, 10.f, 10.f), 2.f, 6);
|
||||
|
||||
TextureDisplacementFacetsData facets{};
|
||||
facets[0] = paint_whole_mesh(cube);
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = 0;
|
||||
layer.depth_mm = 2.0f;
|
||||
layer.tiling_scale = 5.0f;
|
||||
layer.image_data = make_flat_gray_png(255); // full depth everywhere
|
||||
|
||||
TextureDisplacementOptions options = classic_options();
|
||||
options.pipeline_v2 = v2;
|
||||
options.v2_refine_mm = 2.f;
|
||||
options.v2_max_triangles_k = 0;
|
||||
|
||||
const auto lowest_after = [&](float drop) {
|
||||
const indexed_triangle_set out = build_texture_displacement(cube, { layer }, facets, options, {}, nullptr,
|
||||
Transform3d::Identity(), nullptr, nullptr, nullptr,
|
||||
nullptr, drop);
|
||||
REQUIRE_FALSE(out.vertices.empty());
|
||||
float lowest = std::numeric_limits<float>::max();
|
||||
for (const Vec3f &v : out.vertices)
|
||||
lowest = std::min(lowest, v.z());
|
||||
return lowest;
|
||||
};
|
||||
|
||||
CAPTURE(v2);
|
||||
// The cube's bottom is z = 0. With the object resting 5 mm lower, the 2 mm relief fits above it...
|
||||
CHECK_THAT(lowest_after(5.f), WithinAbs(-2.0f, 1e-3f));
|
||||
// ...with the object resting 1 mm lower, it is clamped at that plane...
|
||||
CHECK_THAT(lowest_after(1.f), WithinAbs(-1.0f, 1e-3f));
|
||||
// ...and as the lowest part, at its own bottom.
|
||||
CHECK_THAT(lowest_after(0.f), WithinAbs(0.0f, 1e-3f));
|
||||
}
|
||||
|
||||
// Longest edge over the shortest altitude: 1.15 for an equilateral triangle, 2 for a right isosceles
|
||||
// one, unbounded for a needle.
|
||||
static float triangle_aspect(const Vec3f &a, const Vec3f &b, const Vec3f &c)
|
||||
|
||||
Reference in new issue
Block a user