Post-processing
Estella runs full-screen post-processing through ping-pong framebuffers. Add a
PostProcessVolume component, enable the effects you want, and tune their
parameters — in the editor or in code. Effects share the ping-pong buffers, so
several can stack in one frame at no extra target cost beyond the passes.
Built-in effects
Section titled “Built-in effects”| Effect | What it does | Key uniforms |
|---|---|---|
bloom |
Glow on bright areas | threshold, intensity, radius |
tonemap |
ACES filmic tonemapping | exposure |
colorGrade |
Exposure / contrast / saturation / white balance | exposure, contrast, saturation, temperature, tint |
lutGrade |
Color grading through a lookup-table texture | u_lut (texture), intensity |
vignette |
Darkened screen edges | intensity, softness |
chromaticAberration |
RGB fringing | intensity |
blur |
Gaussian blur | intensity |
fxaa |
Fast anti-aliasing | intensity |
lensDistortion |
Barrel / pincushion warp | strength, zoom |
pixelate |
Block quantization | pixelSize |
outline |
Ink-edge outlines (Sobel edge darkening) | intensity, threshold, thickness |
grayscale |
Desaturate | intensity |
The uniform names above are the friendly labels; in code they are u_-prefixed
(u_intensity, u_exposure, …). Most effects are identity at their defaults
(colorGrade, blur, chromaticAberration, fxaa, lensDistortion, lutGrade),
so adding them changes nothing until you dial them in. A few are visible the moment
you add them — grayscale (full desaturation), vignette, bloom, outline, and
tonemap (the ACES curve always applies) — so enable those only when you want them.
HDR (linear color space)
Section titled “HDR (linear color space)”When the project renders in the linear color space (see
Lighting), the effect chain runs in half-float HDR
wherever float render targets are available — always on WebGPU, and on WebGL2 with
the near-universal EXT_color_buffer_float. Light accumulation past 1.0 (a
Light2D with intensity above 1) then survives into the chain instead of
clamping at the 8-bit store:
bloomwiththresholdabove 1 blooms only over-range energy — bright lights glow while ordinary LDR art stays crisp. This is the classic “emissive glow” setup: crank a light’sintensity, setthreshold≈ 1.2–1.5.tonemapreceives true HDR input, so the ACES curve rolls off highlights instead of shaping already-clamped values.- Without a tonemap pass, over-range values simply clip at the final linear→sRGB encode — fine for subtle glow, harsh for large over-exposure.
In gamma mode (or without float-target support) the chain is LDR and behaves as
before; a threshold above ~0.75 then never fires.
Using a PostProcessVolume
Section titled “Using a PostProcessVolume”Add a PostProcessVolume, enable effects, and set their parameters. Volumes
blend where they overlap, so you can drive effects by area (a “underwater”
volume) or fade them in over a transition.
| Property | Type | Default | Description |
|---|---|---|---|
effects |
list | [] |
{ type, enabled, uniforms } per effect. |
isGlobal |
boolean | true |
Apply everywhere vs. only inside the volume shape. |
shape |
'box' | 'sphere' |
'box' |
Bounds shape for a local volume. |
size |
Vec2 | {5, 5} |
Volume extents (local volumes). |
priority |
number | 0 |
Higher-priority volumes win where they overlap. |
weight |
number | 1 |
Blend weight, 0..1 (fade the whole volume). |
blendDistance |
number | 0 |
Falloff distance at the volume edge. |
Programmatic control
Section titled “Programmatic control”For a fixed camera pipeline in code, use the PostProcess resource. A stack starts
empty — add one pass per effect. Get the effect’s shader from the registry with
getEffectDef(type).factory(), then set its u_-prefixed uniforms and bind the
stack to a camera:
import { defineSystem, Query, Res, Camera, PostProcess, getEffectDef } from 'esengine';
const setup = defineSystem([Query(Camera), Res(PostProcess)], (q, pp) => { const grade = getEffectDef('colorGrade')!; for (const [camera] of q) { const stack = pp.createStack(); stack.addPass('colorGrade', grade.factory()) .setUniform('colorGrade', 'u_saturation', 1.3) .setUniform('colorGrade', 'u_contrast', 1.1); pp.bind(camera, stack); // pp.unbind(camera) to remove }});A multi-pass effect like bloom exposes def.multiPass; add each sub-pass
(for (const p of grade.multiPass!) stack.addPass(p.name, p.factory())). For
anything beyond a fixed pipeline, a PostProcessVolume is simpler — it resolves
multi-pass and texture params for you.
| Method | Description |
|---|---|
PostProcess.createStack() |
Create an empty effect stack. |
stack.addPass(name, shader) |
Add an effect pass; shader from getEffectDef(type).factory() (chainable). |
stack.setUniform(pass, uniform, value) |
Set a u_-prefixed parameter (chainable). |
stack.setTexture(pass, uniform, handle) |
Bind a texture parameter, e.g. a LUT (chainable). |
stack.setEnabled(name, on) |
Toggle an already-added pass (chainable). |
PostProcess.bind(camera, stack) / unbind(camera) |
Attach / remove a camera’s stack. |
getEffectDef(type) / getAllEffectDefs() / getEffectTypes() |
Look up effect definitions (shader factory + uniforms). |
Texture parameters (LUTs & masks)
Section titled “Texture parameters (LUTs & masks)”Some effects take a texture, not just scalars. lutGrade samples a color
lookup table — a 2D-packed 1024×32 PNG (32 slices of a 32³ cube) — so you can
author a whole grade in an image editor and drop it in. Add the pass, then bind the
texture with setTexture (or, on a PostProcessVolume, list it under the effect’s
textures: { u_lut: <ref> }):
stack.addPass('lutGrade', getEffectDef('lutGrade')!.factory()) .setTexture('lutGrade', 'u_lut', lutHandle) .setUniform('lutGrade', 'u_intensity', 1);Best practices
Section titled “Best practices”- Order matters — passes run in the order you add them (or, on a volume, the
order of its
effects[]) and share the buffers, so a sensible chain is bloom → tonemap → color grade → vignette. - Use volumes for locality (an area’s mood) and the
PostProcessAPI for a fixed camera pipeline. - Fade with
weighton a volume for smooth transitions instead of toggling. - Keep the stack short on low-end/mobile — each pass is a full-screen draw.
See also
Section titled “See also”- Materials & Shaders — per-object shaders (a separate path).
- Camera — bind a stack per camera.