Dither the present pass #119

Merged
sermuns merged 1 commit from feat/77-present-dither into main 2026-10-21 08:43:11 +00:00
Owner

The swapchain is 8 bits, so the smooth night gradients -- the sky, the fog
falloff, the flashlight cone -- band in it. The present pass now adds
interleaved gradient noise to the tone-mapped color before the sRGB encode.

The amplitude is divided by the slope of the sRGB curve, so the noise lands
half an LSB away in the output at every brightness. A fixed amplitude in
linear light would be stretched about twentyfold by the curve in the darks,
which is exactly where the banding hurts most.

The noise hashes the pixel coordinate, so it does not crawl between frames.
The moon and the HUD are drawn after the blit, so they stay grain-free.

closes #77

How to test

Run the client at night and look at a smooth gradient -- the sky, the fog
falloff, the flashlight cone. The concentric bands become a faint static
grain instead. A native build is the easiest A/B: revert the commit and the
bands come back.

The web build is where it shows best: on WebGPU the swapchain quantization is
the only banding left and it nearly disappears.

Known limit

On GL and WebGL2 the HDR target is Rgba8Unorm, so the night gradient is
already quantized in linear light before the present pass sees it, and a
present-pass dither cannot undo that. Fixing it needs a dither on the HDR
write, which this issue scoped out.

A 👍 reaction on this pull request flags it for merge.

The swapchain is 8 bits, so the smooth night gradients -- the sky, the fog falloff, the flashlight cone -- band in it. The present pass now adds interleaved gradient noise to the tone-mapped color before the sRGB encode. The amplitude is divided by the slope of the sRGB curve, so the noise lands half an LSB away *in the output* at every brightness. A fixed amplitude in linear light would be stretched about twentyfold by the curve in the darks, which is exactly where the banding hurts most. The noise hashes the pixel coordinate, so it does not crawl between frames. The moon and the HUD are drawn after the blit, so they stay grain-free. closes #77 ## How to test Run the client at night and look at a smooth gradient -- the sky, the fog falloff, the flashlight cone. The concentric bands become a faint static grain instead. A native build is the easiest A/B: revert the commit and the bands come back. The web build is where it shows best: on WebGPU the swapchain quantization is the only banding left and it nearly disappears. ## Known limit On GL and WebGL2 the HDR target is `Rgba8Unorm`, so the night gradient is already quantized in linear light before the present pass sees it, and a present-pass dither cannot undo that. Fixing it needs a dither on the HDR write, which this issue scoped out. A 👍 reaction on this pull request flags it for merge.
The swapchain is 8 bits, so the smooth night gradients — the sky, the fog
falloff, the flashlight cone — band in it. The present pass now adds
interleaved gradient noise to the tone-mapped color, before the sRGB encode,
so the noise lands half an LSB away in the output whatever the brightness:
the amplitude is divided by the slope of the sRGB curve, because a fixed
amplitude in linear light would be stretched twentyfold by the curve in the
darks, which is where the banding hurts most.

The noise is a hash of the pixel coordinate, so it does not crawl between
frames. The moon and the HUD are drawn after the blit, so they stay clean.
Sign in to join this conversation.
No description provided.