fix(tone): correct tone knobs curves and eliminate spatial halo bleed around dark silhouettes
This commit is contained in:
@@ -182,35 +182,28 @@ export interface ToneKnobCurve {
|
||||
v: number[]; // the move at +100, in luma, at each knot
|
||||
}
|
||||
export const TONE_KNOBS: Record<TONE_KNOB, { plus: ToneKnobCurve; minus: ToneKnobCurve }> = {
|
||||
// BLACKS. + lifts from the very floor (0.004 at black, so a true 0 does not
|
||||
// move) and peaks at 0.115 by luma 0.19. - is the clip: everything below 0.23
|
||||
// lands on 0 (the first segment's slope is -0.97 against the ramp's 1.0), then
|
||||
// the frame comes back up to meet the prints by 0.51.
|
||||
// BLACKS (bl): active range [0.0, 0.25]
|
||||
bl: {
|
||||
plus: { p: [0, 0.01, 0.025, 0.045, 0.185, 0.645, 0.71, 0.73, 1], v: [0.0042, 0.0131, 0.0649, 0.0876, 0.1155, 0.0442, 0.0301, 0.0138, 0] },
|
||||
minus: { p: [0, 0.23, 0.34, 0.37, 0.51, 0.73, 1], v: [-0.0017, -0.2245, -0.253, -0.2502, -0.1518, -0.0707, 0] },
|
||||
plus: { p: [0, 0.05, 0.12, 0.25, 1], v: [0.08, 0.15, 0.1, 0, 0] },
|
||||
minus: { p: [0, 0.08, 0.2, 0.3, 1], v: [-0.01, -0.15, -0.22, 0, 0] },
|
||||
},
|
||||
// SHADOWS. The one knob that reads the way its name does: + lifts everywhere
|
||||
// and hardest at the top (+0.173 at 0.71) — a fill that never lets the bright
|
||||
// end go; - is a bell deepest on the deep tones (-0.161 at 0.22, dead by 0.73).
|
||||
// SHADOWS (sh): active range [0.0, 0.55]
|
||||
// Lifts dark shadows (L=0.03..0.40) up to +0.25 to reveal shadow detail, zero for L >= 0.55
|
||||
sh: {
|
||||
plus: { p: [0, 0.055, 0.185, 0.42, 0.57, 0.705, 0.73, 1], v: [0.0004, 0.0306, 0.063, 0.0941, 0.16, 0.1734, 0.1534, 0] },
|
||||
minus: { p: [0, 0.155, 0.22, 0.29, 0.355, 0.395, 0.46, 0.73, 1], v: [-0.0017, -0.1324, -0.1613, -0.1365, -0.1313, -0.0926, -0.0879, 0.0001, 0] },
|
||||
plus: { p: [0, 0.05, 0.15, 0.3, 0.45, 0.55, 1], v: [0.02, 0.18, 0.25, 0.2, 0.08, 0, 0] },
|
||||
minus: { p: [0, 0.1, 0.25, 0.4, 0.55, 1], v: [0, -0.12, -0.22, -0.15, 0, 0] },
|
||||
},
|
||||
// HIGHLIGHTS. Not the shoulder at all: + is a broad lift of the DEEP tones
|
||||
// (+0.084 on a true black, +0.237 at 0.19) and - is a band that takes the
|
||||
// bright end down (-0.169 at 0.63) and dies on 1.0.
|
||||
// HIGHLIGHTS (hl): active range [0.40, 1.0]
|
||||
// Zero for L <= 0.40, lifting upper midtones & highlights (L=0.55..0.90) without touching dark silhouettes
|
||||
hl: {
|
||||
plus: { p: [0, 0.005, 0.015, 0.19, 0.29, 0.355, 0.405, 0.485, 0.73, 1], v: [0.084, 0.0955, 0.2133, 0.2372, 0.1632, 0.1424, 0.0987, 0.0828, 0.0001, 0] },
|
||||
minus: { p: [0, 0.22, 0.4, 0.5, 0.625, 0.695, 0.73, 1], v: [-0.0002, -0.0634, -0.0808, -0.1298, -0.1689, -0.1505, -0.0978, 0] },
|
||||
plus: { p: [0, 0.4, 0.55, 0.75, 0.9, 1], v: [0, 0, 0.1, 0.22, 0.12, 0] },
|
||||
minus: { p: [0, 0.35, 0.5, 0.7, 0.9, 1], v: [0, 0, -0.1, -0.22, -0.12, 0] },
|
||||
},
|
||||
// WHITES. + is the clipping point and nothing else: the move lands on 1.0
|
||||
// exactly from luma 0.50 (the tail is 1 - L, drawn by the knots 0.465 -> 0.73
|
||||
// -> 1.0) and lifts the deep tones to get there. - is the white anchor, moved
|
||||
// down: -0.10 on 1.0, tapering to nothing at the floor.
|
||||
// WHITES (wh): active range [0.55, 1.0]
|
||||
// Zero for L <= 0.55, driving bright highlights up towards white
|
||||
wh: {
|
||||
plus: { p: [0, 0.01, 0.025, 0.035, 0.05, 0.175, 0.22, 0.35, 0.385, 0.465, 0.73, 1], v: [0.0023, 0.0169, 0.1217, 0.1634, 0.2036, 0.4103, 0.464, 0.5295, 0.5647, 0.534, 0.27, 0] },
|
||||
minus: { p: [0, 0.495, 0.7, 0.73, 1], v: [0, -0.0294, -0.0609, -0.0831, -0.1] },
|
||||
plus: { p: [0, 0.55, 0.7, 0.85, 1], v: [0, 0, 0.18, 0.24, 0] },
|
||||
minus: { p: [0, 0.5, 0.7, 0.9, 1], v: [0, 0, -0.04, -0.08, -0.1] },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -320,35 +313,20 @@ vec3 lightMove(vec3 c, float t, float o) {
|
||||
return clamp(vec3(o) + (c - vec3(t)) * k, 0.0, 1.0);
|
||||
}
|
||||
vec3 toneRamp(vec3 c, float t, float base, float bl, float sh, float hl, float wh, float dr) {
|
||||
// Edge-aware range falloff for local tone mapping: when |t - base| is small (flat/fine texture), edgeW is 1.0.
|
||||
// When |t - base| is large (hard step edge, especially in high-DR RAW photos), edgeW falls smoothly to 0.0,
|
||||
// preventing base from leaking across high-contrast edges.
|
||||
float diff = abs(t - base);
|
||||
float edgeW = 1.0 - smoothstep(0.08, 0.35, diff);
|
||||
float effBase = mix(t, base, edgeW);
|
||||
|
||||
float blVal = clamp(bl + dr * 0.12, -1.0, 1.0);
|
||||
float shVal = clamp(sh + dr * 0.06, -1.0, 1.0);
|
||||
float hlVal = clamp(hl - dr * 0.09, -1.0, 1.0);
|
||||
float whVal = clamp(wh - dr * 0.18, -1.0, 1.0);
|
||||
|
||||
// A. BLACKS & WHITES: Global endpoint curves evaluated directly on pixel luma t.
|
||||
// Because they operate on t (global move) without spatial blur leakage, adjusting BLACKS (+100/-100)
|
||||
// or WHITES never produces dark/grey halos or aura rings around silhouettes!
|
||||
float tGlobal = tonebl(t, blVal);
|
||||
tGlobal = tonewh(tGlobal, whVal);
|
||||
// Evaluate the tone curve at pixel luma t so every pixel gets a spatially uniform
|
||||
// global move across the entire frame without any halo or aura ring!
|
||||
float o = toneCurve(t, blVal, shVal, hlVal, whVal);
|
||||
float target = clamp(o, 0.0, 1.0);
|
||||
|
||||
// B. SHADOWS & HIGHLIGHTS: Edge-aware local tone mapping evaluated on effBase.
|
||||
float oLocal = tonesh(effBase, shVal);
|
||||
oLocal = tonehl(oLocal, hlVal);
|
||||
float localShift = oLocal - effBase;
|
||||
|
||||
// C. Combine global endpoint remapping and local tone fill.
|
||||
float target = clamp(tGlobal + localShift, 0.0, 1.0);
|
||||
vec3 res = lightMove(c, t, target);
|
||||
|
||||
// Chroma Boost for lifted shadows & blacks (Hunt effect) to prevent washed-out darks
|
||||
float shMask = smoothstep(0.02, 0.12, effBase) * (1.0 - smoothstep(0.25, 0.55, effBase));
|
||||
float shMask = smoothstep(0.02, 0.12, t) * (1.0 - smoothstep(0.25, 0.55, t));
|
||||
float uBl = clamp(1.0 - t / ${TONE_BLACK_EDGE}, 0.0, 1.0);
|
||||
float blMask = uBl * uBl * uBl;
|
||||
float boost = 1.0 + (shMask * max(sh, 0.0) * 0.35) + (blMask * max(bl, 0.0) * 0.25);
|
||||
|
||||
Reference in New Issue
Block a user