Post-processing
Volver a la referencia

Post-processing

The chain that runs over the finished frame: dither, posterize, palette match and LUT grading.

12 símbolos

COLOR_LEVELSvariablenacatamalon

COLOR_LEVELS: { genesis, snes, ps1, poster }

How many steps a channel had on the machines this engine is aimed at.

A Mega Drive wrote three bits a channel, so eight steps. A SNES and a PlayStation wrote five, so thirty-two. poster is not a machine: it is the number that makes the effect obvious when you are looking at what it does rather than trying to be a console.

Propiedades

genesis8
snes32
ps132
poster4
Ver el código · post/builtin/color_levels.ts:12

crtfunctionnacatamalon

crt(options: { scanlines, mask, maskType, curvature, vignette }) => TBuiltinPostEffect<{ scanlines, mask, maskType, curvature, vignette }>

Makes the frame look as if it were on a CRT television: scanlines, a phosphor mask, the bulge of the glass and darker corners. Each part is a number, and a part at 0 is not there at all.

One scanline per row of the game, not of the screen. The lines follow the game's own pixels, the way a console's picture sat on the tube, so a game at 320 × 224 has 224 of them however large the canvas is drawn. When a game row is only one real pixel tall (pixelRatio: 1, the pixel-art default) a line cannot be drawn inside it, so alternate rows are darkened instead, which is what a console's picture looked like on a screen of the same height. At two or more real pixels per row every row gets its own soft line.

The mask is the coloured grid of the tube itself and works on real pixels, so it only looks like a mask at a pixelRatio of 3 or more; at 1 it reads as coloured columns. That is why it starts switched off.

It belongs last in a chain: a palette or a dither decides which colours exist, and the tube is what those colours are shown on.

Parámetros

options{ scanlines, mask, maskType, curvature, vignette }

scanlines, mask, curvature and vignette are how strong each part is, from 0 (absent) to about 1; maskType is 'aperture' (vertical stripes, a Trinitron) or 'slot' (staggered cells, a common TV).

Ejemplo

usePostProcess(crt({ scanlines: 0.35, curvature: 0.08, vignette: 0.3 }));
Ver el código · post/builtin/crt.ts:35

ditherfunctionnacatamalon

dither(options: { levels, strength }) => TBuiltinPostEffect<{ levels, strength }>

Cuts each channel down to a few steps, hiding the bands with an ordered pattern.

This is what the machines of the era did, and doing it here rather than in the artwork means it applies to everything at once: the sprites, the lighting, the gradients a shader made up.

It works in the frame's own colours, with no trip through linear light and back. That is deliberate: the hardware it imitates had no notion of linear light, and a physically tidy version of this bands in the wrong places.

Parámetros

options{ levels, strength }

levels is steps per channel, strength how much of the pattern to mix in.

Ejemplo

usePostProcess({ ...dither({ levels: COLOR_LEVELS.genesis }) });
Ver el código · post/builtin/dither.ts:27

lutGradefunctionnacatamalon

lutGrade(options: { amount }) => TBuiltinPostEffect<{ amount }>

Looks every colour up in a grading table and mixes towards what it says.

This is grading, not limiting. A table moves colours about (warmer, colder, more contrast, a green night); a palette or a dither take colours away. They compose, and in one order only: grade first, limit last. The other way round grades colours the machine could not show and then picks a neighbour for them, which throws away the whole point of the grade.

Parámetros

options{ amount }

amount is how far to go, from the frame untouched at 0 to the table's word at 1.

Ver el código · post/builtin/lut_grade.ts:18

paletteMatchfunctionnacatamalon

paletteMatch(options: { dither }) => TBuiltinPostEffect<{ dither }>

Replaces every colour with the nearest one a palette actually holds.

This is the effect that makes a frame look like a particular machine rather than merely like an old one: a NES palette is fifty-four specific colours, and no amount of cutting channels into steps will land on them.

A small dither shakes each pixel by less than the gap between neighbours before the search, so a slow gradient breaks into a mix of two palette entries instead of a hard edge.

With no palette, or one of a single colour, it gives back what it was handed. A palette that has not arrived yet must cost you the palette and not the picture.

Parámetros

options{ dither }

dither is how much to shake before matching. Zero matches exactly.

Ejemplo

const nes = useLoadPalette({ src: '/palettes/nes.palette' });
usePostProcess({ ...paletteMatch(), palette: nes });
Ver el código · post/builtin/palette_match.ts:30

posterizefunctionnacatamalon

posterize(options: { levels }) => TBuiltinPostEffect<{ levels }>

The same cut as dither, with nothing to hide the seams.

It exists as the other half of the pair rather than as a lesser version: hard bands are a look, and it is the one that shows what the dithering is actually doing when you put the two side by side.

Parámetros

options{ levels }

levels is steps per channel.

Ver el código · post/builtin/posterize.ts:18

usePostProcessfunctionhooknacatamalon

usePostProcess(options: TPostProcessOptions<U>) => TPostEffect<U>

Adds a full-screen effect: a shader that reads the finished frame and gives back what to show.

A material changes one thing; this changes the picture. That is what a palette reduction or a dither has to be, because "what colours may this frame use" is a question about the screen and not about any sprite on it.

The chain belongs to the game; the entry belongs to the scene. Opening a pause menu over a level does not change how the screen looks, so effects do not stack up as scenes do. But the scene that installed one takes it away when it closes.

Order is the order you ask, and order is the picture: each effect reads what the one before it produced. Grade first and limit last, or you will be grading colours the machine cannot show.

Everything but whether it is switched on is read every frame, so writing a number into the uniforms of what this gives back animates the effect with nothing reinstalled.

