From fa68f7653e059da7e2abec351b7a938944a68603 Mon Sep 17 00:00:00 2001 From: sentientstardust Date: Mon, 18 May 2026 10:46:39 +0100 Subject: [PATCH] Show (non-exact) halftone preview in 3d object view --- src/libslic3r/GCode.cpp | 379 +++++---- src/libslic3r/TextureMapping.cpp | 15 +- src/libslic3r/TextureMapping.hpp | 5 +- src/slic3r/GUI/3DScene.cpp | 13 +- src/slic3r/GUI/MMUPaintedTexturePreview.cpp | 823 +++++++++++++++++++- src/slic3r/GUI/MMUPaintedTexturePreview.hpp | 3 + src/slic3r/GUI/Plater.cpp | 11 +- 7 files changed, 993 insertions(+), 256 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index cd57974eff0..85fc92bee00 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -43,6 +44,7 @@ #include #include #include +#include #include #include @@ -7070,6 +7072,15 @@ static float halftone_threshold_for_gcode(float x_mm, float y_mm, float dot_size return std::clamp(radius_area, 0.f, 1.f) - 0.5f; } +static bool is_halftone_dithering_method_for_gcode(int method) +{ + const int clamped_method = std::clamp(method, + int(TextureMappingZone::DitheringClosest), + int(TextureMappingZone::DitheringHalftoneIncreasedDetail)); + return clamped_method == int(TextureMappingZone::DitheringHalftone) || + clamped_method == int(TextureMappingZone::DitheringHalftoneIncreasedDetail); +} + static float halftone_screen_angle_deg_for_gcode(int filament_color_mode, size_t component_idx) { const int clamped_mode = @@ -7138,8 +7149,8 @@ static float dither_pitch_for_gcode(float base_outer_width_mm, const float high_res_step_mm = std::clamp(base_outer_width_mm * 0.20f, 0.04f, 0.12f); const int clamped_method = std::clamp(dithering_method, int(TextureMappingZone::DitheringClosest), - int(TextureMappingZone::DitheringHalftone)); - if (clamped_method == int(TextureMappingZone::DitheringHalftone)) { + int(TextureMappingZone::DitheringHalftoneIncreasedDetail)); + if (is_halftone_dithering_method_for_gcode(clamped_method)) { const float dot_sample_step_mm = std::clamp(std::clamp(halftone_dot_size_mm, TextureMappingZone::MinHalftoneDotSizeMm, @@ -8815,12 +8826,14 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode( return VertexColorOverhangWeightField{}; const int clamped_binary_dither_method = - std::clamp(dithering_method, int(TextureMappingZone::DitheringClosest), int(TextureMappingZone::DitheringHalftone)); + std::clamp(dithering_method, + int(TextureMappingZone::DitheringClosest), + int(TextureMappingZone::DitheringHalftoneIncreasedDetail)); std::vector binary_dither_masks; const bool can_binary_dither = dithering_enabled && !raw_values_mode && - clamped_binary_dither_method != int(TextureMappingZone::DitheringHalftone) && + !is_halftone_dithering_method_for_gcode(clamped_binary_dither_method) && component_count > 0 && std::isfinite(dither_cell_size_mm) && dither_cell_size_mm > EPSILON; @@ -9128,7 +9141,8 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode( static std::vector sample_vertex_color_weight_field_components_for_gcode(const VertexColorOverhangWeightField &weight_field, float x_mm, float y_mm, - bool high_resolution_texture_sampling) + bool high_resolution_texture_sampling, + float smoothing_radius_mm = 0.f) { std::vector fallback = weight_field.fallback_weights; if (fallback.size() < weight_field.component_count) @@ -9207,14 +9221,20 @@ static std::vector sample_vertex_color_weight_field_components_for_gcode( const float sigma_scale = high_resolution_texture_sampling ? 0.45f : 0.7f; const float min_sigma_mm = high_resolution_texture_sampling ? 0.04f : 0.06f; - const float sigma_x_mm = std::max(min_sigma_mm, weight_field.bucket_width_mm * sigma_scale); - const float sigma_y_mm = std::max(min_sigma_mm, weight_field.bucket_height_mm * sigma_scale); + const float safe_smoothing_radius_mm = + std::isfinite(smoothing_radius_mm) ? std::max(0.f, smoothing_radius_mm) : 0.f; + const float sigma_floor_mm = safe_smoothing_radius_mm > EPSILON ? + std::max(min_sigma_mm, safe_smoothing_radius_mm * 0.5f) : + min_sigma_mm; + const float sigma_x_mm = std::max(sigma_floor_mm, weight_field.bucket_width_mm * sigma_scale); + const float sigma_y_mm = std::max(sigma_floor_mm, weight_field.bucket_height_mm * sigma_scale); const float inv_two_sigma_x2 = 1.f / std::max(2.f * sigma_x_mm * sigma_x_mm, 1e-8f); const float inv_two_sigma_y2 = 1.f / std::max(2.f * sigma_y_mm * sigma_y_mm, 1e-8f); const float min_radius_mm = high_resolution_texture_sampling ? 0.16f : 0.30f; const float radius_scale = high_resolution_texture_sampling ? 1.75f : 3.f; - const float max_radius_mm = std::max(min_radius_mm, std::max(weight_field.bucket_width_mm, weight_field.bucket_height_mm) * radius_scale); + const float max_radius_mm = std::max(std::max(min_radius_mm, safe_smoothing_radius_mm), + std::max(weight_field.bucket_width_mm, weight_field.bucket_height_mm) * radius_scale); const float max_radius2 = max_radius_mm * max_radius_mm; const float min_bucket_span_mm = std::max(1e-3f, std::min(weight_field.bucket_width_mm, weight_field.bucket_height_mm)); const int max_ring = std::max(1, int(std::ceil(max_radius_mm / min_bucket_span_mm))); @@ -9373,14 +9393,18 @@ static float sample_vertex_color_weight_field_for_gcode(const VertexColorOverhan float y_mm, size_t component_idx, bool high_resolution_texture_sampling, - bool compact_offset_mode = false) + bool compact_offset_mode = false, + float smoothing_radius_mm = 0.f) { + const float fallback = component_idx < weight_field.fallback_weights.size() ? + weight_field.fallback_weights[component_idx] : 0.f; if (compact_offset_mode && !weight_field.raw_component_weights_from_texture && !weight_field.empty() && component_idx < weight_field.component_count) { std::vector values = sample_vertex_color_weight_field_components_for_gcode(weight_field, x_mm, y_mm, - high_resolution_texture_sampling); + high_resolution_texture_sampling, + smoothing_radius_mm); float max_value = 0.f; for (size_t idx = 0; idx < weight_field.component_count && idx < values.size(); ++idx) max_value = std::max(max_value, clamp01f_for_gcode(values[idx])); @@ -9388,170 +9412,12 @@ static float sample_vertex_color_weight_field_for_gcode(const VertexColorOverhan return clamp01f_for_gcode(values[component_idx] / max_value); } - const float fallback = component_idx < weight_field.fallback_weights.size() ? - weight_field.fallback_weights[component_idx] : 0.f; - if (weight_field.empty() || component_idx >= weight_field.component_count) - return fallback; - if (!std::isfinite(x_mm) || !std::isfinite(y_mm)) - return fallback; - - const float gx_unclamped = (x_mm - weight_field.min_x_mm) / std::max(weight_field.bucket_width_mm, 1e-6f); - const float gy_unclamped = (y_mm - weight_field.min_y_mm) / std::max(weight_field.bucket_height_mm, 1e-6f); - const int cx = std::clamp(int(std::floor(gx_unclamped)), 0, weight_field.bucket_width - 1); - const int cy = std::clamp(int(std::floor(gy_unclamped)), 0, weight_field.bucket_height - 1); - - const float sigma_scale = high_resolution_texture_sampling ? 0.45f : 0.7f; - const float min_sigma_mm = high_resolution_texture_sampling ? 0.04f : 0.06f; - const float sigma_x_mm = std::max(min_sigma_mm, weight_field.bucket_width_mm * sigma_scale); - const float sigma_y_mm = std::max(min_sigma_mm, weight_field.bucket_height_mm * sigma_scale); - const float inv_two_sigma_x2 = 1.f / std::max(2.f * sigma_x_mm * sigma_x_mm, 1e-8f); - const float inv_two_sigma_y2 = 1.f / std::max(2.f * sigma_y_mm * sigma_y_mm, 1e-8f); - - const float min_radius_mm = high_resolution_texture_sampling ? 0.16f : 0.30f; - const float radius_scale = high_resolution_texture_sampling ? 1.75f : 3.f; - const float max_radius_mm = std::max(min_radius_mm, std::max(weight_field.bucket_width_mm, weight_field.bucket_height_mm) * radius_scale); - const float max_radius2 = max_radius_mm * max_radius_mm; - const float min_bucket_span_mm = std::max(1e-3f, std::min(weight_field.bucket_width_mm, weight_field.bucket_height_mm)); - const int max_ring = std::max(1, int(std::ceil(max_radius_mm / min_bucket_span_mm))); - - float weighted_sum = 0.f; - float total_weight = 0.f; - size_t contributing_samples = 0; - - auto process_bucket = [&weight_field, - component_idx, - x_mm, - y_mm, - max_radius2, - inv_two_sigma_x2, - inv_two_sigma_y2, - &weighted_sum, - &total_weight, - &contributing_samples](int bx, int by) { - if (bx < 0 || by < 0 || bx >= weight_field.bucket_width || by >= weight_field.bucket_height) - return; - - const size_t bucket_idx = size_t(by) * size_t(weight_field.bucket_width) + size_t(bx); - if (bucket_idx >= weight_field.buckets.size()) - return; - - for (const uint32_t sample_idx_u32 : weight_field.buckets[bucket_idx]) { - const size_t sample_idx = size_t(sample_idx_u32); - if (sample_idx >= weight_field.sample_x_mm.size() || - sample_idx >= weight_field.sample_y_mm.size() || - sample_idx >= weight_field.sample_weight.size()) - continue; - - const float dx = x_mm - weight_field.sample_x_mm[sample_idx]; - const float dy = y_mm - weight_field.sample_y_mm[sample_idx]; - const float d2 = dx * dx + dy * dy; - if (d2 > max_radius2) - continue; - - const float kernel = std::exp(-(dx * dx) * inv_two_sigma_x2 - (dy * dy) * inv_two_sigma_y2); - const float sample_w = weight_field.sample_weight[sample_idx] * kernel; - if (!std::isfinite(sample_w) || sample_w <= EPSILON) - continue; - - const size_t value_idx = sample_idx * weight_field.component_count + component_idx; - if (value_idx >= weight_field.sample_component_weights.size()) - continue; - - weighted_sum += weight_field.sample_component_weights[value_idx] * sample_w; - total_weight += sample_w; - ++contributing_samples; - } - }; - - for (int ring = 0; ring <= max_ring; ++ring) { - const int min_x = std::max(0, cx - ring); - const int max_x = std::min(weight_field.bucket_width - 1, cx + ring); - const int min_y = std::max(0, cy - ring); - const int max_y = std::min(weight_field.bucket_height - 1, cy + ring); - - if (ring == 0) { - process_bucket(cx, cy); - } else { - for (int x = min_x; x <= max_x; ++x) { - process_bucket(x, min_y); - if (max_y != min_y) - process_bucket(x, max_y); - } - for (int y = min_y + 1; y <= max_y - 1; ++y) { - process_bucket(min_x, y); - if (max_x != min_x) - process_bucket(max_x, y); - } - } - - if (total_weight > EPSILON && contributing_samples >= 12) - break; - } - - if (total_weight > EPSILON) - return clamp01f_for_gcode(weighted_sum / total_weight); - - float nearest_d2 = std::numeric_limits::max(); - float nearest_value = fallback; - const int nearest_ring_limit = std::min(std::max(max_ring + 2, 4), std::max(weight_field.bucket_width, weight_field.bucket_height)); - - for (int ring = 0; ring <= nearest_ring_limit; ++ring) { - const int min_x = std::max(0, cx - ring); - const int max_x = std::min(weight_field.bucket_width - 1, cx + ring); - const int min_y = std::max(0, cy - ring); - const int max_y = std::min(weight_field.bucket_height - 1, cy + ring); - - auto visit_bucket = [&weight_field, component_idx, x_mm, y_mm, &nearest_d2, &nearest_value](int bx, int by) { - if (bx < 0 || by < 0 || bx >= weight_field.bucket_width || by >= weight_field.bucket_height) - return; - - const size_t bucket_idx = size_t(by) * size_t(weight_field.bucket_width) + size_t(bx); - if (bucket_idx >= weight_field.buckets.size()) - return; - - for (const uint32_t sample_idx_u32 : weight_field.buckets[bucket_idx]) { - const size_t sample_idx = size_t(sample_idx_u32); - if (sample_idx >= weight_field.sample_x_mm.size() || sample_idx >= weight_field.sample_y_mm.size()) - continue; - - const float dx = x_mm - weight_field.sample_x_mm[sample_idx]; - const float dy = y_mm - weight_field.sample_y_mm[sample_idx]; - const float d2 = dx * dx + dy * dy; - if (d2 >= nearest_d2) - continue; - - const size_t value_idx = sample_idx * weight_field.component_count + component_idx; - if (value_idx >= weight_field.sample_component_weights.size()) - continue; - - nearest_d2 = d2; - nearest_value = weight_field.sample_component_weights[value_idx]; - } - }; - - if (ring == 0) { - visit_bucket(cx, cy); - } else { - for (int x = min_x; x <= max_x; ++x) { - visit_bucket(x, min_y); - if (max_y != min_y) - visit_bucket(x, max_y); - } - for (int y = min_y + 1; y <= max_y - 1; ++y) { - visit_bucket(min_x, y); - if (max_x != min_x) - visit_bucket(max_x, y); - } - } - - if (nearest_d2 < std::numeric_limits::max() && ring >= 2) - break; - } - - if (nearest_d2 < std::numeric_limits::max()) - return clamp01f_for_gcode(nearest_value); - - return fallback; + const std::vector values = sample_vertex_color_weight_field_components_for_gcode(weight_field, + x_mm, + y_mm, + high_resolution_texture_sampling, + smoothing_radius_mm); + return component_idx < values.size() ? clamp01f_for_gcode(values[component_idx]) : fallback; } static float component_angular_influence_for_gcode(unsigned int active_component_id, @@ -9686,9 +9552,9 @@ std::optional GCode::texture_mapping_seam_hiding_hint(const zone->texture_mapping_mode != int(TextureMappingZone::TextureMappingRawValues); const int dithering_method = std::clamp(zone->dithering_method, int(TextureMappingZone::DitheringClosest), - int(TextureMappingZone::DitheringHalftone)); + int(TextureMappingZone::DitheringHalftoneIncreasedDetail)); const bool halftone_dithering_enabled = - dithering_enabled && dithering_method == int(TextureMappingZone::DitheringHalftone); + dithering_enabled && is_halftone_dithering_method_for_gcode(dithering_method); const float seam_texture_base_width_mm = std::max(0.05f, float(m_config.texture_mapping_outer_wall_gradient_max_line_width.value)); const float dither_pitch_mm = @@ -9760,7 +9626,7 @@ std::optional GCode::texture_mapping_seam_hiding_hint(const component_key_stream << "|de" << (dithering_enabled ? 1 : 0); component_key_stream << "|dm" << dithering_method; component_key_stream << "|dp" << int(std::lround(dither_pitch_mm * 1000.f)); - if (dithering_method == int(TextureMappingZone::DitheringHalftone)) + if (is_halftone_dithering_method_for_gcode(dithering_method)) component_key_stream << "|hdt" << int(std::lround(halftone_dot_size_mm * 1000.f)); else component_key_stream << "|hrz" << int(std::lround(dither_cell_size_mm * 1000.f)); @@ -11007,6 +10873,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, bool high_resolution_texture_sampling { false }; bool dithering_enabled { false }; bool halftone_dithering_enabled { false }; + bool halftone_increased_detail_enabled { false }; bool nonlinear_offset_adjustment { false }; bool compact_offset_mode { false }; bool object_center_mode { false }; @@ -11101,6 +10968,94 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, const double end_mm = halftone_source_cumulative_mm[segment_idx + 1]; return start_mm + (end_mm - start_mm) * std::clamp(fraction, 0.0, 1.0); }; + auto halftone_normalized_source_distance = [&](double source_distance_mm) { + if (!std::isfinite(source_distance_mm)) + source_distance_mm = 0.0; + if (halftone_path_closed && halftone_total_path_mm > EPSILON) { + double d = std::fmod(source_distance_mm, halftone_total_path_mm); + if (d < 0.0) + d += halftone_total_path_mm; + return d; + } + return std::clamp(source_distance_mm, 0.0, halftone_total_path_mm); + }; + auto halftone_source_distance_for_surface_u = [&](float surface_u_mm) { + if (!std::isfinite(surface_u_mm)) + surface_u_mm = 0.f; + if (halftone_path_closed && halftone_total_path_mm > EPSILON) + return halftone_normalized_source_distance(double(surface_u_mm) + halftone_anchor_mm); + return halftone_normalized_source_distance(double(surface_u_mm)); + }; + auto halftone_point_for_source_distance = [&](double source_distance_mm) -> std::optional { + if (path.polyline.points.empty()) + return std::nullopt; + if (path.polyline.points.size() == 1 || halftone_total_path_mm <= EPSILON) + return path.polyline.points.front(); + const double d = halftone_normalized_source_distance(source_distance_mm); + auto upper = std::lower_bound(halftone_source_cumulative_mm.begin(), halftone_source_cumulative_mm.end(), d); + size_t idx = size_t(std::distance(halftone_source_cumulative_mm.begin(), upper)); + if (idx == 0) + idx = 1; + if (idx >= path.polyline.points.size()) + idx = path.polyline.points.size() - 1; + const double start_mm = halftone_source_cumulative_mm[idx - 1]; + const double end_mm = halftone_source_cumulative_mm[idx]; + const double t = end_mm > start_mm + EPSILON ? std::clamp((d - start_mm) / (end_mm - start_mm), 0.0, 1.0) : 0.0; + const Point &a = path.polyline.points[idx - 1]; + const Point &b = path.polyline.points[idx]; + return Point(coord_t(std::llround(double(a.x()) + (double(b.x()) - double(a.x())) * t)), + coord_t(std::llround(double(a.y()) + (double(b.y()) - double(a.y())) * t))); + }; + std::map, float> halftone_cell_tone_cache; + auto halftone_screen_cell_tone_for_gcode = [&](const VertexColorOverhangWeightField &weight_field, + const OuterWallGradientSurfaceCoord &surface_coord, + size_t component_idx, + bool high_resolution_texture_sampling, + float dot_size_mm, + float angle_deg, + float fallback_x_mm, + float fallback_y_mm) { + const float period = std::clamp(dot_size_mm, + TextureMappingZone::MinHalftoneDotSizeMm, + TextureMappingZone::MaxHalftoneDotSizeMm); + if (!std::isfinite(surface_coord.u_mm) || !std::isfinite(surface_coord.v_mm) || period <= EPSILON) + return sample_vertex_color_weight_field_for_gcode(weight_field, + fallback_x_mm, + fallback_y_mm, + component_idx, + high_resolution_texture_sampling, + false, + period * 0.5f); + const float radians = angle_deg * float(PI) / 180.f; + const float c = std::cos(radians); + const float s = std::sin(radians); + const float screen_x = c * surface_coord.u_mm + s * surface_coord.v_mm; + const float screen_y = -s * surface_coord.u_mm + c * surface_coord.v_mm; + const int cell_x = int(std::floor(screen_x / period)); + const int cell_y = int(std::floor(screen_y / period)); + const std::tuple key(component_idx, cell_x, cell_y); + const auto cached = halftone_cell_tone_cache.find(key); + if (cached != halftone_cell_tone_cache.end()) + return cached->second; + const float center_screen_x = (float(cell_x) + 0.5f) * period; + const float center_screen_y = (float(cell_y) + 0.5f) * period; + const float delta_screen_x = center_screen_x - screen_x; + const float delta_screen_y = center_screen_y - screen_y; + const float center_u_mm = surface_coord.u_mm + c * delta_screen_x - s * delta_screen_y; + const double center_source_distance_mm = halftone_source_distance_for_surface_u(center_u_mm); + const std::optional center_point = halftone_point_for_source_distance(center_source_distance_mm); + const float sample_x_mm = center_point ? unscale(center_point->x()) : fallback_x_mm; + const float sample_y_mm = center_point ? unscale(center_point->y()) : fallback_y_mm; + const float tone = sample_vertex_color_weight_field_for_gcode(weight_field, + sample_x_mm, + sample_y_mm, + component_idx, + high_resolution_texture_sampling, + false, + period * 0.5f); + halftone_cell_tone_cache.emplace(key, tone); + return tone; + }; std::vector outer_wall_gradient_segment_mods; OuterWallGradientDynamicContext outer_wall_gradient_dynamic_ctx; @@ -11265,9 +11220,11 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, const bool dithering_enabled = zone->dithering_enabled && !raw_texture_mapping_mode; const int dithering_method = std::clamp(zone->dithering_method, int(TextureMappingZone::DitheringClosest), - int(TextureMappingZone::DitheringHalftone)); + int(TextureMappingZone::DitheringHalftoneIncreasedDetail)); const bool halftone_dithering_enabled = - dithering_enabled && dithering_method == int(TextureMappingZone::DitheringHalftone); + dithering_enabled && is_halftone_dithering_method_for_gcode(dithering_method); + const bool halftone_increased_detail_enabled = + dithering_enabled && dithering_method == int(TextureMappingZone::DitheringHalftoneIncreasedDetail); const float dither_pitch_mm = dither_pitch_for_gcode(base_outer_width_mm, dithering_method, @@ -11370,7 +11327,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, component_key_stream << "|de" << (dithering_enabled ? 1 : 0); component_key_stream << "|dm" << dithering_method; component_key_stream << "|dp" << int(std::lround(dither_pitch_mm * 1000.f)); - if (dithering_method == int(TextureMappingZone::DitheringHalftone)) + if (is_halftone_dithering_method_for_gcode(dithering_method)) component_key_stream << "|hdt" << int(std::lround(halftone_dot_size_mm * 1000.f)); else component_key_stream << "|hrz" << int(std::lround(dither_cell_size_mm * 1000.f)); @@ -11436,6 +11393,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, outer_wall_gradient_dynamic_ctx.high_resolution_texture_sampling = high_resolution_texture_sampling; outer_wall_gradient_dynamic_ctx.dithering_enabled = dithering_enabled; outer_wall_gradient_dynamic_ctx.halftone_dithering_enabled = halftone_dithering_enabled; + outer_wall_gradient_dynamic_ctx.halftone_increased_detail_enabled = + halftone_increased_detail_enabled; outer_wall_gradient_dynamic_ctx.nonlinear_offset_adjustment = nonlinear_offset_adjustment; outer_wall_gradient_dynamic_ctx.compact_offset_mode = compact_offset_mode; outer_wall_gradient_dynamic_ctx.object_center_mode = object_center_mode; @@ -11537,21 +11496,31 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, !vertex_color_weight_field->empty()) { const float mid_x_mm = 0.5f * (unscale(line.a.x()) + unscale(line.b.x())); const float mid_y_mm = 0.5f * (unscale(line.a.y()) + unscale(line.b.y())); - float desired_strength = sample_vertex_color_weight_field_for_gcode( - *vertex_color_weight_field, - mid_x_mm, - mid_y_mm, - active_component_idx, - high_resolution_texture_sampling, - compact_offset_mode && !halftone_dithering_enabled); + const float active_halftone_angle_deg = + halftone_screen_angle_deg_for_gcode(texture_filament_color_mode, active_component_idx); + float desired_strength = + halftone_dithering_enabled && !halftone_increased_detail_enabled ? + halftone_screen_cell_tone_for_gcode(*vertex_color_weight_field, + surface_coord, + active_component_idx, + high_resolution_texture_sampling, + halftone_dot_size_mm, + active_halftone_angle_deg, + mid_x_mm, + mid_y_mm) : + sample_vertex_color_weight_field_for_gcode( + *vertex_color_weight_field, + mid_x_mm, + mid_y_mm, + active_component_idx, + high_resolution_texture_sampling, + compact_offset_mode && !halftone_dithering_enabled); if (halftone_dithering_enabled) desired_strength = halftone_screen_on_for_gcode(desired_strength, surface_coord.u_mm, surface_coord.v_mm, halftone_dot_size_mm, - halftone_screen_angle_deg_for_gcode( - texture_filament_color_mode, - active_component_idx)) ? + active_halftone_angle_deg) ? 1.f : 0.f; inset_strength = std::clamp(1.f - desired_strength, 0.f, 1.f); @@ -12233,7 +12202,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, const Layer *surface_layer = m_layer; auto dynamic_modulation_for_line = - [&outer_wall_gradient_dynamic_ctx, surface_layer](const Line &line, const OuterWallGradientSurfaceCoord &surface_coord) { + [&outer_wall_gradient_dynamic_ctx, surface_layer, &halftone_screen_cell_tone_for_gcode]( + const Line &line, const OuterWallGradientSurfaceCoord &surface_coord) { OuterWallGradientSegmentMod mod; if (!outer_wall_gradient_dynamic_ctx.enabled) return mod; @@ -12285,14 +12255,25 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, outer_wall_gradient_dynamic_ctx.vertex_color_weight_field->component_count) { const float mid_x_mm = 0.5f * (unscale(line.a.x()) + unscale(line.b.x())); const float mid_y_mm = 0.5f * (unscale(line.a.y()) + unscale(line.b.y())); - float desired_strength = sample_vertex_color_weight_field_for_gcode( - *outer_wall_gradient_dynamic_ctx.vertex_color_weight_field, - mid_x_mm, - mid_y_mm, - outer_wall_gradient_dynamic_ctx.active_component_idx, - outer_wall_gradient_dynamic_ctx.high_resolution_texture_sampling, - outer_wall_gradient_dynamic_ctx.compact_offset_mode && - !outer_wall_gradient_dynamic_ctx.halftone_dithering_enabled); + float desired_strength = + outer_wall_gradient_dynamic_ctx.halftone_dithering_enabled && + !outer_wall_gradient_dynamic_ctx.halftone_increased_detail_enabled ? + halftone_screen_cell_tone_for_gcode(*outer_wall_gradient_dynamic_ctx.vertex_color_weight_field, + surface_coord, + outer_wall_gradient_dynamic_ctx.active_component_idx, + outer_wall_gradient_dynamic_ctx.high_resolution_texture_sampling, + outer_wall_gradient_dynamic_ctx.halftone_dot_size_mm, + outer_wall_gradient_dynamic_ctx.active_halftone_angle_deg, + mid_x_mm, + mid_y_mm) : + sample_vertex_color_weight_field_for_gcode( + *outer_wall_gradient_dynamic_ctx.vertex_color_weight_field, + mid_x_mm, + mid_y_mm, + outer_wall_gradient_dynamic_ctx.active_component_idx, + outer_wall_gradient_dynamic_ctx.high_resolution_texture_sampling, + outer_wall_gradient_dynamic_ctx.compact_offset_mode && + !outer_wall_gradient_dynamic_ctx.halftone_dithering_enabled); if (outer_wall_gradient_dynamic_ctx.halftone_dithering_enabled) desired_strength = halftone_screen_on_for_gcode(desired_strength, surface_coord.u_mm, diff --git a/src/libslic3r/TextureMapping.cpp b/src/libslic3r/TextureMapping.cpp index 5724b9471a8..b9636f5b3bd 100644 --- a/src/libslic3r/TextureMapping.cpp +++ b/src/libslic3r/TextureMapping.cpp @@ -603,11 +603,12 @@ static std::string dithering_method_name(int mode) { switch (clamp_int(mode, int(TextureMappingZone::DitheringClosest), - int(TextureMappingZone::DitheringHalftone))) { - case int(TextureMappingZone::DitheringClosest): return "closest"; - case int(TextureMappingZone::DitheringOrderedBayer): return "ordered_bayer"; - case int(TextureMappingZone::DitheringHalftone): return "halftone"; - default: return "floyd_steinberg"; + int(TextureMappingZone::DitheringHalftoneIncreasedDetail))) { + case int(TextureMappingZone::DitheringClosest): return "closest"; + case int(TextureMappingZone::DitheringOrderedBayer): return "ordered_bayer"; + case int(TextureMappingZone::DitheringHalftone): return "halftone"; + case int(TextureMappingZone::DitheringHalftoneIncreasedDetail): return "halftone_increased_detail"; + default: return "floyd_steinberg"; } } @@ -620,6 +621,8 @@ static int dithering_method_from_name(std::string name) return int(TextureMappingZone::DitheringOrderedBayer); if (name == "halftone") return int(TextureMappingZone::DitheringHalftone); + if (name == "halftone_increased_detail" || name == "halftone_detail" || name == "halftone_high_detail") + return int(TextureMappingZone::DitheringHalftoneIncreasedDetail); return int(TextureMappingZone::DitheringFloydSteinberg); } @@ -1254,7 +1257,7 @@ void TextureMappingManager::load_entries(const std::string &serialized, zone.transmission_distance_calibration_mode = transmission_distance_calibration_mode_from_json(texture); zone.preview_opacity_pct = std::clamp(texture.value("preview_opacity_pct", TextureMappingZone::DefaultPreviewOpacityPct), 0.f, 100.f); - zone.preview_simulate_colors = texture.value("simulate_preview_colors", false); + zone.preview_simulate_colors = texture.value("simulate_preview_colors", TextureMappingZone::DefaultPreviewSimulateColors); zone.preview_limit_resolution = texture.value("limit_preview_resolution", true); zone.auto_adjust_filament_selection = texture.value("auto_adjust_filaments", true); zone.filament_strengths_pct = normalize_strengths(floats_from_json(texture.value("strength_pct", nlohmann::json::array()))); diff --git a/src/libslic3r/TextureMapping.hpp b/src/libslic3r/TextureMapping.hpp index 6d631a513dc..f1296447952 100644 --- a/src/libslic3r/TextureMapping.hpp +++ b/src/libslic3r/TextureMapping.hpp @@ -77,7 +77,8 @@ struct TextureMappingZone DitheringClosest = 0, DitheringFloydSteinberg = 1, DitheringOrderedBayer = 2, - DitheringHalftone = 3 + DitheringHalftone = 3, + DitheringHalftoneIncreasedDetail = 4 }; enum TransmissionDistanceCalibrationMode : uint8_t { @@ -121,7 +122,7 @@ struct TextureMappingZone static constexpr bool DefaultHighResolutionSampling = true; static constexpr float DefaultToneGamma = 1.f; static constexpr int DefaultTransmissionDistanceCalibrationMode = int(TDCalibrationAbsolute); - static constexpr bool DefaultPreviewSimulateColors = false; + static constexpr bool DefaultPreviewSimulateColors = true; static constexpr bool DefaultPreviewLimitResolution = true; static constexpr bool DefaultAutoAdjustFilamentSelection = true; diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 31acb1a578e..f41baf3e5f9 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -667,6 +667,8 @@ void GLVolume::simple_render(GLShaderProgram* shader, const bool base_uses_surface_gradient_preview = filament_state_uses_surface_gradient_preview(base_filament_id, num_physical, texture_mgr); const bool base_uses_image_texture_preview = base_uses_texture_preview && !base_uses_surface_gradient_preview; + const bool base_uses_halftone_texture_preview = + texture_preview_halftone_simulation_enabled_for_filament(base_filament_id, num_physical, texture_mgr); const bool has_surface_gradient_preview_state = base_uses_surface_gradient_preview || (has_mmu_segmentation && @@ -681,6 +683,7 @@ void GLVolume::simple_render(GLShaderProgram* shader, !has_mmu_segmentation && !has_texture_mapping_color_preview_data && base_uses_image_texture_preview && + !base_uses_halftone_texture_preview && model_volume_has_complete_texture_preview_data(*model_volume) && GUI::GLModel::Geometry::has_tex_coord(model.get_geometry().format); if (!has_mmu_segmentation && !base_uses_texture_preview) @@ -713,7 +716,7 @@ void GLVolume::simple_render(GLShaderProgram* shader, (preview_visual_signature << 6) + (preview_visual_signature >> 2); preview_visual_signature ^= model_volume_texture_mapping_color_preview_signature(*model_volume) + 0x9e3779b97f4a7c15ull + (preview_visual_signature << 6) + (preview_visual_signature >> 2); - if (has_surface_gradient_preview_state) { + if (has_surface_gradient_preview_state || has_texture_preview_data) { for (int row = 0; row < 4; ++row) { for (int col = 0; col < 4; ++col) { preview_visual_signature ^= std::hash{}(int(std::lround(preview_world_matrix(row, col) * 1000000.0))) + @@ -771,10 +774,7 @@ void GLVolume::simple_render(GLShaderProgram* shader, mmuseg_texture_preview_colors, mmuseg_texture_preview_filament_ids); } - if (has_texture_preview_state && - (has_active_texture_mapping_color_preview_data || - !has_texture_preview_data || - has_surface_gradient_preview_state)) { + if (has_texture_preview_state) { build_mmu_vertex_color_preview_models(*model_volume, triangles_per_type, state_colors, @@ -893,6 +893,8 @@ void GLVolume::render_mmu_texture_preview(const Transform3d &view_matrix, const bool base_uses_surface_gradient_preview = filament_state_uses_surface_gradient_preview(base_filament_id, num_physical, texture_mgr); const bool base_uses_image_texture_preview = base_uses_texture_preview && !base_uses_surface_gradient_preview; + const bool base_uses_halftone_texture_preview = + texture_preview_halftone_simulation_enabled_for_filament(base_filament_id, num_physical, texture_mgr); const bool has_mmu_segmentation = !model_volume->mmu_segmentation_facets.empty(); const bool has_texture_mapping_color_preview_data = model_volume_has_texture_mapping_color_preview_data(*model_volume); @@ -900,6 +902,7 @@ void GLVolume::render_mmu_texture_preview(const Transform3d &view_matrix, !has_mmu_segmentation && !has_texture_mapping_color_preview_data && base_uses_image_texture_preview && + !base_uses_halftone_texture_preview && model_volume_has_complete_texture_preview_data(*model_volume) && GUI::GLModel::Geometry::has_tex_coord(model.get_geometry().format); diff --git a/src/slic3r/GUI/MMUPaintedTexturePreview.cpp b/src/slic3r/GUI/MMUPaintedTexturePreview.cpp index 053489e46d4..5f62e98a090 100644 --- a/src/slic3r/GUI/MMUPaintedTexturePreview.cpp +++ b/src/slic3r/GUI/MMUPaintedTexturePreview.cpp @@ -77,6 +77,7 @@ struct TexturePreviewSimulationSettings int dithering_method = TextureMappingZone::DefaultDitheringMethod; float dithering_resolution_mm = TextureMappingZone::DefaultDitheringResolutionMm; float halftone_dot_size_mm = TextureMappingZone::DefaultHalftoneDotSizeMm; + float texture_preview_mm_per_pixel = TextureMappingZone::DefaultDitheringResolutionMm; float contrast_pct = 100.f; float tone_gamma = 1.f; int generic_solver_lookup_mode = int(TextureMappingZone::GenericSolverClosestMix); @@ -245,6 +246,69 @@ std::array unwrap_triangle_uvs(const Vec2f &uv0, const Vec2f &uv1, con return out; } +float estimated_texture_preview_mm_per_pixel(const ModelVolume &model_volume) +{ + const unsigned int width = model_volume.imported_texture_width; + const unsigned int height = model_volume.imported_texture_height; + if (width == 0 || height == 0) + return TextureMappingZone::DefaultDitheringResolutionMm; + + const indexed_triangle_set &its = model_volume.mesh().its; + if (its.indices.empty() || its.vertices.empty()) + return TextureMappingZone::DefaultDitheringResolutionMm; + + double world_area_mm2 = 0.0; + double uv_area_px2 = 0.0; + const size_t triangle_count = std::min(its.indices.size(), model_volume.imported_texture_uv_valid.size()); + for (size_t tri_idx = 0; tri_idx < triangle_count; ++tri_idx) { + if (model_volume.imported_texture_uv_valid[tri_idx] == 0) + continue; + + const size_t uv_offset = tri_idx * 6; + if (uv_offset + 5 >= model_volume.imported_texture_uvs_per_face.size()) + continue; + + const stl_triangle_vertex_indices &indices = its.indices[tri_idx]; + if (indices[0] < 0 || indices[1] < 0 || indices[2] < 0 || + size_t(indices[0]) >= its.vertices.size() || + size_t(indices[1]) >= its.vertices.size() || + size_t(indices[2]) >= its.vertices.size()) + continue; + + const Vec3d p0 = its.vertices[size_t(indices[0])].cast(); + const Vec3d p1 = its.vertices[size_t(indices[1])].cast(); + const Vec3d p2 = its.vertices[size_t(indices[2])].cast(); + const double tri_area_mm2 = 0.5 * (p1 - p0).cross(p2 - p0).norm(); + if (!std::isfinite(tri_area_mm2) || tri_area_mm2 <= double(k_epsilon)) + continue; + + const std::array uv = unwrap_triangle_uvs( + Vec2f(model_volume.imported_texture_uvs_per_face[uv_offset + 0], model_volume.imported_texture_uvs_per_face[uv_offset + 1]), + Vec2f(model_volume.imported_texture_uvs_per_face[uv_offset + 2], model_volume.imported_texture_uvs_per_face[uv_offset + 3]), + Vec2f(model_volume.imported_texture_uvs_per_face[uv_offset + 4], model_volume.imported_texture_uvs_per_face[uv_offset + 5])); + const Vec2d t0(double(uv[0].x()) * double(width), double(uv[0].y()) * double(height)); + const Vec2d t1(double(uv[1].x()) * double(width), double(uv[1].y()) * double(height)); + const Vec2d t2(double(uv[2].x()) * double(width), double(uv[2].y()) * double(height)); + const Vec2d e0 = t1 - t0; + const Vec2d e1 = t2 - t0; + const double tri_uv_area_px2 = 0.5 * std::abs(e0.x() * e1.y() - e0.y() * e1.x()); + if (!std::isfinite(tri_uv_area_px2) || tri_uv_area_px2 <= double(k_epsilon)) + continue; + + world_area_mm2 += tri_area_mm2; + uv_area_px2 += tri_uv_area_px2; + } + + if (world_area_mm2 <= double(k_epsilon) || uv_area_px2 <= double(k_epsilon)) + return TextureMappingZone::DefaultDitheringResolutionMm; + + const double mm_per_pixel = std::sqrt(world_area_mm2 / uv_area_px2); + if (!std::isfinite(mm_per_pixel) || mm_per_pixel <= 0.0) + return TextureMappingZone::DefaultDitheringResolutionMm; + + return std::clamp(float(mm_per_pixel), 0.005f, 5.f); +} + bool barycentric_weights(const Vec3f &point, const Vec3f &p0, const Vec3f &p1, const Vec3f &p2, Vec3f &weights) { const Vec3f edge_0 = p1 - p0; @@ -265,6 +329,26 @@ bool barycentric_weights(const Vec3f &point, const Vec3f &p0, const Vec3f &p1, c return std::isfinite(weights.x()) && std::isfinite(weights.y()) && std::isfinite(weights.z()); } +bool barycentric_weights_2d(const Vec2f &point, const Vec2f &p0, const Vec2f &p1, const Vec2f &p2, Vec3f &weights) +{ + const Vec2f edge_0 = p1 - p0; + const Vec2f edge_1 = p2 - p0; + const Vec2f delta = point - p0; + const float d00 = edge_0.dot(edge_0); + const float d01 = edge_0.dot(edge_1); + const float d11 = edge_1.dot(edge_1); + const float d20 = delta.dot(edge_0); + const float d21 = delta.dot(edge_1); + const float denom = d00 * d11 - d01 * d01; + if (std::abs(denom) <= k_epsilon) + return false; + + weights.y() = (d11 * d20 - d01 * d21) / denom; + weights.z() = (d00 * d21 - d01 * d20) / denom; + weights.x() = 1.f - weights.y() - weights.z(); + return std::isfinite(weights.x()) && std::isfinite(weights.y()) && std::isfinite(weights.z()); +} + ColorRGBA unpack_vertex_color(uint32_t packed) { return { @@ -586,6 +670,12 @@ std::array limited_simulated_texture_preview_size(unsigned int return { limited_width, limited_height }; } +std::array sample_texture_preview_rgba_bilinear_at_source(const std::vector &rgba, + unsigned int width, + unsigned int height, + double src_x_unclamped, + double src_y_unclamped); + std::array simulated_texture_preview_size(unsigned int width, unsigned int height, const TexturePreviewSimulationSettings &settings, @@ -635,12 +725,22 @@ std::array sample_texture_preview_rgba_bilinear(const std::vec 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, - 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 double src_x = (double(preview_x) + 0.5) * double(width) / double(std::max(1u, preview_width)) - 0.5; + const double src_y = (double(preview_y) + 0.5) * double(height) / double(std::max(1u, preview_height)) - 0.5; + return sample_texture_preview_rgba_bilinear_at_source(rgba, width, height, src_x, src_y); +} + +std::array sample_texture_preview_rgba_bilinear_at_source(const std::vector &rgba, + unsigned int width, + unsigned int height, + double src_x_unclamped, + double src_y_unclamped) +{ + if (width == 0 || height == 0 || rgba.size() < size_t(width) * size_t(height) * 4) + return { 0, 0, 0, 255 }; + + const double src_x = std::clamp(src_x_unclamped, 0.0, double(width - 1)); + const double src_y = std::clamp(src_y_unclamped, 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); @@ -1478,6 +1578,38 @@ std::vector binary_component_visibility_weights_for_texture_preview(const return weights; } +std::vector halftone_component_visibility_weights_for_texture_preview(const TexturePreviewSimulationSettings &settings, + uint32_t mask) +{ + std::vector weights = binary_component_visibility_weights_for_texture_preview(settings, mask); + if (mask == 0) { + std::fill(weights.begin(), weights.end(), 1.f); + return weights; + } + + float total_weight = 0.f; + for (const float weight : weights) + total_weight += std::max(0.f, weight); + if (total_weight > k_epsilon) + return weights; + + for (size_t component_idx = 0; component_idx < weights.size() && component_idx < 31; ++component_idx) + if ((mask & (uint32_t(1) << component_idx)) != 0) + weights[component_idx] = 1.f; + return weights; +} + +ColorRGBA halftone_preview_color_from_mask(const TexturePreviewSimulationSettings &settings, uint32_t mask) +{ + const std::vector weights = halftone_component_visibility_weights_for_texture_preview(settings, mask); + if (settings.component_colors.empty() || weights.empty()) + return { 0.f, 0.f, 0.f, 1.f }; + + const std::array rgb = + mix_color_solver_components(settings.component_colors, weights, ColorSolverMixModel::PigmentPainter); + return { clamp01(rgb[0]), clamp01(rgb[1]), clamp01(rgb[2]), 1.f }; +} + std::vector binary_dither_candidates_for_texture_preview( const TexturePreviewSimulationSettings &settings) { @@ -1620,23 +1752,129 @@ float ordered_bayer_threshold_for_texture_preview(int x, int y) return (float(matrix[by][bx]) + 0.5f) / 64.f - 0.5f; } -float halftone_threshold_for_texture_preview(int x, int y, float dot_size_mm) +bool is_halftone_dithering_method_for_texture_preview(int method) { - const float period_px = - std::clamp(std::clamp(dot_size_mm, - TextureMappingZone::MinHalftoneDotSizeMm, - TextureMappingZone::MaxHalftoneDotSizeMm) / - TextureMappingZone::DefaultDitheringResolutionMm, - 2.f, - TextureMappingZone::MaxHalftoneDotSizeMm / TextureMappingZone::DefaultDitheringResolutionMm); - const float u = float(x) / period_px - std::floor(float(x) / period_px); - const float v = float(y) / period_px - std::floor(float(y) / period_px); + const int clamped_method = std::clamp(method, + int(TextureMappingZone::DitheringClosest), + int(TextureMappingZone::DitheringHalftoneIncreasedDetail)); + return clamped_method == int(TextureMappingZone::DitheringHalftone) || + clamped_method == int(TextureMappingZone::DitheringHalftoneIncreasedDetail); +} + +float halftone_screen_angle_deg_for_texture_preview(int filament_color_mode, size_t component_idx) +{ + const int clamped_mode = + std::clamp(filament_color_mode, int(TextureMappingZone::FilamentColorAny), int(TextureMappingZone::FilamentColorRGBKW)); + if (component_idx < 3) { + static constexpr float primary_angles[3] = {15.f, 75.f, 0.f}; + return primary_angles[component_idx]; + } + + switch (clamped_mode) { + case int(TextureMappingZone::FilamentColorCMYK): + case int(TextureMappingZone::FilamentColorRGBK): + case int(TextureMappingZone::FilamentColorCMYKW): + case int(TextureMappingZone::FilamentColorRGBKW): + if (component_idx == 3) + return 45.f; + if (component_idx == 4) + return 90.f; + break; + case int(TextureMappingZone::FilamentColorCMYW): + case int(TextureMappingZone::FilamentColorRGBW): + if (component_idx == 3) + return 90.f; + break; + case int(TextureMappingZone::FilamentColorBW): + if (component_idx == 0) + return 45.f; + if (component_idx == 1) + return 90.f; + break; + default: + break; + } + + static constexpr float fallback_angles[] = {15.f, 75.f, 0.f, 45.f, 90.f, 30.f, 60.f, 105.f, 120.f, 150.f}; + return fallback_angles[component_idx % (sizeof(fallback_angles) / sizeof(fallback_angles[0]))]; +} + +float halftone_threshold_for_texture_preview(float x_mm, float y_mm, float dot_size_mm) +{ + const float period = std::clamp(dot_size_mm, + TextureMappingZone::MinHalftoneDotSizeMm, + TextureMappingZone::MaxHalftoneDotSizeMm); + const float u = x_mm / period - std::floor(x_mm / period); + const float v = y_mm / period - std::floor(y_mm / period); const float dx = u - 0.5f; const float dy = v - 0.5f; const float radius_area = (dx * dx + dy * dy) / 0.5f; return std::clamp(radius_area, 0.f, 1.f) - 0.5f; } +bool halftone_screen_on_for_texture_preview(float coverage, + float surface_x_mm, + float surface_y_mm, + float dot_size_mm, + float angle_deg) +{ + const float safe_coverage = clamp01(coverage); + if (safe_coverage <= k_epsilon) + return false; + if (safe_coverage >= 1.f - k_epsilon) + return true; + if (!std::isfinite(surface_x_mm) || !std::isfinite(surface_y_mm)) + return safe_coverage >= 0.5f; + + constexpr float pi = 3.14159265358979323846f; + const float radians = angle_deg * pi / 180.f; + const float c = std::cos(radians); + const float s = std::sin(radians); + const float x = c * surface_x_mm + s * surface_y_mm; + const float y = -s * surface_x_mm + c * surface_y_mm; + return halftone_threshold_for_texture_preview(x, y, dot_size_mm) + 0.5f < safe_coverage; +} + +bool texture_preview_uses_halftone_dithering(const TexturePreviewSimulationSettings &settings) +{ + return settings.dithering_enabled && + settings.mapping_mode != int(TextureMappingZone::TextureMappingRawValues) && + is_halftone_dithering_method_for_texture_preview(settings.dithering_method); +} + +int halftone_surface_axis_for_texture_preview(const Vec3f &normal) +{ + const Vec3f abs_normal(std::abs(normal.x()), std::abs(normal.y()), std::abs(normal.z())); + if (abs_normal.x() >= abs_normal.y() && abs_normal.x() >= abs_normal.z()) + return 0; + if (abs_normal.y() >= abs_normal.x() && abs_normal.y() >= abs_normal.z()) + return 1; + return 2; +} + +Vec2f halftone_surface_coords_for_texture_preview(const Vec3f &point, int axis) +{ + if (axis == 0) + return Vec2f(point.y(), point.z()); + if (axis == 1) + return Vec2f(point.x(), point.z()); + return Vec2f(point.x(), point.y()); +} + +Vec2f halftone_surface_coords_for_texture_preview(const Vec3f &point, const Vec3f &normal) +{ + return halftone_surface_coords_for_texture_preview(point, halftone_surface_axis_for_texture_preview(normal)); +} + +float halftone_surface_plane_coord_for_texture_preview(const Vec3f &point, int axis) +{ + if (axis == 0) + return point.x(); + if (axis == 1) + return point.y(); + return point.z(); +} + std::vector optimized_primary_component_weights_for_target(const std::array &target_rgb, size_t component_count, int filament_color_mode, @@ -1784,7 +2022,8 @@ std::vector optimized_primary_component_weights_for_target(const std::arr } std::vector component_weights_for_texture_preview(const TexturePreviewSimulationSettings &settings, - const std::array &sample_rgba) + const std::array &sample_rgba, + bool apply_visibility_adjustments = true) { const size_t component_count = settings.component_colors.size(); if (component_count == 0) @@ -1792,7 +2031,8 @@ std::vector component_weights_for_texture_preview(const TexturePreviewSim if (settings.dithering_enabled && settings.mapping_mode != int(TextureMappingZone::TextureMappingRawValues) && - settings.dithering_method != int(TextureMappingZone::DitheringHalftone)) { + settings.dithering_method != int(TextureMappingZone::DitheringHalftone) && + settings.dithering_method != int(TextureMappingZone::DitheringHalftoneIncreasedDetail)) { const std::vector candidates = binary_dither_candidates_for_texture_preview(settings); const std::array target_oklab = texture_preview_target_oklab(settings, sample_rgba); const size_t candidate_idx = nearest_binary_dither_candidate_for_texture_preview(candidates, target_oklab); @@ -1857,9 +2097,11 @@ std::vector component_weights_for_texture_preview(const TexturePreviewSim value = clamp01(value / max_weight); } - for (size_t idx = 0; idx < desired.size() && idx < settings.component_strength_factors.size(); ++idx) - desired[idx] = clamp01(desired[idx] * settings.component_strength_factors[idx]); - apply_minimum_visibility_offset(desired, settings.minimum_visibility_offset_factor); + if (apply_visibility_adjustments) { + for (size_t idx = 0; idx < desired.size() && idx < settings.component_strength_factors.size(); ++idx) + desired[idx] = clamp01(desired[idx] * settings.component_strength_factors[idx]); + apply_minimum_visibility_offset(desired, settings.minimum_visibility_offset_factor); + } return desired; } @@ -1967,7 +2209,7 @@ std::optional texture_preview_simulation_setti settings.mapping_mode != int(TextureMappingZone::TextureMappingRawValues); settings.dithering_method = std::clamp(zone->dithering_method, int(TextureMappingZone::DitheringClosest), - int(TextureMappingZone::DitheringHalftone)); + int(TextureMappingZone::DitheringHalftoneIncreasedDetail)); settings.dithering_resolution_mm = std::clamp(zone->dithering_resolution_mm, TextureMappingZone::MinDitheringResolutionMm, TextureMappingZone::MaxDitheringResolutionMm); @@ -1975,7 +2217,9 @@ std::optional texture_preview_simulation_setti TextureMappingZone::MinHalftoneDotSizeMm, TextureMappingZone::MaxHalftoneDotSizeMm); const bool halftone_dithering_enabled = - settings.dithering_enabled && settings.dithering_method == int(TextureMappingZone::DitheringHalftone); + settings.dithering_enabled && + (settings.dithering_method == int(TextureMappingZone::DitheringHalftone) || + settings.dithering_method == int(TextureMappingZone::DitheringHalftoneIncreasedDetail)); settings.compact_offset_mode = halftone_dithering_enabled ? false : zone->compact_offset_mode || settings.dithering_enabled; settings.use_legacy_fixed_color_mode = zone->use_legacy_fixed_color_mode; @@ -2054,10 +2298,12 @@ size_t texture_preview_simulation_signature(const ModelVolume &model_volume, mix(std::hash{}(settings.use_legacy_fixed_color_mode ? 1 : 0)); mix(std::hash{}(settings.dithering_enabled ? 1 : 0)); mix(std::hash{}(settings.dithering_method)); - if (settings.dithering_method == int(TextureMappingZone::DitheringHalftone)) + if (settings.dithering_method == int(TextureMappingZone::DitheringHalftone) || + settings.dithering_method == int(TextureMappingZone::DitheringHalftoneIncreasedDetail)) mix(std::hash{}(int(std::lround(settings.halftone_dot_size_mm * 1000.f)))); else mix(std::hash{}(int(std::lround(settings.dithering_resolution_mm * 1000.f)))); + mix(std::hash{}(int(std::lround(settings.texture_preview_mm_per_pixel * 100000.f)))); mix(std::hash{}(settings.generic_solver_lookup_mode)); mix(std::hash{}(settings.generic_solver_mode)); mix(std::hash{}(settings.generic_solver_mix_model)); @@ -2113,20 +2359,120 @@ TexturePreviewSimulationResult build_simulated_texture_preview_result(size_t sig settings.dithering_enabled && settings.mapping_mode != int(TextureMappingZone::TextureMappingRawValues) && settings.dithering_method != int(TextureMappingZone::DitheringHalftone) && + settings.dithering_method != int(TextureMappingZone::DitheringHalftoneIncreasedDetail) && !use_raw_offsets; + const bool use_halftone_dithering = + settings.dithering_enabled && + settings.mapping_mode != int(TextureMappingZone::TextureMappingRawValues) && + is_halftone_dithering_method_for_texture_preview(settings.dithering_method) && + !use_raw_offsets; + const bool halftone_increased_detail = + settings.dithering_method == int(TextureMappingZone::DitheringHalftoneIncreasedDetail); + const float preview_mm_per_source_pixel = + std::clamp(settings.texture_preview_mm_per_pixel, 0.005f, 5.f); const std::vector binary_dither_candidates = use_binary_dithering ? binary_dither_candidates_for_texture_preview(settings) : std::vector{}; std::unordered_map> simulated_color_cache; simulated_color_cache.reserve(std::min(size_t(result.width) * size_t(result.height), size_t(65536))); + std::unordered_map halftone_cell_tone_cache; + if (use_halftone_dithering && !halftone_increased_detail) + halftone_cell_tone_cache.reserve(std::min(size_t(result.width) * size_t(result.height), size_t(65536))); std::vector> floyd_error_current(result.width, { { 0.f, 0.f, 0.f } }); std::vector> floyd_error_next(result.width, { { 0.f, 0.f, 0.f } }); + auto halftone_cell_cache_key = [](size_t component_idx, int cell_x, int cell_y) { + uint64_t key = 1469598103934665603ull; + auto mix = [&key](uint64_t value) { + key ^= value; + key *= 1099511628211ull; + }; + mix(uint64_t(component_idx)); + mix(uint64_t(uint32_t(cell_x))); + mix(uint64_t(uint32_t(cell_y))); + return key; + }; + + auto component_coverage_at_source = [&](double src_x, double src_y, size_t component_idx) { + const std::array source_rgba_sample = + sample_texture_preview_rgba_bilinear_at_source(source_rgba, width, height, src_x, src_y); + 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 sampled_rgba = { + blended_source_color.r(), + blended_source_color.g(), + blended_source_color.b(), + 1.f + }; + const std::vector coverage = component_weights_for_texture_preview(settings, sampled_rgba, false); + return component_idx < coverage.size() ? clamp01(coverage[component_idx]) : 0.f; + }; + + auto halftone_cell_coverage = [&](size_t component_idx, + float surface_x_mm, + float surface_y_mm, + float angle_deg, + float current_coverage) { + if (halftone_increased_detail) + return current_coverage; + + const float period = std::clamp(settings.halftone_dot_size_mm, + TextureMappingZone::MinHalftoneDotSizeMm, + TextureMappingZone::MaxHalftoneDotSizeMm); + if (!std::isfinite(surface_x_mm) || !std::isfinite(surface_y_mm) || period <= k_epsilon) + return current_coverage; + + constexpr float pi = 3.14159265358979323846f; + const float radians = angle_deg * pi / 180.f; + const float c = std::cos(radians); + const float s = std::sin(radians); + const float screen_x = c * surface_x_mm + s * surface_y_mm; + const float screen_y = -s * surface_x_mm + c * surface_y_mm; + const int cell_x = int(std::floor(screen_x / period)); + const int cell_y = int(std::floor(screen_y / period)); + const uint64_t key = halftone_cell_cache_key(component_idx, cell_x, cell_y); + const auto cached = halftone_cell_tone_cache.find(key); + if (cached != halftone_cell_tone_cache.end()) + return cached->second; + + const float center_screen_x = (float(cell_x) + 0.5f) * period; + const float center_screen_y = (float(cell_y) + 0.5f) * period; + const float smooth_radius = period * 0.35f; + const bool use_smoothing_samples = smooth_radius / preview_mm_per_source_pixel >= 1.5f; + const std::array offsets = { + Vec2f(0.f, 0.f), + Vec2f(smooth_radius, 0.f), + Vec2f(-smooth_radius, 0.f), + Vec2f(0.f, smooth_radius), + Vec2f(0.f, -smooth_radius) + }; + const size_t sample_count = use_smoothing_samples ? offsets.size() : size_t(1); + float total = 0.f; + for (size_t sample_idx = 0; sample_idx < sample_count; ++sample_idx) { + const float sample_screen_x = center_screen_x + offsets[sample_idx].x(); + const float sample_screen_y = center_screen_y + offsets[sample_idx].y(); + const float sample_surface_x = c * sample_screen_x - s * sample_screen_y; + const float sample_surface_y = s * sample_screen_x + c * sample_screen_y; + const double src_x = double(sample_surface_x) / double(preview_mm_per_source_pixel) - 0.5; + const double src_y = double(sample_surface_y) / double(preview_mm_per_source_pixel) - 0.5; + total += component_coverage_at_source(src_x, src_y, component_idx); + } + const float coverage = clamp01(total / float(sample_count)); + halftone_cell_tone_cache.emplace(key, coverage); + return coverage; + }; + for (unsigned int y = 0; y < result.height; ++y) { for (unsigned int x = 0; x < result.width; ++x) { + const double src_x = (double(x) + 0.5) * double(width) / double(std::max(1u, result.width)) - 0.5; + const double src_y = (double(y) + 0.5) * double(height) / double(std::max(1u, result.height)) - 0.5; const std::array source_rgba_sample = - sample_texture_preview_rgba_bilinear(source_rgba, width, height, x, y, result.width, result.height); + sample_texture_preview_rgba_bilinear_at_source(source_rgba, width, height, src_x, src_y); 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, @@ -2143,8 +2489,10 @@ TexturePreviewSimulationResult build_simulated_texture_preview_result(size_t sig texture_preview_rgb_cache_key(source_rgb); const size_t idx = (size_t(y) * size_t(result.width) + size_t(x)) * 4; - auto cached_color = !use_raw_offsets && !use_binary_dithering ? simulated_color_cache.find(cache_key) : simulated_color_cache.end(); - if (!use_raw_offsets && !use_binary_dithering && cached_color != simulated_color_cache.end()) { + auto cached_color = !use_raw_offsets && !use_binary_dithering && !use_halftone_dithering ? + simulated_color_cache.find(cache_key) : + simulated_color_cache.end(); + if (!use_raw_offsets && !use_binary_dithering && !use_halftone_dithering && 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]; @@ -2173,11 +2521,33 @@ TexturePreviewSimulationResult build_simulated_texture_preview_result(size_t sig if (component_weights.size() == settings.component_colors.size()) component_weights = raw_offset_print_width_weights_for_texture_preview(settings, component_weights); } else { - if (use_binary_dithering && !binary_dither_candidates.empty()) { + if (use_halftone_dithering) { + const std::vector continuous_coverage = + component_weights_for_texture_preview(settings, sample_rgba, false); + uint32_t mask = 0; + const float surface_x_mm = float((src_x + 0.5) * double(preview_mm_per_source_pixel)); + const float surface_y_mm = float((src_y + 0.5) * double(preview_mm_per_source_pixel)); + for (size_t component_idx = 0; component_idx < settings.component_colors.size() && component_idx < 31; ++component_idx) { + const float angle_deg = + halftone_screen_angle_deg_for_texture_preview(settings.filament_color_mode, component_idx); + const float current_coverage = component_idx < continuous_coverage.size() ? + clamp01(continuous_coverage[component_idx]) : + 0.f; + const float coverage = + halftone_cell_coverage(component_idx, surface_x_mm, surface_y_mm, angle_deg, current_coverage); + if (halftone_screen_on_for_texture_preview(coverage, + surface_x_mm, + surface_y_mm, + settings.halftone_dot_size_mm, + angle_deg)) + mask |= uint32_t(1) << component_idx; + } + component_weights = halftone_component_visibility_weights_for_texture_preview(settings, mask); + } else if (use_binary_dithering && !binary_dither_candidates.empty()) { std::array target_oklab = texture_preview_target_oklab(settings, sample_rgba); const int clamped_method = std::clamp(settings.dithering_method, int(TextureMappingZone::DitheringClosest), - int(TextureMappingZone::DitheringHalftone)); + int(TextureMappingZone::DitheringHalftoneIncreasedDetail)); if (clamped_method == int(TextureMappingZone::DitheringFloydSteinberg)) { for (size_t axis = 0; axis < 3; ++axis) target_oklab[axis] += floyd_error_current[x][axis]; @@ -2188,9 +2558,6 @@ TexturePreviewSimulationResult build_simulated_texture_preview_result(size_t sig if (clamped_method == int(TextureMappingZone::DitheringOrderedBayer)) { thresholded_dither = true; threshold = ordered_bayer_threshold_for_texture_preview(int(x), int(y)) + 0.5f; - } else if (clamped_method == int(TextureMappingZone::DitheringHalftone)) { - thresholded_dither = true; - threshold = halftone_threshold_for_texture_preview(int(x), int(y), settings.halftone_dot_size_mm) + 0.5f; } const size_t candidate_idx = @@ -2239,7 +2606,9 @@ TexturePreviewSimulationResult build_simulated_texture_preview_result(size_t sig const std::array simulated_rgb = activity > k_epsilon ? mix_color_solver_components(settings.component_colors, component_weights, - color_solver_mix_model_from_index(settings.generic_solver_mix_model)) : + use_halftone_dithering ? + ColorSolverMixModel::PigmentPainter : + color_solver_mix_model_from_index(settings.generic_solver_mix_model)) : std::array{ sample_rgba[0], sample_rgba[1], sample_rgba[2] }; const std::array out_rgba = { @@ -2248,7 +2617,7 @@ TexturePreviewSimulationResult build_simulated_texture_preview_result(size_t sig to_u8(simulated_rgb[2]), 255 }; - if (!use_raw_offsets && !use_binary_dithering) + if (!use_raw_offsets && !use_binary_dithering && !use_halftone_dithering) simulated_color_cache.emplace(cache_key, out_rgba); result.rgba[idx + 0] = out_rgba[0]; result.rgba[idx + 1] = out_rgba[1]; @@ -2258,7 +2627,7 @@ TexturePreviewSimulationResult build_simulated_texture_preview_result(size_t sig if (use_binary_dithering && std::clamp(settings.dithering_method, int(TextureMappingZone::DitheringClosest), - int(TextureMappingZone::DitheringHalftone)) == int(TextureMappingZone::DitheringFloydSteinberg)) { + int(TextureMappingZone::DitheringHalftoneIncreasedDetail)) == int(TextureMappingZone::DitheringFloydSteinberg)) { floyd_error_current.swap(floyd_error_next); std::fill(floyd_error_next.begin(), floyd_error_next.end(), std::array{ { 0.f, 0.f, 0.f } }); } @@ -2589,6 +2958,7 @@ const GUI::GLTexture *simulated_texture_preview_texture_for_filament(const Model texture_preview_simulation_settings_for_filament(filament_id, num_physical, texture_mgr, physical_colors); if (!settings.has_value()) return &fallback_texture; + settings->texture_preview_mm_per_pixel = estimated_texture_preview_mm_per_pixel(model_volume); 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); @@ -3126,6 +3496,270 @@ bool build_texture_mapping_color_preview_model_for_state( return true; } +float image_texture_halftone_preview_step_mm(const TexturePreviewSimulationSettings &settings) +{ + const float dot_size = std::clamp(settings.halftone_dot_size_mm, + TextureMappingZone::MinHalftoneDotSizeMm, + TextureMappingZone::MaxHalftoneDotSizeMm); + return std::clamp(dot_size * 0.125f, 0.18f, 0.75f); +} + +int image_texture_halftone_preview_max_subdivision_steps(size_t triangle_count) +{ + if (triangle_count <= 200) + return 96; + if (triangle_count <= 2000) + return 48; + return 24; +} + +uint64_t image_texture_halftone_cell_cache_key(size_t component_idx, int axis, int plane_bucket, int cell_x, int cell_y) +{ + uint64_t key = 1469598103934665603ull; + auto mix = [&key](uint64_t value) { + key ^= value; + key *= 1099511628211ull; + }; + mix(uint64_t(component_idx)); + mix(uint64_t(uint32_t(axis))); + mix(uint64_t(uint32_t(plane_bucket))); + mix(uint64_t(uint32_t(cell_x))); + mix(uint64_t(uint32_t(cell_y))); + return key; +} + +bool build_image_texture_halftone_preview_geometry_for_state( + const indexed_triangle_set &its, + const std::vector &source_rgba, + unsigned int width, + unsigned int height, + const std::vector &texture_uvs_per_face, + const std::vector &texture_uv_valid, + const std::vector &state_triangles, + const TexturePreviewSimulationSettings &settings, + const ColorRGBA &background_color, + const Transform3d &world_matrix, + GUI::GLModel::Geometry &geometry) +{ + if (state_triangles.empty() || + width == 0 || + height == 0 || + source_rgba.size() < size_t(width) * size_t(height) * 4 || + texture_uv_valid.size() < its.indices.size() || + texture_uvs_per_face.size() < its.indices.size() * 6) + return false; + + geometry.format = { GUI::GLModel::Geometry::EPrimitiveType::Triangles, GUI::GLModel::Geometry::EVertexLayout::P3N3C4 }; + geometry.reserve_vertices(state_triangles.size() * 12); + geometry.reserve_indices(state_triangles.size() * 12); + + const bool halftone_increased_detail = + settings.dithering_method == int(TextureMappingZone::DitheringHalftoneIncreasedDetail); + const float period = std::clamp(settings.halftone_dot_size_mm, + TextureMappingZone::MinHalftoneDotSizeMm, + TextureMappingZone::MaxHalftoneDotSizeMm); + const float target_step_mm = image_texture_halftone_preview_step_mm(settings); + const int max_subdivision_steps = image_texture_halftone_preview_max_subdivision_steps(state_triangles.size()); + std::unordered_map halftone_cell_tone_cache; + if (!halftone_increased_detail) + halftone_cell_tone_cache.reserve(std::min(state_triangles.size() * size_t(8), size_t(65536))); + + auto source_coverage_at_uv = [&](const Vec2f &uv, size_t component_idx) { + const std::array source_rgba_sample = + sample_texture_preview_rgba_bilinear_at_source(source_rgba, + width, + height, + double(uv.x()) * double(width) - 0.5, + double(uv.y()) * double(height) - 0.5); + 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 sampled_rgba = { + blended_source_color.r(), + blended_source_color.g(), + blended_source_color.b(), + 1.f + }; + const std::vector coverage = component_weights_for_texture_preview(settings, sampled_rgba, false); + return component_idx < coverage.size() ? clamp01(coverage[component_idx]) : 0.f; + }; + + auto interpolate_point = [](const std::array &vertices, const Vec3f &barycentric) { + return vertices[0] * barycentric.x() + vertices[1] * barycentric.y() + vertices[2] * barycentric.z(); + }; + auto interpolate_uv = [](const std::array &uvs, const Vec3f &barycentric) { + return uvs[0] * barycentric.x() + uvs[1] * barycentric.y() + uvs[2] * barycentric.z(); + }; + auto bary_from_lattice = [](int i, int j, int steps) { + const float inv_steps = 1.f / float(std::max(1, steps)); + return Vec3f(1.f - float(i + j) * inv_steps, float(i) * inv_steps, float(j) * inv_steps); + }; + auto bary_reasonable_for_uv = [](const Vec3f &barycentric) { + return barycentric.x() >= -0.25f && barycentric.y() >= -0.25f && barycentric.z() >= -0.25f && + barycentric.x() <= 1.25f && barycentric.y() <= 1.25f && barycentric.z() <= 1.25f; + }; + + unsigned int vertex_index = 0; + for (const TriangleSelector::FacetStateTriangle &triangle : state_triangles) { + if (triangle.source_triangle < 0) + continue; + + const size_t source_triangle = size_t(triangle.source_triangle); + if (source_triangle >= its.indices.size() || + source_triangle >= texture_uv_valid.size() || + texture_uv_valid[source_triangle] == 0) + continue; + + const size_t uv_offset = source_triangle * 6; + if (uv_offset + 5 >= texture_uvs_per_face.size()) + continue; + + const stl_triangle_vertex_indices &source_indices = its.indices[source_triangle]; + if (source_indices[0] < 0 || source_indices[1] < 0 || source_indices[2] < 0) + continue; + if (size_t(source_indices[0]) >= its.vertices.size() || + size_t(source_indices[1]) >= its.vertices.size() || + size_t(source_indices[2]) >= its.vertices.size()) + continue; + + const Vec3f source_p0 = its.vertices[size_t(source_indices[0])].cast(); + const Vec3f source_p1 = its.vertices[size_t(source_indices[1])].cast(); + const Vec3f source_p2 = its.vertices[size_t(source_indices[2])].cast(); + const std::array source_uvs = unwrap_triangle_uvs( + Vec2f(texture_uvs_per_face[uv_offset + 0], texture_uvs_per_face[uv_offset + 1]), + Vec2f(texture_uvs_per_face[uv_offset + 2], texture_uvs_per_face[uv_offset + 3]), + Vec2f(texture_uvs_per_face[uv_offset + 4], texture_uvs_per_face[uv_offset + 5])); + + Vec3f normal = (triangle.vertices[1] - triangle.vertices[0]).cross(triangle.vertices[2] - triangle.vertices[0]); + const float normal_len = normal.norm(); + if (normal_len <= k_epsilon) + continue; + normal /= normal_len; + const Vec3f offset = normal * k_preview_offset; + + const std::array leaf_vertices = { triangle.vertices[0], triangle.vertices[1], triangle.vertices[2] }; + std::array leaf_uvs; + bool valid_leaf = true; + for (size_t vertex_idx = 0; vertex_idx < leaf_vertices.size(); ++vertex_idx) { + Vec3f barycentric = Vec3f::Zero(); + if (!barycentric_weights(leaf_vertices[vertex_idx], source_p0, source_p1, source_p2, barycentric)) { + valid_leaf = false; + break; + } + leaf_uvs[vertex_idx] = interpolate_uv(source_uvs, barycentric); + } + if (!valid_leaf) + continue; + + const std::array world_vertices = { + (world_matrix * leaf_vertices[0].cast()).cast(), + (world_matrix * leaf_vertices[1].cast()).cast(), + (world_matrix * leaf_vertices[2].cast()).cast() + }; + Vec3f world_normal = (world_vertices[1] - world_vertices[0]).cross(world_vertices[2] - world_vertices[0]); + const float world_normal_len = world_normal.norm(); + if (world_normal_len <= k_epsilon) + world_normal = normal; + else + world_normal /= world_normal_len; + + const int surface_axis = halftone_surface_axis_for_texture_preview(world_normal); + const std::array surface_vertices = { + halftone_surface_coords_for_texture_preview(world_vertices[0], surface_axis), + halftone_surface_coords_for_texture_preview(world_vertices[1], surface_axis), + halftone_surface_coords_for_texture_preview(world_vertices[2], surface_axis) + }; + const float max_edge_mm = std::max({ (world_vertices[1] - world_vertices[0]).norm(), + (world_vertices[2] - world_vertices[1]).norm(), + (world_vertices[0] - world_vertices[2]).norm() }); + if (!std::isfinite(max_edge_mm) || max_edge_mm <= k_epsilon) + continue; + const int steps = std::clamp(int(std::ceil(max_edge_mm / std::max(target_step_mm, k_epsilon))), 1, max_subdivision_steps); + + auto color_at_bary = [&](const Vec3f &barycentric) { + const Vec2f uv = interpolate_uv(leaf_uvs, barycentric); + const Vec3f world_point = interpolate_point(world_vertices, barycentric); + const Vec2f surface_coord = halftone_surface_coords_for_texture_preview(world_point, surface_axis); + const float plane_coord = halftone_surface_plane_coord_for_texture_preview(world_point, surface_axis); + uint32_t mask = 0; + for (size_t component_idx = 0; component_idx < settings.component_colors.size() && component_idx < 31; ++component_idx) { + const float angle_deg = + halftone_screen_angle_deg_for_texture_preview(settings.filament_color_mode, component_idx); + float coverage = source_coverage_at_uv(uv, component_idx); + if (!halftone_increased_detail && period > k_epsilon) { + constexpr float pi = 3.14159265358979323846f; + const float radians = angle_deg * pi / 180.f; + const float c = std::cos(radians); + const float s = std::sin(radians); + const float screen_x = c * surface_coord.x() + s * surface_coord.y(); + const float screen_y = -s * surface_coord.x() + c * surface_coord.y(); + const int cell_x = int(std::floor(screen_x / period)); + const int cell_y = int(std::floor(screen_y / period)); + const float plane_bucket_size = std::max(0.5f, period * 0.5f); + const int plane_bucket = int(std::floor(plane_coord / plane_bucket_size)); + const uint64_t key = + image_texture_halftone_cell_cache_key(component_idx, surface_axis, plane_bucket, cell_x, cell_y); + const auto cached = halftone_cell_tone_cache.find(key); + if (cached != halftone_cell_tone_cache.end()) { + coverage = cached->second; + } else { + const float center_screen_x = (float(cell_x) + 0.5f) * period; + const float center_screen_y = (float(cell_y) + 0.5f) * period; + const Vec2f center_surface(c * center_screen_x - s * center_screen_y, + s * center_screen_x + c * center_screen_y); + Vec3f center_bary = Vec3f::Zero(); + Vec2f center_uv = uv; + if (barycentric_weights_2d(center_surface, + surface_vertices[0], + surface_vertices[1], + surface_vertices[2], + center_bary) && + bary_reasonable_for_uv(center_bary)) + center_uv = interpolate_uv(leaf_uvs, center_bary); + coverage = source_coverage_at_uv(center_uv, component_idx); + halftone_cell_tone_cache.emplace(key, coverage); + } + } + if (halftone_screen_on_for_texture_preview(coverage, + surface_coord.x(), + surface_coord.y(), + period, + angle_deg)) + mask |= uint32_t(1) << component_idx; + } + return halftone_preview_color_from_mask(settings, mask); + }; + + auto emit_subtriangle = [&](const Vec3f &b0, const Vec3f &b1, const Vec3f &b2) { + const Vec3f centroid_bary = (b0 + b1 + b2) / 3.f; + const ColorRGBA color = color_at_bary(centroid_bary); + geometry.add_vertex(interpolate_point(leaf_vertices, b0) + offset, normal, color); + geometry.add_vertex(interpolate_point(leaf_vertices, b1) + offset, normal, color); + geometry.add_vertex(interpolate_point(leaf_vertices, b2) + offset, normal, color); + geometry.add_triangle(vertex_index, vertex_index + 1, vertex_index + 2); + vertex_index += 3; + }; + + for (int i = 0; i < steps; ++i) { + for (int j = 0; j < steps - i; ++j) { + const Vec3f b0 = bary_from_lattice(i, j, steps); + const Vec3f b1 = bary_from_lattice(i + 1, j, steps); + const Vec3f b2 = bary_from_lattice(i, j + 1, steps); + emit_subtriangle(b0, b1, b2); + if (j < steps - i - 1) { + const Vec3f b3 = bary_from_lattice(i + 1, j + 1, steps); + emit_subtriangle(b1, b3, b2); + } + } + } + } + + return !geometry.is_empty(); +} + size_t texture_preview_state_triangles_signature(const std::vector &state_triangles) { size_t signature = 1469598103934665603ull; @@ -3211,6 +3845,24 @@ size_t texture_preview_color_facets_source_signature(const ModelVolume return signature; } +size_t texture_preview_image_halftone_source_signature(const ModelVolume &model_volume, + const std::vector &state_triangles, + const Transform3d &world_matrix) +{ + size_t signature = model_volume_texture_preview_signature(model_volume); + auto mix = [&signature](size_t value) { + signature ^= value + 0x9e3779b97f4a7c15ull + (signature << 6) + (signature >> 2); + }; + mix(reinterpret_cast(model_volume.mesh_ptr().get())); + mix(model_volume.mesh().its.vertices.size()); + mix(model_volume.mesh().its.indices.size()); + mix(texture_preview_state_triangles_signature(state_triangles)); + for (int row = 0; row < 4; ++row) + for (int col = 0; col < 4; ++col) + mix(std::hash{}(int(std::lround(world_matrix(row, col) * 1000000.0)))); + return signature; +} + size_t texture_preview_vertex_color_simulation_cache_key(const ModelVolume &model_volume, unsigned int filament_id, size_t source_kind, @@ -3350,6 +4002,68 @@ bool build_simulated_texture_mapping_color_preview_model_for_state( out_model); } +bool build_simulated_image_texture_halftone_preview_model_for_state( + const ModelVolume &model_volume, + const std::vector &state_triangles, + unsigned int filament_id, + const TexturePreviewSimulationSettings &simulation_settings, + const Transform3d &world_matrix, + size_t preview_owner_key, + GUI::GLModel &out_model) +{ + if (!model_volume_has_texture_preview_data(model_volume) || + state_triangles.empty() || + !texture_preview_uses_halftone_dithering(simulation_settings)) + return false; + + const size_t source_signature = + texture_preview_image_halftone_source_signature(model_volume, state_triangles, world_matrix); + const size_t simulation_signature = texture_preview_simulation_signature(model_volume, source_signature, simulation_settings); + const size_t cache_key = texture_preview_vertex_color_simulation_cache_key(model_volume, filament_id, 3u, preview_owner_key); + std::shared_ptr mesh = model_volume.mesh_ptr(); + std::vector source_rgba(model_volume.imported_texture_rgba.begin(), + model_volume.imported_texture_rgba.end()); + std::vector texture_uvs_per_face(model_volume.imported_texture_uvs_per_face.begin(), + model_volume.imported_texture_uvs_per_face.end()); + std::vector texture_uv_valid(model_volume.imported_texture_uv_valid.begin(), + model_volume.imported_texture_uv_valid.end()); + const unsigned int width = model_volume.imported_texture_width; + const unsigned int height = model_volume.imported_texture_height; + std::vector triangles = state_triangles; + const ColorRGBA background_color = texture_mapping_background_color_for_preview(model_volume); + TexturePreviewSimulationSettings settings = simulation_settings; + return consume_or_queue_vertex_color_simulation(cache_key, + simulation_signature, + [simulation_signature, + mesh = std::move(mesh), + source_rgba = std::move(source_rgba), + width, + height, + texture_uvs_per_face = std::move(texture_uvs_per_face), + texture_uv_valid = std::move(texture_uv_valid), + triangles = std::move(triangles), + background_color, + world_matrix, + settings = std::move(settings)]() mutable { + TexturePreviewVertexColorSimulationResult result; + result.signature = simulation_signature; + prepare_texture_preview_simulation_settings(settings); + build_image_texture_halftone_preview_geometry_for_state(mesh->its, + source_rgba, + width, + height, + texture_uvs_per_face, + texture_uv_valid, + triangles, + settings, + background_color, + world_matrix, + result.geometry); + return result; + }, + out_model); +} + float normalize_angle(float angle) { if (!std::isfinite(angle)) @@ -3877,6 +4591,16 @@ bool texture_preview_simulation_enabled_for_filament(unsigned int return texture_preview_simulation_settings_for_filament(filament_id, num_physical, texture_mgr, physical_colors).has_value(); } +bool texture_preview_halftone_simulation_enabled_for_filament(unsigned int filament_id, + size_t num_physical, + const TextureMappingManager *texture_mgr) +{ + const std::vector physical_colors = physical_filament_colors_for_texture_preview(num_physical); + std::optional settings = + texture_preview_simulation_settings_for_filament(filament_id, num_physical, texture_mgr, physical_colors); + return settings.has_value() && texture_preview_uses_halftone_dithering(*settings); +} + bool texture_preview_simulation_enabled_for_all_filaments(const std::vector &filament_ids, size_t num_physical, const TextureMappingManager *texture_mgr) @@ -3913,12 +4637,17 @@ bool build_mmu_texture_preview_models( if (!model_volume_has_texture_preview_data(model_volume)) return false; + const std::vector physical_colors = physical_filament_colors_for_texture_preview(num_physical); bool built_any = false; for (size_t state_id = 0; state_id < triangles_per_type.size(); ++state_id) { const unsigned int filament_id = filament_id_for_state(state_id, base_filament_id); const TextureMappingZone *zone = zone_for_filament(filament_id, num_physical, texture_mgr); if (zone == nullptr || !is_image_zone(*zone)) continue; + std::optional simulation_settings = + texture_preview_simulation_settings_for_filament(filament_id, num_physical, texture_mgr, physical_colors); + if (simulation_settings && texture_preview_uses_halftone_dithering(*simulation_settings)) + continue; GUI::GLModel model; if (!build_texture_preview_model_for_state(model_volume, triangles_per_type[state_id], model)) @@ -3995,12 +4724,23 @@ bool build_mmu_vertex_color_preview_models( const bool has_texture_mapping_color_preview = has_texture_mapping_color_override || model_volume_has_texture_mapping_color_preview_data(model_volume); const bool has_texture_preview = model_volume_has_texture_preview_data(model_volume); - if (!has_texture_mapping_color_preview && (has_texture_preview || !model_volume_has_vertex_color_preview_data(model_volume))) - continue; - std::optional simulation_settings = texture_preview_simulation_settings_for_filament(filament_id, num_physical, texture_mgr, physical_colors); - if (has_texture_mapping_color_preview) { + const bool has_image_halftone_preview = + has_texture_preview && + !has_texture_mapping_color_preview && + simulation_settings && + texture_preview_uses_halftone_dithering(*simulation_settings); + if (has_image_halftone_preview) { + if (!build_simulated_image_texture_halftone_preview_model_for_state(model_volume, + triangles_per_type[state_id], + filament_id, + *simulation_settings, + world_matrix, + preview_owner_key, + model)) + continue; + } else if (has_texture_mapping_color_preview) { const ColorFacetsAnnotation *preview_override = has_texture_mapping_color_override ? texture_mapping_color_facets_override : nullptr; if (simulation_settings) { @@ -4026,6 +4766,8 @@ bool build_mmu_vertex_color_preview_models( continue; } } else { + if (has_texture_preview || !model_volume_has_vertex_color_preview_data(model_volume)) + continue; if (simulation_settings) { if (!build_simulated_vertex_color_preview_model_for_state(model_volume, triangles_per_type[state_id], @@ -4213,7 +4955,8 @@ size_t texture_preview_settings_signature(size_t num_physical, const TextureMapp signature_mix(std::hash{}(zone.use_legacy_fixed_color_mode ? 1 : 0)); signature_mix(std::hash{}(zone.dithering_enabled ? 1 : 0)); signature_mix(std::hash{}(zone.dithering_method)); - if (zone.dithering_method == int(TextureMappingZone::DitheringHalftone)) + if (zone.dithering_method == int(TextureMappingZone::DitheringHalftone) || + zone.dithering_method == int(TextureMappingZone::DitheringHalftoneIncreasedDetail)) signature_mix_float(zone.halftone_dot_size_mm, 1000.f); else signature_mix_float(zone.dithering_resolution_mm, 1000.f); diff --git a/src/slic3r/GUI/MMUPaintedTexturePreview.hpp b/src/slic3r/GUI/MMUPaintedTexturePreview.hpp index e23895e6956..dd5ef210011 100644 --- a/src/slic3r/GUI/MMUPaintedTexturePreview.hpp +++ b/src/slic3r/GUI/MMUPaintedTexturePreview.hpp @@ -71,6 +71,9 @@ void clear_texture_preview_simulation_cache(); bool texture_preview_simulation_enabled_for_filament(unsigned int filament_id, size_t num_physical, const TextureMappingManager *texture_mgr); +bool texture_preview_halftone_simulation_enabled_for_filament(unsigned int filament_id, + size_t num_physical, + const TextureMappingManager *texture_mgr); bool texture_preview_simulation_enabled_for_all_filaments(const std::vector &filament_ids, size_t num_physical, const TextureMappingManager *texture_mgr); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index a78fb8c188c..4d283775c8d 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1321,10 +1321,11 @@ public: dithering_choices.Add(_L("Floyd-Steinberg")); dithering_choices.Add(_L("Ordered Bayer")); dithering_choices.Add(_L("Halftone")); + dithering_choices.Add(_L("Halftone (increased detail)")); m_dithering_method_choice = new wxChoice(experimental_page, wxID_ANY, wxDefaultPosition, wxDefaultSize, dithering_choices); m_dithering_method_choice->SetSelection(std::clamp(dithering_method, int(TextureMappingZone::DitheringClosest), - int(TextureMappingZone::DitheringHalftone))); + int(TextureMappingZone::DitheringHalftoneIncreasedDetail))); dithering_row->Add(m_dithering_method_choice, 1, wxALIGN_CENTER_VERTICAL); m_dithering_enabled_checkbox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &) { update_dithering_options_visibility(true); @@ -1634,7 +1635,7 @@ public: return m_dithering_method_choice ? std::clamp(m_dithering_method_choice->GetSelection(), int(TextureMappingZone::DitheringClosest), - int(TextureMappingZone::DitheringHalftone)) : + int(TextureMappingZone::DitheringHalftoneIncreasedDetail)) : TextureMappingZone::DefaultDitheringMethod; } float dithering_resolution_mm() const @@ -1960,9 +1961,11 @@ private: const int method = m_dithering_method_choice != nullptr ? std::clamp(m_dithering_method_choice->GetSelection(), int(TextureMappingZone::DitheringClosest), - int(TextureMappingZone::DitheringHalftone)) : + int(TextureMappingZone::DitheringHalftoneIncreasedDetail)) : TextureMappingZone::DefaultDitheringMethod; - const bool halftone = enabled && method == int(TextureMappingZone::DitheringHalftone); + const bool halftone = enabled && + (method == int(TextureMappingZone::DitheringHalftone) || + method == int(TextureMappingZone::DitheringHalftoneIncreasedDetail)); if (m_dithering_method_choice != nullptr) m_dithering_method_choice->Enable(enabled); if (m_dithering_resolution_panel != nullptr)