fix(raw): keep the blown highlights neutral after the tone curve

The blown gate draws a clipped pixel at its own maximum -- one value across
the three channels, so the frame's highlights carry no cast -- but the
per-channel tone curve runs after it and re-tints what the gate had just
made neutral. One cubic a channel, fitted on a grid that has no block left
to fit where the frame ran out (previewMatch drops the fully blown ones),
so at the plateau the three curves agree only at 1.0 and part company
either side of it.

On the ORF this was reported on, the moonlit sky came back 254,255,255 and
253,255,254 -- red under green across a quarter of the frame.

The gate is the develop's own statement that the pixel had no colour of its
own, so it is re-read on the value that leaves the shader. The blown pixels
now measure 254.95,254.94,254.97 (R/G 1.0000) against 253.44,254.94,254.51
(0.9941) before; their two most common triples, 254,255,255 at 149315 and
253,255,254 at 122127, collapse to 255,255,255 at 278099. The bright bands
land at -0.0,-0.0,-0.0 against the embedded preview where they were
-2.1,-0.0,-1.0, and midtones and the lower half are untouched.
This commit is contained in:
2026-10-06 10:45:15 +07:00
parent 9aa9bc0db0
commit ce691aa8cb
2 changed files with 46 additions and 24 deletions
+11 -2
View File
@@ -140,7 +140,8 @@ half4 main(float2 pos) {
// mx is the value to hold: it is the pixel's own lightness.
float hi = max(max(n.r, n.g), n.b);
hi = max(hi, max(max(lin.r, lin.g), lin.b));
rgb = mix(rgb, float3(mx), smoothstep(0.99, 1.02, hi));
float blown = smoothstep(0.99, 1.02, hi);
rgb = mix(rgb, float3(mx), blown);
// The overflow used to fade towards white — mix(rgb / mx, 1, 1 - 1 / mx) —
// which put every pixel of a blown sky on exactly 1.0 and threw the two stops
// the sensor held above white away with it: LIGHT's HIGHLIGHT row then had a
@@ -173,7 +174,15 @@ half4 main(float2 pos) {
// fitted against (see previewMatch.ts). No rolloff: white is one of the values
// the curve is fitted on, and it is pinned there, so a blown pixel still lands on
// white while the frame's own highlights stay where the knee above left them.
return half4(half3(tone(t0, e.r), tone(t1, e.g), tone(t2, e.b)), 1.0);
float3 o = float3(tone(t0, e.r), tone(t1, e.g), tone(t2, e.b));
// ...and the neutral the gate above drew is what the curve undoes: one cubic a
// channel, fitted on a grid that has no block left to fit where the frame ran out
// (the blown blocks are excluded, see previewMatch), so at the plateau the three
// curves agree only at 1.0 and part company either side of it — measured on the
// ORF, a 255,255,255 sky came back 254,255,255 and 253,255,254, red under green
// across a quarter of the frame. The gate is the develop's own statement that the
// pixel had no colour of its own, so it is re-read here, on the value that leaves.
return half4(half3(mix(o, float3(max(max(o.r, o.g), o.b)), blown)), 1.0);
}
`;