Make the colour mixer's saturation reach muted colours

Every photograph with a colour-mixer saturation edit now renders differently:
a raised band is stronger, most of all on muted colours.

The mixer matched bands and judged saturation on scene-linear values, and
scaled chroma by the same factor whatever a colour started at. Against the
photographer's earlier exports of two looks (~90 photographs, their raws, by
encoded hue), a sky band raised by 58 there lifted muted sky blues about
2.1×; here the mixer gave 1.15×, and less in the muted tones that carry most
of a sky or a shadowed snowfield.

Bands are now matched and saturation judged on display-encoded values. A
raised band pushes muted colours hardest and tapers to nothing at full
saturation, at a gain of 3.0, which at the same value lifts muted sky blues
about as those exports did. Lowering saturation still scales every colour
alike. Hue shifts work on the same encoded colour; luminance still scales in
linear light.

The bundled presets that use the mixer, and those whose colour was tuned
against the default rendering, are rescaled to the amount of colour they
had: Vivid 1.30, Vivid warm 1.30, Vivid landscape 1.38, Vivid, strong 1.45,
Vivid portrait 1.15, Punch 1.12, Blue sky 1.08, Deep blue sky 1.12, Polariser
1.26, Blue sky, golden land 1.13 — mean CIELAB chroma over the default
rendering, on 30 raws from the library. Negative values (skin protection)
are left as written.
This commit is contained in:
2026-10-04 05:28:22 -04:00
parent 5ffd54ba43
commit 3f8f909e41
5 changed files with 66 additions and 40 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ vibrance.vibrance = 10
blacks_whites.blacks = -8
clarity.amount = 12
contrast.contrast = 18
vibrance.vibrance = 32
vibrance.vibrance = 36
[preset Recover the sky]
blacks_whites.whites = -10
+13 -13
View File
@@ -15,38 +15,38 @@ drpl 1
[preset Blue sky]
colour_mixer.azure_lum = -20
colour_mixer.azure_sat = 25
colour_mixer.azure_sat = 18
colour_mixer.blue_lum = -15
colour_mixer.blue_sat = 20
colour_mixer.blue_sat = 14
highlights_shadows.highlights = -15
[preset Deep blue sky]
colour_mixer.azure_hue = 10
colour_mixer.azure_lum = -30
colour_mixer.azure_sat = 35
colour_mixer.azure_sat = 22
colour_mixer.blue_lum = -25
colour_mixer.blue_sat = 30
colour_mixer.cyan_sat = 10
colour_mixer.blue_sat = 19
colour_mixer.cyan_sat = 6
highlights_shadows.highlights = -30
[preset Polariser]
colour_mixer.azure_hue = 10
colour_mixer.azure_lum = -35
colour_mixer.azure_sat = 40
colour_mixer.azure_sat = 29
colour_mixer.blue_lum = -30
colour_mixer.blue_sat = 35
colour_mixer.blue_sat = 26
colour_mixer.cyan_lum = -10
colour_mixer.cyan_sat = 15
colour_mixer.cyan_sat = 11
dehaze.amount = 20
highlights_shadows.highlights = -35
vibrance.vibrance = 10
vibrance.vibrance = 7
[preset Blue sky, golden land]
colour_mixer.azure_lum = -20
colour_mixer.azure_sat = 25
colour_mixer.azure_sat = 16
colour_mixer.blue_lum = -15
colour_mixer.blue_sat = 20
colour_mixer.orange_sat = 12
colour_mixer.blue_sat = 13
colour_mixer.orange_sat = 8
colour_mixer.yellow_hue = -10
colour_mixer.yellow_sat = 15
colour_mixer.yellow_sat = 10
highlights_shadows.highlights = -20
+18 -18
View File
@@ -21,15 +21,15 @@ drpl 1
[preset Vivid]
contrast.contrast = 10
saturation.saturation = 10
vibrance.vibrance = 39
saturation.saturation = 11
vibrance.vibrance = 42
[preset Vivid, strong]
blacks_whites.blacks = -10
clarity.amount = 8
contrast.contrast = 18
saturation.saturation = 18
vibrance.vibrance = 54
saturation.saturation = 19
vibrance.vibrance = 57
# Foliage and sky: green and chartreuse for leaves and grass, azure and blue
# for sky and water, a little yellow for dry grass and stone. The skin bands
@@ -38,37 +38,37 @@ vibrance.vibrance = 54
[preset Vivid landscape]
clarity.amount = 10
colour_mixer.azure_lum = -10
colour_mixer.azure_sat = 27
colour_mixer.azure_sat = 25
colour_mixer.blue_lum = -10
colour_mixer.blue_sat = 21
colour_mixer.chartreuse_sat = 21
colour_mixer.green_sat = 27
colour_mixer.yellow_sat = 14
colour_mixer.blue_sat = 20
colour_mixer.chartreuse_sat = 20
colour_mixer.green_sat = 25
colour_mixer.yellow_sat = 13
contrast.contrast = 12
saturation.saturation = 7
vibrance.vibrance = 34
vibrance.vibrance = 32
# Golden hour: oranges and yellows up and a warm cast laid over the
# highlights only, so shadows stay clean rather than muddy.
[preset Vivid warm]
colour_grading.highlight_hue = 45
colour_grading.highlight_strength = 12
colour_mixer.orange_sat = 20
colour_mixer.red_sat = 11
colour_mixer.yellow_sat = 20
colour_mixer.orange_sat = 17
colour_mixer.red_sat = 9
colour_mixer.yellow_sat = 17
contrast.contrast = 8
vibrance.vibrance = 34
vibrance.vibrance = 29
# People: everything around the subject gets richer while skin does not.
# Vibrance already protects skin; the orange and red bands are then held a
# little below where they started, because a face is the one colour every
# viewer knows the right value of.
[preset Vivid portrait]
colour_mixer.azure_sat = 15
colour_mixer.blue_sat = 18
colour_mixer.green_sat = 18
colour_mixer.azure_sat = 14
colour_mixer.blue_sat = 17
colour_mixer.green_sat = 17
colour_mixer.orange_sat = -10
colour_mixer.red_sat = -5
contrast.contrast = 6
saturation.saturation = -5
vibrance.vibrance = 37
vibrance.vibrance = 35
+32 -6
View File
@@ -39,6 +39,16 @@ use crate::ops::helpers;
pub const ID: OpId = OpId("colour_mixer");
/// How much a raised saturation band adds to a muted colour, per unit of its
/// value; the push tapers linearly to nothing at full saturation.
///
/// Measured, not chosen. Fitted against the photographer's earlier exports
/// (two looks, ~90 photographs), a sky band raised by 58 there lifted muted
/// sky blues about 2.1×; at 3.0 a band here does about the same at the same
/// value, so imported values stay inside the slider's range (dr-preset-xmp
/// carries the per-band factors). Lowering saturation is unaffected.
pub const SAT_GAIN: f32 = 3.0;
/// The twelve bands, in hue order starting at red.
///
/// Twelve rather than Lightroom's eight: the extra bands fall between the
@@ -333,9 +343,16 @@ impl Operation for ColourMixer {
// Only the bands the user actually touched contribute code. A single
// adjusted band therefore costs one weight evaluation rather than
// twelve — the composition property applied within an operation.
let mut lines = String::from(
let lines = String::from(
"\
let hcl = rgb_to_hcl(c);
// Bands are matched, and saturation judged, on display-encoded values: in
// scene-linear light a muted colour reads as strongly saturated and its hue
// sits away from where it is seen, so a band set on what the photograph
// shows would land on other colours.
let lin = max(c, vec3<f32>(0.0));
let e = pow(lin, vec3<f32>(1.0 / 2.2));
let SAT_GAIN = @SAT_GAIN@;
let hcl = rgb_to_hcl(e);
let hue = hcl.x;
let chroma = hcl.y;
let hi = hcl.z;
@@ -350,6 +367,8 @@ if (chroma > 0.0001) {
",
);
let mut lines = lines.replace("@SAT_GAIN@", &format!("{SAT_GAIN:.4}"));
for (b, band) in BANDS.iter().enumerate() {
let v = self.values[b];
if v.iter().all(|x| *x == 0.0) {
@@ -384,10 +403,17 @@ if (chroma > 0.0001) {
// yellow-green to green, not enough to turn it blue by accident.
let new_hue = hue + d_hue * 30.0;
// Saturation scales chroma; luminance scales the whole colour.
let new_chroma = clamp(chroma * (1.0 + d_sat), 0.0, hi);
c = hue_to_rgb_scale(new_hue, new_chroma, hi);
c = c * exp2(d_lum);
// Saturation. Raising it pushes muted colours hardest and tapers to
// nothing at full saturation, as the eye expects a mixer to; the
// gain makes a value deliver the strength it names, measured
// against the photographer's earlier exports. Lowering it scales
// every colour alike, so -100 is grey.
let sat = chroma / max(hi, 0.00001);
let gain = select(1.0 + d_sat, 1.0 + d_sat * SAT_GAIN * (1.0 - sat), d_sat > 0.0);
let new_chroma = clamp(chroma * gain, 0.0, hi);
let shifted = hue_to_rgb_scale(new_hue, new_chroma, hi);
// Back to linear light, where luminance scales the whole colour.
c = pow(shifted, vec3<f32>(2.2)) * exp2(d_lum);
}
}
c = max(c, vec3<f32>(0.0));",
File diff suppressed because one or more lines are too long