web: grade the colour matrix before the tone pass
A SkPaint runs its shader BEFORE its colourFilter, so setting the exposure matrix on the same paint as the tone shader landed the gain after the tone pass: HIGHLIGHT -10 rolled a bright pixel back to 0.78, +EXPOSURE then multiplied it by 1.2 and +0.15 and it clamped back to 1.0 — the HIGHLIGHT slider looked dead the moment exposure went up. The matrix now renders into its own image and the tone/cinema chain samples that. Measured on the real engine (HIGHLIGHT -10 first, then EXPOSURE +10): top end stays 0.780 (was 1.000), midtone 0.502 -> 0.722.
This commit is contained in:
@@ -185,6 +185,23 @@ function screenSharpenImage(image: any, amount = 0.5): any {
|
||||
return convolveImage(image, amount);
|
||||
}
|
||||
|
||||
// The colour matrix, applied on its own into a fresh image. A SkPaint runs its
|
||||
// shader before its colourFilter, so the graded pixels have to exist as an
|
||||
// image before the tone/cinema pass can read them (see renderPhoto step 3).
|
||||
function gradeThrough(image: any, w: number, h: number, colorFilter: any): any {
|
||||
const surf = createSurface(w, h);
|
||||
if (!surf) return null;
|
||||
try {
|
||||
const p = Skia.Paint();
|
||||
p.setColorFilter(colorFilter);
|
||||
surf.getCanvas().drawImage(image, 0, 0, p);
|
||||
p.dispose();
|
||||
return surf.makeImageSnapshot() ?? null;
|
||||
} finally {
|
||||
surf.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot → clear → blur redraw: the blur must not sit on top of the sharp
|
||||
// pixels it is meant to replace.
|
||||
function drawBlurred(canvas: any, surface: any, w: number, h: number, sigma: number): void {
|
||||
@@ -305,47 +322,60 @@ export async function renderPhoto(input: RenderInput): Promise<RenderResult | nu
|
||||
const adjustments = recipe.adjustments;
|
||||
|
||||
// 3. Colour matrix (+2^EV gain unless the camera already biased the shot).
|
||||
// The matrix grades into its OWN image first, because a SkPaint runs its
|
||||
// shader before its colourFilter: with both on one paint the exposure gain
|
||||
// landed AFTER the tone pass, so +EXPOSURE took the 0.78 a rolled-back
|
||||
// highlight came out at and pushed it straight back to 1.0 — the HIGHLIGHT
|
||||
// slider looked dead the moment exposure went up (measured, see the order
|
||||
// probe). The tone shader's child is the graded image, so the highlight
|
||||
// mask finally sees the exposed value it is meant to pull back.
|
||||
const evStops = adjustments.exposureCompensation ?? 0;
|
||||
const evMatrix = options?.evFromCamera
|
||||
? getSkiaColorMatrix(recipe.baseFilter, adjustments)
|
||||
: applyExposureGain(getSkiaColorMatrix(recipe.baseFilter, adjustments), evStops);
|
||||
const colorFilter = own(Skia.ColorFilter.MakeMatrix(evMatrix));
|
||||
paint.setColorFilter(colorFilter);
|
||||
const matrixIsIdentity = evMatrix.every((v, i) => v === (i % 6 === 0 ? 1 : 0));
|
||||
|
||||
// 3b. Tone shader.
|
||||
const tone = getToneUniforms(adjustments, recipe.baseFilter);
|
||||
let toneShader: any = null;
|
||||
// 3c. Cinema seasonal grade (cinema → tone → image).
|
||||
const cinema = getCinemaUniforms(recipe.cinema);
|
||||
const cinemaOn = !!cinema && cinemaIsActive(cinema.flat);
|
||||
let paintShader: any = null;
|
||||
let gradeImage = skImage;
|
||||
const graded = (toneIsActive(tone) || cinemaOn) && !matrixIsIdentity;
|
||||
if (graded) {
|
||||
const snapshot = gradeThrough(skImage, width, height, colorFilter);
|
||||
if (snapshot) {
|
||||
own(snapshot);
|
||||
gradeImage = snapshot;
|
||||
}
|
||||
}
|
||||
const imageShaderOf = () =>
|
||||
own(
|
||||
gradeImage.makeShaderOptions(
|
||||
Skia.TileMode.Clamp, Skia.TileMode.Clamp, Skia.FilterMode.Linear, Skia.MipmapMode.None
|
||||
)
|
||||
);
|
||||
if (toneIsActive(tone)) {
|
||||
const { toneEffect: effect } = effects();
|
||||
if (effect) {
|
||||
const imageShader = own(
|
||||
skImage.makeShaderOptions(
|
||||
Skia.TileMode.Clamp, Skia.TileMode.Clamp, Skia.FilterMode.Linear, Skia.MipmapMode.None
|
||||
)
|
||||
);
|
||||
toneShader = own(effect.makeShaderWithChildren(toneUniformArray(tone), [imageShader]));
|
||||
if (toneShader) paint.setShader(toneShader);
|
||||
toneShader = own(effect.makeShaderWithChildren(toneUniformArray(tone), [imageShaderOf()]));
|
||||
}
|
||||
}
|
||||
|
||||
// 3c. Cinema seasonal grade (cinema → tone → image).
|
||||
const cinema = getCinemaUniforms(recipe.cinema);
|
||||
let paintShader = toneShader;
|
||||
if (cinema && cinemaIsActive(cinema.flat)) {
|
||||
if (cinemaOn) {
|
||||
const { cinemaEffect: effect } = effects();
|
||||
if (effect) {
|
||||
const child =
|
||||
toneShader ??
|
||||
own(skImage.makeShaderOptions(
|
||||
Skia.TileMode.Clamp, Skia.TileMode.Clamp, Skia.FilterMode.Linear, Skia.MipmapMode.None
|
||||
));
|
||||
const cs = own(effect.makeShaderWithChildren(cinema.flat, [child]));
|
||||
if (cs) {
|
||||
paintShader = cs;
|
||||
paint.setShader(paintShader);
|
||||
}
|
||||
const cs = own(effect.makeShaderWithChildren(cinema.flat, [toneShader ?? imageShaderOf()]));
|
||||
if (cs) paintShader = cs;
|
||||
}
|
||||
}
|
||||
if (!paintShader && toneShader) paintShader = toneShader;
|
||||
// Only one of the two may tint the pixels: the graded image already carries
|
||||
// the matrix, an ungraded draw still needs it.
|
||||
if (paintShader) paint.setShader(paintShader);
|
||||
else if (!graded) paint.setColorFilter(colorFilter);
|
||||
|
||||
// 4. Denoise (blur only — CLARITY moved to its own pass below, see header).
|
||||
if (adjustments.denoise > 0) {
|
||||
@@ -382,24 +412,22 @@ export async function renderPhoto(input: RenderInput): Promise<RenderResult | nu
|
||||
const strength = (adjustments.hdf ?? 0) / 10;
|
||||
const sigma = width * (0.006 + 0.024 * strength);
|
||||
const { glowEffect: effect } = effects();
|
||||
const srcShader =
|
||||
paintShader ??
|
||||
own(skImage.makeShaderOptions(
|
||||
Skia.TileMode.Clamp, Skia.TileMode.Clamp, Skia.FilterMode.Linear, Skia.MipmapMode.None
|
||||
));
|
||||
const srcShader = paintShader ?? imageShaderOf();
|
||||
const glowShader =
|
||||
effect != null ? own(effect.makeShaderWithChildren(glowUniformArray(), [srcShader])) : null;
|
||||
const glowPaint = own(Skia.Paint());
|
||||
glowPaint.setBlendMode(Skia.BlendMode.Screen);
|
||||
glowPaint.setAlphaf(0.28 + 0.52 * strength);
|
||||
// The bloom reads the graded pixels now (step 3), so the matrix must not
|
||||
// land on them a second time; it only wraps the filter when ungraded.
|
||||
const bloomGrade = (inner?: any) =>
|
||||
graded ? undefined : own(Skia.ImageFilter.MakeColorFilter(inner ?? colorFilter, null));
|
||||
const bloomBlur = () => own(Skia.ImageFilter.MakeBlur(sigma, sigma, Skia.TileMode.Clamp, null));
|
||||
if (glowShader != null) {
|
||||
glowPaint.setShader(glowShader);
|
||||
glowPaint.setImageFilter(
|
||||
own(Skia.ImageFilter.MakeCompose(
|
||||
own(Skia.ImageFilter.MakeBlur(sigma, sigma, Skia.TileMode.Clamp, null)),
|
||||
own(Skia.ImageFilter.MakeColorFilter(colorFilter, null))
|
||||
))
|
||||
);
|
||||
const grade = bloomGrade();
|
||||
const blur = bloomBlur();
|
||||
glowPaint.setImageFilter(grade ? own(Skia.ImageFilter.MakeCompose(blur, grade)) : blur);
|
||||
canvas.drawRect(Skia.XYWHRect(0, 0, width, height), glowPaint);
|
||||
} else {
|
||||
const hiMatrix = [
|
||||
@@ -408,23 +436,16 @@ export async function renderPhoto(input: RenderInput): Promise<RenderResult | nu
|
||||
0, 0, 2.5, 0, -1.5,
|
||||
0, 0, 0, 1, 0,
|
||||
];
|
||||
glowPaint.setImageFilter(
|
||||
own(Skia.ImageFilter.MakeCompose(
|
||||
own(Skia.ImageFilter.MakeBlur(sigma, sigma, Skia.TileMode.Clamp, null)),
|
||||
own(Skia.ImageFilter.MakeColorFilter(
|
||||
own(Skia.ColorFilter.MakeCompose(
|
||||
own(Skia.ColorFilter.MakeMatrix(hiMatrix)),
|
||||
colorFilter
|
||||
)),
|
||||
null
|
||||
))
|
||||
))
|
||||
const grade = bloomGrade(
|
||||
own(Skia.ColorFilter.MakeCompose(own(Skia.ColorFilter.MakeMatrix(hiMatrix)), colorFilter))
|
||||
);
|
||||
const blur = bloomBlur();
|
||||
glowPaint.setImageFilter(grade ? own(Skia.ImageFilter.MakeCompose(blur, grade)) : blur);
|
||||
if (paintShader) {
|
||||
glowPaint.setShader(paintShader);
|
||||
canvas.drawRect(Skia.XYWHRect(0, 0, width, height), glowPaint);
|
||||
} else {
|
||||
canvas.drawImage(skImage, 0, 0, glowPaint);
|
||||
canvas.drawImage(gradeImage, 0, 0, glowPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user