Post-processing
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 dull | no sRGB/gamma encode at the end, or doing it twice |
| Bloom looks like a grey smear, not light | it ran after tone mapping, on clamped values |
| Highlights go grey instead of white | Reinhard desaturating; use a filmic curve |
| Bloom flickers on small bright specks | a sub-pixel highlight crossing the threshold; prefilter or use a soft knee |
| Blur is square and banded | box kernel instead of Gaussian, or too few taps for the radius |
| Effect chain is slow despite a trivial scene | cost is bandwidth × passes; drop to half-resolution targets |
| TAA leaves smears behind moving objects | missing or wrong motion vectors |