Dither before you quantize

Every neon-on-dark frame ends with a dark gradient, and dark gradients band. Not because the maths is wrong but because 8 bits is genuinely not enough resolution near black, and the fix is to add noise smaller than the error you are trying to hide.

A ramp quantized to 5 bits so the effect is visible. Left: raw, and the bands are obvious. Middle: plus half a step of noise. Right: noise that also changes over time.

Left has obvious steps. Middle has none, and the noise that removed them is plus or minus half of one quantizer step. Right adds a temporal axis, which trades a faint shimmer for even smoother bands: right on a still image, wrong on anything that gets video compressed.

That demo quantizes to 5 bits, not 8, and it is exaggerated deliberately. The real case is a dark gradient at 8 bits, which tops out around 12% brightness, and at that level neither the banding nor the fix is visible in a screenshot on most displays. The phenomenon is identical; only the scale is turned up so you can see what is being claimed.

Why it works

Quantization turns a smooth ramp into a staircase because every value in a range rounds to the same output. Adding noise before rounding means values near a boundary sometimes round up and sometimes round down, in proportion to how close they were. The average over a small neighborhood becomes the true value again. You have traded a correlated error, which the eye sees as an edge, for an uncorrelated one, which it sees as texture.

Position matters more than the noise function

Dither has to be the last thing before the 8-bit write. After the tonemap, after the grade, after everything. Put it earlier and the operations downstream re-correlate the error and you get banding back, plus noise.

col = tonemap(col);
col = grade(col);
col += (ign(fragCoord) - 0.5) / 255.0;   // LAST
// then write

The amplitude is one code value peak to peak, not more. Bigger is not safer here, it is just visible noise.

Which noise

Blue noise is the best answer and needs a texture. Interleaved gradient noise is one dot product and a fract, and it is close enough that it is the sensible default for a shader that has no texture budget. A plain hash is the worst of the three: its error is white, so it clumps, and clumps are exactly what the eye finds.

Rules of thumb

  1. Amplitude is plus or minus half a code value. One over 255, not more.
  2. It goes last, after tonemap and grade, immediately before the write.
  3. Dark gradients are where this matters. The bright end rarely needs it.
  4. Animate the noise for stills; hold it still for anything that will be video compressed, or the encoder spends its bitrate on your dither.
  5. If banding survives dithering, the problem is upstream: you are probably quantizing twice.

All 61 notes How to use them Credits