mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-21 16:02:37 +00:00
- Bake: each layer is sampled only on its own painted area; analytic projections used to stack every layer over every painted region, so the top layer's texture showed on all of them (colour sampler too) - Auto resolution follows the texture's texel size and sharpness again - Unwrap: charts cut by each face's own normal (a cube gives 6 islands, not 12 triangles); non-disk charts (tubes, closed shells) are split until they flatten; connected nets test real triangle overlap, grow from the largest chart and are packed side by side - UV edits are stored per unwrapped copy, so dragging a seam vertex no longer moves its copies in neighbouring islands - UV pane: tool strip with unwrap settings moved in from the panel, sharp HiDPI icons, clearer island/edge/selection drawing with hover, texture picker from the thumbnail, texture no longer lost on reopen (GL state from the 3D view, background upload retries) - Panel: whole-model select/erase as icons in the tools row; inactive layers' paint shown muted; colour textures shown in colour in the picker - Built-in displacement texture library - Tests for unwrap segmentation, connected nets, UV edits and per-layer sampling
193 lines
9.4 KiB
C++
193 lines
9.4 KiB
C++
#include "TextureDisplacementBakeJob.hpp"
|
|
|
|
#include <algorithm>
|
|
|
|
#include "libslic3r/Model.hpp"
|
|
#include "libslic3r/TriangleSelector.hpp"
|
|
|
|
#include "slic3r/GUI/GLCanvas3D.hpp"
|
|
#include "slic3r/GUI/GUI_App.hpp"
|
|
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
|
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
|
|
#include "slic3r/GUI/I18N.hpp"
|
|
#include "slic3r/GUI/Plater.hpp"
|
|
#include "slic3r/Utils/UndoRedo.hpp"
|
|
|
|
namespace Slic3r::GUI {
|
|
|
|
TextureDisplacementBakeJob::TextureDisplacementBakeJob(TextureDisplacementBakeInput &&input, std::function<void()> on_finished)
|
|
: m_input(std::move(input)), m_on_finished(std::move(on_finished))
|
|
{
|
|
}
|
|
|
|
void TextureDisplacementBakeJob::process(Ctl &ctl)
|
|
{
|
|
const std::string status = _u8L("Baking 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 matters for more than cosmetics: the framework's progress notification only
|
|
// grows a close button once it reaches 100%, so a job that reports 0 and nothing else leaves an
|
|
// uncloseable notification pinned on screen. It also carries the Cancel button's effect into the
|
|
// 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.
|
|
TextureColorRequest color_request;
|
|
TextureColorRequest *color = nullptr;
|
|
if (!m_input.color.empty()) {
|
|
color_request.quantize = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette);
|
|
if (!m_input.color.palette_pure.empty())
|
|
color_request.quantize_pure = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette_pure);
|
|
color_request.resolve = GLGizmoTextureDisplacement::make_mix_resolver(
|
|
m_input.color.palette, m_input.color.mix_mode, m_input.color.layer_height,
|
|
m_input.color.dither_cell_mm);
|
|
color_request.despeckle_passes = m_input.color.despeckle_passes;
|
|
color_request.out_triangle = &m_triangle_color;
|
|
if (color_request.quantize)
|
|
color = &color_request;
|
|
}
|
|
|
|
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));
|
|
|
|
// 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.
|
|
ctl.update_status(100, status);
|
|
}
|
|
|
|
void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &eptr)
|
|
{
|
|
struct OnExit
|
|
{
|
|
std::function<void()> fn;
|
|
~OnExit() { if (fn) fn(); }
|
|
} on_exit{m_on_finished};
|
|
|
|
if (canceled || eptr || m_result.empty())
|
|
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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
volume->set_mesh(std::move(m_result));
|
|
volume->set_new_unique_id();
|
|
volume->calculate_convex_hull();
|
|
|
|
// 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. Safe to index straight onto the new mesh - the bake is
|
|
// topology-preserving, so triangle i is still triangle i.
|
|
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)
|
|
if (m_triangle_color[i] > 0)
|
|
selector.set_facet(int(i), EnforcerBlockerType(m_triangle_color[i]));
|
|
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().
|
|
const bool topology_changed = m_input.base_mesh.indices.size() != volume->mesh().its.indices.size();
|
|
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();
|
|
}
|
|
|
|
ModelObject *object = volume->get_object();
|
|
if (object == nullptr)
|
|
return;
|
|
|
|
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);
|
|
};
|
|
|
|
// Standard mode's Bake is remesh -> subdivide -> displace under a single snapshot, and this job runs
|
|
// long after that snapshot's scope has closed. Adding one here would put an undo step *between* the
|
|
// subdivision and the displacement: the first Undo would land on a mesh carrying every added
|
|
// triangle and no relief at all, and pressing Bake again from there would subdivide that mesh a
|
|
// second time. So the caller says who owns the undo step.
|
|
if (m_input.take_snapshot) {
|
|
Plater::TakeSnapshot snapshot(plater, _u8L("Bake texture displacement"), UndoRedo::SnapshotType::GizmoAction);
|
|
commit();
|
|
} else {
|
|
commit();
|
|
}
|
|
}
|
|
|
|
void queue_texture_displacement_bake(const ModelVolume &volume, const TextureColorSettings &color,
|
|
std::function<void()> on_finished, bool take_snapshot)
|
|
{
|
|
TextureDisplacementBakeInput input;
|
|
input.color = color;
|
|
input.take_snapshot = take_snapshot;
|
|
input.volume_id = volume.id();
|
|
input.base_mesh = volume.mesh().its;
|
|
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)));
|
|
}
|
|
|
|
} // namespace Slic3r::GUI
|