From 533aaaed284d31c2f3268b9f31bee8168e3f4fc7 Mon Sep 17 00:00:00 2001 From: ExPikaPaka Date: Mon, 12 Oct 2026 12:22:30 +0200 Subject: [PATCH] feat: add Hue to the Image tool's colour correction Turns every colour of the picture around the colour wheel without changing how light it is. This matters more here than in a photo editor: the filament set is fixed and arbitrary, so hue is what lets the picture meet the palette rather than the other way round. With orange loaded but no red, a small turn puts the reds onto the orange instead of onto whatever happens to be nearest. Implemented as Rodrigues' rotation of the colour about the grey axis, then the Rec. 709 luma is restored exactly: the rotation preserves R+G+B, not the weighted luma, and without the correction turning the wheel would also change which areas read as light and dark. Measured, same scene and harness as the other corrections: run out tris coloured max offset mm filament shares image, no correction 12066 4052 0.000005 1:71% 2:25% 3:4% image, hue +60 8208 268 0.000005 2:100% image, hue -120 8208 268 0.000005 3:100% The reddish brick moves onto a different filament in each direction and the surface does not move. --- src/libslic3r/TextureDisplacement.cpp | 27 +++++++++++++++++-- src/libslic3r/TextureDisplacement.hpp | 5 ++-- src/slic3r/GUI/Gizmos/GLGizmoImage.cpp | 5 ++++ .../GUI/Gizmos/GLGizmoTextureDisplacement.hpp | 6 ++--- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/TextureDisplacement.cpp b/src/libslic3r/TextureDisplacement.cpp index e564147af8..ff6b95456c 100644 --- a/src/libslic3r/TextureDisplacement.cpp +++ b/src/libslic3r/TextureDisplacement.cpp @@ -264,7 +264,7 @@ bool height_texture_has_color(const TextureDisplacementLayer &layer) bool has_color_adjustments(const TextureDisplacementLayer &layer) { return layer.brightness != 0.f || layer.contrast != 0.f || layer.saturation != 1.f || - layer.gamma != 1.f || layer.color_invert; + layer.gamma != 1.f || layer.color_invert || layer.hue != 0.f; } void apply_color_adjustments(DecodedHeightTexture &tex, const TextureDisplacementLayer &layer) @@ -279,6 +279,14 @@ void apply_color_adjustments(DecodedHeightTexture &tex, const TextureDisplacemen const float sat = std::max(0.f, layer.saturation); const float inv_g = 1.f / std::max(0.01f, layer.gamma); const bool negate = layer.color_invert; + // Hue is a rotation of the colour about the grey axis. Rodrigues' formula about (1,1,1)/sqrt(3), + // then the luma is put back exactly (the rotation preserves R+G+B, not the Rec. 709 weighting), + // so turning the wheel changes which filament an area matches without also changing how light + // it is. + const float hue_rad = layer.hue * float(M_PI) / 180.f; + const float hue_cos = std::cos(hue_rad), hue_sin = std::sin(hue_rad); + const bool rotate = layer.hue != 0.f; + constexpr float LW[3] = { 0.2126f, 0.7152f, 0.0722f }; // Rec. 709 luma // Each output byte depends only on its own pixel, so the rows are independent. At 1024 x 1024 // this runs on every preview rebuild and every bake, which is what makes it worth splitting. @@ -297,10 +305,25 @@ void apply_color_adjustments(DecodedHeightTexture &tex, const TextureDisplacemen v += bright; c[k] = v; } + if (rotate) { + const float luma = LW[0] * c[0] + LW[1] * c[1] + LW[2] * c[2]; + // The component of the colour across the grey axis, which is what a hue is. + const float mean = (c[0] + c[1] + c[2]) / 3.f; + const float p[3] = { c[0] - mean, c[1] - mean, c[2] - mean }; + // k x p for the unit grey axis k = (1,1,1)/sqrt(3). + constexpr float INV_SQRT3 = 0.57735027f; + const float x[3] = { INV_SQRT3 * (p[1] - p[2]), INV_SQRT3 * (p[2] - p[0]), + INV_SQRT3 * (p[0] - p[1]) }; + for (int k = 0; k < 3; ++k) + c[k] = mean + p[k] * hue_cos + x[k] * hue_sin; + const float luma_now = LW[0] * c[0] + LW[1] * c[1] + LW[2] * c[2]; + for (float &v : c) + v += luma - luma_now; + } if (sat != 1.f) { // Rec. 709 luma, the same weights the rest of the colour path uses to decide // what counts as light and dark. - const float luma = 0.2126f * c[0] + 0.7152f * c[1] + 0.0722f * c[2]; + const float luma = LW[0] * c[0] + LW[1] * c[1] + LW[2] * c[2]; for (float &v : c) v = luma + (v - luma) * sat; } diff --git a/src/libslic3r/TextureDisplacement.hpp b/src/libslic3r/TextureDisplacement.hpp index 5f9168ad86..7dea145d9a 100644 --- a/src/libslic3r/TextureDisplacement.hpp +++ b/src/libslic3r/TextureDisplacement.hpp @@ -299,6 +299,7 @@ struct TextureDisplacementLayer // so a layer that never meets the Image tool behaves exactly as it did. float brightness = 0.f; // -1 .. 1, added last, after contrast float contrast = 0.f; // -1 .. 1, pivoted on mid grey; -1 flattens to grey, 1 doubles the slope + float hue = 0.f; // -180 .. 180 degrees, rotated about the grey axis at constant luma float saturation = 1.f; // 0 .. 2, 1 leaves it alone, 0 is grayscale float gamma = 1.f; // 0.2 .. 5, 1 leaves it alone; above 1 lifts the midtones bool color_invert = false; // negative of the colour, as opposed to `invert` which flips relief @@ -313,7 +314,7 @@ struct TextureDisplacementLayer static_cast(blend_mode), midlevel, island_padding_mm, lscm_seam_edges, view_project_right, view_project_up, smoothing, edge_smoothing, edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled, brightness, - contrast, saturation, gamma, color_invert); + contrast, saturation, gamma, color_invert, hue); } template void load(Archive &ar) { @@ -325,7 +326,7 @@ struct TextureDisplacementLayer tile_method_int, projection_method_int, lscm_seam_angle_deg, islands, blend_mode_int, midlevel, island_padding_mm, lscm_seam_edges, view_project_right, view_project_up, smoothing, edge_smoothing, edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective, - view_project_matrix, color_enabled, brightness, contrast, saturation, gamma, color_invert); + view_project_matrix, color_enabled, brightness, contrast, saturation, gamma, color_invert, hue); image_data = blob.empty() ? nullptr : std::make_shared>(blob.begin(), blob.end()); tile_method = static_cast(tile_method_int); projection_method = static_cast(projection_method_int); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoImage.cpp b/src/slic3r/GUI/Gizmos/GLGizmoImage.cpp index 4c232a9c18..30dbd07df9 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoImage.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoImage.cpp @@ -522,6 +522,10 @@ void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, flo m_preview_params_dirty |= float_row("##saturation", _L("Saturation"), &layer->saturation, 0.f, 2.f, "%.2f", false); hover_tip(_u8L("How strong the colors are. At 0 the picture is gray, so it prints in your grays and " "blacks; raising it pushes each area towards a purer filament.")); + m_preview_params_dirty |= float_row("##hue", _L("Hue"), &layer->hue, -180.f, 180.f, "%.0f°", false); + hover_tip(_u8L("Turns every color of the picture around the color wheel, without changing how light " + "it is. Your filaments are a fixed, arbitrary set, so this is what lets the picture " + "meet them: if you have orange loaded but no red, a small turn puts the reds on it.")); m_preview_params_dirty |= float_row("##gamma", _L("Gamma"), &layer->gamma, 0.2f, 5.f, "%.2f", true); hover_tip(_u8L("Lifts or drops the midtones without touching the brightest and darkest parts. Use it " "when the middle of the picture is the part coming out wrong.")); @@ -533,6 +537,7 @@ void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, flo layer->brightness = 0.f; layer->contrast = 0.f; layer->saturation = 1.f; + layer->hue = 0.f; layer->gamma = 1.f; layer->color_invert = false; m_preview_params_dirty = true; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp index cb22078cad..2ef072ecdb 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp @@ -159,17 +159,17 @@ public: // pixels while leaving the image_data pointer alone. struct ColorAdjustKey { - float brightness = 0.f, contrast = 0.f, saturation = 1.f, gamma = 1.f; + float brightness = 0.f, contrast = 0.f, saturation = 1.f, gamma = 1.f, hue = 0.f; bool invert = false; bool operator==(const ColorAdjustKey &o) const { return brightness == o.brightness && contrast == o.contrast && saturation == o.saturation && - gamma == o.gamma && invert == o.invert; + gamma == o.gamma && hue == o.hue && invert == o.invert; } bool operator!=(const ColorAdjustKey &o) const { return !(*this == o); } static ColorAdjustKey of(const TextureDisplacementLayer &l) { - return { l.brightness, l.contrast, l.saturation, l.gamma, l.color_invert }; + return { l.brightness, l.contrast, l.saturation, l.gamma, l.hue, l.color_invert }; } }; std::vector m_palette_adjusts; // alongside m_palette_images, one per entry