The calibration commits named DarkRoom's default curve after another product and described vibrance as doing what another editor's does at the same value. The curve is the DNG SDK's reference, so it is called that; vibrance is scaled to deliver the strength its value names, as measured against the photographer's earlier exports. Two test names follow. The measurements and where they came from are unchanged.
93 lines
3.5 KiB
YAML
93 lines
3.5 KiB
YAML
id: vibrance
|
|
label: op.vibrance
|
|
order: 80
|
|
attributes: [colour]
|
|
|
|
doc: |
|
|
Vibrance — saturation weighted toward the muted colours.
|
|
|
|
Where [`saturation`](saturation.yaml) scales every colour's distance from
|
|
grey equally, vibrance scales it *more for muted colours than for already
|
|
saturated ones*, and protects skin tones. The difference matters: pushing
|
|
saturation on a portrait turns faces orange long before the background
|
|
improves, which is precisely the problem vibrance was invented to solve.
|
|
|
|
placement: |
|
|
Before saturation, so the broad control has the last word if both are used.
|
|
|
|
params:
|
|
vibrance:
|
|
label: param.vibrance
|
|
kind: amount
|
|
|
|
uniforms:
|
|
amount:
|
|
value: vibrance / 100 * 1.3
|
|
doc: |
|
|
Scaled so that a value delivers the strength it names. Measured, not
|
|
chosen: fitted on 45 of the photographer's earlier exports whose only
|
|
colour setting was a vibrance of about +24, against their raws.
|
|
|
|
helpers: [luminance]
|
|
|
|
wgsl: |
|
|
let luma = luminance(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<f32>(0.0)), vec3<f32>(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, 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;
|
|
c = mix(vec3<f32>(luma), c, 1.0 + strength);
|
|
c = max(c, vec3<f32>(0.0));
|
|
|
|
tests:
|
|
- name: it_starts_neutral
|
|
expect_active: false
|
|
|
|
- name: the_amount_is_the_measured_scale
|
|
why: |
|
|
Fitted against the photographer's earlier exports, so a value delivers
|
|
the strength it names.
|
|
set: { vibrance: 100 }
|
|
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<f32>(0.0)), vec3<f32>(1.0 / 2.2));"]
|
|
|
|
- name: muted_colours_get_more_than_saturated_ones
|
|
why: |
|
|
The one property that distinguishes vibrance from saturation. Without
|
|
the falloff term this node would be a duplicate of its neighbour.
|
|
expect_wgsl: ["let falloff = (1.0 - sat) * (1.0 - sat);"]
|
|
|
|
- name: skin_tones_are_protected
|
|
why: |
|
|
The reason vibrance exists. A portrait pushed on plain saturation goes
|
|
orange long before the background improves.
|
|
expect_wgsl: ["let skin_guard = 1.0 - is_skin * 0.5;"]
|