Grain does not survive the delivery encode

A shader looks correct in the engine, gets captured, gets encoded, and arrives on a store page looking soft and blotchy. Nothing in the shader changed. The encode is a stage in the pipeline and it has opinions about high frequencies.

The same amplitude of grain, both halves put through the same simulated encode, an 8×8 DCT that keeps its ten lowest frequencies. Left: grain authored at one pixel. Right: grain authored at three pixels.

The left half is where per-pixel grain goes: most of it is gone (about 40 percent of its amplitude survives), and what is left has turned into low-frequency blotching that is worse than no grain at all. The right half still has grain, about 80 percent of it, because grain at three pixels puts its energy in frequencies the encoder keeps.

To be clear about what is on screen: both halves are a simulation, not an encoder. Each 8×8 block goes through the same DCT a codec uses, and only the ten lowest-frequency coefficients are kept, which is what a starved quantizer does to the rest. There is no motion search, rate control or deblocking. It is directionally right and it is not x264.

Why per-pixel noise is the first thing to go

Every mainstream codec transforms 8×8 or 16×16 blocks into frequency coefficients, then quantizes them, and the quantizer is coarser for high frequencies because that is where the eye is least sensitive. Per-pixel noise is entirely high frequency by construction. It occupies the coefficients the encoder is most willing to zero.

Worse, it does not go quietly. Grain is incompressible, so the encoder spends bits on it until it hits the rate cap and then starts taking bits from everything else. A grainy capture at a fixed bitrate has a softer image and no grain, which is the opposite of both goals.

The scales that survive

The exact numbers move with bitrate and resolution, which is the reason to test rather than to take the table. The shape of the answer does not move.

The better answer is to not bake it

Grain is cheap to generate and expensive to transmit, which is an unusually clear argument for generating it at the far end:

The same logic, applied to everything else fine

Grain is the clearest case but not the only one. Anything that lives at the pixel scale is negotiating with the encoder:

The check that costs ten minutes

Encode a capture at the bitrate the platform actually uses, then look at the encoded file rather than the master. Not a screenshot of the timeline: the delivered artifact. Most of these problems are invisible until that step and obvious immediately after it.

Rules of thumb

  1. Per-pixel grain does not survive a delivery encode. It costs bits and arrives as blotching.
  2. If it must be baked in, author it at three to four pixels and verify on the encoded file.
  3. Better: generate grain at the display, or use AV1's grain synthesis and send a clean picture.
  4. Dither belongs after delivery too, for exactly the same reason.
  5. Grain steals bits from the rest of the image. A grainy capture at a fixed rate is softer everywhere.
  6. Judge every fine detail on the delivered file, never on the master.

All 61 notes How to use them Credits

If you find anything wrong, let me know at hello@andrewdetwiler.com and I'll get it updated.