light: the pixel rides o/t, not a held chroma — SHADOW and BLACK drained a dark red to 0.505 of its saturation, it is 0.742 now

This commit is contained in:
2026-09-30 16:26:42 +07:00
parent 75ff444621
commit 851563ed73
2 changed files with 55 additions and 34 deletions
+27 -17
View File
@@ -139,24 +139,34 @@ float lin(float e0, float e1, float x) {
// between 0.25 and 0.5, and DR's own shadow lift slopes DOWN through that
// stretch, which is a fold at 0.238.
//
// Lightness takes the curve; the colour rides the difference. The pixel moves to
// its new luma and carries its own chroma with it — the three channel
// differences are scaled by ONE number, so the hue cannot move and a grey cannot
// pick up a cast (a neutral has no difference to carry, and lands on o exactly).
// Lightness takes the curve; the colour rides the ratio. The pixel moves to its
// new luma, and it gets there by scaling its three channels by ONE number,
// k = o / t: the doc's R_new = R_old * Luma_new / Luma_old. The differences move
// with the light, so the hue cannot turn, the saturation cannot be drained by a
// lift — an HSL saturation is a ratio of differences and never sees a common
// scale — and a neutral, with no difference to carry, lands on o exactly.
//
// The doc's ratio (R_new = R_old * Luma_new / Luma_old) is the other reading of
// the same sentence, and it is what this pass used to do. It is exact — until
// the result stops fitting. Past 1.0 a channel clips, the differences stop being
// scaled together, and the hue goes with them: measured on the scratchpad probe
// (hl-variants.mjs), a skin tone at 24.0° came back at 48.0° at HIGHLIGHT +100,
// and a warm white at 37° at 57.4°. Under L = 0.5 the same ratio also multiplies
// whatever cast a near-black pixel had — x30 on a shadow with a hair of warmth,
// which is colour noise amplified, the reason the old arbitrary 0.55..1.35 clamp
// was there.
// The scale used to be 1.0 instead: keep the chroma, move the luma. That holds
// the hue and loses the colour, which is the complaint the ratio answers — the
// scratchpad probe (sh-bl-hue.mjs) measured a dark red at 0.746 of saturation
// coming back at 0.505 with SHADOW at +100, and at 0.370 with SHADOW and BLACK
// both at +100. A chroma held under a rising luma IS a colour going grey, and it
// is the two sliders that lift a shadow (SHADOW, BLACK) that raise the luma of a
// dark pixel the furthest.
//
// So the scale is the chroma's own (1.0) and the only thing that pulls it back
// is the cube: a pixel with no room left gives up saturation instead of hue, and
// one that the curve has actually driven to 1.0 arrives at white.
// What the ratio cannot do on its own is fit, and that is what the caps below are
// for: past the ceiling a channel clips outright, the three stop being scaled
// together and the hue goes with them (measured on the other probe, a skin tone
// at 24.0° came back at 48.0° at HIGHLIGHT +100, a warm white at 37° at 57.4°).
// Pulling the ONE scale back while the cube has room for no more costs saturation
// instead: a pixel the cube is against arrives neutral before it arrives wrong,
// and one the curve has driven all the way to 1.0 arrives at white.
//
// Below t = 0.0004 there is no ratio worth the name: dividing by what is left of
// a pixel that has almost no light on it takes whatever cast the last code value
// of 8-bit noise left there and multiplies it by the pedestal the BLACK knob just
// lifted — colour noise, amplified to the size of the lift. The scale stays 1.0
// down there and the pixel takes the pedestal as the flat grey it is.
//
// ONE move of the light, and everything in this file that changes how bright a
// pixel is goes through it: a tone knob, a mask's tone knob, and the exposure
@@ -166,7 +176,7 @@ float lin(float e0, float e1, float x) {
// a channel reaches the ceiling, where the three clip by different amounts and
// the hue goes with them.
vec3 lightMove(vec3 c, float t, float o) {
float k = 1.0;
float k = t > 0.0004 ? o / t : 1.0;
float hiC = max(max(c.r, c.g), c.b);
float loC = min(min(c.r, c.g), c.b);
if (hiC > t) k = min(k, (1.0 - o) / (hiC - t));