Skip to content

Commit 8d8ed13

Browse files
authored
Merge pull request #92 from MathGeniusJodie/main
Add an article
2 parents fb8a508 + 2eec3e5 commit 8d8ed13

8 files changed

Lines changed: 104 additions & 0 deletions

File tree

10.5 KB
Loading
363 KB
Loading
331 KB
Loading
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
title: 'Ordered Dithering is Useful and Good'
3+
slug: 'ordered-dithering-is-useful-and-good'
4+
description: 'When ordered dithering with Bayer Matrices beats blue noise, and why you should use it everywhere'
5+
date: '2026-04-09'
6+
authors: ['jodie']
7+
tags: ['dithering', 'glsl', 'opengl', 'optimization', 'article', 'bandwidth']
8+
---
9+
10+
Dithering is the old trick of using patterns to fake more colors than you actually have.
11+
If you've ever noticed the crosshatch pattern in old video games, that's ordered dithering.
12+
It uses a repeating grid called a Bayer Matrix. It's carefully constructed so that adjacent
13+
values are as far apart as possible, which maximally spreads error. Modern game
14+
developers, in their infinite wisdom, have decided that this mathematical perfection is
15+
a problem to be fixed. The regular pattern is too noticeable, they say. Instead they make
16+
a fancy precomputed texture that has the same properties but with no regular pattern: Blue Noise.
17+
18+
<!-- truncate -->
19+
20+
Sure, blue noise looks great. But somewhere along the way we forgot why we're dithering in the
21+
first place: to save memory. If you need a texture lookup to do dithering, you're spending
22+
bandwidth to save bandwidth, it makes no sense. Ordered dithering is just 18 instructions
23+
to compute from scratch.
24+
25+
```glsl
26+
float dither256x256(uvec2 fragCoord){
27+
uint x = fragCoord.x ^ fragCoord.y;
28+
uint y = fragCoord.y;
29+
uint z = x << 16 | y;
30+
z |= z << 12;
31+
z &= 0xF0F0F0F0u;
32+
z |= z >> 6;
33+
z &= 0x33333333u;
34+
z |= z << 3;
35+
z &= 0xaaaaaaaau;
36+
z = z >> 9 | z << 6;
37+
z &= 0x7fffffu;
38+
return uintBitsToFloat(
39+
floatBitsToUint(1.) | z
40+
) - 1.5; // or -1.0 if you want 0..1
41+
// instead of -0.5..0.5
42+
}
43+
```
44+
I came up with this independently around 2016[\*](#note-id-0). I found recently that the general idea of
45+
interleaving and reversing bits was first described
46+
[here](https://bisqwit.iki.fi/story/howto/dither/jy/)[\*](#note-id-0). But I'm
47+
the first person to make this optimized implementation[\*](#note-id-0). Which is probably why it hasn't caught on yet.
48+
49+
When you're doing graphics programming, fast dithering should always be in your metaphorical
50+
tool belt. You should *never* reach for higher bit depth without having first tried
51+
dithering. Here's an example of how even rgb8 is overkill. This is a comparison between
52+
rgb8 and rgb453. Can you tell which one is which?
53+
54+
![Bit Depth 1 — rgb453 with dithering](rgb453.png)
55+
56+
![Bit Depth 2 — rgb8 original](original.png)
57+
58+
Now here's without the dithering:
59+
60+
![rgb453 without dithering](rgb453nodither.png)
61+
62+
This is how huge the difference is!
63+
You're throwing away half your ram by not dithering all your buffers.
64+
65+
## Not Just for Quantization
66+
67+
You can (and should) use Bayer Matrices everywhere you would use random sampling,
68+
like for picking ray directions for lighting.
69+
70+
![One sample per pixel — Noise on the left, Bayer Matrix on the right](ibl-1s.png)
71+
72+
At only one sample per pixel, you can see how the Bayer Matrix grain could
73+
easily be smoothed with a light blur. The noise grain has no chance.
74+
75+
![8 samples per pixel — Noise on the left, Bayer Matrix on the right](ibl-8s.png)
76+
77+
At eight samples per pixel, the Bayer Matrix sampling looks smooth already
78+
while the noise sampling still looks rough.
79+
80+
Blurring white noise just gets you a splotchy mess, which you have to fix
81+
by doing more samples, or a wider blur. Choosing to use white noise is the
82+
dead raccoon in the river that makes everything downstream shit itself.
83+
84+
For temporal effects, you can also use the 1D equivalent of a bayer matrix:
85+
reversing the bits for the index of the current frame, which gives this pattern:
86+
87+
![1D bit-reversal pattern](1d.png)
88+
89+
---
90+
91+
With technological progress, as pixel densities get higher and higher,
92+
dither patterns become harder and harder to see and vram gets more and more
93+
strained. Ordered dithering isn't just the past, it's the future as well!
94+
95+
## Notes
96+
97+
#### \* As far as I know. {#note-id-0}
405 KB
Loading
244 KB
Loading
204 KB
Loading

blog/authors.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,10 @@ evelyn:
7070
name: evelyn
7171
title: artisinal artificial intelligence
7272

73+
jodie:
74+
name: Jodie
75+
title: The Tonemap Guy
76+
url: https://jodie.website
77+
image_url: https://jodie.website/logo.svg
78+
socials:
79+
github: MathGeniusJodie

0 commit comments

Comments
 (0)