From 86094ffd9ba4941f00e86c723c71abe37a3927b8 Mon Sep 17 00:00:00 2001 From: sentientstardust Date: Tue, 5 May 2026 00:32:25 +0100 Subject: [PATCH] Merge ImageMap raw offset atlas format --- src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/Format/bbs_3mf.cpp | 103 ++- src/libslic3r/GCode.cpp | 502 ++++++++++-- .../ImageMapRawFilamentOffsetAtlas.cpp | 511 ++++++++++++ .../ImageMapRawFilamentOffsetAtlas.hpp | 59 ++ src/libslic3r/Model.cpp | 12 +- src/libslic3r/Model.hpp | 18 +- src/libslic3r/Print.cpp | 16 + src/libslic3r/PrintApply.cpp | 3 + src/libslic3r/PrintConfig.cpp | 7 + src/libslic3r/PrintConfig.hpp | 1 + src/libslic3r/TextureMapping.cpp | 31 +- src/libslic3r/TextureMapping.hpp | 9 +- src/slic3r/GUI/GUI_Factories.cpp | 7 +- src/slic3r/GUI/GUI_ObjectTable.cpp | 7 +- .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 769 +++++++++++++++++- .../GUI/Gizmos/GLGizmoMmuSegmentation.hpp | 4 + src/slic3r/GUI/MMUPaintedTexturePreview.cpp | 409 +++++++++- src/slic3r/GUI/Plater.cpp | 8 +- 19 files changed, 2351 insertions(+), 127 deletions(-) create mode 100644 src/libslic3r/ImageMapRawFilamentOffsetAtlas.cpp create mode 100644 src/libslic3r/ImageMapRawFilamentOffsetAtlas.hpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 1f35c414e04..6e0e8105f05 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -277,6 +277,8 @@ set(lisbslic3r_sources Geometry/VoronoiUtils.cpp Geometry/VoronoiUtils.hpp Geometry/VoronoiVisualUtils.hpp + ImageMapRawFilamentOffsetAtlas.cpp + ImageMapRawFilamentOffsetAtlas.hpp Int128.hpp KDTreeIndirect.hpp Layer.cpp diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 44756de6dc6..facf4c641b5 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -7,6 +7,7 @@ #include "../GCode.hpp" #include "../Geometry.hpp" #include "../GCode/ThumbnailData.hpp" +#include "../ImageMapRawFilamentOffsetAtlas.hpp" #include "../PNGReadWrite.hpp" #include "../Semver.hpp" #include "../TextureMapping.hpp" @@ -785,6 +786,64 @@ static bool has_imported_obj_texture_payload(const ModelVolume &volume) volume.imported_texture_uvs_per_face.size() >= triangle_count * 6; } +static bool has_imported_raw_atlas_texture_payload(const ModelVolume &volume) +{ + return volume.imported_texture_width > 0 && + volume.imported_texture_height > 0 && + volume.imported_texture_raw_channels > 0 && + volume.imported_texture_raw_filament_offsets.size() >= + size_t(volume.imported_texture_width) * + size_t(volume.imported_texture_height) * + size_t(volume.imported_texture_raw_channels); +} + +static std::vector raw_atlas_filaments_from_metadata(const std::string &metadata_json, uint32_t channels) +{ + std::vector filaments; + try { + const nlohmann::json root = nlohmann::json::parse(metadata_json); + const nlohmann::json entries = root.value("filaments", nlohmann::json::array()); + if (entries.is_array()) { + for (const nlohmann::json &entry : entries) { + if (!entry.is_object()) + continue; + ImageMapRawFilament filament; + filament.slot = unsigned(std::max(0, entry.value("slot", 0))); + filament.color = entry.value("color", std::string()); + filament.hex = entry.value("hex", std::string()); + filaments.emplace_back(std::move(filament)); + } + } + } catch (...) { + filaments.clear(); + } + + if (filaments.empty()) { + filaments.reserve(channels); + for (uint32_t channel = 0; channel < channels; ++channel) { + ImageMapRawFilament filament; + filament.slot = channel + 1; + filament.color = "custom"; + filament.hex = "#FFFFFF"; + filaments.emplace_back(std::move(filament)); + } + } + return filaments; +} + +static ImageMapRawFilamentOffsetAtlas raw_atlas_from_model_volume(const ModelVolume &volume) +{ + ImageMapRawFilamentOffsetAtlas atlas; + atlas.width = volume.imported_texture_width; + atlas.height = volume.imported_texture_height; + atlas.channels = volume.imported_texture_raw_channels; + atlas.offsets.assign(volume.imported_texture_raw_filament_offsets.begin(), volume.imported_texture_raw_filament_offsets.end()); + atlas.mask.assign(size_t(atlas.width) * size_t(atlas.height), 255); + atlas.metadata_json = volume.imported_texture_raw_metadata_json; + atlas.filaments = raw_atlas_filaments_from_metadata(volume.imported_texture_raw_metadata_json, atlas.channels); + return atlas; +} + static bool has_imported_obj_material_payload(const ModelVolume &volume) { return has_imported_vertex_color_payload(volume) || has_imported_obj_texture_payload(volume); @@ -5901,9 +5960,22 @@ static void append_triangle_material_data(std::vector &uv_valid, volume->imported_texture_uv_valid = source.uv_valid; volume->imported_texture_uvs_per_face.assign(source.uvs_per_face.begin(), source.uvs_per_face.begin() + triangle_count * 6); - volume->imported_texture_rgba = std::move(imported_rgba); - volume->imported_texture_width = imported_width; - volume->imported_texture_height = imported_height; + ImageMapRawFilamentOffsetAtlas raw_atlas; + if (decode_image_map_raw_filament_offset_atlas(imported_rgba, imported_width, imported_height, raw_atlas, nullptr)) { + volume->imported_texture_rgba = image_map_raw_filament_offset_preview_rgba(raw_atlas); + volume->imported_texture_width = raw_atlas.width; + volume->imported_texture_height = raw_atlas.height; + volume->imported_texture_raw_channels = raw_atlas.channels; + volume->imported_texture_raw_filament_offsets = std::move(raw_atlas.offsets); + volume->imported_texture_raw_metadata_json = std::move(raw_atlas.metadata_json); + } else { + volume->imported_texture_rgba = std::move(imported_rgba); + volume->imported_texture_width = imported_width; + volume->imported_texture_height = imported_height; + volume->imported_texture_raw_filament_offsets.clear(); + volume->imported_texture_raw_channels = 0; + volume->imported_texture_raw_metadata_json.clear(); + } } } /* @@ -7898,8 +7970,11 @@ static void append_triangle_material_data(std::vector &uv_valid, && (shared_volume->imported_texture_uvs_per_face == volume->imported_texture_uvs_per_face) && (shared_volume->imported_texture_uv_valid == volume->imported_texture_uv_valid) && (shared_volume->imported_texture_rgba == volume->imported_texture_rgba) + && (shared_volume->imported_texture_raw_filament_offsets == volume->imported_texture_raw_filament_offsets) && (shared_volume->imported_texture_width == volume->imported_texture_width) - && (shared_volume->imported_texture_height == volume->imported_texture_height)) + && (shared_volume->imported_texture_height == volume->imported_texture_height) + && (shared_volume->imported_texture_raw_channels == volume->imported_texture_raw_channels) + && (shared_volume->imported_texture_raw_metadata_json == volume->imported_texture_raw_metadata_json)) { auto data = iter->second.first; const_cast<_BBS_3MF_Exporter *>(this)->m_volume_paths.insert({volume, {data->sub_path, data->volumes_objectID.find(iter->second.second)->second}}); @@ -8130,11 +8205,25 @@ static void append_triangle_material_data(std::vector &uv_valid, if (texture_targets.find(texture_resource.texture_part_path) != texture_targets.end()) continue; + std::vector png_source_rgba; + uint32_t png_width = volume->imported_texture_width; + uint32_t png_height = volume->imported_texture_height; + if (has_imported_raw_atlas_texture_payload(*volume)) { + const ImageMapRawFilamentOffsetAtlas raw_atlas = raw_atlas_from_model_volume(*volume); + std::string encode_error; + if (!encode_image_map_raw_filament_offset_atlas(raw_atlas, png_source_rgba, png_width, png_height, &encode_error)) { + add_error(encode_error.empty() ? "Unable to encode ImageMap raw filament offset atlas texture image" : encode_error); + return false; + } + } else { + png_source_rgba.assign(volume->imported_texture_rgba.begin(), volume->imported_texture_rgba.end()); + } + size_t png_size = 0; void *png_data = tdefl_write_image_to_png_file_in_memory_ex( - static_cast(volume->imported_texture_rgba.data()), - int(volume->imported_texture_width), - int(volume->imported_texture_height), + static_cast(png_source_rgba.data()), + int(png_width), + int(png_height), 4, &png_size, MZ_DEFAULT_COMPRESSION, diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index e116c6a2761..e65e6073fe1 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -15,6 +15,7 @@ #include "ShortestPath.hpp" #include "Print.hpp" #include "TextureMapping.hpp" +#include "ImageMapRawFilamentOffsetAtlas.hpp" #include "Utils.hpp" #include "ClipperUtils.hpp" #include "libslic3r.h" @@ -32,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -2000,6 +2002,9 @@ namespace DoExport { } } // namespace DoExport +static bool print_has_raw_offset_texture_data_without_raw_zone_for_gcode(const Print &print); +static bool print_has_raw_offset_texture_zone_without_raw_data_for_gcode(const Print &print); + bool GCode::is_BBL_Printer() { if (m_curr_print) @@ -2038,6 +2043,18 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu GCodeProcessor::s_IsBBLPrinter = print->is_BBL_printer(); m_writer.set_is_bbl_machine(print->is_BBL_printer()); print->set_started(psGCodeExport); + if (print_has_raw_offset_texture_data_without_raw_zone_for_gcode(*print)) { + print->active_step_add_warning( + PrintStateBase::WarningLevel::NON_CRITICAL, + _(L("An object contains raw filament offset texture data, but it is not assigned to a texture mapping zone " + "set to Raw filament offset mode. Slicing will use the preview texture instead of the raw offsets for that object."))); + } + if (print_has_raw_offset_texture_zone_without_raw_data_for_gcode(*print)) { + print->active_step_add_warning( + PrintStateBase::WarningLevel::NON_CRITICAL, + _(L("An object is assigned to a Raw filament offset texture mapping zone, but it does not contain raw filament " + "offset atlas data. Slicing will interpret its regular RGB texture channels as raw offsets."))); + } // check if any custom gcode contains keywords used by the gcode processor to // produce time estimation and gcode toolpaths @@ -6101,6 +6118,107 @@ static std::array unpack_rgba_u32(uint32_t packed_rgba) return { clamp01f_for_gcode(r), clamp01f_for_gcode(g), clamp01f_for_gcode(b), clamp01f_for_gcode(a) }; } +static constexpr const char *TEXTURE_MAPPING_BACKGROUND_COLOR_CONFIG_KEY = "texture_mapping_background_color"; + +static int texture_mapping_color_hex_digit_for_gcode(char ch) +{ + return ch >= '0' && ch <= '9' ? ch - '0' : + ch >= 'a' && ch <= 'f' ? ch - 'a' + 10 : + ch >= 'A' && ch <= 'F' ? ch - 'A' + 10 : -1; +} + +static std::optional> parse_texture_mapping_color_hex_for_gcode(const std::string &text) +{ + if (text.empty()) + return std::nullopt; + + const size_t hash_pos = text.find('#'); + const size_t start = hash_pos == std::string::npos ? 0 : hash_pos + 1; + if (start + 6 > text.size()) + return std::nullopt; + + uint32_t packed = 0; + for (size_t idx = 0; idx < 6; ++idx) { + const int value = texture_mapping_color_hex_digit_for_gcode(text[start + idx]); + if (value < 0) + return std::nullopt; + packed = (packed << 4) | uint32_t(value); + } + + uint32_t alpha = 255; + if (start + 8 <= text.size()) { + alpha = 0; + for (size_t idx = 6; idx < 8; ++idx) { + const int value = texture_mapping_color_hex_digit_for_gcode(text[start + idx]); + if (value < 0) + return std::nullopt; + alpha = (alpha << 4) | uint32_t(value); + } + } + + return std::array { + clamp01f_for_gcode(float((packed >> 16) & 0xFFu) / 255.f), + clamp01f_for_gcode(float((packed >> 8) & 0xFFu) / 255.f), + clamp01f_for_gcode(float(packed & 0xFFu) / 255.f), + clamp01f_for_gcode(float(alpha & 0xFFu) / 255.f) + }; +} + +static std::optional> texture_mapping_background_color_from_config_for_gcode(const ModelConfigObject &config) +{ + if (!config.has(TEXTURE_MAPPING_BACKGROUND_COLOR_CONFIG_KEY)) + return std::nullopt; + + const ConfigOptionString *opt = dynamic_cast(config.option(TEXTURE_MAPPING_BACKGROUND_COLOR_CONFIG_KEY)); + if (opt == nullptr) + return std::nullopt; + + std::optional> color = parse_texture_mapping_color_hex_for_gcode(opt->value); + if (color) + (*color)[3] = 1.f; + return color; +} + +static std::optional> texture_mapping_background_color_from_metadata_for_gcode(const ColorFacetsAnnotation &annotation) +{ + const std::string &metadata = annotation.metadata_json(); + const std::string key = "\"background_color\":\"#"; + const size_t start = metadata.find(key); + if (start == std::string::npos || start + key.size() + 8 > metadata.size()) + return std::nullopt; + + std::optional> color = + parse_texture_mapping_color_hex_for_gcode(metadata.substr(start + key.size() - 1, 9)); + if (color) + (*color)[3] = 1.f; + return color; +} + +static std::array texture_mapping_background_color_for_gcode(const ModelVolume &volume) +{ + if (std::optional> color = texture_mapping_background_color_from_config_for_gcode(volume.config)) + return *color; + if (volume.get_object() != nullptr) { + if (std::optional> color = texture_mapping_background_color_from_config_for_gcode(volume.get_object()->config)) + return *color; + } + if (std::optional> color = texture_mapping_background_color_from_metadata_for_gcode(volume.texture_mapping_color_facets)) + return *color; + return { 1.f, 1.f, 1.f, 1.f }; +} + +static std::array composite_rgba_over_background_for_gcode(const std::array &rgba, + const std::array &background) +{ + const float alpha = clamp01f_for_gcode(rgba[3]); + return { + clamp01f_for_gcode(rgba[0] * alpha + background[0] * (1.f - alpha)), + clamp01f_for_gcode(rgba[1] * alpha + background[1] * (1.f - alpha)), + clamp01f_for_gcode(rgba[2] * alpha + background[2] * (1.f - alpha)), + 1.f + }; +} + static std::array mix_component_colors_with_filament_mixer_for_gcode(const std::vector> &component_colors, const std::vector &weights) { @@ -6639,6 +6757,225 @@ static std::array sample_texture_rgba_bilinear_for_gcode(const std::ve return out; } +static std::vector sample_texture_raw_offsets_bilinear_for_gcode(const std::vector &offsets, + uint32_t width, + uint32_t height, + uint32_t channels, + float u, + float v) +{ + std::vector out; + if (width == 0 || height == 0 || channels == 0 || + offsets.size() < size_t(width) * size_t(height) * size_t(channels)) + return out; + + const float uu = wrap_repeat01_for_gcode(u); + const float vv = wrap_repeat01_for_gcode(v); + + const float x = uu * float(width > 1 ? width - 1 : 0); + const float y = vv * float(height > 1 ? height - 1 : 0); + const size_t x0 = std::min(size_t(std::floor(x)), size_t(width - 1)); + const size_t y0 = std::min(size_t(std::floor(y)), size_t(height - 1)); + const size_t x1 = std::min(x0 + 1, size_t(width - 1)); + const size_t y1 = std::min(y0 + 1, size_t(height - 1)); + const float tx = x - float(x0); + const float ty = y - float(y0); + + auto sample_channel = [&offsets, width, channels](size_t sx, size_t sy, size_t channel) { + const size_t idx = (sy * size_t(width) + sx) * size_t(channels) + channel; + return float(offsets[idx]) / 255.f; + }; + + out.assign(size_t(channels), 0.f); + for (size_t c = 0; c < out.size(); ++c) { + const float c00 = sample_channel(x0, y0, c); + const float c10 = sample_channel(x1, y0, c); + const float c01 = sample_channel(x0, y1, c); + const float c11 = sample_channel(x1, y1, c); + const float cx0 = c00 + (c10 - c00) * tx; + const float cx1 = c01 + (c11 - c01) * tx; + out[c] = clamp01f_for_gcode(cx0 + (cx1 - cx0) * ty); + } + return out; +} + +static std::array raw_offset_preview_rgba_for_gcode(const std::vector &offsets) +{ + if (offsets.empty()) + return { 0.f, 0.f, 0.f, 1.f }; + if (offsets.size() == 1) + return { offsets[0], offsets[0], offsets[0], 1.f }; + return { + offsets.size() > 0 ? offsets[0] : 0.f, + offsets.size() > 1 ? offsets[1] : 0.f, + offsets.size() > 2 ? offsets[2] : 0.f, + 1.f + }; +} + +static std::vector raw_filament_color_mode_channel_keys_for_gcode(int filament_color_mode, size_t component_count) +{ + std::vector keys; + switch (std::clamp(filament_color_mode, int(TextureMappingZone::FilamentColorAny), int(TextureMappingZone::FilamentColorBW))) { + case int(TextureMappingZone::FilamentColorRGB): + keys = { "R", "G", "B" }; + break; + case int(TextureMappingZone::FilamentColorCMY): + keys = { "C", "M", "Y" }; + break; + case int(TextureMappingZone::FilamentColorCMYK): + keys = { "C", "M", "Y", "K" }; + break; + case int(TextureMappingZone::FilamentColorCMYW): + keys = { "C", "M", "Y", "W" }; + break; + case int(TextureMappingZone::FilamentColorRGBK): + keys = { "R", "G", "B", "K" }; + break; + case int(TextureMappingZone::FilamentColorRGBW): + keys = { "R", "G", "B", "W" }; + break; + case int(TextureMappingZone::FilamentColorBW): + keys = { "K", "W" }; + break; + default: + break; + } + if (keys.size() > component_count) + keys.resize(component_count); + return keys; +} + +static std::vector raw_component_source_channels_for_gcode(const std::string &metadata_json, + uint32_t source_channels, + int filament_color_mode, + size_t component_count) +{ + if (source_channels == 0 || component_count == 0) + return {}; + + const size_t sentinel = std::numeric_limits::max(); + std::vector mapping(component_count, sentinel); + const std::vector filaments = + image_map_raw_filaments_from_metadata_json(metadata_json, source_channels); + if (filaments.size() != size_t(source_channels)) + return {}; + + std::vector source_keys(static_cast(source_channels)); + std::vector used(static_cast(source_channels), 0); + for (size_t channel = 0; channel < filaments.size(); ++channel) { + const std::string key = image_map_raw_filament_channel_key(filaments[channel], channel); + if (key.size() == 1 && image_map_raw_filament_is_standard_color(key)) + source_keys[channel] = key; + } + + const std::vector target_keys = + raw_filament_color_mode_channel_keys_for_gcode(filament_color_mode, component_count); + if (!target_keys.empty()) { + for (size_t component_idx = 0; component_idx < target_keys.size(); ++component_idx) { + for (size_t channel = 0; channel < source_keys.size(); ++channel) { + if (used[channel] == 0 && source_keys[channel] == target_keys[component_idx]) { + mapping[component_idx] = channel; + used[channel] = 1; + break; + } + } + } + } + + size_t next_source = 0; + for (size_t component_idx = 0; component_idx < mapping.size(); ++component_idx) { + if (mapping[component_idx] != sentinel) + continue; + while (next_source < source_keys.size() && + (used[next_source] != 0 || (!target_keys.empty() && !source_keys[next_source].empty()))) + ++next_source; + if (next_source >= source_keys.size()) + continue; + mapping[component_idx] = next_source; + used[next_source] = 1; + ++next_source; + } + + const bool has_mapping = std::any_of(mapping.begin(), mapping.end(), [sentinel](size_t value) { return value != sentinel; }); + return has_mapping ? mapping : std::vector{}; +} + +static std::vector map_raw_sample_to_components_for_gcode(const std::vector &raw_sample, + const std::vector &component_source_channels) +{ + if (component_source_channels.empty()) + return {}; + const size_t sentinel = std::numeric_limits::max(); + std::vector mapped(component_source_channels.size(), 0.f); + for (size_t component_idx = 0; component_idx < component_source_channels.size(); ++component_idx) { + const size_t source_channel = component_source_channels[component_idx]; + if (source_channel != sentinel && source_channel < raw_sample.size()) + mapped[component_idx] = raw_sample[source_channel]; + } + return mapped; +} + +static bool model_volume_has_raw_offset_texture_data_for_gcode(const ModelVolume *volume) +{ + if (volume == nullptr || + volume->imported_texture_width == 0 || + volume->imported_texture_height == 0 || + volume->imported_texture_raw_channels == 0 || + volume->imported_texture_raw_filament_offsets.empty()) + return false; + + return volume->imported_texture_raw_filament_offsets.size() >= + size_t(volume->imported_texture_width) * + size_t(volume->imported_texture_height) * + size_t(volume->imported_texture_raw_channels); +} + +static bool print_has_raw_offset_texture_data_without_raw_zone_for_gcode(const Print &print) +{ + const TextureMappingManager &texture_mgr = print.texture_mapping_manager(); + for (const PrintObject *print_object : print.objects()) { + if (print_object == nullptr || print_object->model_object() == nullptr) + continue; + + for (const ModelVolume *volume : print_object->model_object()->volumes) { + if (volume == nullptr || + !volume->is_model_part() || + !model_volume_has_raw_offset_texture_data_for_gcode(volume)) + continue; + + const unsigned int filament_id = unsigned(std::max(0, volume->extruder_id())); + const TextureMappingZone *zone = texture_mgr.zone_from_id(filament_id); + if (zone == nullptr || + zone->texture_mapping_mode != int(TextureMappingZone::TextureMappingRawValues)) + return true; + } + } + return false; +} + +static bool print_has_raw_offset_texture_zone_without_raw_data_for_gcode(const Print &print) +{ + const TextureMappingManager &texture_mgr = print.texture_mapping_manager(); + for (const PrintObject *print_object : print.objects()) { + if (print_object == nullptr || print_object->model_object() == nullptr) + continue; + + for (const ModelVolume *volume : print_object->model_object()->volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + + const unsigned int filament_id = unsigned(std::max(0, volume->extruder_id())); + const TextureMappingZone *zone = texture_mgr.zone_from_id(filament_id); + if (zone != nullptr && + zone->texture_mapping_mode == int(TextureMappingZone::TextureMappingRawValues) && + !model_volume_has_raw_offset_texture_data_for_gcode(volume)) + return true; + } + } + return false; +} + static std::array unwrap_triangle_uvs_for_sampling_for_gcode(const Vec2f &uv0, const Vec2f &uv1, const Vec2f &uv2) @@ -6934,6 +7271,7 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode( VertexColorOverhangWeightField weight_field; if (component_colors.empty()) return weight_field; + const size_t component_count = component_colors.size(); const ModelObject *model_object = print_object.model_object(); if (model_object == nullptr) @@ -6957,16 +7295,26 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode( const float tone_gamma = (!std::isfinite(texture_tone_gamma) || texture_tone_gamma <= 0.f) ? 1.f : std::clamp(texture_tone_gamma, 0.5f, 3.f); + struct TextureSampleData { + std::array rgba { { 0.f, 0.f, 0.f, 1.f } }; + std::vector raw_component_weights; + }; + struct WeightedTextureSample { float x_mm { 0.f }; float y_mm { 0.f }; std::array rgba { { 0.f, 0.f, 0.f, 1.f } }; + std::vector raw_component_weights; float weight { 0.f }; }; std::vector samples; samples.reserve(8192); - auto accumulate_sample = [&samples](float x_mm, float y_mm, const std::array &rgba, float sample_weight) { + auto accumulate_sample = [&samples](float x_mm, + float y_mm, + const std::array &rgba, + float sample_weight, + std::vector raw_component_weights = {}) { if (!std::isfinite(x_mm) || !std::isfinite(y_mm) || sample_weight <= EPSILON) return; if (!std::isfinite(sample_weight) || @@ -6976,7 +7324,7 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode( !std::isfinite(rgba[3])) return; - samples.push_back({ x_mm, y_mm, rgba, sample_weight }); + samples.push_back({ x_mm, y_mm, rgba, std::move(raw_component_weights), sample_weight }); }; struct LayerPlaneSamplePoint { @@ -6987,7 +7335,7 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode( auto accumulate_layer_plane_triangle_samples = [&](const Vec3d &p0, const Vec3d &p1, const Vec3d &p2, - const auto &sample_rgba_for_barycentric) { + const auto &sample_data_for_barycentric) { if (!use_layer_weighting) return false; @@ -7087,7 +7435,12 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode( barycentric /= barycentric_sum; const Vec3d world_pos = p0 * double(barycentric.x()) + p1 * double(barycentric.y()) + p2 * double(barycentric.z()); - accumulate_sample(float(world_pos.x()), float(world_pos.y()), sample_rgba_for_barycentric(barycentric), sample_weight); + TextureSampleData sample_data = sample_data_for_barycentric(barycentric); + accumulate_sample(float(world_pos.x()), + float(world_pos.y()), + sample_data.rgba, + sample_weight, + std::move(sample_data.raw_component_weights)); } return true; @@ -7097,7 +7450,7 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode( const Vec3d &p1, const Vec3d &p2, const std::array &rgba) { - if (accumulate_layer_plane_triangle_samples(p0, p1, p2, [&rgba](const Vec3f &) { return rgba; })) + if (accumulate_layer_plane_triangle_samples(p0, p1, p2, [&rgba](const Vec3f &) { return TextureSampleData{ rgba, {} }; })) return; const float max_world_edge_mm = std::max({ @@ -7161,6 +7514,7 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode( const indexed_triangle_set &its = mesh_ptr->its; const Transform3d volume_trafo = object_trafo * volume->get_matrix(); + const std::array background_color = texture_mapping_background_color_for_gcode(*volume); if (!volume->texture_mapping_color_facets.empty()) { std::vector color_facets; @@ -7172,8 +7526,7 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode( if (!p0.allFinite() || !p1.allFinite() || !p2.allFinite()) continue; - std::array rgba = unpack_rgba_u32(facet.rgba); - rgba[3] = 1.f; + std::array rgba = composite_rgba_over_background_for_gcode(unpack_rgba_u32(facet.rgba), background_color); accumulate_constant_surface_triangle_samples(p0, p1, p2, rgba); } @@ -7190,6 +7543,18 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode( volume->imported_texture_rgba.size() >= size_t(volume->imported_texture_width) * size_t(volume->imported_texture_height) * 4; if (has_uv_texture) { + const std::vector raw_component_source_channels = + raw_component_source_channels_for_gcode(volume->imported_texture_raw_metadata_json, + volume->imported_texture_raw_channels, + filament_color_mode, + component_count); + const bool use_raw_uv_texture = + raw_values_mode && + raw_component_source_channels.size() == component_count && + volume->imported_texture_raw_filament_offsets.size() >= + size_t(volume->imported_texture_width) * + size_t(volume->imported_texture_height) * + size_t(volume->imported_texture_raw_channels); const auto uv_edge_texel_length = [volume](const Vec2f &a, const Vec2f &b) { const float du = (a.x() - b.x()) * float(volume->imported_texture_width); const float dv = (a.y() - b.y()) * float(volume->imported_texture_height); @@ -7222,17 +7587,35 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode( continue; const std::array tri_uv = unwrap_triangle_uvs_for_sampling_for_gcode(uv0, uv1, uv2); - auto sample_rgba_for_barycentric = [&](const Vec3f &barycentric) { - const Vec2f uv = tri_uv[0] * barycentric.x() + tri_uv[1] * barycentric.y() + tri_uv[2] * barycentric.z(); + auto sample_data_for_uv = [&](const Vec2f &uv) { std::array rgba = sample_texture_rgba_bilinear_for_gcode(volume->imported_texture_rgba, volume->imported_texture_width, volume->imported_texture_height, uv.x(), uv.y()); - rgba[3] = 1.f; - return rgba; + std::vector raw_component_weights; + if (use_raw_uv_texture) { + const std::vector raw_sample = + sample_texture_raw_offsets_bilinear_for_gcode(volume->imported_texture_raw_filament_offsets, + volume->imported_texture_width, + volume->imported_texture_height, + volume->imported_texture_raw_channels, + uv.x(), + uv.y()); + raw_component_weights = map_raw_sample_to_components_for_gcode(raw_sample, raw_component_source_channels); + if (raw_component_weights.size() == component_count) + rgba = raw_offset_preview_rgba_for_gcode(raw_component_weights); + } + if (raw_component_weights.size() != component_count) + rgba = composite_rgba_over_background_for_gcode(rgba, background_color); + return TextureSampleData{ rgba, std::move(raw_component_weights) }; }; - if (accumulate_layer_plane_triangle_samples(p0, p1, p2, sample_rgba_for_barycentric)) { + + auto sample_data_for_barycentric = [&](const Vec3f &barycentric) { + const Vec2f uv = tri_uv[0] * barycentric.x() + tri_uv[1] * barycentric.y() + tri_uv[2] * barycentric.z(); + return sample_data_for_uv(uv); + }; + if (accumulate_layer_plane_triangle_samples(p0, p1, p2, sample_data_for_barycentric)) { sampled_from_uv_texture = true; continue; } @@ -7278,12 +7661,7 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode( const Vec3d world_pos = p0 * double(b0) + p1 * double(b1) + p2 * double(b2); const Vec2f uv = tri_uv[0] * b0 + tri_uv[1] * b1 + tri_uv[2] * b2; - std::array rgba = sample_texture_rgba_bilinear_for_gcode(volume->imported_texture_rgba, - volume->imported_texture_width, - volume->imported_texture_height, - uv.x(), - uv.y()); - rgba[3] = 1.f; + TextureSampleData sample_data = sample_data_for_uv(uv); float sample_weight = area_weight; if (use_layer_weighting) { @@ -7297,7 +7675,11 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode( if (sample_weight <= EPSILON) continue; - accumulate_sample(float(world_pos.x()), float(world_pos.y()), rgba, sample_weight); + accumulate_sample(float(world_pos.x()), + float(world_pos.y()), + sample_data.rgba, + sample_weight, + std::move(sample_data.raw_component_weights)); sampled_from_uv_texture = true; } } @@ -7314,8 +7696,7 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode( for (size_t i = 0; i < its.vertices.size(); ++i) { const Vec3d world_pos = volume_trafo * its.vertices[i].cast(); - std::array rgba = unpack_rgba_u32(volume->imported_vertex_colors_rgba[i]); - rgba[3] = 1.f; + std::array rgba = composite_rgba_over_background_for_gcode(unpack_rgba_u32(volume->imported_vertex_colors_rgba[i]), background_color); float sample_weight = 1.f; if (use_layer_weighting) { const float dz = std::abs(float(world_pos.z()) - layer_z_mm); @@ -7335,7 +7716,6 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode( if (samples.empty()) return VertexColorOverhangWeightField{}; - const size_t component_count = component_colors.size(); const std::vector> fixed_color_solver_component_colors = fixed_color_generic_solver_component_colors_for_gcode(filament_color_mode); const bool use_fixed_color_generic_solver = @@ -7385,48 +7765,54 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode( weight_field.sample_y_mm[sample_idx] = sample.y_mm; weight_field.sample_weight[sample_idx] = sample.weight; - std::array target = { - clamp01f_for_gcode(sample.rgba[0]), - clamp01f_for_gcode(sample.rgba[1]), - clamp01f_for_gcode(sample.rgba[2]) - }; - if (std::abs(tone_gamma - 1.f) > 1e-5f) { - target[0] = apply_texture_tone_gamma_for_gcode(target[0], tone_gamma); - target[1] = apply_texture_tone_gamma_for_gcode(target[1], tone_gamma); - target[2] = apply_texture_tone_gamma_for_gcode(target[2], tone_gamma); - } - std::vector desired(component_count, 0.f); size_t mapped_component_count = component_count; - if (raw_values_mode) { - const float channels[3] = { target[0], target[1], target[2] }; - const size_t channel_count = std::min(component_count, size_t(3)); - for (size_t channel_idx = 0; channel_idx < channel_count; ++channel_idx) - desired[channel_idx] = clamp01f_for_gcode(channels[channel_idx]); - mapped_component_count = channel_count; + const bool has_raw_component_weights = raw_values_mode && sample.raw_component_weights.size() == component_count; + if (has_raw_component_weights) { + for (size_t component_idx = 0; component_idx < component_count; ++component_idx) + desired[component_idx] = clamp01f_for_gcode(sample.raw_component_weights[component_idx]); } else { - std::vector optimized; - if (!use_fixed_color_generic_solver) - optimized = optimized_primary_component_weights_for_target_for_gcode(target, - component_count, - filament_color_mode, - component_colors, - force_sequential_filaments); - if (optimized.size() == component_count) - desired = std::move(optimized); - else { - std::vector best = generic_mix_candidates != nullptr ? - best_component_mix_weights_for_target_for_gcode(*generic_mix_candidates, - target, - generic_solver_lookup_mode, - generic_solver_mode) : - std::vector{}; - if (best.size() == component_count) - desired = std::move(best); + std::array target = { + clamp01f_for_gcode(sample.rgba[0]), + clamp01f_for_gcode(sample.rgba[1]), + clamp01f_for_gcode(sample.rgba[2]) + }; + if (std::abs(tone_gamma - 1.f) > 1e-5f) { + target[0] = apply_texture_tone_gamma_for_gcode(target[0], tone_gamma); + target[1] = apply_texture_tone_gamma_for_gcode(target[1], tone_gamma); + target[2] = apply_texture_tone_gamma_for_gcode(target[2], tone_gamma); + } + + if (raw_values_mode) { + const float channels[3] = { target[0], target[1], target[2] }; + const size_t channel_count = std::min(component_count, size_t(3)); + for (size_t channel_idx = 0; channel_idx < channel_count; ++channel_idx) + desired[channel_idx] = clamp01f_for_gcode(channels[channel_idx]); + mapped_component_count = channel_count; + } else { + std::vector optimized; + if (!use_fixed_color_generic_solver) + optimized = optimized_primary_component_weights_for_target_for_gcode(target, + component_count, + filament_color_mode, + component_colors, + force_sequential_filaments); + if (optimized.size() == component_count) + desired = std::move(optimized); + else { + std::vector best = generic_mix_candidates != nullptr ? + best_component_mix_weights_for_target_for_gcode(*generic_mix_candidates, + target, + generic_solver_lookup_mode, + generic_solver_mode) : + std::vector{}; + if (best.size() == component_count) + desired = std::move(best); + } } } - if (std::abs(contrast_factor - 1.f) > 1e-5f) + if (!has_raw_component_weights && std::abs(contrast_factor - 1.f) > 1e-5f) apply_texture_contrast_to_mapped_components_for_gcode(desired, contrast_factor, mapped_component_count); for (size_t component_idx = 0; component_idx < component_count; ++component_idx) { diff --git a/src/libslic3r/ImageMapRawFilamentOffsetAtlas.cpp b/src/libslic3r/ImageMapRawFilamentOffsetAtlas.cpp new file mode 100644 index 00000000000..5538ea8f050 --- /dev/null +++ b/src/libslic3r/ImageMapRawFilamentOffsetAtlas.cpp @@ -0,0 +1,511 @@ +#include "ImageMapRawFilamentOffsetAtlas.hpp" + +#include +#include +#include +#include +#include +#include + +namespace Slic3r { + +namespace { + +constexpr const char *Magic = "imagemap_raw_filament_offset"; +constexpr size_t MagicSize = 28; +constexpr size_t FixedHeaderSize = 38; + +static void set_error(std::string *error, const std::string &message) +{ + if (error != nullptr) + *error = message; +} + +static uint32_t read_be_u32(const std::vector &bytes, size_t offset) +{ + return (uint32_t(bytes[offset]) << 24) | + (uint32_t(bytes[offset + 1]) << 16) | + (uint32_t(bytes[offset + 2]) << 8) | + uint32_t(bytes[offset + 3]); +} + +static void append_be_u32(std::vector &bytes, uint32_t value) +{ + bytes.emplace_back(uint8_t((value >> 24) & 0xFFu)); + bytes.emplace_back(uint8_t((value >> 16) & 0xFFu)); + bytes.emplace_back(uint8_t((value >> 8) & 0xFFu)); + bytes.emplace_back(uint8_t(value & 0xFFu)); +} + +static bool read_header_bytes(const std::vector &rgba, + uint32_t width, + uint32_t height, + size_t byte_count, + std::vector &bytes) +{ + if (byte_count > std::numeric_limits::max() / 8) + return false; + if (size_t(width) * size_t(height) < byte_count * 8) + return false; + + bytes.assign(byte_count, 0); + for (size_t byte_idx = 0; byte_idx < byte_count; ++byte_idx) { + uint8_t value = 0; + for (size_t bit_idx = 0; bit_idx < 8; ++bit_idx) { + const size_t pixel_idx = byte_idx * 8 + bit_idx; + const size_t rgba_idx = pixel_idx * 4; + if (rgba_idx + 2 >= rgba.size()) + return false; + const unsigned int average = (unsigned(rgba[rgba_idx + 0]) + unsigned(rgba[rgba_idx + 1]) + unsigned(rgba[rgba_idx + 2])) / 3u; + value = uint8_t((value << 1) | (average >= 128u ? 1u : 0u)); + } + bytes[byte_idx] = value; + } + return true; +} + +static void write_header_bytes(std::vector &rgba, const std::vector &bytes) +{ + for (size_t byte_idx = 0; byte_idx < bytes.size(); ++byte_idx) { + const uint8_t value = bytes[byte_idx]; + for (size_t bit_idx = 0; bit_idx < 8; ++bit_idx) { + const bool bit = (value & (uint8_t(1) << (7 - bit_idx))) != 0; + const size_t rgba_idx = (byte_idx * 8 + bit_idx) * 4; + const uint8_t channel = bit ? 255 : 0; + rgba[rgba_idx + 0] = channel; + rgba[rgba_idx + 1] = channel; + rgba[rgba_idx + 2] = channel; + rgba[rgba_idx + 3] = 255; + } + } +} + +static std::string uppercase_ascii(std::string value) +{ + for (char &ch : value) + if (ch >= 'a' && ch <= 'z') + ch = char(ch - 'a' + 'A'); + return value; +} + +static std::string standard_hex_for_color_code(const std::string &color) +{ + const std::string key = uppercase_ascii(color); + if (key == "C") return "#00FFFF"; + if (key == "M") return "#FF00FF"; + if (key == "Y") return "#FFFF00"; + if (key == "K") return "#000000"; + if (key == "W") return "#FFFFFF"; + if (key == "R") return "#FF0000"; + if (key == "G") return "#00FF00"; + if (key == "B") return "#0000FF"; + return "#FFFFFF"; +} + +static nlohmann::json atlas_metadata_json(const ImageMapRawFilamentOffsetAtlas &atlas, uint32_t header_rows) +{ + const uint32_t region_count = (atlas.channels + 2u) / 3u; + nlohmann::json root; + root["format"] = "raw_filament_offset_atlas"; + root["image"] = { + { "width", atlas.width }, + { "height", atlas.height }, + { "channels", atlas.channels } + }; + root["filaments"] = nlohmann::json::array(); + for (const ImageMapRawFilament &filament : atlas.filaments) { + nlohmann::json entry; + entry["slot"] = filament.slot; + entry["color"] = filament.color.empty() ? "custom" : filament.color; + if (!filament.hex.empty()) + entry["hex"] = filament.hex; + root["filaments"].push_back(std::move(entry)); + } + root["regions"] = nlohmann::json::array(); + for (uint32_t region_idx = 0; region_idx < region_count; ++region_idx) { + nlohmann::json channels = nlohmann::json::object(); + const uint32_t first_channel = region_idx * 3u + 1u; + if (first_channel <= atlas.channels) + channels["r"] = first_channel; + if (first_channel + 1u <= atlas.channels) + channels["g"] = first_channel + 1u; + if (first_channel + 2u <= atlas.channels) + channels["b"] = first_channel + 2u; + + nlohmann::json region; + region["x"] = region_idx * atlas.width; + region["y"] = header_rows; + region["width"] = atlas.width; + region["height"] = atlas.height; + region["channels"] = std::move(channels); + if (region_idx == 0) + region["alpha"] = "projection_mask"; + root["regions"].push_back(std::move(region)); + } + return root; +} + +} // namespace + +bool ImageMapRawFilamentOffsetAtlas::valid() const +{ + return width > 0 && + height > 0 && + channels > 0 && + offsets.size() >= size_t(width) * size_t(height) * size_t(channels); +} + +bool image_map_raw_filament_is_standard_color(const std::string &color) +{ + const std::string key = uppercase_ascii(color); + return key == "C" || key == "M" || key == "Y" || key == "K" || + key == "W" || key == "R" || key == "G" || key == "B"; +} + +std::string image_map_raw_filament_channel_key(const ImageMapRawFilament &filament, size_t channel_idx) +{ + const std::string color = uppercase_ascii(filament.color); + if (image_map_raw_filament_is_standard_color(color)) + return color; + + std::string hex = uppercase_ascii(filament.hex); + const unsigned int slot = filament.slot != 0 ? filament.slot : unsigned(channel_idx + 1); + if (!hex.empty()) + return "CUSTOM:" + std::to_string(slot) + ":" + hex; + if (!color.empty()) + return color + ":" + std::to_string(slot); + return "SLOT:" + std::to_string(slot); +} + +std::vector image_map_raw_filaments_for_channels(const std::vector &filaments, + uint32_t channels) +{ + std::vector normalized(static_cast(channels)); + std::vector filled(static_cast(channels), 0); + for (uint32_t channel = 0; channel < channels; ++channel) { + normalized[size_t(channel)].slot = channel + 1; + normalized[size_t(channel)].color = "custom"; + normalized[size_t(channel)].hex = "#FFFFFF"; + } + + size_t next_empty = 0; + for (ImageMapRawFilament filament : filaments) { + size_t target = size_t(channels); + if (filament.slot >= 1 && filament.slot <= channels) + target = size_t(filament.slot - 1); + else { + while (next_empty < filled.size() && filled[next_empty] != 0) + ++next_empty; + if (next_empty < filled.size()) + target = next_empty; + } + if (target >= normalized.size() || filled[target] != 0) + continue; + if (filament.slot == 0) + filament.slot = unsigned(target + 1); + if (filament.color.empty()) + filament.color = "custom"; + if (filament.hex.empty() && !image_map_raw_filament_is_standard_color(filament.color)) + filament.hex = "#FFFFFF"; + normalized[target] = std::move(filament); + filled[target] = 1; + } + return normalized; +} + +std::vector image_map_raw_filaments_from_metadata_json(const std::string &metadata_json, + uint32_t channels) +{ + std::vector filaments; + try { + const nlohmann::json root = nlohmann::json::parse(metadata_json); + const nlohmann::json entries = root.value("filaments", nlohmann::json::array()); + if (entries.is_array()) { + for (const nlohmann::json &entry : entries) { + if (!entry.is_object()) + continue; + ImageMapRawFilament filament; + filament.slot = unsigned(std::max(0, entry.value("slot", 0))); + filament.color = entry.value("color", std::string()); + filament.hex = entry.value("hex", std::string()); + filaments.emplace_back(std::move(filament)); + } + } + } catch (...) { + filaments.clear(); + } + return image_map_raw_filaments_for_channels(filaments, channels); +} + +std::vector image_map_raw_filament_channel_keys(const std::vector &filaments) +{ + std::vector keys; + keys.reserve(filaments.size()); + for (size_t idx = 0; idx < filaments.size(); ++idx) + keys.emplace_back(image_map_raw_filament_channel_key(filaments[idx], idx)); + return keys; +} + +bool decode_image_map_raw_filament_offset_atlas(const std::vector &rgba, + uint32_t atlas_width, + uint32_t atlas_height, + ImageMapRawFilamentOffsetAtlas &out, + std::string *error) +{ + out = {}; + if (atlas_width == 0 || atlas_height == 0 || rgba.size() < size_t(atlas_width) * size_t(atlas_height) * 4) { + set_error(error, "Invalid image dimensions."); + return false; + } + + std::vector fixed; + if (!read_header_bytes(rgba, atlas_width, atlas_height, FixedHeaderSize, fixed)) { + set_error(error, "The image is too small for an ImageMap raw filament offset header."); + return false; + } + if (!std::equal(fixed.begin(), fixed.begin() + MagicSize, Magic)) { + set_error(error, "The image is not an ImageMap raw filament offset atlas."); + return false; + } + if (fixed[MagicSize] != 1u || (fixed[MagicSize + 1] & 1u) == 0u) { + set_error(error, "Unsupported ImageMap raw filament offset atlas header."); + return false; + } + + const uint32_t header_rows = read_be_u32(fixed, MagicSize + 2); + const uint32_t metadata_length = read_be_u32(fixed, MagicSize + 6); + if (header_rows == 0 || header_rows > atlas_height) { + set_error(error, "Invalid ImageMap raw filament offset header row count."); + return false; + } + const size_t total_header_bytes = FixedHeaderSize + size_t(metadata_length); + if (total_header_bytes > std::numeric_limits::max() / 8 || + total_header_bytes * 8 > size_t(header_rows) * size_t(atlas_width)) { + set_error(error, "ImageMap raw filament offset metadata exceeds the declared header rows."); + return false; + } + + std::vector header; + if (!read_header_bytes(rgba, atlas_width, atlas_height, total_header_bytes, header)) { + set_error(error, "Could not read ImageMap raw filament offset metadata."); + return false; + } + + std::string metadata(reinterpret_cast(header.data() + FixedHeaderSize), metadata_length); + nlohmann::json root; + try { + root = nlohmann::json::parse(metadata); + } catch (...) { + set_error(error, "ImageMap raw filament offset metadata is not valid JSON."); + return false; + } + if (!root.is_object() || root.value("format", std::string()) != "raw_filament_offset_atlas") { + set_error(error, "ImageMap raw filament offset metadata has an unsupported format."); + return false; + } + + const nlohmann::json image = root.value("image", nlohmann::json::object()); + const int logical_width = image.value("width", 0); + const int logical_height = image.value("height", 0); + const int logical_channels = image.value("channels", 0); + if (logical_width <= 0 || logical_height <= 0 || logical_channels <= 0) { + set_error(error, "ImageMap raw filament offset metadata has invalid image dimensions."); + return false; + } + + ImageMapRawFilamentOffsetAtlas decoded; + decoded.width = uint32_t(logical_width); + decoded.height = uint32_t(logical_height); + decoded.channels = uint32_t(logical_channels); + decoded.offsets.assign(size_t(decoded.width) * size_t(decoded.height) * size_t(decoded.channels), 0); + decoded.mask.assign(size_t(decoded.width) * size_t(decoded.height), 255); + decoded.metadata_json = metadata; + + const nlohmann::json filaments = root.value("filaments", nlohmann::json::array()); + if (filaments.is_array()) { + for (const nlohmann::json &entry : filaments) { + if (!entry.is_object()) + continue; + ImageMapRawFilament filament; + filament.slot = unsigned(std::max(0, entry.value("slot", 0))); + filament.color = entry.value("color", std::string()); + filament.hex = entry.value("hex", std::string()); + if (filament.hex.empty()) + filament.hex = standard_hex_for_color_code(filament.color); + decoded.filaments.emplace_back(std::move(filament)); + } + } + + const nlohmann::json regions = root.value("regions", nlohmann::json::array()); + if (!regions.is_array() || regions.empty()) { + set_error(error, "ImageMap raw filament offset metadata does not contain regions."); + return false; + } + + for (const nlohmann::json ®ion : regions) { + if (!region.is_object()) + continue; + const int rx = region.value("x", -1); + const int ry = region.value("y", -1); + const int rw = region.value("width", 0); + const int rh = region.value("height", 0); + if (rx < 0 || ry < 0 || rw <= 0 || rh <= 0 || + uint64_t(rx) + uint64_t(rw) > atlas_width || + uint64_t(ry) + uint64_t(rh) > atlas_height) { + set_error(error, "ImageMap raw filament offset region exceeds the atlas image."); + return false; + } + + const nlohmann::json channels = region.value("channels", nlohmann::json::object()); + if (channels.is_object()) { + const std::array, 3> rgb_channels = { + std::make_pair("r", size_t(0)), + std::make_pair("g", size_t(1)), + std::make_pair("b", size_t(2)) + }; + for (const auto &rgb_channel : rgb_channels) { + const int logical_channel = channels.value(rgb_channel.first, 0); + if (logical_channel <= 0 || logical_channel > logical_channels) + continue; + const size_t dst_channel = size_t(logical_channel - 1); + const uint32_t copy_width = std::min(decoded.width, uint32_t(rw)); + const uint32_t copy_height = std::min(decoded.height, uint32_t(rh)); + for (uint32_t y = 0; y < copy_height; ++y) { + for (uint32_t x = 0; x < copy_width; ++x) { + const size_t src_idx = (size_t(ry + int(y)) * size_t(atlas_width) + size_t(rx + int(x))) * 4 + rgb_channel.second; + const size_t dst_idx = (size_t(y) * size_t(decoded.width) + size_t(x)) * size_t(decoded.channels) + dst_channel; + decoded.offsets[dst_idx] = rgba[src_idx]; + } + } + } + } + + if (region.value("alpha", std::string()) == "projection_mask") { + const uint32_t copy_width = std::min(decoded.width, uint32_t(rw)); + const uint32_t copy_height = std::min(decoded.height, uint32_t(rh)); + for (uint32_t y = 0; y < copy_height; ++y) { + for (uint32_t x = 0; x < copy_width; ++x) { + const size_t src_idx = (size_t(ry + int(y)) * size_t(atlas_width) + size_t(rx + int(x))) * 4 + 3; + decoded.mask[size_t(y) * size_t(decoded.width) + size_t(x)] = rgba[src_idx]; + } + } + } + } + + out = std::move(decoded); + return true; +} + +bool encode_image_map_raw_filament_offset_atlas(const ImageMapRawFilamentOffsetAtlas &atlas, + std::vector &rgba, + uint32_t &atlas_width, + uint32_t &atlas_height, + std::string *error) +{ + rgba.clear(); + atlas_width = 0; + atlas_height = 0; + if (!atlas.valid()) { + set_error(error, "Invalid ImageMap raw filament offset atlas data."); + return false; + } + + const uint32_t region_count = (atlas.channels + 2u) / 3u; + if (region_count == 0 || atlas.width > std::numeric_limits::max() / region_count) { + set_error(error, "ImageMap raw filament offset atlas is too wide."); + return false; + } + + atlas_width = atlas.width * region_count; + uint32_t header_rows = 1; + std::string metadata; + for (int iter = 0; iter < 8; ++iter) { + metadata = atlas_metadata_json(atlas, header_rows).dump(); + const size_t bits = (FixedHeaderSize + metadata.size()) * 8; + const uint32_t needed_rows = uint32_t((bits + size_t(atlas_width) - 1) / size_t(atlas_width)); + if (needed_rows == header_rows) + break; + header_rows = std::max(needed_rows, 1); + } + metadata = atlas_metadata_json(atlas, header_rows).dump(); + if ((FixedHeaderSize + metadata.size()) * 8 > size_t(header_rows) * size_t(atlas_width)) { + set_error(error, "Could not fit ImageMap raw filament offset metadata header."); + return false; + } + if (header_rows > std::numeric_limits::max() - atlas.height) { + set_error(error, "ImageMap raw filament offset atlas is too tall."); + return false; + } + + atlas_height = header_rows + atlas.height; + rgba.assign(size_t(atlas_width) * size_t(atlas_height) * 4, 255); + for (uint32_t y = 0; y < header_rows; ++y) { + for (uint32_t x = 0; x < atlas_width; ++x) { + const size_t idx = (size_t(y) * size_t(atlas_width) + size_t(x)) * 4; + rgba[idx + 0] = 0; + rgba[idx + 1] = 0; + rgba[idx + 2] = 0; + rgba[idx + 3] = 255; + } + } + + for (uint32_t region_idx = 0; region_idx < region_count; ++region_idx) { + const uint32_t region_x = region_idx * atlas.width; + const uint32_t first_channel = region_idx * 3u; + for (uint32_t y = 0; y < atlas.height; ++y) { + for (uint32_t x = 0; x < atlas.width; ++x) { + const size_t dst = (size_t(header_rows + y) * size_t(atlas_width) + size_t(region_x + x)) * 4; + for (uint32_t c = 0; c < 3; ++c) { + const uint32_t channel = first_channel + c; + rgba[dst + c] = channel < atlas.channels ? + atlas.offsets[(size_t(y) * size_t(atlas.width) + size_t(x)) * size_t(atlas.channels) + size_t(channel)] : + 0; + } + rgba[dst + 3] = region_idx == 0 && atlas.mask.size() >= size_t(atlas.width) * size_t(atlas.height) ? + atlas.mask[size_t(y) * size_t(atlas.width) + size_t(x)] : + 255; + } + } + } + + std::vector header; + header.reserve(FixedHeaderSize + metadata.size()); + header.insert(header.end(), Magic, Magic + MagicSize); + header.emplace_back(1); + header.emplace_back(1); + append_be_u32(header, header_rows); + append_be_u32(header, uint32_t(metadata.size())); + header.insert(header.end(), metadata.begin(), metadata.end()); + write_header_bytes(rgba, header); + return true; +} + +std::vector image_map_raw_filament_offset_preview_rgba(const ImageMapRawFilamentOffsetAtlas &atlas) +{ + std::vector preview; + if (!atlas.valid()) + return preview; + + preview.assign(size_t(atlas.width) * size_t(atlas.height) * 4, 255); + const bool grayscale = atlas.channels == 1; + for (uint32_t y = 0; y < atlas.height; ++y) { + for (uint32_t x = 0; x < atlas.width; ++x) { + const size_t pixel = size_t(y) * size_t(atlas.width) + size_t(x); + const size_t src = pixel * size_t(atlas.channels); + const size_t dst = pixel * 4; + if (grayscale) { + preview[dst + 0] = atlas.offsets[src]; + preview[dst + 1] = atlas.offsets[src]; + preview[dst + 2] = atlas.offsets[src]; + } else { + preview[dst + 0] = atlas.channels > 0 ? atlas.offsets[src + 0] : 0; + preview[dst + 1] = atlas.channels > 1 ? atlas.offsets[src + 1] : 0; + preview[dst + 2] = atlas.channels > 2 ? atlas.offsets[src + 2] : 0; + } + preview[dst + 3] = atlas.mask.size() > pixel ? atlas.mask[pixel] : 255; + } + } + return preview; +} + +} // namespace Slic3r diff --git a/src/libslic3r/ImageMapRawFilamentOffsetAtlas.hpp b/src/libslic3r/ImageMapRawFilamentOffsetAtlas.hpp new file mode 100644 index 00000000000..dad30a8eb8e --- /dev/null +++ b/src/libslic3r/ImageMapRawFilamentOffsetAtlas.hpp @@ -0,0 +1,59 @@ +#ifndef slic3r_ImageMapRawFilamentOffsetAtlas_hpp_ +#define slic3r_ImageMapRawFilamentOffsetAtlas_hpp_ + +#include +#include +#include +#include + +namespace Slic3r { + +struct ImageMapRawFilament +{ + unsigned int slot { 0 }; + std::string color; + std::string hex; +}; + +struct ImageMapRawFilamentOffsetAtlas +{ + uint32_t width { 0 }; + uint32_t height { 0 }; + uint32_t channels { 0 }; + std::vector filaments; + std::vector offsets; + std::vector mask; + std::string metadata_json; + + bool valid() const; +}; + +bool decode_image_map_raw_filament_offset_atlas(const std::vector &rgba, + uint32_t atlas_width, + uint32_t atlas_height, + ImageMapRawFilamentOffsetAtlas &out, + std::string *error = nullptr); + +bool encode_image_map_raw_filament_offset_atlas(const ImageMapRawFilamentOffsetAtlas &atlas, + std::vector &rgba, + uint32_t &atlas_width, + uint32_t &atlas_height, + std::string *error = nullptr); + +bool image_map_raw_filament_is_standard_color(const std::string &color); + +std::string image_map_raw_filament_channel_key(const ImageMapRawFilament &filament, size_t channel_idx); + +std::vector image_map_raw_filaments_for_channels(const std::vector &filaments, + uint32_t channels); + +std::vector image_map_raw_filaments_from_metadata_json(const std::string &metadata_json, + uint32_t channels); + +std::vector image_map_raw_filament_channel_keys(const std::vector &filaments); + +std::vector image_map_raw_filament_offset_preview_rgba(const ImageMapRawFilamentOffsetAtlas &atlas); + +} // namespace Slic3r + +#endif diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index dd91b20eccd..8694a516a87 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -795,8 +795,11 @@ Model Model::read_from_file(const std::string& volume->imported_texture_uvs_per_face.clear(); volume->imported_texture_uv_valid.clear(); volume->imported_texture_rgba.clear(); + volume->imported_texture_raw_filament_offsets.clear(); volume->imported_texture_width = 0; volume->imported_texture_height = 0; + volume->imported_texture_raw_channels = 0; + volume->imported_texture_raw_metadata_json.clear(); bool has_imported_usable_uv_texture_data = false; const size_t triangle_count = volume->mesh().its.indices.size(); @@ -830,6 +833,9 @@ Model Model::read_from_file(const std::string& volume->imported_texture_width = atlas_width; volume->imported_texture_height = atlas_height; volume->imported_texture_rgba = std::move(atlas_rgba); + volume->imported_texture_raw_filament_offsets.clear(); + volume->imported_texture_raw_channels = 0; + volume->imported_texture_raw_metadata_json.clear(); has_imported_usable_uv_texture_data = has_any_valid_uv_face; } } @@ -3357,6 +3363,7 @@ void ModelVolume::assign_new_unique_ids_recursive() imported_texture_uvs_per_face.set_new_unique_id(); imported_texture_uv_valid.set_new_unique_id(); imported_texture_rgba.set_new_unique_id(); + imported_texture_raw_filament_offsets.set_new_unique_id(); fuzzy_skin_facets.set_new_unique_id(); } @@ -4857,8 +4864,11 @@ static bool model_volume_texture_mapping_data_matches(const ModelVolume &mv_old, model_volume_imported_vector_matches(mv_old.imported_texture_uvs_per_face, mv_new.imported_texture_uvs_per_face) && model_volume_imported_vector_matches(mv_old.imported_texture_uv_valid, mv_new.imported_texture_uv_valid) && model_volume_imported_vector_matches(mv_old.imported_texture_rgba, mv_new.imported_texture_rgba) && + model_volume_imported_vector_matches(mv_old.imported_texture_raw_filament_offsets, mv_new.imported_texture_raw_filament_offsets) && mv_old.imported_texture_width == mv_new.imported_texture_width && - mv_old.imported_texture_height == mv_new.imported_texture_height; + mv_old.imported_texture_height == mv_new.imported_texture_height && + mv_old.imported_texture_raw_channels == mv_new.imported_texture_raw_channels && + mv_old.imported_texture_raw_metadata_json == mv_new.imported_texture_raw_metadata_json; } bool model_texture_mapping_color_data_changed(const ModelObject& mo, const ModelObject& mo_new) diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index a0e72e41bb9..0530ee3ae6d 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -1059,8 +1059,11 @@ public: ModelVolumeImportedVector imported_texture_uvs_per_face; ModelVolumeImportedVector imported_texture_uv_valid; ModelVolumeImportedVector imported_texture_rgba; + ModelVolumeImportedVector imported_texture_raw_filament_offsets; uint32_t imported_texture_width{0}; uint32_t imported_texture_height{0}; + uint32_t imported_texture_raw_channels{0}; + std::string imported_texture_raw_metadata_json; // List of mesh facets painted for fuzzy skin. FacetsAnnotation fuzzy_skin_facets; @@ -1193,6 +1196,7 @@ public: this->imported_texture_uvs_per_face.set_new_unique_id(); this->imported_texture_uv_valid.set_new_unique_id(); this->imported_texture_rgba.set_new_unique_id(); + this->imported_texture_raw_filament_offsets.set_new_unique_id(); this->fuzzy_skin_facets.set_new_unique_id(); } @@ -1307,8 +1311,11 @@ private: imported_texture_uvs_per_face(other.imported_texture_uvs_per_face), imported_texture_uv_valid(other.imported_texture_uv_valid), imported_texture_rgba(other.imported_texture_rgba), + imported_texture_raw_filament_offsets(other.imported_texture_raw_filament_offsets), imported_texture_width(other.imported_texture_width), imported_texture_height(other.imported_texture_height), + imported_texture_raw_channels(other.imported_texture_raw_channels), + imported_texture_raw_metadata_json(other.imported_texture_raw_metadata_json), fuzzy_skin_facets(other.fuzzy_skin_facets), cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape) { assert(this->id().valid()); @@ -1376,7 +1383,7 @@ private: friend class cereal::access; friend class UndoRedo::StackImpl; // Used for deserialization, therefore no IDs are allocated. - ModelVolume() : ObjectBase(-1), config(-1), supported_facets(-1), seam_facets(-1), mmu_segmentation_facets(-1), texture_mapping_color_facets(-1), imported_vertex_colors_rgba(-1), imported_texture_uvs_per_face(-1), imported_texture_uv_valid(-1), imported_texture_rgba(-1), fuzzy_skin_facets(-1), object(nullptr) { + ModelVolume() : ObjectBase(-1), config(-1), supported_facets(-1), seam_facets(-1), mmu_segmentation_facets(-1), texture_mapping_color_facets(-1), imported_vertex_colors_rgba(-1), imported_texture_uvs_per_face(-1), imported_texture_uv_valid(-1), imported_texture_rgba(-1), imported_texture_raw_filament_offsets(-1), fuzzy_skin_facets(-1), object(nullptr) { assert(this->id().invalid()); assert(this->config.id().invalid()); assert(this->supported_facets.id().invalid()); @@ -1408,7 +1415,8 @@ private: cereal::load_by_value(ar, imported_texture_uvs_per_face); cereal::load_by_value(ar, imported_texture_uv_valid); cereal::load_by_value(ar, imported_texture_rgba); - ar(imported_texture_width, imported_texture_height); + cereal::load_by_value(ar, imported_texture_raw_filament_offsets); + ar(imported_texture_width, imported_texture_height, imported_texture_raw_channels, imported_texture_raw_metadata_json); cereal::load_by_value(ar, fuzzy_skin_facets); mesh_changed |= t != fuzzy_skin_facets.timestamp(); cereal::load_by_value(ar, config); @@ -1420,8 +1428,11 @@ private: imported_texture_uvs_per_face.clear(); imported_texture_uv_valid.clear(); imported_texture_rgba.clear(); + imported_texture_raw_filament_offsets.clear(); imported_texture_width = 0; imported_texture_height = 0; + imported_texture_raw_channels = 0; + imported_texture_raw_metadata_json.clear(); texture_mapping_color_facets.reset(); } assert(m_mesh); @@ -1446,7 +1457,8 @@ private: cereal::save_by_value(ar, imported_texture_uvs_per_face); cereal::save_by_value(ar, imported_texture_uv_valid); cereal::save_by_value(ar, imported_texture_rgba); - ar(imported_texture_width, imported_texture_height); + cereal::save_by_value(ar, imported_texture_raw_filament_offsets); + ar(imported_texture_width, imported_texture_height, imported_texture_raw_channels, imported_texture_raw_metadata_json); cereal::save_by_value(ar, fuzzy_skin_facets); cereal::save_by_value(ar, config); cereal::save(ar, text_configuration); diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index ac0f55465b6..eee98a0c10b 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -261,6 +261,20 @@ static int prime_tower_texture_mapping_color_mode_for_print(int prime_tower_colo } } +static bool model_volume_texture_mapping_data_equal_for_print_sharing(const ModelVolume &lhs, const ModelVolume &rhs) +{ + return lhs.texture_mapping_color_facets.equals(rhs.texture_mapping_color_facets) && + lhs.imported_vertex_colors_rgba == rhs.imported_vertex_colors_rgba && + lhs.imported_texture_uvs_per_face == rhs.imported_texture_uvs_per_face && + lhs.imported_texture_uv_valid == rhs.imported_texture_uv_valid && + lhs.imported_texture_rgba == rhs.imported_texture_rgba && + lhs.imported_texture_raw_filament_offsets == rhs.imported_texture_raw_filament_offsets && + lhs.imported_texture_width == rhs.imported_texture_width && + lhs.imported_texture_height == rhs.imported_texture_height && + lhs.imported_texture_raw_channels == rhs.imported_texture_raw_channels && + lhs.imported_texture_raw_metadata_json == rhs.imported_texture_raw_metadata_json; +} + template class PrintState; template class PrintState; @@ -2330,6 +2344,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) return false; if (!model_volume1.mmu_segmentation_facets.equals(model_volume2.mmu_segmentation_facets)) return false; + if (!model_volume_texture_mapping_data_equal_for_print_sharing(model_volume1, model_volume2)) + return false; if (!model_volume1.fuzzy_skin_facets.equals(model_volume2.fuzzy_skin_facets)) return false; if (model_volume1.config.get() != model_volume2.config.get()) diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index f6ba8dde7f4..ef14d3bff3f 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -87,8 +87,11 @@ static inline void model_volume_list_copy_configs(ModelObject &model_object_dst, mv_dst.imported_texture_uvs_per_face = mv_src.imported_texture_uvs_per_face; mv_dst.imported_texture_uv_valid = mv_src.imported_texture_uv_valid; mv_dst.imported_texture_rgba = mv_src.imported_texture_rgba; + mv_dst.imported_texture_raw_filament_offsets = mv_src.imported_texture_raw_filament_offsets; mv_dst.imported_texture_width = mv_src.imported_texture_width; mv_dst.imported_texture_height = mv_src.imported_texture_height; + mv_dst.imported_texture_raw_channels = mv_src.imported_texture_raw_channels; + mv_dst.imported_texture_raw_metadata_json = mv_src.imported_texture_raw_metadata_json; assert(mv_dst.fuzzy_skin_facets.id() == mv_src.fuzzy_skin_facets.id()); mv_dst.fuzzy_skin_facets.assign(mv_src.fuzzy_skin_facets); //FIXME what to do with the materials? diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index ce81fe37832..ab145f0abc4 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2338,6 +2338,13 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionString("")); + def = this->add("texture_mapping_background_color", coString); + def->label = L("Texture mapping background color"); + def->tooltip = L("Background color used when texture mapping samples translucent color data."); + def->gui_flags = "serialized"; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionString("#FFFFFFFF")); + def = this->add("texture_mapping_global_settings", coString); def->label = L("Texture mapping global settings"); def->tooltip = L("Serialized global texture mapping settings."); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 15079a3a041..2454cbcebe6 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -901,6 +901,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionPercent, elefant_foot_layers_density)) ((ConfigOptionFloat, max_bridge_length)) ((ConfigOptionFloatOrPercent, line_width)) + ((ConfigOptionString, texture_mapping_background_color)) // Force the generation of solid shells between adjacent materials/volumes. ((ConfigOptionBool, interface_shells)) ((ConfigOptionFloat, layer_height)) diff --git a/src/libslic3r/TextureMapping.cpp b/src/libslic3r/TextureMapping.cpp index d5cf37608c4..297d333fe57 100644 --- a/src/libslic3r/TextureMapping.cpp +++ b/src/libslic3r/TextureMapping.cpp @@ -840,18 +840,41 @@ bool TextureMappingManager::duplicate_zone(size_t zone_index, return true; } -unsigned int TextureMappingManager::find_image_texture_zone_id(size_t) const +unsigned int TextureMappingManager::find_image_texture_zone_id(size_t, + bool allow_raw_values, + bool prefer_raw_values) const { + auto selectable = [allow_raw_values](const TextureMappingZone &zone) { + const bool raw_values = zone.texture_mapping_mode == int(TextureMappingZone::TextureMappingRawValues); + return zone.enabled && !zone.deleted && zone.is_image_texture() && zone.zone_id != 0 && (allow_raw_values || !raw_values); + }; + auto find_raw = [this, selectable](bool raw_values) { + for (const TextureMappingZone &zone : m_zones) + if (selectable(zone) && + (zone.texture_mapping_mode == int(TextureMappingZone::TextureMappingRawValues)) == raw_values) + return zone.zone_id; + return 0u; + }; + + if (allow_raw_values && prefer_raw_values) { + const unsigned int raw_id = find_raw(true); + if (raw_id != 0) + return raw_id; + } + for (const TextureMappingZone &zone : m_zones) - if (zone.enabled && !zone.deleted && zone.is_image_texture() && zone.zone_id != 0) + if (selectable(zone)) return zone.zone_id; return 0; } unsigned int TextureMappingManager::ensure_image_texture_zone(size_t num_physical, - const std::vector &filament_colours) + const std::vector &filament_colours, + bool allow_raw_values, + bool prefer_raw_values) { - if (unsigned int existing = find_image_texture_zone_id(num_physical); existing != 0) + if (unsigned int existing = find_image_texture_zone_id(num_physical, allow_raw_values, prefer_raw_values); + existing != 0) return existing; TextureMappingZone *zone = add_zone(num_physical, filament_colours, int(TextureMappingZone::ImageTexture)); return zone != nullptr ? zone->zone_id : 0; diff --git a/src/libslic3r/TextureMapping.hpp b/src/libslic3r/TextureMapping.hpp index 21b439c4971..38017532514 100644 --- a/src/libslic3r/TextureMapping.hpp +++ b/src/libslic3r/TextureMapping.hpp @@ -265,8 +265,13 @@ public: size_t num_physical, const std::vector &filament_colours); - unsigned int find_image_texture_zone_id(size_t num_physical) const; - unsigned int ensure_image_texture_zone(size_t num_physical, const std::vector &filament_colours); + unsigned int find_image_texture_zone_id(size_t num_physical, + bool allow_raw_values = false, + bool prefer_raw_values = false) const; + unsigned int ensure_image_texture_zone(size_t num_physical, + const std::vector &filament_colours, + bool allow_raw_values = false, + bool prefer_raw_values = false); std::string serialize_entries(); void load_entries(const std::string &serialized, const std::vector &filament_colours); diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 9c9776c6d78..1aacb5d9c54 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -57,11 +57,14 @@ static wxString texture_mapping_menu_label(const TextureMappingZone &zone) if (zone.is_2d_gradient()) return _L("Texture Mapping 2D Gradient"); const std::string color_model = TextureMappingManager::filament_color_mode_name(zone.filament_color_mode); + const bool raw_offset = zone.texture_mapping_mode == int(TextureMappingZone::TextureMappingRawValues); if (color_model == "any") - return _L("Texture Mapping"); + return raw_offset ? _L("Texture Mapping Raw Offset") : _L("Texture Mapping"); wxString color_model_text = from_u8(color_model); color_model_text.MakeUpper(); - return _L("Texture Mapping ") + color_model_text; + return raw_offset ? + _L("Texture Mapping Raw Offset ") + color_model_text : + _L("Texture Mapping ") + color_model_text; } static wxString filament_menu_item_name(int filament_id_1based) diff --git a/src/slic3r/GUI/GUI_ObjectTable.cpp b/src/slic3r/GUI/GUI_ObjectTable.cpp index 8e00258bc37..3b10a949197 100644 --- a/src/slic3r/GUI/GUI_ObjectTable.cpp +++ b/src/slic3r/GUI/GUI_ObjectTable.cpp @@ -35,11 +35,14 @@ static wxString texture_mapping_table_label(const TextureMappingZone &zone) if (zone.is_2d_gradient()) return _L("Texture Mapping 2D Gradient"); const std::string color_model = TextureMappingManager::filament_color_mode_name(zone.filament_color_mode); + const bool raw_offset = zone.texture_mapping_mode == int(TextureMappingZone::TextureMappingRawValues); if (color_model == "any") - return _L("Texture Mapping"); + return raw_offset ? _L("Raw Offset Texture Mapping Zone") : _L("Texture Mapping"); wxString color_model_text = from_u8(color_model); color_model_text.MakeUpper(); - return _L("Texture Mapping ") + color_model_text; + return raw_offset ? + _L("Raw Offset Texture Mapping ") + color_model_text : + _L("Texture Mapping ") + color_model_text; } //min row count diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index 8b8c0e28ed5..38a8626bfa5 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -16,6 +16,7 @@ #include "libslic3r/PresetBundle.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/TextureMapping.hpp" +#include "libslic3r/filament_mixer.h" #include "slic3r/Utils/UndoRedo.hpp" #include "GLGizmoUtils.hpp" @@ -31,7 +32,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -165,7 +168,7 @@ static size_t display_filament_index_for_requested_id(const std::vector physical_colors = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); physical_colors.resize(num_physical, "#26A69A"); - if (unsigned int existing_id = mgr.find_image_texture_zone_id(num_physical); existing_id != 0) { + if (unsigned int existing_id = mgr.find_image_texture_zone_id(num_physical, allow_raw_values, prefer_raw_values); existing_id != 0) { if (TextureMappingZone *zone = mgr.zone_from_id(existing_id); zone == nullptr || !TextureMappingManager::auto_adjust_texture_component_ids(*zone, num_physical, physical_colors)) { return existing_id; @@ -183,7 +186,7 @@ static unsigned int ensure_texture_mapping_zone() } else if (num_physical < 2) { return 0; } else { - mgr.ensure_image_texture_zone(num_physical, physical_colors); + mgr.ensure_image_texture_zone(num_physical, physical_colors, allow_raw_values, prefer_raw_values); } const std::string texture_serialized = mgr.serialize_entries(); @@ -205,7 +208,202 @@ static unsigned int ensure_texture_mapping_zone() if (wxGetApp().mainframe != nullptr) wxGetApp().mainframe->on_config_changed(print_cfg); - return mgr.find_image_texture_zone_id(num_physical); + return mgr.find_image_texture_zone_id(num_physical, allow_raw_values, prefer_raw_values); +} + +static void persist_texture_mapping_zone_updates() +{ + if (wxGetApp().preset_bundle == nullptr) + return; + + TextureMappingManager &mgr = wxGetApp().preset_bundle->texture_mapping_zones; + const std::string texture_serialized = mgr.serialize_entries(); + DynamicPrintConfig *print_cfg = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + if (ConfigOptionString *opt = print_cfg->option("texture_mapping_definitions")) + opt->value = texture_serialized; + else + print_cfg->set_key_value("texture_mapping_definitions", new ConfigOptionString(texture_serialized)); + + if (ConfigOptionString *opt = wxGetApp().preset_bundle->project_config.option("texture_mapping_definitions")) + opt->value = texture_serialized; + else + wxGetApp().preset_bundle->project_config.set_key_value("texture_mapping_definitions", new ConfigOptionString(texture_serialized)); + + wxGetApp().sidebar().update_texture_mapping_panel(false); + wxGetApp().sidebar().update_dynamic_filament_list(); + if (auto *print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT)) + print_tab->update_dirty(); + if (wxGetApp().mainframe != nullptr) + wxGetApp().mainframe->on_config_changed(print_cfg); +} + +static constexpr size_t MaxImageProjectionRawOffsetChannels = 4; + +struct RawAtlasProjectionLayout +{ + std::vector filaments; + std::vector channel_keys; + std::vector atlas_to_target_channel; +}; + +static bool model_volume_has_raw_atlas_texture_data(const ModelVolume *volume) +{ + if (volume == nullptr || + volume->imported_texture_width == 0 || + volume->imported_texture_height == 0 || + volume->imported_texture_raw_channels == 0 || + volume->imported_texture_raw_filament_offsets.empty()) + return false; + return volume->imported_texture_raw_filament_offsets.size() >= + size_t(volume->imported_texture_width) * + size_t(volume->imported_texture_height) * + size_t(volume->imported_texture_raw_channels); +} + +static bool add_raw_layout_channel(RawAtlasProjectionLayout &layout, + const std::string &key, + const ImageMapRawFilament &filament, + std::string *error) +{ + if (std::find(layout.channel_keys.begin(), layout.channel_keys.end(), key) != layout.channel_keys.end()) + return true; + if (layout.channel_keys.size() >= MaxImageProjectionRawOffsetChannels) { + if (error != nullptr) { + *error = GUI::format("This raw filament offset atlas would require more than %1% raw offset channels on the selected object.", + MaxImageProjectionRawOffsetChannels); + } + return false; + } + layout.channel_keys.emplace_back(key); + layout.filaments.emplace_back(filament); + return true; +} + +static bool raw_channel_keys_are_unique(const std::vector &keys) +{ + for (size_t idx = 0; idx < keys.size(); ++idx) + for (size_t other = idx + 1; other < keys.size(); ++other) + if (keys[idx] == keys[other]) + return false; + return true; +} + +static bool raw_atlas_projection_layout_for_object(const ModelObject &object, + const ImageMapRawFilamentOffsetAtlas &atlas, + RawAtlasProjectionLayout &layout, + std::string *error) +{ + layout = {}; + if (!atlas.valid()) { + if (error != nullptr) + *error = "The selected raw filament offset atlas is invalid."; + return false; + } + + for (const ModelVolume *volume : object.volumes) { + if (volume == nullptr || !volume->is_model_part() || !model_volume_has_raw_atlas_texture_data(volume)) + continue; + const std::vector volume_filaments = + image_map_raw_filaments_from_metadata_json(volume->imported_texture_raw_metadata_json, volume->imported_texture_raw_channels); + const std::vector volume_keys = image_map_raw_filament_channel_keys(volume_filaments); + if (!raw_channel_keys_are_unique(volume_keys)) { + if (error != nullptr) + *error = "The selected object's existing raw filament offset metadata has duplicate channels."; + return false; + } + for (size_t channel = 0; channel < volume_keys.size(); ++channel) + if (!add_raw_layout_channel(layout, volume_keys[channel], volume_filaments[channel], error)) + return false; + } + + const std::vector atlas_filaments = + image_map_raw_filaments_for_channels(atlas.filaments, atlas.channels); + const std::vector atlas_keys = image_map_raw_filament_channel_keys(atlas_filaments); + if (!raw_channel_keys_are_unique(atlas_keys)) { + if (error != nullptr) + *error = "The selected raw filament offset atlas has duplicate channels."; + return false; + } + + for (size_t channel = 0; channel < atlas_keys.size(); ++channel) + if (!add_raw_layout_channel(layout, atlas_keys[channel], atlas_filaments[channel], error)) + return false; + + layout.atlas_to_target_channel.assign(atlas_keys.size(), size_t(-1)); + for (size_t atlas_channel = 0; atlas_channel < atlas_keys.size(); ++atlas_channel) { + const auto it = std::find(layout.channel_keys.begin(), layout.channel_keys.end(), atlas_keys[atlas_channel]); + if (it == layout.channel_keys.end()) { + if (error != nullptr) + *error = "The selected raw filament offset atlas is not compatible with the selected object."; + return false; + } + layout.atlas_to_target_channel[atlas_channel] = size_t(std::distance(layout.channel_keys.begin(), it)); + } + return true; +} + +static std::string raw_layout_metadata_json(uint32_t width, uint32_t height, const RawAtlasProjectionLayout &layout) +{ + nlohmann::json root; + root["format"] = "raw_filament_offset_atlas"; + root["image"] = { + { "width", width }, + { "height", height }, + { "channels", layout.filaments.size() } + }; + root["filaments"] = nlohmann::json::array(); + for (size_t idx = 0; idx < layout.filaments.size(); ++idx) { + const ImageMapRawFilament &filament = layout.filaments[idx]; + nlohmann::json entry; + entry["slot"] = filament.slot != 0 ? filament.slot : unsigned(idx + 1); + entry["color"] = filament.color.empty() ? "custom" : filament.color; + if (!filament.hex.empty()) + entry["hex"] = filament.hex; + root["filaments"].push_back(std::move(entry)); + } + return root.dump(); +} + +static void configure_texture_mapping_zone_for_raw_atlas(unsigned int texture_mapping_filament_id, + const RawAtlasProjectionLayout &layout) +{ + if (texture_mapping_filament_id == 0 || wxGetApp().preset_bundle == nullptr || layout.filaments.empty()) + return; + + TextureMappingZone *zone = wxGetApp().preset_bundle->texture_mapping_zones.zone_from_id(texture_mapping_filament_id); + if (zone == nullptr) + return; + + const unsigned int max_physical = unsigned(std::min(size_t(std::max(wxGetApp().filaments_cnt(), 0)), 9)); + std::vector ids; + ids.reserve(layout.filaments.size()); + auto add_id = [&ids, max_physical, &layout](unsigned int id) { + if (id == 0 || + id > max_physical || + ids.size() >= layout.filaments.size() || + std::find(ids.begin(), ids.end(), id) != ids.end()) + return; + ids.emplace_back(id); + }; + for (const ImageMapRawFilament &filament : layout.filaments) + add_id(filament.slot); + for (unsigned int id = 1; id <= max_physical && ids.size() < layout.filaments.size(); ++id) + add_id(id); + + std::string encoded; + encoded.reserve(ids.size()); + for (const unsigned int id : ids) + encoded.push_back(char('0' + id)); + + zone->texture_mapping_mode = int(TextureMappingZone::TextureMappingRawValues); + zone->preview_simulate_colors = true; + zone->auto_adjust_filament_selection = false; + if (!encoded.empty()) { + zone->component_ids = std::move(encoded); + zone->component_a = ids.front(); + zone->component_b = ids.size() > 1 ? ids[1] : ids.front(); + } + persist_texture_mapping_zone_updates(); } static bool model_volume_has_imported_image_texture_data(const ModelVolume *volume) @@ -594,12 +792,317 @@ static ColorRGBA rgb_metadata_background_color(const ColorFacetsAnnotation &anno return unpack_vertex_color_rgba_for_conversion(packed); } +static bool set_texture_mapping_background_config(ModelConfigObject &config, const ColorRGBA &color) +{ + const uint32_t packed = pack_vertex_color_rgba(color); + char buffer[16]; + std::snprintf(buffer, + sizeof(buffer), + "#%02X%02X%02X%02X", + unsigned((packed >> 24) & 0xFFu), + unsigned((packed >> 16) & 0xFFu), + unsigned((packed >> 8) & 0xFFu), + unsigned(packed & 0xFFu)); + const std::string value(buffer); + if (const ConfigOptionString *opt = dynamic_cast(config.option("texture_mapping_background_color")); + opt != nullptr && opt->value == value) + return false; + config.set("texture_mapping_background_color", value); + return true; +} + +static bool set_managed_color_data_background_color(ModelObject &object, const ColorRGBA &color) +{ + ColorRGBA background = color; + background.a(1.f); + bool changed = set_texture_mapping_background_config(object.config, background); + const std::string metadata = rgb_metadata_json(background); + for (ModelVolume *volume : object.volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + changed |= set_texture_mapping_background_config(volume->config, background); + if (!volume->texture_mapping_color_facets.empty() && volume->texture_mapping_color_facets.metadata_json() != metadata) { + volume->texture_mapping_color_facets.set_metadata_json(metadata); + changed = true; + } + } + return changed; +} + +static wxColour wx_colour_from_color_rgba(const ColorRGBA &color) +{ + auto to_u8 = [](float value) { + return static_cast(std::clamp(value, 0.f, 1.f) * 255.f + 0.5f); + }; + return wxColour(to_u8(color.r()), to_u8(color.g()), to_u8(color.b())); +} + +static ColorRGBA color_rgba_from_wx_colour(const wxColour &color) +{ + return ColorRGBA(float(color.Red()) / 255.f, + float(color.Green()) / 255.f, + float(color.Blue()) / 255.f, + 1.f); +} + +static ColorRGBA managed_color_data_background_color(const ModelObject *object) +{ + if (object == nullptr) + return ColorRGBA(1.f, 1.f, 1.f, 1.f); + + auto read_config_color = [](const ModelConfigObject &config) -> std::optional { + if (!config.has("texture_mapping_background_color")) + return std::nullopt; + const ConfigOptionString *opt = dynamic_cast(config.option("texture_mapping_background_color")); + if (opt == nullptr) + return std::nullopt; + const std::string &text = opt->value; + const size_t hash_pos = text.find('#'); + const size_t start = hash_pos == std::string::npos ? 0 : hash_pos + 1; + if (start + 6 > text.size()) + return std::nullopt; + unsigned int values[3] = { 255, 255, 255 }; + for (size_t channel = 0; channel < 3; ++channel) { + int value = 0; + for (size_t digit = 0; digit < 2; ++digit) { + const char ch = text[start + channel * 2 + digit]; + const int hex = ch >= '0' && ch <= '9' ? ch - '0' : + ch >= 'a' && ch <= 'f' ? ch - 'a' + 10 : + ch >= 'A' && ch <= 'F' ? ch - 'A' + 10 : -1; + if (hex < 0) + return std::nullopt; + value = (value << 4) | hex; + } + values[channel] = unsigned(value); + } + return ColorRGBA(float(values[0]) / 255.f, float(values[1]) / 255.f, float(values[2]) / 255.f, 1.f); + }; + + if (std::optional color = read_config_color(object->config)) + return *color; + for (const ModelVolume *volume : object->volumes) + if (volume != nullptr && volume->is_model_part() && !volume->texture_mapping_color_facets.empty()) + return rgb_metadata_background_color(volume->texture_mapping_color_facets); + return ColorRGBA(1.f, 1.f, 1.f, 1.f); +} + static void refresh_imported_texture_storage(ModelVolume &volume) { std::vector refreshed(volume.imported_texture_rgba.begin(), volume.imported_texture_rgba.end()); volume.imported_texture_rgba.swap(refreshed); } +static void refresh_imported_texture_raw_storage(ModelVolume &volume) +{ + std::vector refreshed(volume.imported_texture_raw_filament_offsets.begin(), + volume.imported_texture_raw_filament_offsets.end()); + volume.imported_texture_raw_filament_offsets.swap(refreshed); +} + +static void clear_imported_texture_raw_atlas(ModelVolume &volume) +{ + volume.imported_texture_raw_filament_offsets.clear(); + volume.imported_texture_raw_channels = 0; + volume.imported_texture_raw_metadata_json.clear(); +} + +static ColorRGBA raw_filament_color_for_projection_preview(const ImageMapRawFilament &filament) +{ + const std::string key = image_map_raw_filament_channel_key(filament, 0); + if (key == "C") + return ColorRGBA(0.f, 0.75f, 0.75f, 1.f); + if (key == "M") + return ColorRGBA(0.9f, 0.f, 0.75f, 1.f); + if (key == "Y") + return ColorRGBA(0.95f, 0.85f, 0.f, 1.f); + if (key == "K") + return ColorRGBA(0.05f, 0.05f, 0.05f, 1.f); + if (key == "W") + return ColorRGBA(1.f, 1.f, 1.f, 1.f); + if (key == "R") + return ColorRGBA(1.f, 0.f, 0.f, 1.f); + if (key == "G") + return ColorRGBA(0.f, 0.75f, 0.f, 1.f); + if (key == "B") + return ColorRGBA(0.f, 0.25f, 1.f, 1.f); + + unsigned char rgba[4] = { 255, 255, 255, 255 }; + if (!filament.hex.empty()) + GUI::BitmapCache::parse_color4(filament.hex, rgba); + return ColorRGBA(float(rgba[0]) / 255.f, float(rgba[1]) / 255.f, float(rgba[2]) / 255.f, 1.f); +} + +static ColorRGBA simulated_preview_color_from_raw_offsets(const std::vector &filament_colors, + const uint8_t *values, + size_t value_count, + uint8_t alpha) +{ + if (values == nullptr || value_count == 0 || filament_colors.empty()) + return ColorRGBA(0.f, 0.f, 0.f, float(alpha) / 255.f); + + bool has_base = false; + float out_r = 0.f; + float out_g = 0.f; + float out_b = 0.f; + float accumulated = 0.f; + for (size_t idx = 0; idx < filament_colors.size() && idx < value_count; ++idx) { + const float weight = std::clamp(float(values[idx]) / 255.f, 0.f, 1.f); + if (weight <= EPSILON) + continue; + + if (!has_base) { + out_r = filament_colors[idx].r(); + out_g = filament_colors[idx].g(); + out_b = filament_colors[idx].b(); + accumulated = weight; + has_base = true; + continue; + } + + const float t = weight / std::max(float(EPSILON), accumulated + weight); + float mixed_r = out_r; + float mixed_g = out_g; + float mixed_b = out_b; + Slic3r::filament_mixer_lerp_float(out_r, + out_g, + out_b, + filament_colors[idx].r(), + filament_colors[idx].g(), + filament_colors[idx].b(), + t, + &mixed_r, + &mixed_g, + &mixed_b); + out_r = std::clamp(mixed_r, 0.f, 1.f); + out_g = std::clamp(mixed_g, 0.f, 1.f); + out_b = std::clamp(mixed_b, 0.f, 1.f); + accumulated += weight; + } + + if (!has_base) + return ColorRGBA(0.f, 0.f, 0.f, float(alpha) / 255.f); + return ColorRGBA(out_r, out_g, out_b, float(alpha) / 255.f); +} + +static ColorRGBA preview_color_from_raw_offsets(const std::vector &values, uint8_t alpha) +{ + if (values.empty()) + return ColorRGBA(0.f, 0.f, 0.f, float(alpha) / 255.f); + if (values.size() == 1) { + const float gray = float(values.front()) / 255.f; + return ColorRGBA(gray, gray, gray, float(alpha) / 255.f); + } + return ColorRGBA(float(values[0]) / 255.f, + float(values.size() > 1 ? values[1] : 0) / 255.f, + float(values.size() > 2 ? values[2] : 0) / 255.f, + float(alpha) / 255.f); +} + +static std::vector raw_offset_pixel_values(const ModelVolume &volume, uint32_t x, uint32_t y) +{ + std::vector values(size_t(volume.imported_texture_raw_channels), 0); + if (volume.imported_texture_width == 0 || volume.imported_texture_raw_channels == 0) + return values; + const size_t idx = + (size_t(y) * size_t(volume.imported_texture_width) + size_t(x)) * + size_t(volume.imported_texture_raw_channels); + if (idx + values.size() > volume.imported_texture_raw_filament_offsets.size()) + return values; + std::copy(volume.imported_texture_raw_filament_offsets.begin() + idx, + volume.imported_texture_raw_filament_offsets.begin() + idx + values.size(), + values.begin()); + return values; +} + +static bool refresh_imported_texture_preview_from_raw_offsets(ModelVolume &volume) +{ + if (!model_volume_has_raw_atlas_texture_data(&volume)) + return false; + const size_t pixel_count = size_t(volume.imported_texture_width) * size_t(volume.imported_texture_height); + bool changed = false; + if (volume.imported_texture_rgba.size() != pixel_count * 4) { + volume.imported_texture_rgba.assign(pixel_count * 4, 255); + changed = true; + } + + std::vector values(size_t(volume.imported_texture_raw_channels), 0); + for (size_t pixel_idx = 0; pixel_idx < pixel_count; ++pixel_idx) { + const size_t raw_idx = pixel_idx * size_t(volume.imported_texture_raw_channels); + if (raw_idx + values.size() > volume.imported_texture_raw_filament_offsets.size()) + break; + std::copy(volume.imported_texture_raw_filament_offsets.begin() + raw_idx, + volume.imported_texture_raw_filament_offsets.begin() + raw_idx + values.size(), + values.begin()); + const ColorRGBA preview = preview_color_from_raw_offsets(values, 255); + const uint8_t r = uint8_t(std::clamp(preview.r(), 0.f, 1.f) * 255.f + 0.5f); + const uint8_t g = uint8_t(std::clamp(preview.g(), 0.f, 1.f) * 255.f + 0.5f); + const uint8_t b = uint8_t(std::clamp(preview.b(), 0.f, 1.f) * 255.f + 0.5f); + const size_t rgba_idx = pixel_idx * 4; + if (volume.imported_texture_rgba[rgba_idx + 0] != r || + volume.imported_texture_rgba[rgba_idx + 1] != g || + volume.imported_texture_rgba[rgba_idx + 2] != b || + volume.imported_texture_rgba[rgba_idx + 3] != 255) { + volume.imported_texture_rgba[rgba_idx + 0] = r; + volume.imported_texture_rgba[rgba_idx + 1] = g; + volume.imported_texture_rgba[rgba_idx + 2] = b; + volume.imported_texture_rgba[rgba_idx + 3] = 255; + changed = true; + } + } + return changed; +} + +static bool merge_imported_texture_raw_atlas(ModelVolume &volume, const RawAtlasProjectionLayout &layout) +{ + if (layout.filaments.empty() || volume.imported_texture_width == 0 || volume.imported_texture_height == 0) + return false; + const size_t expected_size = + size_t(volume.imported_texture_width) * + size_t(volume.imported_texture_height) * + layout.filaments.size(); + std::vector merged(expected_size, 0); + if (model_volume_has_raw_atlas_texture_data(&volume)) { + const std::vector old_filaments = + image_map_raw_filaments_from_metadata_json(volume.imported_texture_raw_metadata_json, volume.imported_texture_raw_channels); + const std::vector old_keys = image_map_raw_filament_channel_keys(old_filaments); + std::vector old_to_new(size_t(volume.imported_texture_raw_channels), size_t(-1)); + for (size_t old_channel = 0; old_channel < old_keys.size() && old_channel < old_to_new.size(); ++old_channel) { + const auto found = std::find(layout.channel_keys.begin(), layout.channel_keys.end(), old_keys[old_channel]); + if (found != layout.channel_keys.end()) + old_to_new[old_channel] = size_t(std::distance(layout.channel_keys.begin(), found)); + } + + const size_t pixel_count = size_t(volume.imported_texture_width) * size_t(volume.imported_texture_height); + for (size_t pixel_idx = 0; pixel_idx < pixel_count; ++pixel_idx) { + const size_t old_base = pixel_idx * size_t(volume.imported_texture_raw_channels); + const size_t new_base = pixel_idx * layout.filaments.size(); + for (size_t old_channel = 0; old_channel < old_to_new.size(); ++old_channel) { + const size_t new_channel = old_to_new[old_channel]; + if (new_channel != size_t(-1) && + old_base + old_channel < volume.imported_texture_raw_filament_offsets.size() && + new_base + new_channel < merged.size()) + merged[new_base + new_channel] = volume.imported_texture_raw_filament_offsets[old_base + old_channel]; + } + } + } + + const std::string metadata = + raw_layout_metadata_json(volume.imported_texture_width, volume.imported_texture_height, layout); + const uint32_t merged_channels = uint32_t(layout.filaments.size()); + const bool changed = + volume.imported_texture_raw_channels != merged_channels || + volume.imported_texture_raw_filament_offsets.size() != expected_size || + volume.imported_texture_raw_metadata_json != metadata || + !std::equal(volume.imported_texture_raw_filament_offsets.begin(), + volume.imported_texture_raw_filament_offsets.end(), + merged.begin(), + merged.end()); + volume.imported_texture_raw_channels = merged_channels; + volume.imported_texture_raw_metadata_json = metadata; + volume.imported_texture_raw_filament_offsets = std::move(merged); + return changed; +} + static std::optional sample_rgb_color_facets(const std::vector &facets, const std::unordered_map> &facets_by_source_triangle, int source_triangle, @@ -1364,6 +1867,38 @@ static ColorRGBA sample_rgba_bilinear_clamped(const std::vector &rgba, a); } +static std::vector sample_raw_offsets_bilinear_clamped(const ImageMapRawFilamentOffsetAtlas &atlas, float u, float v) +{ + std::vector values; + if (!atlas.valid()) + return values; + + values.assign(atlas.channels, 0); + u = std::clamp(u, 0.f, 1.f); + v = std::clamp(v, 0.f, 1.f); + const float x = u * float(atlas.width > 1 ? atlas.width - 1 : 0); + const float y = v * float(atlas.height > 1 ? atlas.height - 1 : 0); + const size_t x0 = std::min(size_t(std::floor(x)), size_t(atlas.width - 1)); + const size_t y0 = std::min(size_t(std::floor(y)), size_t(atlas.height - 1)); + const size_t x1 = std::min(x0 + 1, size_t(atlas.width - 1)); + const size_t y1 = std::min(y0 + 1, size_t(atlas.height - 1)); + const float tx = x - float(x0); + const float ty = y - float(y0); + + auto channel = [&atlas](size_t sx, size_t sy, size_t ch) { + return float(atlas.offsets[(sy * size_t(atlas.width) + sx) * size_t(atlas.channels) + ch]); + }; + for (size_t ch = 0; ch < values.size(); ++ch) { + const float c00 = channel(x0, y0, ch); + const float c10 = channel(x1, y0, ch); + const float c01 = channel(x0, y1, ch); + const float c11 = channel(x1, y1, ch); + const float value = (c00 + (c10 - c00) * tx) + ((c01 + (c11 - c01) * tx) - (c00 + (c10 - c00) * tx)) * ty; + values[ch] = uint8_t(std::clamp(int(std::lround(value)), 0, 255)); + } + return values; +} + static ColorRGBA blend_projection_color(const ColorRGBA &base, const ColorRGBA &overlay, float opacity) { const float alpha = std::clamp(overlay.a(), 0.f, 1.f) * std::clamp(opacity, 0.f, 1.f); @@ -1491,6 +2026,28 @@ static std::optional projected_image_color_at_point(const ProjectionC return sample_rgba_bilinear_clamped(*context.image_rgba, context.image_width, context.image_height, u, v); } +static std::vector projected_raw_offsets_at_point(const ProjectionContext &context, + const ImageMapRawFilamentOffsetAtlas &atlas, + const Transform3d &world_matrix, + const Vec3f &point) +{ + if (!atlas.valid() || context.overlay_width <= 0.f || context.overlay_height <= 0.f) + return {}; + + Vec2f screen = Vec2f::Zero(); + if (!project_point_to_screen(context, world_matrix * point.cast(), screen)) + return {}; + if (screen.x() < context.overlay_left || + screen.y() < context.overlay_top || + screen.x() > context.overlay_left + context.overlay_width || + screen.y() > context.overlay_top + context.overlay_height) + return {}; + + const float u = (screen.x() - context.overlay_left) / context.overlay_width; + const float v = (screen.y() - context.overlay_top) / context.overlay_height; + return sample_raw_offsets_bilinear_clamped(atlas, u, v); +} + static bool projection_triangle_intersects_overlay(const ProjectionContext &context, const Transform3d &world_matrix, const std::array &vertices) @@ -2170,6 +2727,7 @@ static bool initialize_generated_image_texture(ModelVolume &volum volume.imported_texture_width = GENERATED_IMAGE_TEXTURE_SIZE; volume.imported_texture_height = GENERATED_IMAGE_TEXTURE_SIZE; volume.imported_texture_rgba.assign(size_t(GENERATED_IMAGE_TEXTURE_SIZE) * size_t(GENERATED_IMAGE_TEXTURE_SIZE) * 4, 0); + clear_imported_texture_raw_atlas(volume); for (size_t idx = 0; idx < size_t(GENERATED_IMAGE_TEXTURE_SIZE) * size_t(GENERATED_IMAGE_TEXTURE_SIZE); ++idx) { volume.imported_texture_rgba[idx * 4 + 0] = r; volume.imported_texture_rgba[idx * 4 + 1] = g; @@ -2218,6 +2776,28 @@ static bool write_rgba_pixel(std::vector &rgba, uint32_t width, uint32_ return true; } +static bool write_raw_offset_pixel(std::vector &offsets, + uint32_t width, + uint32_t channels, + uint32_t x, + uint32_t y, + const std::vector &values) +{ + if (width == 0 || channels == 0 || values.empty()) + return false; + const size_t idx = (size_t(y) * size_t(width) + size_t(x)) * size_t(channels); + if (idx + size_t(channels) > offsets.size()) + return false; + + bool changed = false; + for (size_t channel = 0; channel < size_t(channels); ++channel) { + const uint8_t value = channel < values.size() ? values[channel] : 0; + changed = changed || offsets[idx + channel] != value; + offsets[idx + channel] = value; + } + return changed; +} + static ColorRGBA read_rgba_pixel(const std::vector &rgba, uint32_t width, uint32_t x, uint32_t y) { if (width == 0) @@ -3323,6 +3903,7 @@ static bool clear_object_managed_color_data(ModelObject &object, ManagedColorDat volume->imported_texture_uvs_per_face.clear(); volume->imported_texture_uv_valid.clear(); volume->imported_texture_rgba.clear(); + clear_imported_texture_raw_atlas(*volume); volume->imported_texture_width = 0; volume->imported_texture_height = 0; changed = true; @@ -4218,6 +4799,14 @@ public: , m_on_object_changed(std::move(on_object_changed)) { wxBoxSizer *main_sizer = new wxBoxSizer(wxVERTICAL); + wxBoxSizer *background_sizer = new wxBoxSizer(wxHORIZONTAL); + background_sizer->Add(new wxStaticText(this, wxID_ANY, _L("Background color")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + m_background_picker = new wxColourPickerCtrl(this, + wxID_ANY, + wx_colour_from_color_rgba(managed_color_data_background_color(m_object))); + background_sizer->Add(m_background_picker, 0, wxALIGN_CENTER_VERTICAL); + main_sizer->Add(background_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(16)); + wxFlexGridSizer *grid = new wxFlexGridSizer(5, 8, 14); grid->AddGrowableCol(2, 1); @@ -4248,6 +4837,9 @@ public: CenterOnParent(); Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { EndModal(wxID_CLOSE); }, wxID_CLOSE); + m_background_picker->Bind(wxEVT_COLOURPICKER_CHANGED, [this](wxColourPickerEvent &event) { + set_background_color(event.GetColour()); + }); } private: @@ -4370,6 +4962,22 @@ private: refresh_rows(); } + void set_background_color(const wxColour &color) + { + if (m_object == nullptr || !color.IsOk()) + return; + + const ColorRGBA background = color_rgba_from_wx_colour(color); + if (managed_color_data_background_color(m_object) == background) + return; + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Set color data background", UndoRedo::SnapshotType::GizmoAction); + if (!set_managed_color_data_background_color(*m_object, background)) + return; + + refresh_object_after_change(); + } + void notify_object_changed() { if (m_on_object_changed) @@ -4418,6 +5026,7 @@ private: GLCanvas3D &m_canvas; ModelObject *m_object = nullptr; std::function m_on_object_changed; + wxColourPickerCtrl *m_background_picker = nullptr; std::vector m_rows; }; @@ -5542,6 +6151,7 @@ void GLGizmoMmuSegmentation::bake_selected_object_image_texture_to_vertex_colors volume->imported_texture_uvs_per_face.clear(); volume->imported_texture_uv_valid.clear(); volume->imported_texture_rgba.clear(); + clear_imported_texture_raw_atlas(*volume); volume->imported_texture_width = 0; volume->imported_texture_height = 0; baked = true; @@ -5914,6 +6524,7 @@ void GLGizmoMmuSegmentation::clear_selected_object_image_texture_data() volume->imported_texture_uvs_per_face.clear(); volume->imported_texture_uv_valid.clear(); volume->imported_texture_rgba.clear(); + clear_imported_texture_raw_atlas(*volume); volume->imported_texture_width = 0; volume->imported_texture_height = 0; cleared = true; @@ -7295,6 +7906,7 @@ CommonGizmosDataID GLGizmoImageProjection::on_get_requirements() const bool GLGizmoImageProjection::load_projection_image() { m_image_error.clear(); + m_raw_atlas = {}; wxFileDialog dialog(wxGetApp().mainframe, _L("Load projection image"), "", @@ -7313,6 +7925,42 @@ bool GLGizmoImageProjection::load_projection_image() return false; } + ImageMapRawFilamentOffsetAtlas raw_atlas; + std::string raw_atlas_error; + const bool loaded_raw_atlas = + decode_image_map_raw_filament_offset_atlas(rgba, width, height, raw_atlas, &raw_atlas_error); + if (loaded_raw_atlas) { + ModelObject *object = selected_model_object(); + RawAtlasProjectionLayout raw_layout; + if (object == nullptr || !raw_atlas_projection_layout_for_object(*object, raw_atlas, raw_layout, &raw_atlas_error)) { + m_image_path.clear(); + m_image_rgba.clear(); + m_image_width = 0; + m_image_height = 0; + m_raw_atlas = {}; + m_overlay_texture.reset(); + m_overlay_texture_dirty = false; + m_show_overlay = false; + m_image_error = raw_atlas_error.empty() ? + _u8L("The selected raw filament offset atlas is not compatible with the selected object.") : + raw_atlas_error; + m_parent.set_as_dirty(); + return false; + } + + std::vector preview = image_map_raw_filament_offset_preview_rgba(raw_atlas); + if (preview.empty()) { + m_image_error = _u8L("Unable to preview the selected raw filament offset atlas."); + return false; + } + rgba = std::move(preview); + width = raw_atlas.width; + height = raw_atlas.height; + m_raw_atlas = std::move(raw_atlas); + if (!projection_mode_allowed(m_projection_mode)) + m_projection_mode_initialized = false; + } + m_image_path = into_u8(dialog.GetPath()); m_image_rgba = std::move(rgba); m_image_width = width; @@ -7330,6 +7978,7 @@ void GLGizmoImageProjection::clear_projection_image() m_image_rgba.clear(); m_image_width = 0; m_image_height = 0; + m_raw_atlas = {}; m_overlay_texture.reset(); m_overlay_texture_dirty = false; m_show_overlay = false; @@ -7426,6 +8075,8 @@ void GLGizmoImageProjection::update_default_projection_mode() GLGizmoImageProjection::ProjectionMode GLGizmoImageProjection::default_projection_mode() const { + if (m_raw_atlas.valid()) + return ProjectionMode::ImageTexture; if (selected_object_has_rgb_data()) return ProjectionMode::RGBData; return ProjectionMode::ImageTexture; @@ -7433,6 +8084,8 @@ GLGizmoImageProjection::ProjectionMode GLGizmoImageProjection::default_projectio bool GLGizmoImageProjection::projection_mode_allowed(ProjectionMode mode) const { + if (m_raw_atlas.valid()) + return mode == ProjectionMode::ImageTexture; if (mode == ProjectionMode::ImageTexture) return true; if (selected_object_has_rgb_data()) @@ -7475,6 +8128,17 @@ bool GLGizmoImageProjection::selected_object_has_rgb_data() const return false; } +bool GLGizmoImageProjection::selected_object_has_raw_atlas_texture_data() const +{ + const ModelObject *object = selected_model_object(); + if (object == nullptr) + return false; + for (const ModelVolume *volume : object->volumes) + if (volume != nullptr && volume->is_model_part() && model_volume_has_raw_atlas_texture_data(volume)) + return true; + return false; +} + void GLGizmoImageProjection::on_render_input_window(float x, float y, float bottom_limit) { update_default_projection_mode(); @@ -7540,10 +8204,10 @@ void GLGizmoImageProjection::on_render_input_window(float x, float y, float bott if (ImGui::BeginCombo("##projection_mode", mode_labels[mode])) { for (int idx = 0; idx < 3; ++idx) { const ProjectionMode candidate = ProjectionMode(idx); - if (!projection_mode_allowed(candidate)) - continue; + const bool allowed = projection_mode_allowed(candidate); const bool selected = m_projection_mode == candidate; - if (ImGui::Selectable(mode_labels[idx], selected)) { + const ImGuiSelectableFlags flags = allowed ? ImGuiSelectableFlags_None : ImGuiSelectableFlags_Disabled; + if (ImGui::Selectable(mode_labels[idx], selected, flags) && allowed) { mode = idx; m_projection_mode = candidate; } @@ -7586,6 +8250,26 @@ bool GLGizmoImageProjection::project_image_to_selected_object() if (object == nullptr || m_image_rgba.empty()) return false; + RawAtlasProjectionLayout raw_layout; + if (m_raw_atlas.valid()) { + std::string raw_atlas_error; + if (!raw_atlas_projection_layout_for_object(*object, m_raw_atlas, raw_layout, &raw_atlas_error)) { + m_image_error = raw_atlas_error.empty() ? + _u8L("The selected raw filament offset atlas is not compatible with the selected object.") : + raw_atlas_error; + m_image_path.clear(); + m_image_rgba.clear(); + m_image_width = 0; + m_image_height = 0; + m_raw_atlas = {}; + m_overlay_texture.reset(); + m_overlay_texture_dirty = false; + m_show_overlay = false; + m_parent.set_as_dirty(); + return false; + } + } + update_default_projection_mode(); if (!projection_mode_allowed(m_projection_mode)) return false; @@ -7607,8 +8291,21 @@ bool GLGizmoImageProjection::project_image_to_selected_object() if (!changed) return false; + unsigned int raw_texture_mapping_filament_id = 0; + if (m_raw_atlas.valid()) { + raw_texture_mapping_filament_id = ensure_texture_mapping_zone(true, true); + if (raw_texture_mapping_filament_id != 0) { + object->config.set("extruder", int(raw_texture_mapping_filament_id)); + for (ModelVolume *volume : object->volumes) + if (volume != nullptr && volume->is_model_part()) + volume->config.set("extruder", int(raw_texture_mapping_filament_id)); + configure_texture_mapping_zone_for_raw_atlas(raw_texture_mapping_filament_id, raw_layout); + } + } + if (!object_is_whole_image_texture_mapped_without_regions(*object)) { - const unsigned int texture_mapping_filament_id = ensure_texture_mapping_zone(); + const unsigned int texture_mapping_filament_id = + raw_texture_mapping_filament_id != 0 ? raw_texture_mapping_filament_id : ensure_texture_mapping_zone(); if (texture_mapping_filament_id != 0) { const Selection &selection = m_parent.get_selection(); const int instance_idx = selection.get_instance_idx(); @@ -7803,6 +8500,17 @@ bool GLGizmoImageProjection::project_to_image_texture(ModelObject *object) const ProjectionVisibility visibility = m_pass_through_model ? ProjectionVisibility() : build_projection_visibility(context, m_parent, object, instance_idx); + const bool raw_atlas_projection = m_raw_atlas.valid(); + RawAtlasProjectionLayout raw_layout; + if (raw_atlas_projection) { + std::string raw_atlas_error; + if (!raw_atlas_projection_layout_for_object(*object, m_raw_atlas, raw_layout, &raw_atlas_error)) { + m_image_error = raw_atlas_error.empty() ? + _u8L("The selected raw filament offset atlas is not compatible with the selected object.") : + raw_atlas_error; + return false; + } + } bool changed = false; for (size_t volume_idx = 0; volume_idx < object->volumes.size(); ++volume_idx) { @@ -7823,10 +8531,17 @@ bool GLGizmoImageProjection::project_to_image_texture(ModelObject *object) continue; } + bool volume_changed = generated_texture; + if (raw_atlas_projection) { + volume_changed |= merge_imported_texture_raw_atlas(*volume, raw_layout); + volume_changed |= refresh_imported_texture_preview_from_raw_offsets(*volume); + } else if (model_volume_has_raw_atlas_texture_data(volume)) { + clear_imported_texture_raw_atlas(*volume); + volume_changed = true; + } const VolumeColorSource source = build_volume_color_source(*volume); const bool rewrite_texture_base = generated_texture || !volume->texture_mapping_color_facets.empty(); const std::vector source_texture_rgba(volume->imported_texture_rgba.begin(), volume->imported_texture_rgba.end()); - bool volume_changed = generated_texture; for (size_t tri_idx = 0; tri_idx < its.indices.size(); ++tri_idx) { const stl_triangle_vertex_indices &tri = its.indices[tri_idx]; @@ -7930,7 +8645,39 @@ bool GLGizmoImageProjection::project_to_image_texture(ModelObject *object) continue; } } else { - color = apply_projection_color(color, *projected, context, true); + if (raw_atlas_projection) { + std::vector atlas_raw_values = + projected_raw_offsets_at_point(context, m_raw_atlas, world_matrix, point); + if (atlas_raw_values.empty()) + continue; + const uint32_t wrapped_x = wrapped_texture_pixel(x_px, volume->imported_texture_width); + const uint32_t wrapped_y = wrapped_texture_pixel(y_px, volume->imported_texture_height); + std::vector raw_values = raw_offset_pixel_values(*volume, wrapped_x, wrapped_y); + if (raw_values.size() != size_t(volume->imported_texture_raw_channels)) + raw_values.assign(size_t(volume->imported_texture_raw_channels), 0); + const float alpha = projection_overlay_alpha(*projected, context); + for (size_t atlas_channel = 0; + atlas_channel < atlas_raw_values.size() && + atlas_channel < raw_layout.atlas_to_target_channel.size(); + ++atlas_channel) { + const size_t target_channel = raw_layout.atlas_to_target_channel[atlas_channel]; + if (target_channel >= raw_values.size()) + continue; + const float base = float(raw_values[target_channel]); + const float projected_value = float(atlas_raw_values[atlas_channel]); + raw_values[target_channel] = uint8_t(std::clamp( + int(std::lround(base * (1.f - alpha) + projected_value * alpha)), 0, 255)); + } + volume_changed |= write_raw_offset_pixel(volume->imported_texture_raw_filament_offsets, + volume->imported_texture_width, + volume->imported_texture_raw_channels, + wrapped_x, + wrapped_y, + raw_values); + color = preview_color_from_raw_offsets(raw_values, 255); + } else { + color = apply_projection_color(color, *projected, context, true); + } } } else if (!rewrite_texture_base) { continue; @@ -7947,6 +8694,8 @@ bool GLGizmoImageProjection::project_to_image_texture(ModelObject *object) } if (volume_changed) { refresh_imported_texture_storage(*volume); + if (raw_atlas_projection) + refresh_imported_texture_raw_storage(*volume); changed = true; } } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp index e758e2cf787..3a8c73f29a5 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp @@ -3,6 +3,8 @@ #include "GLGizmoPainterBase.hpp" +#include "libslic3r/ImageMapRawFilamentOffsetAtlas.hpp" + #include #include #include @@ -336,6 +338,7 @@ private: bool selected_object_has_image_texture_data() const; bool selected_object_has_vertex_color_data() const; bool selected_object_has_rgb_data() const; + bool selected_object_has_raw_atlas_texture_data() const; bool project_image_to_selected_object(); bool project_to_vertex_colors(ModelObject *object); bool project_to_image_texture(ModelObject *object); @@ -350,6 +353,7 @@ private: std::vector m_image_rgba; uint32_t m_image_width = 0; uint32_t m_image_height = 0; + ImageMapRawFilamentOffsetAtlas m_raw_atlas; GLTexture m_overlay_texture; bool m_overlay_texture_dirty = false; bool m_show_overlay = true; diff --git a/src/slic3r/GUI/MMUPaintedTexturePreview.cpp b/src/slic3r/GUI/MMUPaintedTexturePreview.cpp index f5009f907d5..6883b8f7f72 100644 --- a/src/slic3r/GUI/MMUPaintedTexturePreview.cpp +++ b/src/slic3r/GUI/MMUPaintedTexturePreview.cpp @@ -9,6 +9,7 @@ #include "libslic3r/Config.hpp" #include "libslic3r/Geometry.hpp" +#include "libslic3r/ImageMapRawFilamentOffsetAtlas.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/TextureMapping.hpp" #include "libslic3r/filament_mixer.h" @@ -36,6 +37,7 @@ constexpr float k_polygon_offset_units = -1.f; constexpr float k_epsilon = 1e-6f; constexpr unsigned int k_simulated_texture_preview_max_edge = 1024; constexpr size_t k_simulated_texture_preview_max_pixels = 1024ull * 1024ull; +constexpr const char *TEXTURE_MAPPING_BACKGROUND_COLOR_CONFIG_KEY = "texture_mapping_background_color"; struct TexturePreviewMixCandidate { @@ -322,10 +324,119 @@ unsigned char to_u8(float value) return static_cast(clamp01(value) * 255.f + 0.5f); } -void make_texture_preview_rgba_opaque(std::vector &rgba) +int texture_mapping_color_hex_digit_for_preview(char ch) { - for (size_t idx = 3; idx < rgba.size(); idx += 4) - rgba[idx] = 255; + return ch >= '0' && ch <= '9' ? ch - '0' : + ch >= 'a' && ch <= 'f' ? ch - 'a' + 10 : + ch >= 'A' && ch <= 'F' ? ch - 'A' + 10 : -1; +} + +std::optional parse_texture_mapping_color_hex_for_preview(const std::string &text) +{ + if (text.empty()) + return std::nullopt; + + const size_t hash_pos = text.find('#'); + const size_t start = hash_pos == std::string::npos ? 0 : hash_pos + 1; + if (start + 6 > text.size()) + return std::nullopt; + + uint32_t packed = 0; + for (size_t idx = 0; idx < 6; ++idx) { + const int value = texture_mapping_color_hex_digit_for_preview(text[start + idx]); + if (value < 0) + return std::nullopt; + packed = (packed << 4) | uint32_t(value); + } + + uint32_t alpha = 255; + if (start + 8 <= text.size()) { + alpha = 0; + for (size_t idx = 6; idx < 8; ++idx) { + const int value = texture_mapping_color_hex_digit_for_preview(text[start + idx]); + if (value < 0) + return std::nullopt; + alpha = (alpha << 4) | uint32_t(value); + } + } + + return ColorRGBA(float((packed >> 16) & 0xFFu) / 255.f, + float((packed >> 8) & 0xFFu) / 255.f, + float(packed & 0xFFu) / 255.f, + float(alpha & 0xFFu) / 255.f); +} + +ColorRGBA opaque_texture_mapping_background_color_for_preview(ColorRGBA color) +{ + color.a(1.f); + return color; +} + +std::optional texture_mapping_background_color_from_config_for_preview(const ModelConfigObject &config) +{ + if (!config.has(TEXTURE_MAPPING_BACKGROUND_COLOR_CONFIG_KEY)) + return std::nullopt; + + const ConfigOptionString *opt = dynamic_cast(config.option(TEXTURE_MAPPING_BACKGROUND_COLOR_CONFIG_KEY)); + if (opt == nullptr) + return std::nullopt; + + const std::optional color = parse_texture_mapping_color_hex_for_preview(opt->value); + return color ? std::optional(opaque_texture_mapping_background_color_for_preview(*color)) : std::nullopt; +} + +std::optional texture_mapping_background_color_from_metadata_for_preview(const ColorFacetsAnnotation &annotation) +{ + const std::string &metadata = annotation.metadata_json(); + const std::string key = "\"background_color\":\"#"; + const size_t start = metadata.find(key); + if (start == std::string::npos || start + key.size() + 8 > metadata.size()) + return std::nullopt; + + const std::optional color = parse_texture_mapping_color_hex_for_preview(metadata.substr(start + key.size() - 1, 9)); + return color ? std::optional(opaque_texture_mapping_background_color_for_preview(*color)) : std::nullopt; +} + +ColorRGBA texture_mapping_background_color_for_preview(const ModelVolume &model_volume, + const ColorFacetsAnnotation *color_source = nullptr) +{ + if (std::optional color = texture_mapping_background_color_from_config_for_preview(model_volume.config)) + return *color; + if (model_volume.get_object() != nullptr) { + if (std::optional color = texture_mapping_background_color_from_config_for_preview(model_volume.get_object()->config)) + return *color; + } + if (color_source != nullptr) { + if (std::optional color = texture_mapping_background_color_from_metadata_for_preview(*color_source)) + return *color; + } + if (std::optional color = texture_mapping_background_color_from_metadata_for_preview(model_volume.texture_mapping_color_facets)) + return *color; + return ColorRGBA(1.f, 1.f, 1.f, 1.f); +} + +ColorRGBA composite_texture_mapping_color_over_background_for_preview(const ColorRGBA &color, const ColorRGBA &background) +{ + const float alpha = clamp01(color.a()); + return ColorRGBA(clamp01(color.r() * alpha + background.r() * (1.f - alpha)), + clamp01(color.g() * alpha + background.g() * (1.f - alpha)), + clamp01(color.b() * alpha + background.b() * (1.f - alpha)), + 1.f); +} + +void composite_texture_preview_rgba_over_background(std::vector &rgba, const ColorRGBA &background) +{ + for (size_t idx = 0; idx + 3 < rgba.size(); idx += 4) { + const ColorRGBA color(float(rgba[idx + 0]) / 255.f, + float(rgba[idx + 1]) / 255.f, + float(rgba[idx + 2]) / 255.f, + float(rgba[idx + 3]) / 255.f); + const ColorRGBA blended = composite_texture_mapping_color_over_background_for_preview(color, background); + rgba[idx + 0] = to_u8(blended.r()); + rgba[idx + 1] = to_u8(blended.g()); + rgba[idx + 2] = to_u8(blended.b()); + rgba[idx + 3] = 255; + } } void configure_texture_preview_sampler(const GUI::GLTexture &texture) @@ -374,13 +485,13 @@ std::array limited_simulated_texture_preview_size(unsigned int return { limited_width, limited_height }; } -std::array sample_texture_preview_rgb_bilinear(const std::vector &rgba, - unsigned int width, - unsigned int height, - unsigned int preview_x, - unsigned int preview_y, - unsigned int preview_width, - unsigned int preview_height) +std::array sample_texture_preview_rgba_bilinear(const std::vector &rgba, + unsigned int width, + unsigned int height, + unsigned int preview_x, + unsigned int preview_y, + unsigned int preview_width, + unsigned int preview_height) { const double src_x = std::clamp((double(preview_x) + 0.5) * double(width) / double(std::max(1u, preview_width)) - 0.5, 0.0, @@ -404,7 +515,150 @@ std::array sample_texture_preview_rgb_bilinear(const std::vect return static_cast(std::clamp(int(std::lround(top * (1.0 - ty) + bottom * ty)), 0, 255)); }; - return { sample_channel(0), sample_channel(1), sample_channel(2) }; + return { sample_channel(0), sample_channel(1), sample_channel(2), sample_channel(3) }; +} + +std::vector sample_texture_preview_raw_offsets_bilinear(const std::vector &offsets, + unsigned int width, + unsigned int height, + unsigned int channels, + unsigned int preview_x, + unsigned int preview_y, + unsigned int preview_width, + unsigned int preview_height) +{ + std::vector values(channels, 0.f); + if (width == 0 || height == 0 || channels == 0 || + offsets.size() < size_t(width) * size_t(height) * size_t(channels)) + return values; + + const double src_x = std::clamp((double(preview_x) + 0.5) * double(width) / double(std::max(1u, preview_width)) - 0.5, + 0.0, + double(width - 1)); + const double src_y = std::clamp((double(preview_y) + 0.5) * double(height) / double(std::max(1u, preview_height)) - 0.5, + 0.0, + double(height - 1)); + const unsigned int x0 = std::min(width - 1, unsigned(std::floor(src_x))); + const unsigned int y0 = std::min(height - 1, unsigned(std::floor(src_y))); + const unsigned int x1 = std::min(width - 1, x0 + 1); + const unsigned int y1 = std::min(height - 1, y0 + 1); + const double tx = src_x - double(x0); + const double ty = src_y - double(y0); + + auto channel_at = [&offsets, width, channels](unsigned int x, unsigned int y, unsigned int channel) { + return double(offsets[(size_t(y) * size_t(width) + size_t(x)) * size_t(channels) + size_t(channel)]) / 255.0; + }; + for (unsigned int channel = 0; channel < channels; ++channel) { + const double top = channel_at(x0, y0, channel) * (1.0 - tx) + channel_at(x1, y0, channel) * tx; + const double bottom = channel_at(x0, y1, channel) * (1.0 - tx) + channel_at(x1, y1, channel) * tx; + values[size_t(channel)] = clamp01(float(top * (1.0 - ty) + bottom * ty)); + } + return values; +} + +std::vector raw_filament_color_mode_channel_keys_for_texture_preview(int filament_color_mode, size_t component_count) +{ + std::vector keys; + switch (std::clamp(filament_color_mode, + int(TextureMappingZone::FilamentColorAny), + int(TextureMappingZone::FilamentColorBW))) { + case int(TextureMappingZone::FilamentColorRGB): + keys = { "R", "G", "B" }; + break; + case int(TextureMappingZone::FilamentColorCMY): + keys = { "C", "M", "Y" }; + break; + case int(TextureMappingZone::FilamentColorCMYK): + keys = { "C", "M", "Y", "K" }; + break; + case int(TextureMappingZone::FilamentColorCMYW): + keys = { "C", "M", "Y", "W" }; + break; + case int(TextureMappingZone::FilamentColorRGBK): + keys = { "R", "G", "B", "K" }; + break; + case int(TextureMappingZone::FilamentColorRGBW): + keys = { "R", "G", "B", "W" }; + break; + case int(TextureMappingZone::FilamentColorBW): + keys = { "K", "W" }; + break; + default: + break; + } + if (keys.size() > component_count) + keys.resize(component_count); + return keys; +} + +std::vector raw_component_source_channels_for_texture_preview(const std::string &metadata_json, + unsigned int source_channels, + int filament_color_mode, + size_t component_count) +{ + if (source_channels == 0 || component_count == 0) + return {}; + + const size_t sentinel = std::numeric_limits::max(); + std::vector mapping(component_count, sentinel); + const std::vector filaments = + image_map_raw_filaments_from_metadata_json(metadata_json, source_channels); + if (filaments.size() != size_t(source_channels)) + return {}; + + std::vector source_keys(static_cast(source_channels)); + std::vector used(static_cast(source_channels), 0); + for (size_t channel = 0; channel < filaments.size(); ++channel) { + const std::string key = image_map_raw_filament_channel_key(filaments[channel], channel); + if (key.size() == 1 && image_map_raw_filament_is_standard_color(key)) + source_keys[channel] = key; + } + + const std::vector target_keys = + raw_filament_color_mode_channel_keys_for_texture_preview(filament_color_mode, component_count); + if (!target_keys.empty()) { + for (size_t component_idx = 0; component_idx < target_keys.size(); ++component_idx) { + for (size_t channel = 0; channel < source_keys.size(); ++channel) { + if (used[channel] == 0 && source_keys[channel] == target_keys[component_idx]) { + mapping[component_idx] = channel; + used[channel] = 1; + break; + } + } + } + } + + size_t next_source = 0; + for (size_t component_idx = 0; component_idx < mapping.size(); ++component_idx) { + if (mapping[component_idx] != sentinel) + continue; + while (next_source < source_keys.size() && + (used[next_source] != 0 || (!target_keys.empty() && !source_keys[next_source].empty()))) + ++next_source; + if (next_source >= source_keys.size()) + continue; + mapping[component_idx] = next_source; + used[next_source] = 1; + ++next_source; + } + + const bool has_mapping = std::any_of(mapping.begin(), mapping.end(), [sentinel](size_t value) { return value != sentinel; }); + return has_mapping ? mapping : std::vector{}; +} + +std::vector map_raw_sample_to_components_for_texture_preview(const std::vector &raw_sample, + const std::vector &component_source_channels) +{ + if (component_source_channels.empty()) + return {}; + const size_t sentinel = std::numeric_limits::max(); + std::vector mapped(component_source_channels.size(), 0.f); + for (size_t component_idx = 0; component_idx < component_source_channels.size(); ++component_idx) { + const size_t source_channel = component_source_channels[component_idx]; + if (source_channel != sentinel && source_channel < raw_sample.size()) + mapped[component_idx] = raw_sample[source_channel]; + } + return mapped; } unsigned int texture_preview_rgb_cache_key(const std::array &rgb, bool quantize) @@ -1302,6 +1556,10 @@ TexturePreviewSimulationResult build_simulated_texture_preview_result(size_t sig unsigned int width, unsigned int height, std::vector source_rgba, + std::vector source_raw_offsets, + unsigned int source_raw_channels, + std::vector source_raw_component_channels, + ColorRGBA background_color, TexturePreviewSimulationSettings settings) { TexturePreviewSimulationResult result; @@ -1320,6 +1578,10 @@ TexturePreviewSimulationResult build_simulated_texture_preview_result(size_t sig prepare_texture_preview_simulation_settings(settings); const bool use_generic_solver = !settings.generic_mix_candidates.empty(); + const bool use_raw_offsets = + settings.mapping_mode == int(TextureMappingZone::TextureMappingRawValues) && + source_raw_component_channels.size() == settings.component_colors.size() && + source_raw_offsets.size() >= size_t(width) * size_t(height) * size_t(source_raw_channels); std::unordered_map> simulated_color_cache; simulated_color_cache.reserve(std::min(size_t(result.width) * size_t(result.height), @@ -1327,13 +1589,26 @@ TexturePreviewSimulationResult build_simulated_texture_preview_result(size_t sig for (unsigned int y = 0; y < result.height; ++y) { for (unsigned int x = 0; x < result.width; ++x) { - const std::array source_rgb = - sample_texture_preview_rgb_bilinear(source_rgba, width, height, x, y, result.width, result.height); - const unsigned int cache_key = texture_preview_rgb_cache_key(source_rgb, use_generic_solver); + const std::array source_rgba_sample = + sample_texture_preview_rgba_bilinear(source_rgba, width, height, x, y, result.width, result.height); + const ColorRGBA blended_source_color = + composite_texture_mapping_color_over_background_for_preview(ColorRGBA(float(source_rgba_sample[0]) / 255.f, + float(source_rgba_sample[1]) / 255.f, + float(source_rgba_sample[2]) / 255.f, + float(source_rgba_sample[3]) / 255.f), + background_color); + const std::array source_rgb = { + to_u8(blended_source_color.r()), + to_u8(blended_source_color.g()), + to_u8(blended_source_color.b()) + }; + const unsigned int cache_key = use_raw_offsets ? + unsigned(std::numeric_limits::max()) : + texture_preview_rgb_cache_key(source_rgb, use_generic_solver); const size_t idx = (size_t(y) * size_t(result.width) + size_t(x)) * 4; - auto cached_color = simulated_color_cache.find(cache_key); - if (cached_color != simulated_color_cache.end()) { + auto cached_color = !use_raw_offsets ? simulated_color_cache.find(cache_key) : simulated_color_cache.end(); + if (!use_raw_offsets && cached_color != simulated_color_cache.end()) { result.rgba[idx + 0] = cached_color->second[0]; result.rgba[idx + 1] = cached_color->second[1]; result.rgba[idx + 2] = cached_color->second[2]; @@ -1342,12 +1617,26 @@ TexturePreviewSimulationResult build_simulated_texture_preview_result(size_t sig } const std::array sample_rgba = { - float(source_rgb[0]) / 255.f, - float(source_rgb[1]) / 255.f, - float(source_rgb[2]) / 255.f, + blended_source_color.r(), + blended_source_color.g(), + blended_source_color.b(), 1.f }; - const std::vector component_weights = component_weights_for_texture_preview(settings, sample_rgba); + std::vector component_weights; + if (use_raw_offsets) { + const std::vector raw_sample = + sample_texture_preview_raw_offsets_bilinear(source_raw_offsets, + width, + height, + source_raw_channels, + x, + y, + result.width, + result.height); + component_weights = map_raw_sample_to_components_for_texture_preview(raw_sample, source_raw_component_channels); + } else { + component_weights = component_weights_for_texture_preview(settings, sample_rgba); + } float activity = 0.f; for (const float weight : component_weights) activity = std::max(activity, clamp01(weight)); @@ -1362,7 +1651,8 @@ TexturePreviewSimulationResult build_simulated_texture_preview_result(size_t sig to_u8(simulated_rgb[2]), 255 }; - simulated_color_cache.emplace(cache_key, out_rgba); + if (!use_raw_offsets) + simulated_color_cache.emplace(cache_key, out_rgba); result.rgba[idx + 0] = out_rgba[0]; result.rgba[idx + 1] = out_rgba[1]; result.rgba[idx + 2] = out_rgba[2]; @@ -1506,6 +1796,7 @@ const GUI::GLTexture *simulated_texture_preview_texture_for_filament(const Model if (!settings.has_value()) return &fallback_texture; + const ColorRGBA background_color = texture_mapping_background_color_for_preview(model_volume); const size_t simulation_signature = texture_preview_simulation_signature(model_volume, source_texture_signature, *settings); auto &cache = texture_preview_simulation_cache(); const size_t cache_key = texture_preview_simulation_cache_key(model_volume, filament_id); @@ -1540,17 +1831,33 @@ const GUI::GLTexture *simulated_texture_preview_texture_for_filament(const Model const unsigned int width = model_volume.imported_texture_width; const unsigned int height = model_volume.imported_texture_height; std::vector source_rgba(model_volume.imported_texture_rgba.begin(), model_volume.imported_texture_rgba.end()); + std::vector source_raw_offsets(model_volume.imported_texture_raw_filament_offsets.begin(), + model_volume.imported_texture_raw_filament_offsets.end()); + const unsigned int source_raw_channels = model_volume.imported_texture_raw_channels; TexturePreviewSimulationSettings simulation_settings = *settings; + std::vector source_raw_component_channels = + raw_component_source_channels_for_texture_preview(model_volume.imported_texture_raw_metadata_json, + source_raw_channels, + simulation_settings.filament_color_mode, + simulation_settings.component_colors.size()); entry.pending_future = std::async(std::launch::async, [simulation_signature, width, height, source_rgba = std::move(source_rgba), + source_raw_offsets = std::move(source_raw_offsets), + source_raw_channels, + source_raw_component_channels = std::move(source_raw_component_channels), + background_color, simulation_settings = std::move(simulation_settings)]() mutable { return build_simulated_texture_preview_result(simulation_signature, width, height, std::move(source_rgba), + std::move(source_raw_offsets), + source_raw_channels, + std::move(source_raw_component_channels), + background_color, std::move(simulation_settings)); }); } @@ -1652,19 +1959,28 @@ bool build_vertex_color_preview_model_for_state(const ModelVolume std::unordered_map simulated_color_cache; if (simulation_settings != nullptr) simulated_color_cache.reserve(std::min(model_volume.imported_vertex_colors_rgba.size(), size_t(65536))); - auto source_vertex_color = [simulation_settings, &simulated_color_cache](uint32_t packed) { - const ColorRGBA source_color = unpack_vertex_color(packed); + const ColorRGBA background_color = texture_mapping_background_color_for_preview(model_volume); + auto preview_color = [simulation_settings, &simulated_color_cache, background_color](const ColorRGBA &source_color) { + const ColorRGBA blended_source = + composite_texture_mapping_color_over_background_for_preview(source_color, background_color); if (simulation_settings == nullptr) - return source_color; + return blended_source; - auto cached = simulated_color_cache.find(packed); + const uint32_t key = (uint32_t(std::clamp(blended_source.r(), 0.f, 1.f) * 255.f + 0.5f) << 24) | + (uint32_t(std::clamp(blended_source.g(), 0.f, 1.f) * 255.f + 0.5f) << 16) | + (uint32_t(std::clamp(blended_source.b(), 0.f, 1.f) * 255.f + 0.5f) << 8) | + uint32_t(std::clamp(blended_source.a(), 0.f, 1.f) * 255.f + 0.5f); + auto cached = simulated_color_cache.find(key); if (cached != simulated_color_cache.end()) return cached->second; - const ColorRGBA simulated_color = simulated_texture_preview_color_for_vertex_color(&source_color, simulation_settings); - simulated_color_cache.emplace(packed, simulated_color); + const ColorRGBA simulated_color = simulated_texture_preview_color_for_vertex_color(&blended_source, simulation_settings); + simulated_color_cache.emplace(key, simulated_color); return simulated_color; }; + auto source_vertex_color = [](uint32_t packed) { + return unpack_vertex_color(packed); + }; unsigned int vertex_index = 0; for (const TriangleSelector::FacetStateTriangle &triangle : state_triangles) { @@ -1710,7 +2026,7 @@ bool build_vertex_color_preview_model_for_state(const ModelVolume valid_leaf = false; break; } - leaf_colors[vertex_idx] = interpolate_color(source_colors, barycentric); + leaf_colors[vertex_idx] = preview_color(interpolate_color(source_colors, barycentric)); } if (!valid_leaf) continue; @@ -1842,19 +2158,22 @@ bool build_texture_mapping_color_preview_model_for_state( std::unordered_map simulated_color_cache; if (simulation_settings != nullptr) simulated_color_cache.reserve(std::min(color_facets.size(), size_t(65536))); - auto preview_color = [simulation_settings, &simulated_color_cache](const ColorRGBA &source_color) { + const ColorRGBA background_color = texture_mapping_background_color_for_preview(model_volume, color_source); + auto preview_color = [simulation_settings, &simulated_color_cache, background_color](const ColorRGBA &source_color) { + const ColorRGBA blended_source = + composite_texture_mapping_color_over_background_for_preview(source_color, background_color); if (simulation_settings == nullptr) - return source_color; + return blended_source; - const uint32_t key = (uint32_t(std::clamp(source_color.r(), 0.f, 1.f) * 255.f + 0.5f) << 24) | - (uint32_t(std::clamp(source_color.g(), 0.f, 1.f) * 255.f + 0.5f) << 16) | - (uint32_t(std::clamp(source_color.b(), 0.f, 1.f) * 255.f + 0.5f) << 8) | - uint32_t(std::clamp(source_color.a(), 0.f, 1.f) * 255.f + 0.5f); + const uint32_t key = (uint32_t(std::clamp(blended_source.r(), 0.f, 1.f) * 255.f + 0.5f) << 24) | + (uint32_t(std::clamp(blended_source.g(), 0.f, 1.f) * 255.f + 0.5f) << 16) | + (uint32_t(std::clamp(blended_source.b(), 0.f, 1.f) * 255.f + 0.5f) << 8) | + uint32_t(std::clamp(blended_source.a(), 0.f, 1.f) * 255.f + 0.5f); auto cached = simulated_color_cache.find(key); if (cached != simulated_color_cache.end()) return cached->second; - const ColorRGBA simulated_color = simulated_texture_preview_color_for_vertex_color(&source_color, simulation_settings); + const ColorRGBA simulated_color = simulated_texture_preview_color_for_vertex_color(&blended_source, simulation_settings); simulated_color_cache.emplace(key, simulated_color); return simulated_color; }; @@ -2621,10 +2940,21 @@ size_t model_volume_texture_preview_signature(const ModelVolume &model_volume) mix(size_t(model_volume.imported_texture_height)); mix(model_volume.imported_texture_rgba.size()); mix(reinterpret_cast(model_volume.imported_texture_rgba.data())); + mix(size_t(model_volume.imported_texture_raw_channels)); + mix(std::hash{}(model_volume.imported_texture_raw_metadata_json)); + mix(model_volume.imported_texture_raw_filament_offsets.size()); + mix(reinterpret_cast(model_volume.imported_texture_raw_filament_offsets.data())); mix(model_volume.imported_texture_uvs_per_face.size()); mix(reinterpret_cast(model_volume.imported_texture_uvs_per_face.data())); mix(model_volume.imported_texture_uv_valid.size()); mix(reinterpret_cast(model_volume.imported_texture_uv_valid.data())); + const ColorRGBA background = texture_mapping_background_color_for_preview(model_volume); + auto background_signature_component = [](float value) { + return size_t(std::clamp(value, 0.f, 1.f) * 255.f + 0.5f); + }; + mix(background_signature_component(background.r())); + mix(background_signature_component(background.g())); + mix(background_signature_component(background.b())); return signature; } @@ -2650,6 +2980,13 @@ size_t model_volume_texture_mapping_color_preview_signature(const ModelVolume &m mix(size_t(color)); for (const char ch : data.metadata_json) mix(size_t(static_cast(ch))); + const ColorRGBA background = texture_mapping_background_color_for_preview(model_volume); + auto background_signature_component = [](float value) { + return size_t(std::clamp(value, 0.f, 1.f) * 255.f + 0.5f); + }; + mix(background_signature_component(background.r())); + mix(background_signature_component(background.g())); + mix(background_signature_component(background.b())); return signature; } @@ -2666,7 +3003,7 @@ bool ensure_model_volume_texture_preview(const ModelVolume &model_volume, texture.reset(); std::vector texture_data(model_volume.imported_texture_rgba.begin(), model_volume.imported_texture_rgba.end()); - make_texture_preview_rgba_opaque(texture_data); + composite_texture_preview_rgba_over_background(texture_data, texture_mapping_background_color_for_preview(model_volume)); if (!texture.load_from_raw_data(std::move(texture_data), model_volume.imported_texture_width, model_volume.imported_texture_height)) { texture_signature = 0; return false; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index fa0b45115c0..6cdc59b68ec 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -639,11 +639,15 @@ static std::vector texture_mapping_channel_labels(int filament_color_m static wxString texture_mapping_summary(const TextureMappingZone &zone, size_t num_physical) { + const bool raw_offset = zone.is_image_texture() && zone.texture_mapping_mode == int(TextureMappingZone::TextureMappingRawValues); wxString summary = zone.is_2d_gradient() ? _L("2D Gradient") : from_u8(TextureMappingManager::filament_color_mode_name(zone.filament_color_mode)); if (!zone.is_2d_gradient() && summary == "any") - summary = _L("Texture"); - else + summary = raw_offset ? _L("Raw Offset Texture") : _L("Texture"); + else { summary.MakeUpper(); + if (raw_offset) + summary = _L("Raw Offset ") + summary; + } const std::vector ids = texture_mapping_selected_ids(zone, num_physical); summary += " ";