Preserve model textures and remap them to the new mesh when 'simplify model' is used

This commit is contained in:
sentientstardust
2026-05-10 21:45:01 +01:00
parent aecead1f8c
commit 4b33dae806
10 changed files with 1619 additions and 29 deletions

4
.gitignore vendored
View File

@@ -45,4 +45,6 @@ test.js
.clangd
internal_docs/
*.flatpak
/flatpak-repo/
/flatpak-repo/
*.3mf
*.gcode

View File

@@ -304,6 +304,8 @@ set(lisbslic3r_sources
ModelArrange.hpp
Model.cpp
Model.hpp
ModelTextureDataRemap.cpp
ModelTextureDataRemap.hpp
MTUtils.hpp
MultiMaterialSegmentation.cpp
MultiMaterialSegmentation.hpp

View File

@@ -4580,11 +4580,23 @@ bool ColorFacetsAnnotation::set_from_triangle_sampler(const ModelVolume
float split_color_threshold,
const TextureMappingColorSubdivisionDepths &subdivision_depths,
const std::vector<bool> *resample_triangles,
const TextureMappingColorLeafResamplePredicate &resample_leaf)
const TextureMappingColorLeafResamplePredicate &resample_leaf,
const TextureMappingColorProgressFn &progress_fn)
{
return this->set_from_triangle_sampler(mv.mesh().its, sampler, max_depth, split_color_threshold, subdivision_depths, resample_triangles, resample_leaf, progress_fn);
}
bool ColorFacetsAnnotation::set_from_triangle_sampler(const indexed_triangle_set &its,
const TextureMappingColorSampler &sampler,
int max_depth,
float split_color_threshold,
const TextureMappingColorSubdivisionDepths &subdivision_depths,
const std::vector<bool> *resample_triangles,
const TextureMappingColorLeafResamplePredicate &resample_leaf,
const TextureMappingColorProgressFn &progress_fn)
{
TriangleColorSplittingData new_data;
new_data.metadata_json = m_data.metadata_json;
const indexed_triangle_set &its = mv.mesh().its;
new_data.triangles_to_split.reserve(its.indices.size());
if (resample_triangles != nullptr) {
new_data.bitstream.reserve(m_data.bitstream.size());
@@ -4600,6 +4612,21 @@ bool ColorFacetsAnnotation::set_from_triangle_sampler(const ModelVolume
max_depth = std::clamp(max_depth, 0, 7);
split_color_threshold = std::max(split_color_threshold, 0.f);
const size_t total_triangles = its.indices.size();
int last_progress = -1;
auto report_progress = [&progress_fn, total_triangles, &last_progress](size_t completed_triangles) {
if (!progress_fn)
return;
const int progress = total_triangles == 0 ?
100 :
int((uint64_t(completed_triangles) * 100u) / uint64_t(total_triangles));
if (progress != last_progress) {
last_progress = progress;
progress_fn(completed_triangles, total_triangles);
}
};
report_progress(0);
size_t preserved_mapping_idx = 0;
auto existing_triangle_range = [this, &preserved_mapping_idx](size_t tri_idx,
int &bitstream_start,
@@ -4655,13 +4682,20 @@ bool ColorFacetsAnnotation::set_from_triangle_sampler(const ModelVolume
};
for (size_t tri_idx = 0; tri_idx < its.indices.size(); ++tri_idx) {
auto report_triangle_done = [&report_progress, tri_idx]() {
report_progress(tri_idx + 1);
};
const auto &tri = its.indices[tri_idx];
if (tri[0] < 0 || tri[1] < 0 || tri[2] < 0)
if (tri[0] < 0 || tri[1] < 0 || tri[2] < 0) {
report_triangle_done();
continue;
}
if (size_t(tri[0]) >= its.vertices.size() ||
size_t(tri[1]) >= its.vertices.size() ||
size_t(tri[2]) >= its.vertices.size())
size_t(tri[2]) >= its.vertices.size()) {
report_triangle_done();
continue;
}
const std::array<Vec3f, 3> vertices = {
its.vertices[size_t(tri[0])].cast<float>(),
@@ -4676,8 +4710,10 @@ bool ColorFacetsAnnotation::set_from_triangle_sampler(const ModelVolume
if (resample_triangles != nullptr &&
tri_idx < resample_triangles->size() &&
!(*resample_triangles)[tri_idx] &&
append_preserved_triangle(tri_idx))
append_preserved_triangle(tri_idx)) {
report_triangle_done();
continue;
}
int triangle_min_depth = 0;
int triangle_max_depth = max_depth;
@@ -4714,8 +4750,10 @@ bool ColorFacetsAnnotation::set_from_triangle_sampler(const ModelVolume
0,
triangle_min_depth,
triangle_max_depth,
split_color_threshold))
split_color_threshold)) {
report_triangle_done();
continue;
}
new_data.bitstream.resize(new_bitstream_start);
new_data.colors_rgba.resize(new_color_start);
}
@@ -4729,7 +4767,9 @@ bool ColorFacetsAnnotation::set_from_triangle_sampler(const ModelVolume
triangle_min_depth,
triangle_max_depth,
split_color_threshold);
report_triangle_done();
}
report_progress(total_triangles);
new_data.triangles_to_split.shrink_to_fit();
new_data.bitstream.shrink_to_fit();

