From 8e6c1493e81051f76b0d0c9efec380854f167344 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Fri, 18 Sep 2026 10:33:32 +0700 Subject: [PATCH] web: grade the colour matrix before the tone pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docker/frontend/src/engine/exportEngine.ts | 113 ++++++++++++--------- 1 file changed, 67 insertions(+), 46 deletions(-) diff --git a/docker/frontend/src/engine/exportEngine.ts b/docker/frontend/src/engine/exportEngine.ts index d6145e7..98e76b4 100644 --- a/docker/frontend/src/engine/exportEngine.ts +++ b/docker/frontend/src/engine/exportEngine.ts @@ -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 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 + 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