mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-12 18:31:18 +00:00
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%.
133 lines
6.0 KiB
C++
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
|