Files
OrcaSlicer/src/slic3r/GUI/Jobs/TextureDisplacementPrepareJob.cpp
T
ExPikaPaka 18cef4f51d Texture a model through its own UV channel, and keep its colours through a bake
Importing a model that already carries colours was broken for texture
displacement in two ways: the UV layout it was authored against was thrown
away and re-unwrapped, and a bake overwrote the colours it arrived with.

A model's own UV channel is now kept. ModelVolume::texture_uv holds three uvs
per triangle, in the mesh's own triangle order and with V flipped to the
convention DecodedHeightTexture samples in. It is filled from a textured OBJ's
vt data on import and carried across every stage that replaces the mesh - the
colour import's painted mesh, the gizmo's remesh and subdivide, the default
pipeline's rebuild - by transfer_uv_channel(), which gives each new corner the
uv of its closest point on the surface it came from. A new mapping,
TextureProjectionMethod::ModelUV, samples that channel instead of solving an
unwrap, and a new layer on a volume that has one starts there. Its uv is
already normalized, so it keeps the layer's rotation and offset but not the
millimetre tile size (the slider is disabled) or the non-square aspect
correction; with no channel it falls back to Triplanar, as LSCM does when a
patch will not flatten.

A bake with colour off on every layer now leaves the model's colours where
they were. The default pipeline rebuilds the topology, which left
mmu_segmentation_facets describing a triangle set that no longer existed.
ModelVolume::restore_painting() cannot carry it: its remap wants a target
triangle overlapping the source paint to within 0.01 mm, and a baked surface
has moved by the relief depth. transfer_facet_paint() gives each new facet the
state at its own foot point on the pre-bake surface instead, and the bake
commit runs it over all four paint channels. The classic path is untouched,
since there triangle i is still triangle i.

Blend gains an Override entry next to Add: the layer replaces the displacement
under it rather than combining with it, which is what the stack's colour
already does where two layers overlap. The combo's order is the panel's, not
the enum's, so stored blend modes are unaffected.

Measured on a textured OBJ of 85224 triangles (uv_color_probe in the bake
harness): the channel survives import intact, the relief follows the model's
own uv on 98.7% of vertices against 51.3% for a re-unwrap, and a colour-off
bake through the default pipeline keeps the colours on 99.8% of the baked
surface where it previously kept none. Through a coloured 3mf, 100%.
2026-10-12 07:57:15 +02:00

133 lines
6.0 KiB
C++

#include "TextureDisplacementPrepareJob.hpp"
#include <algorithm>
#include <functional>
#include <exception>
#include "libslic3r/TextureDisplacement.hpp"
#include <cstddef>
#include <memory>
#include <optional>
#include <utility>
#include <string>
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/Jobs/Worker.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleSelector.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
#include "libslic3r/CutUtils.hpp"
#include "libslic3r/TriangleMesh.hpp"
namespace Slic3r::GUI {
TextureDisplacementPrepareJob::TextureDisplacementPrepareJob(
TextureDisplacementPrepareInput &&input, std::function<void(TextureDisplacementPrepareOutcome)> on_finished)
: m_input(std::move(input)), m_on_finished(std::move(on_finished))
{
}
void TextureDisplacementPrepareJob::process(Ctl &ctl)
{
const std::string status = _u8L("Preparing mesh for texture displacement");
ctl.update_status(1, status);
// Only ever touches m_input (captured by value before this job was queued) and local state - never
// the live Model - so this is safe to run concurrently with the UI thread.
//
// The progress hook is not just cosmetic: the framework's notification only grows a close button
// once it reaches 100%, and below that it shows Cancel, which is wired through here. Reporting is
// throttled to whole percentage points because every call repaints the notification and wakes the
// idle loop.
int last_reported = 1;
m_result = GLGizmoTextureDisplacement::prepare_mesh(m_input.base_mesh, m_input.masks, m_input.layers,
m_input.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;
});
ctl.update_status(100, status); // always finish at 100: this is what closes the notification
}
void TextureDisplacementPrepareJob::finalize(bool canceled, std::exception_ptr &eptr)
{
TextureDisplacementPrepareOutcome outcome = TextureDisplacementPrepareOutcome::Failed;
struct OnExit
{
std::function<void(TextureDisplacementPrepareOutcome)> fn;
const TextureDisplacementPrepareOutcome *outcome;
~OnExit() { if (fn) fn(*outcome); }
} on_exit{m_on_finished, &outcome};
if (canceled || eptr)
return;
if (m_result.paint_lost) {
outcome = TextureDisplacementPrepareOutcome::PaintLost;
return;
}
if (m_result.mesh.indices.empty()) {
outcome = TextureDisplacementPrepareOutcome::Unchanged;
return;
}
Plater *plater = wxGetApp().plater();
ModelVolume *volume = get_model_volume(m_input.volume_id, plater->model().objects);
// The lookup doubles as a staleness check: anything that replaces a volume's mesh also gives it a
// new id (set_new_unique_id()), so a prepare computed from a mesh that has since been replaced -
// by an undo, another bake, or a boolean - simply fails to find its volume and commits nothing.
if (volume == nullptr)
return;
ModelObject *object = volume->get_object();
if (object == nullptr)
return;
{
Plater::TakeSnapshot snapshot(plater, m_input.snapshot_name, UndoRedo::SnapshotType::GizmoAction);
// The four standard paint channels ride across on ModelVolume's own spatial remap. The eight
// texture-displacement masks do not go through it - prepare_mesh() has already carried them,
// exactly, from the source triangles the refinement records - so they are put back after
// restore_painting(), which resets every extra facet before remapping the channels it knows.
std::optional<TriangleSelector::SavedPainting> saved_painting = volume->save_painting();
volume->set_mesh(TriangleMesh(std::move(m_result.mesh)));
volume->set_new_unique_id();
volume->calculate_convex_hull();
volume->restore_painting(saved_painting);
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
volume->texture_displacement_facet(i).set_data(std::move(m_result.masks[size_t(i)]));
// 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);
if (ObjectList *obj_list = wxGetApp().obj_list()) {
const ModelObjectPtrs &objs = plater->model().objects;
auto it = std::find(objs.begin(), objs.end(), object);
if (it != objs.end())
obj_list->update_info_items(size_t(it - objs.begin()));
}
plater->changed_object(*object);
}
outcome = TextureDisplacementPrepareOutcome::Committed;
}
void queue_texture_displacement_prepare(TextureDisplacementPrepareInput &&input,
std::function<void(TextureDisplacementPrepareOutcome)> on_finished)
{
auto &worker = wxGetApp().plater()->get_ui_job_worker();
queue_job(worker, std::make_unique<TextureDisplacementPrepareJob>(std::move(input), std::move(on_finished)));
}
} // namespace Slic3r::GUI