fix(tone): read the ramp's base from the pixel's own neighbourhood, so highlights and shadows move a region and keep the detail in it
This commit is contained in:
@@ -69,6 +69,10 @@ const {
|
||||
TONE_BLACK_EDGE,
|
||||
TONE_BASE_RADIUS,
|
||||
TONE_BASE_SIGMA,
|
||||
TONE_BASE_TAPS,
|
||||
TONE_BASE_RANGE_EV,
|
||||
TONE_BASE_SKSL,
|
||||
toneBaseStep,
|
||||
toneUniformArray,
|
||||
} = await import(pathToFileURL(join(dir, 'toneShader.mjs')).href);
|
||||
|
||||
@@ -187,11 +191,24 @@ assert.doesNotMatch(sksl, /float blackA = |float whiteA = /, 'the amplitudes are
|
||||
assert.doesNotMatch(sksl, /o \+= dr \* 0\.12/, 'DR is an additive term again — it folds the flat stretch at 0.238');
|
||||
assert.doesNotMatch(sksl, /float lin\(/, 'the linear-light knee ahead of the knobs came back');
|
||||
assert.doesNotMatch(sksl, /clamp\(0\.25 \+ 0\.25/, 'a knob is clamped against a neighbour knot again');
|
||||
assert.match(sksl, /float target = clamp\(o, 0\.0, 1\.0\);/);
|
||||
assert.doesNotMatch(sksl, /float target = base > 0\.0004 \? o \* t \/ base : t;/, 'the gain-scaled detail came back — a tone knob softens again');
|
||||
// The ramp is read at the BASE (Base' + Detail), and the pixel's own difference
|
||||
// from it rides through un-scaled. These pin the two lines the whole tone pass
|
||||
// hangs on: read at `t`, a knob is a global curve again (the HIGHLIGHT/SHADOW
|
||||
// local-contrast report), and scaled by the neighbourhood's gain, a knob softens
|
||||
// the frame's texture into its level (the report before that).
|
||||
assert.match(sksl, /float o = toneCurve\(base, blVal, shVal, hlVal, whVal\);/);
|
||||
assert.match(sksl, /float target = clamp\(o \+ \(t - base\), 0\.0, 1\.0\);/);
|
||||
assert.doesNotMatch(sksl, /float o = toneCurve\(t, /, 'the ramp is read at the pixel again — a global curve');
|
||||
assert.match(sksl, /vec3 res = lightMove\(c, t, target\);/);
|
||||
assert.doesNotMatch(sksl, /float target = base > 0\.0004 \? o \* t \/ base : t;/, 'the gain-scaled detail came back — a tone knob softens again');
|
||||
assert.match(sksl, /vec3 res = lightMove\(c, t, target\);/);
|
||||
// The base layer itself: an edge-aware pass, not the gaussian the ramp fell back
|
||||
// to (a gaussian average lands ACROSS an edge, and the halo follows the knob).
|
||||
assert.match(TONE_BASE_SKSL, /uniform float2 dir;/);
|
||||
assert.match(TONE_BASE_SKSL, /float ref = toneBaseMed3\(/);
|
||||
assert.match(TONE_BASE_SKSL, /float d1 = \(a1 - ref\) \/ \(ref \+ TONE_BASE_PED\) \/ TONE_BASE_RANGE;/);
|
||||
assert.doesNotMatch(TONE_BASE_SKSL, /toneBaseMed3\(l0, ref, /, 'the reference is the median of the wrong triple');
|
||||
assert.ok(toneBaseStep(1200, 600) === 4 && toneBaseStep(400, 300) === 1 && toneBaseStep(4000, 3000) === 13,
|
||||
`the base step drifted: ${toneBaseStep(1200, 600)}, ${toneBaseStep(400, 300)}, ${toneBaseStep(4000, 3000)}`);
|
||||
assert.doesNotMatch(sksl, /lightMove\(c, t, clamp\(o, 0\.0, 1\.0\)\)/, 'the ramp is read at the pixel again — a global curve');
|
||||
assert.doesNotMatch(sksl, /mix\(a0, a1, smoothstep/, 'the ramp is smoothstepped again');
|
||||
// The linear-light knee that used to run ahead of all this is GONE from the tone
|
||||
@@ -447,8 +464,8 @@ function lightMove(rgb, t, o) {
|
||||
function rebuild(rgb, k, base) {
|
||||
const t = lumaOf(rgb);
|
||||
const b = base ?? t;
|
||||
const o = ramp(t, k).o;
|
||||
const target = clamp01(o);
|
||||
const o = ramp(b, k).o;
|
||||
const target = clamp01(o + (t - b));
|
||||
const out = lightMove(rgb, t, target);
|
||||
return { out, clamped: out.map((c) => clamp01(c)), o, t, base: b, target };
|
||||
}
|
||||
@@ -543,8 +560,9 @@ for (const [rgb, knobs] of [
|
||||
//
|
||||
// read at the pixel every pixel lands on its own o, so the band's spread
|
||||
// comes out multiplied by whatever slope the curve has
|
||||
// over it — 1.2347 of its own spread at this deflection,
|
||||
// the SHADOW +100 table steepening through 0.26..0.50.
|
||||
// over it — 0.779 of its own spread at this deflection,
|
||||
// the SHADOW +100 table flattening through 0.26..0.50
|
||||
// (its +0.25 lift saturates and the knots bend over).
|
||||
// read at the base every pixel of ONE neighbourhood takes the same move,
|
||||
// o(base) - base, and its own difference from the base is
|
||||
// added to it, so the texture inside the region comes out
|
||||
@@ -560,6 +578,14 @@ const band = Array.from({ length: 32 }, (_, i) => 0.26 + 0.24 * (i / 31));
|
||||
const spread = (xs) => Math.max(...xs) - Math.min(...xs);
|
||||
const bandBase = band.reduce((a, b) => a + b, 0) / band.length;
|
||||
const movedGlobally = band.map((t) => rebuild([t, t, t], { sh: 1 }).clamped[0]);
|
||||
// ...and measured, so the two readings cannot drift apart in the source while
|
||||
// the numbers above stay prose: the global one scales the band by the curve's
|
||||
// slope (0.78 at SHADOW +100 — the lift saturates over this quarter and the
|
||||
// texture shrinks with it), the base one moves it and leaves its spread alone.
|
||||
const movedOnBase = band.map((t) => rebuild([t, t, t], { sh: 1 }, bandBase).clamped[0]);
|
||||
const slopeOverBand = spread(movedGlobally) / spread(band);
|
||||
assert.ok(slopeOverBand < 0.9, `the SHADOW +100 table stopped flattening through 0.26..0.50: ${slopeOverBand}`);
|
||||
near4(spread(movedOnBase), spread(band));
|
||||
|
||||
// Every knob on zero is the identity, whatever base is handed in
|
||||
for (const b of [0.01, 0.1, 0.35, 0.7, 0.99])
|
||||
|
||||
Reference in New Issue
Block a user