From e8a0c0d07697751fe2ff054e35fc4c5c1d6b9293 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 23 Sep 2026 10:01:49 +0700 Subject: [PATCH] web: print the grain as clumps, not as static MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GRAIN was one value hash per cell, sampled straight off the pixel grid: a square lattice at the picture's own axes, the same field in every session and in every photo, and — measured on a flat gray frame, where every deviation IS grain — a spread that was flat rather than emulsion-like (neighbour correlation rho(1) = 0.183, so half the noise was one pixel wide). The noise is now an emulsion. Three uncorrelated hashes are averaged into a density (a bell, the way an emulsion's density swings, instead of the flat spread of a single hash), that density is read as value noise with a smoothstep cell, and the result is three octaves of it — the cell, then 2x and 4x that cell — over a lattice turned 20 degrees off the picture's axes, so no grid shows through. Only coarser octaves: a finer one (tried 2.043x, 0.72px) goes sub-pixel and rho(1) falls to 0, i.e. back to static. The whole domain is offset by a seed rolled once per page load, so two visitors never print the same clumps while the preview, the compare copy and the file of one session still print the same roll. Same flat 3000x2000 frame, preview render 1600x1067, GRAIN 10: sigma 52.53 -> 50.15 (the 2.95 gain keeps the spread the AMOUNT knob was tuned against, since the repo notes the slider was calibrated on the old field), rho(1) 0.183 -> 0.275, and the three channels still move together — the 6.9 of sigma 50 that is left over is the Overlay blend meeting the frame's own tint, the plane itself is one gray value in all three. Re-rendering the same frame draws the same clumps byte for byte; a reload rolls a new seed and a new field at the same strength. --- docker/frontend/src/engine/exportEngine.ts | 40 +++++++++++++++++----- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/docker/frontend/src/engine/exportEngine.ts b/docker/frontend/src/engine/exportEngine.ts index 0b1e9dd..37078e0 100644 --- a/docker/frontend/src/engine/exportEngine.ts +++ b/docker/frontend/src/engine/exportEngine.ts @@ -109,6 +109,11 @@ function release(owned: SkDisposable[], item: SkDisposable | null): void { disposeAll([item]); } +// The grain's roll of film: one seed per page load, hashed into the noise's +// domain so two runs never print the same clumps, while every render inside one +// session — the preview, the compare copy and the file — prints the same one. +const GRAIN_SEED = [Math.random() * 61.7, Math.random() * 43.9] as const; + function createSurface(width: number, height: number) { return Skia.Surface.MakeOffscreen(width, height) ?? Skia.Surface.Make(width, height); } @@ -458,6 +463,11 @@ export async function renderPhoto(input: RenderInput): Promise