View File

@@ -849,6 +849,7 @@ using TextureMappingColorSampler = std::function<uint32_t(size_t, const Vec3f &,
using TextureMappingColorSubdivisionDepths = std::function<std::pair<int, int>(size_t, const std::array<Vec3f, 3> &)>;
using TextureMappingColorLeafResamplePredicate =
std::function<bool(size_t, const std::array<Vec3f, 3> &, const std::array<Vec3f, 3> &, uint32_t)>;
using TextureMappingColorProgressFn = std::function<void(size_t, size_t)>;
class ColorFacetsAnnotation final : public ObjectWithTimestamp {
public:
@@ -891,7 +892,16 @@ public:
float split_color_threshold = 0.045f,
const TextureMappingColorSubdivisionDepths &subdivision_depths = {},
const std::vector<bool> *resample_triangles = nullptr,
const TextureMappingColorLeafResamplePredicate &resample_leaf = {});
const TextureMappingColorLeafResamplePredicate &resample_leaf = {},
const TextureMappingColorProgressFn &progress_fn = {});
bool set_from_triangle_sampler(const indexed_triangle_set &its,
const TextureMappingColorSampler &sampler,
int max_depth = 2,
float split_color_threshold = 0.045f,
const TextureMappingColorSubdivisionDepths &subdivision_depths = {},
const std::vector<bool> *resample_triangles = nullptr,
const TextureMappingColorLeafResamplePredicate &resample_leaf = {},
const TextureMappingColorProgressFn &progress_fn = {});
void get_facet_triangles(const ModelVolume &mv, std::vector<ColorFacetTriangle> &facets) const;
void get_facet_triangles(const indexed_triangle_set &its, std::vector<ColorFacetTriangle> &facets) const;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
// original author: sentientstardust
#ifndef slic3r_ModelTextureDataRemap_hpp_
#define slic3r_ModelTextureDataRemap_hpp_
#include "Model.hpp"
#include "TriangleMesh.hpp"
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>
namespace Slic3r {
enum class SimplifyColorSource
{
None,
RgbaData,
ImageTexture,
VertexColors
};
struct SimplifyTextureDataSnapshot
{
SimplifyColorSource source { SimplifyColorSource::None };
indexed_triangle_set source_mesh;
std::vector<ColorFacetTriangle> rgba_facets;
std::string rgba_metadata_json;
std::vector<uint8_t> texture_rgba;
std::vector<float> texture_uvs_per_face;
std::vector<uint8_t> texture_uv_valid;
std::vector<uint8_t> texture_raw_filament_offsets;
uint32_t texture_width { 0 };
uint32_t texture_height { 0 };
uint32_t texture_raw_channels { 0 };
std::string texture_raw_metadata_json;
int uv_map_generator_version { 0 };
std::vector<uint32_t> vertex_colors_rgba;
};
struct SimplifyTextureDataResult
{
SimplifyColorSource source { SimplifyColorSource::None };
bool remap_failed { false };
bool used_fallback_rgba { false };
std::unique_ptr<ColorFacetsAnnotation> rgba_data;
std::vector<uint8_t> texture_rgba;
std::vector<float> texture_uvs_per_face;
std::vector<uint8_t> texture_uv_valid;
std::vector<uint8_t> texture_raw_filament_offsets;
uint32_t texture_width { 0 };
uint32_t texture_height { 0 };
uint32_t texture_raw_channels { 0 };
std::string texture_raw_metadata_json;
int uv_map_generator_version { 0 };
std::vector<uint32_t> vertex_colors_rgba;
};
using SimplifyTextureCancelFn = std::function<void()>;
using SimplifyTextureProgressFn = std::function<void(int)>;
SimplifyTextureDataSnapshot snapshot_simplify_texture_data(const ModelVolume &volume);
SimplifyTextureDataResult remap_simplify_texture_data(const SimplifyTextureDataSnapshot &snapshot,
const indexed_triangle_set &simplified_mesh,
const SimplifyTextureCancelFn &throw_on_cancel = {},
const SimplifyTextureProgressFn &status_fn = {});
void apply_simplify_texture_data_result(ModelVolume &volume, SimplifyTextureDataResult &&result);
} // namespace Slic3r
#endif

