web: fix halo artifacts on highlight/shadow/white/black adjustments with edge-aware tone mapping and update docker packages
This commit is contained in:
@@ -35,7 +35,7 @@ writeFileSync(
|
||||
);
|
||||
const {
|
||||
TONE_SKSL, TONE_BASE_RADIUS, TONE_BASE_SIGMA, getToneUniforms, toneUniformArray, toneIsActive,
|
||||
TONE_BLACK_EDGE, TONE_WHITE_EDGE, TONE_BLACK_LIFT, TONE_BLACK_CRUSH, TONE_HIGH_GAIN, TONE_WHITE_GAIN,
|
||||
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.
|
||||
@@ -85,54 +85,31 @@ function render(srcValue, baseValue) {
|
||||
return pixels[0];
|
||||
}
|
||||
|
||||
// The ramp, in the same arithmetic the shader runs: four COMPACT windows of the
|
||||
// encoded luma — BLACK's toe dying on 0.18, SHADOW's bell over the deep tones,
|
||||
// HIGHLIGHT against the headroom that is left, WHITE's Hermite on the shoulder —
|
||||
// applied ONE AFTER THE OTHER and clamped on the way to the next, so the four
|
||||
// moves compose instead of summing and the ramp needs no guard. SHADOW carries
|
||||
// the whole knob at its own window, so SHADOW +100 reads toneCurve(b, 0, 1, 0, 0)
|
||||
// (b and a1 are gone with the bumps; see the note at the head of toneRamp).
|
||||
// The ramp, in the same arithmetic the shader runs: ONE KNOB is the measured
|
||||
// knot table (TONE_KNOBS in toneShader.ts) drawn STRAIGHT between the knots,
|
||||
// scaled by the amount, added to the luma it reads and clamped — the minus side
|
||||
// of zero reads the minus table — and the four are applied ONE AFTER THE OTHER
|
||||
// and clamped on the way to the next, so the four moves compose instead of
|
||||
// summing. SHADOW carries the whole knob on its own, so SHADOW +100 reads
|
||||
// toneCurve(b, 0, 1, 0, 0).
|
||||
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
||||
const smoothstep = (e0, e1, x) => {
|
||||
const u = clamp01((x - e0) / (e1 - e0));
|
||||
return u * u * (3 - 2 * u);
|
||||
};
|
||||
const toneBlackW = (L) => {
|
||||
const u = clamp01(1 - L / TONE_BLACK_EDGE);
|
||||
return u * u * u;
|
||||
};
|
||||
const toneShadowW = (L) => smoothstep(0.02, 0.12, L) * (1 - smoothstep(0.25, 0.55, L));
|
||||
const toneHighW = (L) => smoothstep(0.45, 0.65, L) * (1 - smoothstep(0.92, 1.0, L));
|
||||
const toneWhiteW = (L) => {
|
||||
const u = clamp01((L - TONE_WHITE_EDGE) / (1 - TONE_WHITE_EDGE));
|
||||
return u * u;
|
||||
};
|
||||
// toneBlack, as the shader emits it: the lift is the doc's own, the crush is the
|
||||
// toe's EXPONENT (monotone for any rate, and the only form that moves a visible
|
||||
// amount inside a window that is 1 only at the floor).
|
||||
const toneBlack = (L, bl) => {
|
||||
const q = toneBlackW(L);
|
||||
if (bl > 0) return L + TONE_BLACK_LIFT * bl * q * (Math.sqrt(L) - L);
|
||||
return L >= TONE_BLACK_EDGE
|
||||
? L
|
||||
: TONE_BLACK_EDGE * (L / TONE_BLACK_EDGE) ** (1 + TONE_BLACK_CRUSH * -bl * q);
|
||||
const clamp = (x, lo, hi) => Math.min(hi, Math.max(lo, x));
|
||||
const poly = (c, L) => {
|
||||
let v = c.v[0];
|
||||
for (let i = 1; i < c.p.length; i++)
|
||||
v += (c.v[i] - c.v[i - 1]) * clamp((L - c.p[i - 1]) / (c.p[i] - c.p[i - 1]), 0, 1);
|
||||
return v;
|
||||
};
|
||||
const knob = (L, k, amt) =>
|
||||
clamp01(L + (amt >= 0 ? amt * poly(TONE_KNOBS[k].plus, L) : -amt * poly(TONE_KNOBS[k].minus, L)));
|
||||
// toneCurve, move for move, with the clamp the shader puts after each one.
|
||||
// `afterBlack` is the L the SHADOW move reads.
|
||||
function toneCurve(L, bl, sh, hl, wh) {
|
||||
let q = toneBlackW(L);
|
||||
L = toneBlack(L, bl);
|
||||
L = clamp01(L);
|
||||
const afterBlack = L;
|
||||
q = toneShadowW(L);
|
||||
L *= 1 + sh * q * (1 - L) ** 1.8;
|
||||
L = clamp01(L);
|
||||
q = toneHighW(L);
|
||||
L += TONE_HIGH_GAIN * hl * q * Math.max(L - 0.5, 0) ** 1.5 * (1 - L) * (1 - L);
|
||||
L = clamp01(L);
|
||||
q = toneWhiteW(L);
|
||||
L += TONE_WHITE_GAIN * wh * q * (1 - L) * L;
|
||||
return { o: clamp01(L), afterBlack };
|
||||
const afterBlack = knob(L, 'bl', bl);
|
||||
return {
|
||||
o: knob(knob(knob(afterBlack, 'sh', sh), 'hl', hl), 'wh', wh),
|
||||
afterBlack,
|
||||
};
|
||||
}
|
||||
const ramp = (b, k = {}) => toneCurve(b, k.bl ?? 0, k.sh ?? 0, k.hl ?? 0, k.wh ?? 0);
|
||||
const srcValue = 128; // the pixel: 0.501961 encoded
|
||||
@@ -143,7 +120,11 @@ const b = baseValue / 255;
|
||||
// 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 expected = Math.round(255 * clamp01(ramp(b, { sh: 1 }).o + (t - b)));
|
||||
const diff = Math.abs(t - b);
|
||||
const smooth = (x) => x * x * (3 - 2 * x);
|
||||
const edgeW = 1.0 - (diff <= 0.08 ? 0 : diff >= 0.35 ? 1 : smooth((diff - 0.08) / 0.27));
|
||||
const effBase = t * (1 - edgeW) + b * edgeW;
|
||||
const expected = Math.round(255 * clamp01(ramp(effBase, { sh: 1 }).o + (t - effBase)));
|
||||
|
||||
const got = render(srcValue, baseValue);
|
||||
assert.ok(
|
||||
|
||||
Reference in New Issue
Block a user