Parámetros

optionsTPostProcessOptions<U>

What the effect is, and what to set its knobs to.

Ejemplo

const Level = () => {
    const nes = useLoadPalette({ src: '/palettes/nes.palette' });

    usePostProcess({ ...paletteMatch(), palette: nes });

    return createScene();
};
Ver el código · hooks/post/use_post_process.ts:68

TBuiltinPostEffecttypenacatamalon

One of the engine's own effects, ready to be spread into usePostProcess.

Both halves are required here, unlike a game's own effect. These are the engine's, and an engine effect that ran on one card and not the other would be a hole in the promise that the two backends draw the same thing, rather than the honest degradation a hand-written WGSL file gets.

Propiedades

namestring
fragmentstring
fragmentGlslstring
uniformsU
Ver el código · post/builtin/types/t_builtin.ts:16

TPostChaintypenacatamalon

TPostChain: TPostChainEntry[]

The whole chain, in order.

An array rather than a bag of named things, for the reason the input map shares: order is the meaning here. Each effect reads what the one before it produced, so grading before limiting and limiting before grading are two different pictures, not two spellings of one.

Ver el código · post/types/t_post_chain.ts:60

TPostChainEntrytypenacatamalon

One effect as a project writes it down.

An entry names its effect one of two ways, and that split is the whole design:

  • builtin is a key the engine already knows. There is no file to fetch and no path that can point at nothing, so a project that only reduces its colours needs no shaders/ folder at all.
  • shader is a path to a .wgsl, exactly as a material names one.

The arithmetic the engine knows is a key; the look you invented is a file.

Propiedades

shaderstring

Where the effect is written. Empty when builtin names one instead.

builtinopcionalstring

A key from the engine's own four. Wins over shader if somebody wrote both.

uniformsopcionalTUniformValues

Values laid over whatever the file or the built-in starts its knobs at.

paletteopcionalstring

A .palette file whose colours this effect matches against.

lutopcionalstring

A grading table: a .cube, or a strip image. Never both this and palette.

enabledopcionalboolean

false keeps the entry in the chain, in its place, without running it. Default true.

nameopcionalstring

What the editor calls it in its list. Defaults to the file's own name.

Ver el código · post/types/t_post_chain.ts:18

TPostEffecttypenacatamalon

An effect the game has installed: a shader that reads the finished frame and gives back what should be shown instead.

This record is what crosses into the backend, by reference and never copied into something new. The compiled shader is cached against this object's identity, so building a fresh one each frame would mean compiling each frame and never once finding the cache.

Everything except enabled is read every frame, so changing a number in uniforms animates the effect with nothing reinstalled: the same rule a material's knobs follow.

It holds the palette or the table as the asset, not as an uploaded picture, because a file lands after the effect was installed. Keeping the picture here would mean keeping the null it was at the moment the effect was made.

Propiedades

type'post'
idstring
namestring | null

What a warning about it will call it, and what the editor lists. Never an identity.

fragmentstring | null

fn effect(color: vec4<f32>, uv: vec2<f32>) -> vec4<f32>, and anything it calls.

fragmentGlslstring | null

The same hook in GLSL, without which the effect is WebGPU only and degrades to nothing.

uniformsU

The knobs. Read every frame, so writing into this is how a game drives an effect.

uniformSigTUniformSignature

What kind each one is. Fixed when the effect is made: the block's layout depends on it.

paletteTPalette | null

The colours this effect matches against, for one that reduces to a palette.

A palette and a table are read at opposite ends of a chain and no shader is both, so an effect carries at most one of the two.

lutTLut | null

The grading table this effect looks colours up in.

sourceTPostSource
enabledboolean

Whether it runs. A switched-off effect stays in the chain where it is, so turning it back on does not move it out from between the two it sat between.

effectTShader | null

The file it came from, so it can be filled in when that lands.

Ver el código · post/types/t_post_effect.ts:45

TPostProcessOptionstypenacatamalon

What usePostProcess is asked for.

There are three ways to say what the effect is, and they are the same three a material has: write it here, name a file, or spread one of the engine's own.

Propiedades

nameopcionalstring

A name, which is what a warning about this effect will call it, and what an editor lists.

fragmentopcionalstring

The hook, in WGSL: fn effect(color: vec4<f32>, uv: vec2<f32>) -> vec4<f32>.

fragmentGlslopcionalstring

The same hook in GLSL, without which the effect is WebGPU only.

effectopcionalTShader | string

A file from useLoadShader, or the name one was loaded under. Wins over fragment.

uniformsopcionalU

The knobs, and what they start at. Laid over whatever a file or a built-in says.

uniformSigopcionalTUniformSignature

What kind each knob is, when it is already known.

Left out, it is worked out from the values, which is what a hand-written effect wants. The engine's own four pass it because they already know, and because spreading one of them in is meant to be a whole effect rather than a starting point.

paletteopcionalTPalette | null

The colours to match against, from useLoadPalette.

lutopcionalTLut | null

The grading table, from useLoadLut. Never set alongside palette.

enabledopcionalboolean

Whether it runs at all. Default true.

Ejemplo

declare const CRT_WGSL: string;
declare const CRT_GLSL: string;

usePostProcess({ fragment: CRT_WGSL, fragmentGlsl: CRT_GLSL, uniforms: { curve: 0.3 } });
usePostProcess({ effect: useLoadShader({ src: '/shaders/crt.wgsl' }) });
usePostProcess({ ...dither({ levels: COLOR_LEVELS.genesis }) });
Ver el código · post/types/t_post_options.ts:29