Post-processing

08-post-processing.md · updated 2026-10-15

Render the scene to a texture instead of to the screen and the frame stops being geometry: it becomes an image you can do arithmetic on. Draw one full-screen quad, sample that texture in a fragment shader, and every operation an image editor has is now available to you per frame. Almost everything that makes a modern frame look modern happens here, after the last triangle.

Render targets and ping-ponging

A render target is just a texture you are allowed to draw into; a pass is one full-screen read plus one full-screen write. Multi-pass effects need two of them: read A, write B, swap, read B, write A. That swap is ping-ponging, and you need it because a shader cannot safely read the target it is writing. Budget in passes, not triangles — one full-screen quad is two triangles and can still cost more than the whole scene.

Convolution kernels

A kernel is a weighted sum of a pixel's neighbours; the weights are the whole effect. Equal weights blur, a positive centre with negative neighbours sharpens, and opposed weights (Sobel) measure the gradient and give you edge detection.

A Gaussian blur is separable: blurring horizontally and then vertically gives exactly the same result as one 2D kernel. That turns (2r+1)² taps into 2(2r+1) — at radius 8, 289 taps become 34. Always do it as two passes.

The HDR tail: bloom, tone map, gamma

Bloom is threshold → downsample → blur → add back, and it only works on values that are still above 1.0. Lighting produces values far brighter than white; if you tone map first, everything bright becomes 1.0 and there is nothing left to threshold. Downsampling before blurring is what makes a wide, cheap glow.

Tone mapping compresses an unbounded range into 0–1. Reinhard x/(1+x) is the one-liner; ACES-style filmic curves keep more contrast and roll highlights off towards white rather than towards grey. Whatever you choose, it runs near the end — and the last thing of all is the sRGB/gamma encode, roughly pow(x, 1/2.2). Skip that and the whole frame looks dark and muddy, because you sent linear light to a display that expects encoded values.

Mood and aliasing

Vignette (darken towards the corners) and chromatic aberration (sample R and B at a small offset) are a few lines each and do more for the feel of a frame than their cost suggests. FXAA and TAA are the post-pass answer to jagged edges: FXAA finds edges in the final image and smears along them; TAA jitters the camera each frame and blends history, which is sharper but needs motion vectors and brings ghosting.

Demo

A post-processing chain — toggle and reorder passes over a procedural HDR scene, and watch the cost in full-screen passes.

Gotchas

Symptom Cause
Whole frame looks dark and dullno sRGB/gamma encode at the end, or doing it twice
Bloom looks like a grey smear, not lightit ran after tone mapping, on clamped values
Highlights go grey instead of whiteReinhard desaturating; use a filmic curve
Bloom flickers on small bright specksa sub-pixel highlight crossing the threshold; prefilter or use a soft knee
Blur is square and bandedbox kernel instead of Gaussian, or too few taps for the radius
Effect chain is slow despite a trivial scenecost is bandwidth × passes; drop to half-resolution targets
TAA leaves smears behind moving objectsmissing or wrong motion vectors

Open the demo →

← all notes & demos