diff --git a/core/dr-pipeline/ops/vibrance.yaml b/core/dr-pipeline/ops/vibrance.yaml index dd51f23..c034b14 100644 --- a/core/dr-pipeline/ops/vibrance.yaml +++ b/core/dr-pipeline/ops/vibrance.yaml @@ -21,24 +21,41 @@ params: kind: amount uniforms: - amount: vibrance / 100 + amount: + value: vibrance / 100 * 1.3 + doc: | + Scaled so that a value here does what the same value of Lightroom's + Vibrance does. Measured, not chosen: fitted on 45 Lightroom exports whose + only colour setting was Vibrance (about +24), against their raws. -helpers: [luminance, tone_position, colour_saturation] +helpers: [luminance] wgsl: | let luma = luminance(c); - let sat = colour_saturation(c); + + // How saturated a colour *looks*, so measured on display-encoded values. + // In scene-linear light an ordinary tan reads as 0.78 saturated and the + // falloff below would leave it a twentieth of the effect; encoded, it reads + // as 0.5, which is what the eye sees. + let e = pow(max(c, vec3(0.0)), vec3(1.0 / 2.2)); + let e_hi = max(e.r, max(e.g, e.b)); + let e_lo = min(e.r, min(e.g, e.b)); + let sat = select(0.0, (e_hi - e_lo) / e_hi, e_hi > 0.00001); // The vibrance curve: full effect on grey, tapering to nothing on colours // that are already saturated. Squaring the falloff keeps the mid-range // responsive while still protecting the extremes. let falloff = (1.0 - sat) * (1.0 - sat); - // Skin protection. Skin sits in a narrow band of hue where red leads green - // leads blue; pushing it is what makes vibrance look wrong on portraits. - // Detected by channel ordering rather than a hue angle, which costs a - // conversion and buys nothing here. - let is_skin = f32(c.r > c.g && c.g > c.b); + // Skin protection, for skin: hues between about 10 and 50 degrees (red + // leading, green between red and blue) that are not strongly saturated. + // Red-over-green-over-blue alone is every warm colour in a photograph — + // wood, sand, brick, sunlit grass — and halving all of them is most of why + // vibrance used to do so little. + let span = max(e_hi - e_lo, 0.00001); + let skin_hue = select(0.0, 60.0 * (e.g - e.b) / span, e.r >= e.g && e.g >= e.b); + let in_band = smoothstep(4.0, 12.0, skin_hue) * (1.0 - smoothstep(42.0, 52.0, skin_hue)); + let is_skin = in_band * (1.0 - smoothstep(0.45, 0.7, sat)) * f32(e.r >= e.g && e.g >= e.b); let skin_guard = 1.0 - is_skin * 0.5; let strength = amount * falloff * skin_guard; @@ -49,9 +66,18 @@ tests: - name: it_starts_neutral expect_active: false - - name: the_amount_is_normalised_to_unit_range + - name: the_amount_is_lightrooms_scale + why: | + Fitted against Lightroom exports: DarkRoom's vibrance at a value does + what Lightroom's does at the same value. set: { vibrance: 100 } - expect: { amount: 1.0 } + expect: { amount: 1.3 } + + - name: saturation_is_judged_as_displayed + why: | + Judged in scene-linear light, ordinary warm colours read as nearly + saturated and get almost none of the effect. + expect_wgsl: ["let e = pow(max(c, vec3(0.0)), vec3(1.0 / 2.2));"] - name: muted_colours_get_more_than_saturated_ones why: |