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:
@@ -1,6 +1,8 @@
|
||||
// The tone ramp is drawn through a BASE layer, and that base is now a blurred
|
||||
// CHILD of the pass rather than a ring of point samples inside it — see
|
||||
// TONE_BASE_RADIUS in toneShader.ts for the mottle that ring caused. Two things
|
||||
// The tone ramp is drawn through a BASE layer, and that base is a CHILD of the
|
||||
// pass — the edge-aware pass of TONE_BASE_SKSL, run by the caller — rather than a
|
||||
// ring of point samples inside it or a blur of it: see TONE_BASE_RADIUS in
|
||||
// toneShader.ts for the mottle a ring caused and for the halo a gaussian one
|
||||
// does. Two things
|
||||
// can go wrong with that and neither is a crash: the shader stops compiling once
|
||||
// it takes a second child (SkSL is only checked at runtime, and no other check
|
||||
// here compiles TONE_SKSL as a whole), or the second child is wired to the wrong
|
||||
@@ -34,12 +36,16 @@ writeFileSync(
|
||||
),
|
||||
);
|
||||
const {
|
||||
TONE_SKSL, TONE_BASE_RADIUS, TONE_BASE_SIGMA, getToneUniforms, toneUniformArray, toneIsActive,
|
||||
TONE_KNOBS,
|
||||
TONE_SKSL, TONE_BASE_SKSL, TONE_BASE_RADIUS, TONE_BASE_SIGMA, TONE_BASE_TAPS, toneBaseStep,
|
||||
getToneUniforms, toneUniformArray, toneIsActive, TONE_KNOBS,
|
||||
} = await import(pathToFileURL(join(dir, 'toneShader.mjs')).href);
|
||||
|
||||
// The shader has to NAME a base child — a ring of taps would not need one.
|
||||
// The shader has to NAME a base child — a ring of taps would not need one — and
|
||||
// the base pass has to take its own two, one per axis.
|
||||
assert.match(TONE_SKSL, /uniform shader base;/);
|
||||
assert.match(TONE_BASE_SKSL, /uniform shader src;/);
|
||||
assert.match(TONE_BASE_SKSL, /uniform float2 dir;/);
|
||||
assert.ok(TONE_BASE_TAPS > 0 && toneBaseStep(1200, 600) * TONE_BASE_TAPS >= 1200 * TONE_BASE_RADIUS - 0.5);
|
||||
|
||||
const CanvasKit = await CanvasKitInit({
|
||||
locateFile: () => fileURLToPath(new URL('../node_modules/canvaskit-wasm/bin/full/canvaskit.wasm', import.meta.url)),
|
||||
@@ -118,17 +124,34 @@ const t = srcValue / 255;
|
||||
const b = baseValue / 255;
|
||||
// The neighbourhood's new luma, plus the pixel's own DIFFERENCE from the base —
|
||||
// Base' + Detail, the reconstruction the shader emits (see the note in
|
||||
// toneRamp). The ratio Base' * (Input / Base) is what this used to predict, and
|
||||
// it is what took the texture out of the frame at BLACK -100.
|
||||
const diff = Math.abs(t - b);
|
||||
const smooth = (x) => x * x * (3 - 2 * x);
|
||||
const expected = Math.round(255 * clamp01(ramp(t, { sh: 1 }).o));
|
||||
// toneRamp). Two other reconstructions were here before it: the ramp read at the
|
||||
// PIXEL (a global curve, and the local-contrast report), and Base' * (Input /
|
||||
// Base), the gain-weighted detail that took the texture out of the frame at
|
||||
// BLACK -100.
|
||||
const detail = (pixel, base) => clamp01(ramp(base, { sh: 1 }).o + (pixel - base));
|
||||
const expected = Math.round(255 * detail(t, b));
|
||||
|
||||
const got = render(srcValue, baseValue);
|
||||
assert.ok(
|
||||
Math.abs(got - expected) <= 2,
|
||||
`a base of ${b.toFixed(4)} under a pixel of ${t.toFixed(4)} gave ${got}, the ramp predicts ${expected}`,
|
||||
);
|
||||
// The same instruction on the other side of the pixel: a base BRIGHTER than it
|
||||
// takes the neighbourhood's move and hands the pixel its own difference back, so
|
||||
// the pixel lands BELOW the neighbourhood's new luma. One move per
|
||||
// neighbourhood, not one per pixel — which is exactly what a global curve
|
||||
// cannot say.
|
||||
const brighterValue = 180;
|
||||
const brighter = render(srcValue, brighterValue);
|
||||
const brighterWant = Math.round(255 * detail(t, brighterValue / 255));
|
||||
assert.ok(
|
||||
Math.abs(brighter - brighterWant) <= 2,
|
||||
`a base of ${(brighterValue / 255).toFixed(4)} over a pixel of ${t.toFixed(4)} gave ${brighter}, Base'+Detail predicts ${brighterWant}`,
|
||||
);
|
||||
assert.ok(
|
||||
brighter < got,
|
||||
`a brighter base did not pull the pixel down (${brighter} against ${got}) — the base is not in the move`,
|
||||
);
|
||||
// With the base handed in as the sharp image the difference is exactly zero and
|
||||
// the pass is the GLOBAL move — the ramp at the pixel — which is what a mask (no
|
||||
// neighbourhood of its own) needs it to be. It is NOT the identity any more:
|
||||
@@ -161,6 +184,8 @@ assert.equal(
|
||||
|
||||
console.log(
|
||||
`tone base ok: pixel ${srcValue} over a base of ${baseValue} -> ${got} ` +
|
||||
`(ramp predicts ${expected}, global move ${globalMove}, sharp-base ${selfBase}); ` +
|
||||
`radius ${TONE_BASE_RADIUS} of the frame, sigma ${TONE_BASE_SIGMA} of it`,
|
||||
`(Base'+Detail predicts ${expected}, global move ${globalMove}, sharp-base ${selfBase}); ` +
|
||||
`over a base of ${brighterValue} -> ${brighter} (predicts ${brighterWant}); ` +
|
||||
`radius ${TONE_BASE_RADIUS} of the frame, ${TONE_BASE_TAPS} taps per side, step ` +
|
||||
`${toneBaseStep(1200, 600)} px at 1200, sigma ${TONE_BASE_SIGMA} of the radius`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user