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