View File

@@ -1,3 +1,5 @@
// original author: sentientstardust
#include "TextureMapping.hpp"
#include "ColorSolver.hpp"

View File

@@ -1,3 +1,5 @@
// original author: sentientstardust
#ifndef slic3r_TextureMapping_hpp_
#define slic3r_TextureMapping_hpp_

View File

@@ -13,6 +13,9 @@
#include <glad/gl.h>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <thread>
namespace Slic3r::GUI {
@@ -163,12 +166,16 @@ void GLGizmoSimplify::on_render_input_window(float x, float y, float bottom_limi
bool is_cancelling = false;
bool is_worker_running = false;
bool is_result_ready = false;
int progress = 0;
bool skip_color_conversion_requested = false;
bool color_conversion_in_progress = false;
float progress = 0.f;
{
std::lock_guard lk(m_state_mutex);
is_cancelling = m_state.status == State::cancelling;
is_worker_running = m_state.status == State::running;
is_result_ready = bool(m_state.result);
skip_color_conversion_requested = m_state.skip_color_conversion_requested;
color_conversion_in_progress = m_state.color_conversion_in_progress;
progress = m_state.progress;
}
@@ -336,6 +343,21 @@ void GLGizmoSimplify::on_render_input_window(float x, float y, float bottom_limi
m_imgui->disabled_end(); // use_count
m_imgui->bbl_checkbox(_L("Show wireframe").c_str(), m_show_wireframe);
const wxString skip_color_conversion_label = color_conversion_in_progress ?
_L("Skip color conversion (in progress)") :
_L("Skip color conversion");
m_imgui->disabled_begin(!is_worker_running || is_cancelling || skip_color_conversion_requested);
m_imgui->push_cancel_button_style();
if (m_imgui->bbl_button(skip_color_conversion_label)) {
skip_color_conversion_request();
} else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
if (skip_color_conversion_requested)
ImGui::SetTooltip("%s", _u8L("Color conversion will be skipped.").c_str());
else if (!is_worker_running)
ImGui::SetTooltip("%s", _u8L("Color conversion can only be skipped while processing.").c_str());
}
m_imgui->pop_cancel_button_style();
m_imgui->disabled_end();
// draw progress bar
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding,12);
@@ -343,9 +365,15 @@ void GLGizmoSimplify::on_render_input_window(float x, float y, float bottom_limi
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,ImVec2(10,20));
if (is_worker_running) { // apply or preview
// draw progress bar
std::string progress_text = GUI::format("%1%", std::to_string(progress)) + "%%";
char progress_buffer[32];
const float displayed_progress = std::clamp(progress, 0.f, 100.f);
if (std::abs(displayed_progress - std::round(displayed_progress)) < 0.05f)
std::snprintf(progress_buffer, sizeof(progress_buffer), "%.0f%%", displayed_progress);
else
std::snprintf(progress_buffer, sizeof(progress_buffer), "%.1f%%", displayed_progress);
std::string progress_text = progress_buffer;
ImVec2 progress_size(bottom_left_width - space_size, 0.0f);
ImGui::BBLProgressBar2(progress / 100., progress_size);
ImGui::BBLProgressBar2(displayed_progress / 100.f, progress_size);
ImGui::SameLine();
ImGui::AlignTextToFramePadding();
ImGui::TextColored(ImVec4(0.42f, 0.42f, 0.42f, 1.00f), progress_text.c_str());
@@ -407,6 +435,16 @@ void GLGizmoSimplify::stop_worker_thread_request()
m_state.status = State::Status::cancelling;
}
void GLGizmoSimplify::skip_color_conversion_request()
{
{
std::lock_guard lk(m_state_mutex);
if (m_state.status == State::running)
m_state.skip_color_conversion_requested = true;
}
request_rerender(true);
}
// Following is called from a UI thread when the worker terminates
// worker calls it through a CallAfter.
@@ -424,8 +462,8 @@ void GLGizmoSimplify::worker_finished()
m_worker.join();
if (GLGizmoBase::m_state == Off)
return;
if (m_state.result)
init_model(*m_state.result);
if (m_state.result && m_state.result->mesh)
init_model(*m_state.result->mesh);
if (m_state.config != m_configuration || m_state.mv != m_volume) {
// Settings were changed, restart the worker immediately.
process();
@@ -471,13 +509,16 @@ void GLGizmoSimplify::process()
m_state.config = m_configuration;
m_state.mv = m_volume;
m_state.status = State::running;
m_state.skip_color_conversion_requested = false;
m_state.color_conversion_in_progress = false;
// Create a copy of current mesh to pass to the worker thread.
// Using unique_ptr instead of pass-by-value to avoid an extra
// copy (which would happen when passing to std::thread).
SimplifyTextureDataSnapshot texture_snapshot = snapshot_simplify_texture_data(*m_volume);
auto its = std::make_unique<indexed_triangle_set>(m_volume->mesh().its);
m_worker = std::thread([this](std::unique_ptr<indexed_triangle_set> its) {
m_worker = std::thread([this](std::unique_ptr<indexed_triangle_set> its, SimplifyTextureDataSnapshot texture_snapshot) {
// Checks that the UI thread did not request cancellation, throws if so.
std::function<void(void)> throw_on_cancel = [this]() {
@@ -488,11 +529,24 @@ void GLGizmoSimplify::process()
// Called by worker thread, updates progress bar.
// Using CallAfter so the rerequest function is run in UI thread.
std::function<void(int)> statusfn = [this](int percent) {
std::function<void(float)> statusfn = [this](float percent) {
std::lock_guard lk(m_state_mutex);
m_state.progress = percent;
m_state.progress = std::clamp(percent, 0.f, 100.f);
call_after_if_active([this]() { request_rerender(); });
};
auto set_color_conversion_in_progress = [this](bool in_progress) {
{
std::lock_guard lk(m_state_mutex);
m_state.color_conversion_in_progress = in_progress;
}
call_after_if_active([this]() { request_rerender(); });
};
auto throw_on_color_conversion_stop = [this, &throw_on_cancel]() {
throw_on_cancel();
std::lock_guard lk(m_state_mutex);
if (m_state.skip_color_conversion_requested)
throw SimplifyColorConversionSkippedException();
};
// Initialize.
uint32_t triangle_count = 0;
@@ -503,33 +557,64 @@ void GLGizmoSimplify::process()
triangle_count = m_state.config.wanted_count;
if (! m_state.config.use_count)
max_error = m_state.config.max_error;
m_state.progress = 0;
m_state.progress = 0.f;
m_state.result.reset();
m_state.color_conversion_in_progress = false;
m_state.status = State::Status::running;
}
// Start the actual calculation.
try {
its_quadric_edge_collapse(*its, triangle_count, &max_error, throw_on_cancel, statusfn);
auto decimate_statusfn = [&statusfn](int percent) {
statusfn(float(std::clamp(percent, 0, 100)) * 0.9f);
};
its_quadric_edge_collapse(*its, triangle_count, &max_error, throw_on_cancel, decimate_statusfn);
statusfn(90);
SimplifyTextureDataResult texture_result;
set_color_conversion_in_progress(true);
try {
throw_on_color_conversion_stop();
texture_result =
remap_simplify_texture_data(texture_snapshot, *its, throw_on_color_conversion_stop, [&statusfn](int percent) {
statusfn(90.f + float(std::clamp(percent, 0, 100)) * 0.1f);
});
} catch (SimplifyColorConversionSkippedException &) {
texture_result = SimplifyTextureDataResult();
} catch (...) {
set_color_conversion_in_progress(false);
throw;
}
set_color_conversion_in_progress(false);
statusfn(100);
auto result = std::make_unique<SimplifyResult>();
result->mesh = std::move(its);
result->texture_data = std::move(texture_result);
std::lock_guard lk(m_state_mutex);
if (m_state.status == State::Status::running) {
m_state.status = State::Status::idle;
m_state.color_conversion_in_progress = false;
m_state.result = std::move(result);
} else if (m_state.status == State::Status::cancelling) {
m_state.color_conversion_in_progress = false;
m_state.status = State::Status::idle;
}
} catch (SimplifyCanceledException &) {
std::lock_guard lk(m_state_mutex);
m_state.color_conversion_in_progress = false;
m_state.status = State::idle;
}
std::lock_guard lk(m_state_mutex);
if (m_state.status == State::Status::running) {
// We were not cancelled, the result is valid.
m_state.status = State::Status::idle;
m_state.result = std::move(its);
}
// Update UI. Use CallAfter so the function is run on UI thread.
call_after_if_active([this]() { worker_finished(); });
}, std::move(its));
}, std::move(its), std::move(texture_snapshot));
}
void GLGizmoSimplify::apply_simplify() {
if (!m_state.result || !m_state.result->mesh)
return;
const Selection& selection = m_parent.get_selection();
int object_idx = selection.get_object_idx();
@@ -540,7 +625,10 @@ void GLGizmoSimplify::apply_simplify() {
ModelVolume* mv = get_model_volume(selection, wxGetApp().model());
assert(mv == m_volume);
mv->set_mesh(std::move(*m_state.result));
const bool remap_failed = m_state.result->texture_data.remap_failed;
const bool used_fallback_rgba = m_state.result->texture_data.used_fallback_rgba;
mv->set_mesh(std::move(*m_state.result->mesh));
apply_simplify_texture_data_result(*mv, std::move(m_state.result->texture_data));
m_state.result.reset();
mv->calculate_convex_hull();
mv->invalidate_convex_hull_2d();
@@ -550,6 +638,17 @@ void GLGizmoSimplify::apply_simplify() {
// fix hollowing, sla support points, modifiers, ...
plater->changed_mesh(object_idx);
if (used_fallback_rgba) {
plater->get_notification_manager()->push_notification(
NotificationType::CustomNotification,
NotificationManager::NotificationLevel::PrintInfoNotificationLevel,
_u8L("Image texture UVs could not be regenerated after simplification; texture colors were preserved as RGBA data."));
} else if (remap_failed) {
plater->get_notification_manager()->push_notification(
NotificationType::CustomNotification,
NotificationManager::NotificationLevel::PrintInfoNotificationLevel,
_u8L("Image texture UVs could not be regenerated after simplification; stale texture data was cleared."));
}
// Fix warning icon in object list
wxGetApp().obj_list()->update_item_error_icon(object_idx, -1);
close();

View File

@@ -5,6 +5,7 @@
// which overrides our localization "L" macro.
#include "GLGizmoBase.hpp"
#include "slic3r/GUI/3DScene.hpp"
#include "libslic3r/ModelTextureDataRemap.hpp"
#include "admesh/stl.h" // indexed_triangle_set
#include <mutex>
#include <thread>
@@ -46,6 +47,7 @@ private:
void process();
void stop_worker_thread_request();
void skip_color_conversion_request();
void worker_finished();
void create_gui_cfg();
@@ -86,6 +88,12 @@ private:
// Following struct is accessed by both UI and worker thread.
// Accesses protected by a mutex.
struct SimplifyResult
{
std::unique_ptr<indexed_triangle_set> mesh;
SimplifyTextureDataResult texture_data;
};
struct State {
enum Status {
idle,
@@ -94,10 +102,12 @@ private:
};
Status status = idle;
int progress = 0; // percent of done work
float progress = 0.f; // percent of done work
Configuration config; // Configuration we started with.
const ModelVolume* mv = nullptr;
std::unique_ptr<indexed_triangle_set> result;
bool skip_color_conversion_requested = false;
bool color_conversion_in_progress = false;
std::unique_ptr<SimplifyResult> result;
};
std::thread m_worker;
@@ -141,6 +151,15 @@ private:
return L("Model simplification has been canceled");
}
};
class SimplifyColorConversionSkippedException: public std::exception
{
public:
const char *what() const throw()
{
return L("Color conversion has been skipped");
}
};
};
} // namespace GUI