web: make a mask's knobs the moves the frame-wide row of the same name makes
A gradient mask carried its own copy of the six tonal and spatial formulas, and
four of them had drifted from the columns of the same name. HIGHLIGHT was
inverted: the single shift `0.5 * (shadows*ms - highlights*mh)` put the knob's
`x` on the shadow mask and its `y` on the highlight mask, so turning HIGHLIGHT
up pulled the bright band DOWN and turning it down lifted it — measured at the
mask, +9 moved the top band -0.2295 and -9 moved it +0.0454, and the whole
frame's HIGHLIGHT row reads the other way. WHITE and BLACK were a flat gain on
the end each one owns (`c * (1 + 0.5*k*mh)`), which scales every pixel above the
midtone by the same fraction and so drags the near-whites into the greys rather
than leaving them white: a lowered WHITE took the top band down -0.2117 while
the middle band moved -0.0036, and a raised BLACK pushed the middle band up
+0.0195 for +0.0414 at the bottom — the lift went everywhere except where it was
asked for. CLARITY's negative side was the same unsharp as its positive side
with the sign flipped — `c + k*(c - blur)` with k negative — which is a soften
only in name: it sank the mask's whites (top band -0.0543 at -9) and left the
mask's own contrast where it was (dhp -0.0008), the opposite of what the knob is
named for.
DEHAZE ran after CLARITY, so a mask sharpened its haze and then tried to remove
it; frame-wide the two are the other way round, and for a reason.
The four now mean inside a mask what they mean on the whole frame, because
toneShader.ts is the reference the app's own rows are written from and the same
name on the same knob should not be two different moves. HIGHLIGHT and SHADOW
are TONE_SKSL's additive luma shifts — HIGHLIGHT weighted by the headroom it has
left (1 - t) so it cannot drag a blown white to grey, SHADOW by its own floor —
with the colour difference riding along at TONE_SKSL's damped gain so a lift
cannot collapse a colour. WHITE and BLACK are TONE_SKSL's per-channel point
moves, cubic in each channel's distance from the end it owns, so the toe and the
shoulder move and the midtones stay put. DEHAZE runs before CLARITY, the
frame-wide order. CLARITY's negative side is a real soften, `mix(c, blur, -k)`
toward the same bilateral reference its positive side works against. TONE_SKSL's
two smoothsteps (0.50..1.00 and 0.00..0.55) replace the mask shader's own pair,
and the soft masks are computed in float and narrowed once, the way the
frame-wide shader does it.
Measured in one harness, one photo, one session (the mask's own middle box, knob
at +-9, before -> after on the mask, with the frame-wide knob of the same name
as the reference it is now written from):
- HIGHLIGHT +9: top band -0.2295 -> +0.0298 (frame-wide +0.0547), dark band
0.0000 -> -0.0001 — the lift is a lift, and the inversion is gone.
HIGHLIGHT -9: +0.0454 -> -0.0385 (frame-wide -0.0674).
- WHITE -9: top band -0.2117 -> -0.0646 (frame-wide -0.0860) — a lowered
white stays a white instead of becoming a grey — while the middle band goes
-0.0036 -> -0.0150 (frame-wide -0.0139), which is the move a white ends up
making when it is a point move rather than a gain.
- BLACK +9: bottom band +0.0414 -> +0.0997 (frame-wide +0.1036) and the middle
+0.0195 -> +0.0347 (frame-wide +0.0402) — it goes to the toe it owns.
- CLARITY -9: dhp -0.0008 -> -0.0149 (frame-wide -0.0200) — negative CLARITY
softens now — and the top band -0.0543 -> -0.0113, so it no longer pays for
that soften by sinking the mask's whites. CLARITY +9 was already right
(+0.0333 both sides) and stays.
- DEHAZE, the one knob with nothing on the negative side: unchanged at -9
(-0.1490 both sides, the pass order was the only thing wrong with it), and
the mask and the frame-wide row now agree across the whole range
(1/3/5/7/9 at -0.0124/-0.0397/-0.0711/-0.1074/-0.1490 on the mask against
-0.0133/-0.0423/-0.0759/-0.1151/-0.1619 frame-wide), monotone.
DEHAZE's own numbers are therefore not a mask-only bug: an estimate of the haze
that reads a mask differently from the frame would be inside `atmosphericLight`
and `DEHAZE_MAX_OMEGA`, which both paths share, and changing either moves the
frame-wide DEHAZE column too — left as it is rather than changed under a mask
report.
Everything else about the mask is untouched: `dctrl` is +0.0000 on all six
knobs (a knob still moves the mask's own pixels and nothing outside it), the
shader compiles and the stage draws with no page error.
Not ported: nothing. `shared/utils/gradientMask.ts` is the web engine's own file
and the phone's renderer has no gradient mask to mirror.
Probes: measure-mask-knobs (the six knobs on a selected mask, before and after),
measure-frame-knobs (the same six frame-wide, the reference the mask is now
written from), measure-parity (both phases in one run so the two are the same
photo in the same session), png-parity-report (the before half of that run died
in its frame phase and left no log, so its already-captured mask frames are
re-read off the PNGs with the same box and the same bands), measure-dehaze-curve
(DEHAZE 1..9 on the mask against 1..9 frame-wide, for monotonicity and for the
pass order).
This commit is contained in:
@@ -145,16 +145,19 @@ export function maskUniforms(
|
||||
return u;
|
||||
}
|
||||
|
||||
// The colour inside a mask, in the spec's own order and its own formulas:
|
||||
// exposure first (a power of two, so a stop is a stop), then contrast about the
|
||||
// middle, then saturation as a mix away from the pixel's own REC-709 luma. Then
|
||||
// the spec's section 2 and 3 on top — HIGHLIGHT and SHADOW through the two
|
||||
// smoothstep soft masks its own formula names (`M_shadow = 1 - smoothstep(0,
|
||||
// 0.5, L)`, `M_highlight = smoothstep(0.5, 1, L)`), WHITE and BLACK as a gain on
|
||||
// the end each one owns, and the two spatial ones against the blurred reference
|
||||
// the caller hands in. The result is clamped to the range a file can hold — the
|
||||
// spec's own guard, and it is `mix`ed back over the base by the mask's alpha, so
|
||||
// a mask at half strength is half of the move rather than the whole of it.
|
||||
// The colour inside a mask, in the spec's own order and, for the knobs the app
|
||||
// also has frame-wide, in the app's own formulas: exposure first (a power of
|
||||
// two, so a stop is a stop), then contrast about the middle, then saturation as
|
||||
// a mix away from the pixel's own REC-709 luma. Then the spec's section 2 and 3
|
||||
// on top — HIGHLIGHT and SHADOW through the two smoothstep soft masks its own
|
||||
// formula names, WHITE and BLACK as the per-channel point moves TONE_SKSL
|
||||
// makes, and the two spatial ones against the blurred reference the caller hands
|
||||
// in. Every one of them means inside the mask what it means on the whole frame
|
||||
// (toneShader.ts is the reference the four tonal ones are written from), because
|
||||
// the same name on the same knob should not be two different moves. The result
|
||||
// is clamped to the range a file can hold — the spec's own guard, and it is
|
||||
// `mix`ed back over the base by the mask's alpha, so a mask at half strength is
|
||||
// half of the move rather than the whole of it.
|
||||
//
|
||||
// `blur` is the frame's bilateral reference (the same one CLARITY and DEHAZE use
|
||||
// frame-wide) and `air` the atmospheric light; both are the constants 0 when the
|
||||
@@ -166,23 +169,29 @@ half3 maskAdjust(half3 c, float4 a, float4 tone, float4 fx${spatial ? ', half3 b
|
||||
c = c * half(pow(2.0, a.x));
|
||||
c = (c - half(0.5)) * half(1.0 + a.y) + half(0.5);
|
||||
half l = dot(clamp(c, half3(0.0), half3(1.0)), half3(0.2126, 0.7152, 0.0722));
|
||||
// The spec's soft masks, computed in float and narrowed: smoothstep on half
|
||||
// is one more type the shader does not have to guess at.
|
||||
// The tonal four, in TONE_SKSL's own formulas: a mask's HIGHLIGHT is meant to
|
||||
// be the same move as the whole-frame HIGHLIGHT on a smaller area, not a
|
||||
// second opinion about what the name means. HIGHLIGHT and SHADOW are additive
|
||||
// shifts of the luma, HIGHLIGHT weighted by the headroom left (1 - l) so it
|
||||
// cannot drag a blown white to grey, and the colour difference rides along at
|
||||
// a damped gain (TONE_SKSL's own cg) so a lift or a pull cannot collapse a
|
||||
// colour. WHITE and BLACK are per-channel point moves, cubic in each channel's
|
||||
// own distance from the end it owns: the toe and the shoulder move, the
|
||||
// midtones do not, and a white that is lowered stays white.
|
||||
// The spec's soft masks, computed in float and narrowed: smoothstep on half is
|
||||
// one more type the shader does not have to guess at.
|
||||
float lf = clamp(float(l), 0.0, 1.0);
|
||||
float ms = 1.0 - smoothstep(0.0, 0.5, lf);
|
||||
float mh = smoothstep(0.5, 1.0, lf);
|
||||
// Half a stop of tone at the knob's own ceiling, the weight the frame-wide
|
||||
// HIGHLIGHT/SHADOW pass uses; + lifts, - pulls back, and the two masks cannot
|
||||
// both be 1 at the same pixel.
|
||||
c = clamp(c + half3(half(0.5 * (tone.y * ms - tone.x * mh))), half3(0.0), half3(1.0));
|
||||
c = c * half(1.0 + 0.5 * tone.z * mh);
|
||||
c = c * half(1.0 + 0.5 * tone.w * ms);
|
||||
float mh = smoothstep(0.50, 1.00, lf);
|
||||
float ms = 1.0 - smoothstep(0.00, 0.55, lf);
|
||||
half lifted = half(clamp(lf + tone.x * mh * (1.0 - lf) + tone.y * 0.34 * ms, 0.0, 1.0));
|
||||
half cg = clamp(lifted / max(l, half(0.0004)), half(0.55), half(1.35));
|
||||
c = clamp(half3(lifted) + (c - half3(l)) * cg, half3(0.0), half3(1.0));
|
||||
half3 dk = half3(1.0) - c;
|
||||
c = clamp(c + half(tone.w * 0.18) * dk * dk * dk + half(tone.z * 0.18) * c * c * c, half3(0.0), half3(1.0));
|
||||
half nl = dot(clamp(c, half3(0.0), half3(1.0)), half3(0.2126, 0.7152, 0.0722));
|
||||
c = mix(half3(nl), c, half(1.0 + a.z));
|
||||
${spatial ? ` if (fx.x != 0.0) {
|
||||
// CLARITY (doc section 3.1): the pixel against its own blurred surroundings.
|
||||
c = clamp(c + half3(half(fx.x * ${CLARITY_GAIN.toFixed(1)})) * (c - blur), half3(0.0), half3(1.0));
|
||||
}
|
||||
${spatial ? ` // DEHAZE before CLARITY, the frame-wide order and for the frame-wide reason:
|
||||
// sharpening haze only makes it read as detail.
|
||||
if (fx.y != 0.0) {
|
||||
// DEHAZE (doc section 3.2): the dark channel of the patch is the haze.
|
||||
half3 aa = half3(half(max(air.x, 0.05)), half(max(air.y, 0.05)), half(max(air.z, 0.05)));
|
||||
@@ -190,6 +199,19 @@ ${spatial ? ` if (fx.x != 0.0) {
|
||||
half t = clamp(half(1.0 - fx.y * ${DEHAZE_MAX_OMEGA} * clamp(dark, half(0.0), half(1.0))), half(${DEHAZE_FLOOR_T}), half(1.0));
|
||||
c = clamp((c - aa) / t + aa, half3(0.0), half3(1.0));
|
||||
}
|
||||
// CLARITY (doc section 3.1): the pixel against its own blurred surroundings.
|
||||
// Positive sharpens. Negative SOFTENS toward that same reference — a masked
|
||||
// CLARITY -10 is a soften of the mask's own detail, which is the property the
|
||||
// knob is named for, and not the inverted unsharp it used to be (that only
|
||||
// sank the mask's whites and left its contrast where it was).
|
||||
// ponytail: the reference is the bilateral one (CLARITY_BLUR_SPAN, 6% of the
|
||||
// frame), not the frame-wide mist blur — the mask pass is not handed a second
|
||||
// blurred child. Add one when a negative CLARITY wants a wider soften.
|
||||
if (fx.x > 0.0) {
|
||||
c = clamp(c + half3(half(fx.x * ${CLARITY_GAIN.toFixed(1)})) * (c - blur), half3(0.0), half3(1.0));
|
||||
} else if (fx.x < 0.0) {
|
||||
c = mix(c, blur, half(clamp(-fx.x, 0.0, 1.0)));
|
||||
}
|
||||
` : ''} return clamp(c, half3(0.0), half3(1.0));
|
||||
}
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user