surma-dev-e4sfuv.txt (30214B)
1 [1]← Back to home 2 3 Ditherpunk — The article I wish I had about monochrome image dithering 4 5 2021-01-04 6 7 I always loved the visual aesthetic of dithering but never knew how it’s done. 8 So I did some research. This article may contain traces of nostalgia and none 9 of Lena. 10 11 How did I get here? (You can skip this) 12 13 I am late to the party, but I finally played [2]“Return of the Obra Dinn”, the 14 most recent game by [3]Lucas Pope of [4]“Papers Please” fame. Obra Dinn is a 15 story puzzler that I can only recommend, but what piqued my curiosity as a 16 software engineer is that it is a 3D game (using the [5]Unity game engine) but 17 rendered using only 2 colors with dithering. Apparently, this has been dubbed 18 “Ditherpunk”, and I love that. 19 20 [obradinn] Screenshot of “Return of the Obra Dinn”. 21 22 Dithering, so my original understanding, was a technique to place pixels using 23 only a few colors from a palette in a clever way to trick your brain into 24 seeing many colors. Like in the picture, where you probably feel like there are 25 multiple brightness levels when in fact there’s only two: Full brightness and 26 black. 27 28 The fact that I have never seen a 3D game with dithering like this probably 29 stems from the fact that color palettes are mostly a thing of the past. You may 30 remember running Windows 95 with 16 colors and playing games like “Monkey 31 Island” on it. 32 33 [win95] Windows 95 configured to use 16 colors. Now spend hours trying to find 34 the right floppy disk with the drivers to get the “256 colors” or, gasp, “True 35 Color” show up. [monkeyisland16] Screenshot of “The Secret of Monkey Island” 36 using 16 colors. 37 38 For a long time now, however, we have had 8 bits per channel per pixel, 39 allowing each pixel on your screen to assume one of 16 million colors. With HDR 40 and wide gamut on the horizon, things are moving even further away to ever 41 requiring any form of dithering. And yet Obra Dinn used it anyway and rekindled 42 a long forgotten love for me. Knowing a tiny bit about dithering from my work 43 on [6]Squoosh, I was especially impressed with Obra Dinn’s ability to keep the 44 dithering stable while I moved and rotated the camera through 3D space and I 45 wanted to understand how it all worked. 46 47 As it turns out, Lucas Pope wrote a [7]forum post where he explains which 48 dithering techniques he uses and how he applies them to 3D space. He put 49 extensive work into making the dithering stable when camera movements occur. 50 Reading that forum post kicked me down the rabbit hole, which this blog post 51 tries to summarize. 52 53 Dithering 54 55 What is Dithering? 56 57 According to Wikipedia, “Dither is an intentionally applied form of noise used 58 to randomize quantization error”, and is a technique not only limited to 59 images. It is actually a technique used to this day on audio recordings, but 60 that is yet another rabbit hole to fall into another time. Let’s dissect that 61 definition in the context of images. First up: Quantization. 62 63 Quantization 64 65 Quantization is the process of mapping a large set of values to a smaller, 66 usually finite, set of values. For the remainder of this article, I am going to 67 use two images as examples: 68 69 [dark-original] Example image #1: A black-and-white photograph of San 70 Francisco’s Golden Gate Bridge, downscaled to 400x267 ([8]higher resolution). 71 [light-original] Example image #2: A black-and-white photograph of San 72 Francisco’s Bay Bridge, downscaled to 253x400 ([9]higher resolution). 73 74 Both black-and-white photos use 256 different shades of gray. If we wanted to 75 use fewer colors — for example just black and white to achieve monochromaticity 76 — we have to change every pixel to be either pure black or pure white. In this 77 scenario, the colors black and white are called our “color palette” and the 78 process of changing pixels that do not use a color from the palette is called 79 “quantization”. Because not all colors from the original images are in the 80 color palette, this will inevitably introduce an error called the “quantization 81 error”. The naïve solution is to quantizer each pixel to the color in the 82 palette that is closest to the pixel’s original color. 83 84 Note: Defining which colors are “close to each other” is open to 85 interpretation and depends on how you measure the distance between two 86 colors. I suppose ideally we’d measure distance in a psycho-visual way, but 87 most of the articles I found simply used the euclidian distance in the RGB 88 cube, i.e. Δred2+Δgreen2+Δblue2\sqrt{\Delta\text{red}^2 + \Delta\text 89 {green}^2 + \Delta\text{blue}^2}Δred2+Δgreen2+Δblue2. 90 91 With our palette only consisting of black and white, we can use the brightness 92 of a pixel to decide which color to quantize to. A brightness of 0 means black, 93 a brightness of 1 means white, everything else is in-between, ideally 94 correlating with human perception such that a brightness of 0.5 is a nice 95 mid-gray. To quantize a given color, we only need to check if the color’s 96 brightness is greater or less than 0.5 and quantize to white and black 97 respectively. Applying this quantization to the image above yields an... 98 unsatisfying result. 99 100 grayscaleImage.mapSelf(brightness => 101 brightness > 0.5 102 ? 1.0 103 : 0.0 104 ); 105 106 Note: The code samples in this article are real but built on top of a 107 helper class GrayImageF32N0F8 I wrote for the [10]demo of this article. 108 It’s similar to the web’s [11]ImageData, but uses Float32Array, only has 109 one color channel, represents values between 0.0 and 1.0 and has a whole 110 bunch of helper functions. The source code is available in [12]the lab. 111 112 [dark-quantized] [light-quantized] Each pixel has been quantized to the either 113 black or white depending on its brightness. 114 115 Gamma 116 117 I had finished writing this article and just wanted to “quickly” look what a 118 black-to-white gradient looks like with the different dithering algorithms. The 119 results showed me that I failed to consider the thing that always becomes a 120 problem when working with images: color spaces. I had written the sentence 121 “ideally correlating with human perception” without actually following it 122 myself. 123 124 My [13]demo is implemented using web technologies, most notably <canvas> and 125 ImageData, which are — at the time of writing — specified to use [14]sRGB. It’s 126 an old color space specification (from 1996) whose value-to-color mapping was 127 modeled to mirror the behavior of CRT monitors. While barely anyone uses CRTs 128 these days, it’s still considered the “safe” color space that gets correctly 129 displayed on every display. As such, it is the default on the web platform. 130 However, sRGB is not linear, meaning that (0.5,0.5,0.5)(0.5, 0.5, 0.5)(0.5,0.5, 131 0.5) in sRGB is not the color a human sees when you mix 50% of (0,0,0)(0, 0, 0) 132 (0,0,0) and (1,1,1)(1, 1, 1)(1,1,1). Instead, it’s the color you get when you 133 pump half the power of full white through your Cathode-Ray Tube (CRT). 134 135 [gradient-srgb] A gradient and how it looks when dithered in sRGB color space. 136 137 138 Warning: I set image-rendering: pixelated; on most of the images in this 139 article. This allows you to zoom in and truly see the pixels. However, on 140 devices with fraction devicePixelRatio, this might introduce artifacts. If 141 in doubt, open the image separate in a new tab. 142 143 As this image shows, the dithered gradient gets bright way too quickly. If we 144 want 0.5 be the color in the middle of pure black and white (as perceived by a 145 human), we need to convert from sRGB to linear RGB space, which can be done 146 with a process called “gamma correction”. Wikipedia lists the following 147 formulas to convert between sRGB and linear RGB. 148 149 srgbToLinear(b)={b12.92b≤0.04045(b+0.0551.055)γotherwiselinearToSrgb(b)= 150 {12.92⋅bb≤0.00313081.055⋅b1γ−0.055otherwise(γ=2.4)\begin{array}{rcl} \text 151 {srgbToLinear}(b) & = & \left\{\begin{array}{ll} \frac{b}{12.92} & b \le 152 0.04045 \\ \left( \frac{b + 0.055}{1.055}\right)^{\gamma} & \text{otherwise} \ 153 end{array}\right.\\ \text{linearToSrgb}(b) & = & \left\{\begin{array}{ll} 12.92 154 \cdot b & b \le 0.0031308 \\ 1.055 \cdot b^\frac{1}{\gamma} - 0.055 & \text 155 {otherwise} \end{array}\right.\\ (\gamma = 2.4) \end{array}\\ srgbToLinear(b) 156 linearToSrgb(b)(γ=2.4)=={12.92b(1.055b+0.055)γb≤0.04045otherwise{12.92⋅b1 157 .055⋅bγ1−0.055b≤0.0031308otherwise 158 159 Formulas to convert between sRGB and linear RGB color space. What beauties they 160 are 🙄. So intuitive. 161 162 With these conversions in place, dithering produces (more) accurate results: 163 164 [gradient-linear] A gradient and how it looks when dithered in linear RGB color 165 space. 166 167 Random noise 168 169 Back to Wikipedia’s definition of dithering: “Intentionally applied form of 170 noise used to randomize quantization error”. We got the quantization down, and 171 now it says to add noise. Intentionally. 172 173 Instead of quantizing each pixel directly, we add noise with a value between 174 -0.5 and 0.5 to each pixel. The idea is that some pixels will now be quantized 175 to the “wrong” color, but how often that happens depends on the pixel’s 176 original brightness. Black will always remain black, white will always remain 177 white, a mid-gray will be dithered to black roughly 50% of the time. 178 Statistically, the overall quantization error is reduced and our brains are 179 eager to do the rest and help you see the, uh, big picture. 180 181 grayscaleImage.mapSelf(brightness => 182 brightness + (Math.random() - 0.5) > 0.5 183 ? 1.0 184 : 0.0 185 ); 186 187 [dark-random] [light-random] Random noise [-0.5; 0.5] has been added to each 188 pixel before quantization. 189 190 I found this quite surprising! It is by no means good — video games from the 191 90s have shown us that we can do better — but this is a very low effort and 192 quick way to get more detail into a monochrome image. And if I was to take 193 “dithering” literally, I’d end my article here. But there’s more… 194 195 Ordered Dithering 196 197 Instead of talking about what kind of noise to add to an image before 198 quantizing it, we can also change our perspective and talk about adjusting the 199 quantization threshold. 200 201 // Adding noise 202 grayscaleImage.mapSelf(brightness => 203 brightness + Math.random() - 0.5 > 0.5 204 ? 1.0 205 : 0.0 206 ); 207 208 // Adjusting the threshold 209 grayscaleImage.mapSelf(brightness => 210 brightness > Math.random() 211 ? 1.0 212 : 0.0 213 ); 214 215 In the context of monochrome dithering, where the quantization threshold is 216 0.5, these two approaches are equivalent: 217 218 brightness+rand()−0.5>0.5⇔brightness>1.0−rand()⇔brightness>rand()\begin{array} 219 {} & \mathrm{brightness} + \mathrm{rand}() - 0.5 & > & 0.5 \\ \Leftrightarrow & 220 \mathrm{brightness} & > & 1.0 - \mathrm{rand}() \\ \Leftrightarrow & \mathrm 221 {brightness} &>& \mathrm{rand}() \end{array} ⇔⇔brightness+rand()−0.5brightnes 222 sbrightness>>>0.51.0−rand()rand() 223 224 The upside of this approach is that we can talk about a “threshold map”. 225 Threshold maps can be visualized to make it easier to reason about why a 226 resulting image looks the way it does. They can also be precomputed and reused, 227 which makes the dithering process deterministic and parallelizable per pixel. 228 As a result, the dithering can happen on the GPU as a shader. This is what Obra 229 Dinn does! There are a couple of different approaches to generating these 230 threshold maps, but all of them introduce some kind of order to the noise that 231 is added to the image, hence the name “ordered dithering”. 232 233 The threshold map for the random dithering above, literally a map full of 234 random thresholds, is also called “white noise”. The name comes from a term in 235 signal processing where every frequency has the same intensity, just like in 236 white light. 237 238 [whitenoise] The threshold map for O.G. dithering is, by definition, white 239 noise. 240 241 Bayer Dithering 242 243 “Bayer dithering” uses a Bayer matrix as the threshold map. They are named 244 after Bryce Bayer, inventor of the [15]Bayer filter, which is in use to this 245 day in digital cameras. Each pixel on the sensor can only detect brightness, 246 but by cleverly arranging colored filters in front of the individual pixels, we 247 can reconstruct color images through [16]demosaicing. The pattern for the 248 filters is the same pattern used in Bayer dithering. 249 250 Bayer matrices come in various sizes which I ended up calling “levels”. Bayer 251 Level 0 is 2×22 \times 22×2 matrix. Bayer Level 1 is a 4×44 \times 44×4 matrix. 252 Bayer Level nnn is a 2n+1×2n+12^{n+1} \times 2^{n+1}2n+1×2n+1 matrix. A level 253 nnn matrix can be recursively calculated from level n−1n-1n−1 (although 254 Wikipedia also lists an [17]per-cell algorithm). If your image happens to be 255 bigger than your bayer matrix, you can tile the threshold map. 256 257 Bayer(0)=(0231)\begin{array}{rcl} \text{Bayer}(0) & = & \left( \begin{array} 258 {cc} 0 & 2 \\ 3 & 1 \\ \end{array} \right) \\ \end{array} Bayer(0)=(0321) 259 260 Bayer(n)=(4⋅Bayer(n−1)+04⋅Bayer(n−1)+24⋅Bayer(n−1)+34⋅Bayer(n−1)+1)\begin 261 {array}{c} \text{Bayer}(n) = \\ \left( \begin{array}{cc} 4 \cdot \text{Bayer} 262 (n-1) + 0 & 4 \cdot \text{Bayer}(n-1) + 2 \\ 4 \cdot \text{Bayer}(n-1) + 3 & 4 263 \cdot \text{Bayer}(n-1) + 1 \\ \end{array} \right) \end{array} Bayer(n)=(4⋅ 264 Bayer(n−1)+04⋅Bayer(n−1)+34⋅Bayer(n−1)+24⋅Bayer(n−1)+1) 265 266 Recursive definition of Bayer matrices. 267 268 A level nnn Bayer matrix contains the numbers 000 to 22n+22^{2n+2}22n+2. Once 269 you normalize the Bayer matrix, i.e. divide by 22n+22^{2n+2}22n+2, you can use 270 it as a threshold map: 271 272 const bayer = generateBayerLevel(level); 273 grayscaleImage.mapSelf((brightness, { x, y }) => 274 brightness > bayer.valueAt(x, y, { wrap: true }) 275 ? 1.0 276 : 0.0 277 ); 278 279 One thing to note: Bayer dithering using matrices as defined above will render 280 an image lighter than it originally was. For example: An area where every pixel 281 has a brightness of 1255=0.4%\frac{1}{255} = 0.4\%2551=0.4%, a level 0 Bayer 282 matrix of size 2×22\times22×2 will make one out of the four pixels white, 283 resulting in an average brightness of 25%25\%25%. This error gets smaller with 284 higher Bayer levels, but a fundamental bias remains. 285 286 [bayerbias] The almost-black areas in the image are getting noticeably 287 brighter. 288 289 In our dark test image, the sky is not pure black and made significantly 290 brighter when using Bayer Level 0. While it gets better with higher levels, an 291 alternative solution is to flip the bias and make images render darker by 292 inverting the way we use the Bayer matrix: 293 294 const bayer = generateBayerLevel(level); 295 grayscaleImage.mapSelf((brightness, { x, y }) => 296 // 👇 297 brightness > 1 - bayer.valueAt(x, y, { wrap: true }) 298 ? 1.0 299 : 0.0 300 ); 301 302 I have used the original Bayer definition for the light image and the inverted 303 version for the dark image. I personally found Level 1 and 3 the most 304 aesthetically pleasing. 305 306 [dark-bayer0] [light-bayer0] Bayer Dithering Level 0. [dark-bayer1] 307 [light-bayer1] Bayer Dithering Level 1. [dark-bayer2] [light-bayer2] Bayer 308 Dithering Level 2. [dark-bayer3] [light-bayer3] Bayer Dithering Level 3. 309 310 Blue noise 311 312 Both white noise and Bayer dithering have drawbacks, of course. Bayer 313 dithering, for example, is very structured and will look quite repetitive, 314 especially at lower levels. White noise is random, meaning that there 315 inevitably will be clusters of bright pixels and voids of darker pixels in the 316 threshold map. This can be made more obvious by squinting or, if that is too 317 much work for you, through blurring the threshold map algorithmically. These 318 clusters and voids can affect the output of the dithering process negatively. 319 If darker areas of the image fall into one of the clusters, details will get 320 lost in the dithered output (and vice-versa for brighter areas falling into 321 voids). 322 323 [whitenoiseblur] Clear clusters and voids remain visible even after applying a 324 Gaussian blur (σ = 1.5). 325 326 There is a variant of noise called “blue noise”, that addresses this issue. It 327 is called blue noise because higher frequencies have higher intensities 328 compared to the lower frequencies, just like blue light. By removing or 329 dampening the lower frequencies, cluster and voids become less pronounced. Blue 330 noise dithering is just as fast to apply to an image as white noise dithering — 331 it’s just a threshold map in the end — but generating blue noise is a bit 332 harder and expensive. 333 334 The most common algorithm to generate blue noise seems to be the 335 “void-and-cluster method” by [18]Robert Ulichney. Here is the [19]original 336 whitepaper. I found the way the algorithm is described quite unintuitive and, 337 now that I have implemented it, I am convinced it is explained in an 338 unnecessarily abstract fashion. But it is quite clever! 339 340 The algorithm is based on the idea that you can find a pixel that is part of 341 cluster or a void by applying a [20]Gaussian Blur to the image and finding the 342 brightest (or darkest) pixel in the blurred image respectively. After 343 initializing a black image with a couple of randomly placed white pixels, the 344 algorithm proceeds to continuously swap cluster pixels and void pixels to 345 spread the white pixels out as evenly as possible. Afterwards, every pixel gets 346 a number between 0 and n (where n is the total number of pixels) according to 347 their importance for forming clusters and voids. For more details, see the [21] 348 paper. 349 350 My implementation works fine but is not very fast, as I didn’t spend much time 351 optimizing. It takes about 1 minute to generate a 64×64 blue noise texture on 352 my 2018 MacBook, which is sufficient for these purposes. If something faster is 353 needed, a promising optimization would be to apply the Gaussian Blur not in the 354 spatial domain but in the frequency domain instead. 355 356 Excursion: Of course knowing this nerd-sniped me into implementing it. The 357 reason this optimization is so promising is because convolution (which is 358 the underlying operation of a Gaussian blur) has to loop over each field of 359 the Gaussian kernel for each pixel in the image. However, if you convert 360 both the image as well as the Gaussian kernel to the frequency domain 361 (using one of the many Fast Fourier Transform algorithms), convolution 362 becomes an element-wise multiplication. Since my targeted blue noise size 363 is a power of two, I could implement the well-explored [22]in-place variant 364 of the Cooley-Tukey FFT algorithm. After [23]some initial hickups, it did 365 end up cutting the blue noise generation time by 50%. I still wrote pretty 366 garbage-y code, so there’s a lot more to room for optimizations. 367 368 [bluenoiseblur] A 64×64 blue noise with a Gaussian blur applied (σ = 1.5). No 369 clear structures remain. 370 371 As blue noise is based on a Gaussian Blur, which is calculated on a torus (a 372 fancy way of saying that Gaussian blur wraps around at the edges), blue noise 373 will also tile seamlessly. So we can use the 64×64 blue noise and repeat it to 374 cover the entire image. Blue noise dithering has a nice, even distribution 375 without showing any obvious patterns, balancing rendering of details and 376 organic look. 377 378 [dark-bluenoise] [light-bluenoise] Blue noise dithering. 379 380 Error diffusion 381 382 All the previous techniques rely on the fact that quantization errors will 383 statistically even out because the thresholds in the threshold maps are 384 uniformly distributed. A different approach to quantization is the concept of 385 error diffusion, which is most likely what you have read about if you have ever 386 researched image dithering before. In this approach we don’t just quantize and 387 hope that on average the quantization error remains negligible. Instead, we 388 measure the quantization error and diffuse the error onto neighboring pixels, 389 influencing how they will get quantized. We are effectively changing the image 390 we want to dither as we go along. This makes the process inherently sequential. 391 392 Foreshadowing: One big advantage of error diffusion algorithms that we 393 won’t touch on in this post is that they can handle arbitrary color 394 palettes, while ordered dithering requires your color palette to be evenly 395 spaced. More on that another time. 396 397 Almost all error diffusion ditherings that I am going to look at use a 398 “diffusion matrix”, which defines how the quantization error from the current 399 pixel gets distributed across the neighboring pixels. For these matrices it is 400 often assumed that the image’s pixels are traversed top-to-bottom, 401 left-to-right — the same way us westerners read text. This is important as the 402 error can only be diffused to pixels that haven’t been quantized yet. If you 403 find yourself traversing an image in a different order than the diffusion 404 matrix assumes, flip the matrix accordingly. 405 406 “Simple” 2D error diffusion 407 408 The naïve approach to error diffusion shares the quantization error between the 409 pixel below the current one and the one to the right, which can be described 410 with the following matrix: 411 412 (∗0.50.50)\left(\begin{array}{cc} * & 0.5 \\ 0.5 & 0 \\ \end{array} \right) (∗0 413 .50.50) 414 415 Diffusion matrix that shares half the error to 2 neightboring pixels, * marking 416 the current pixel. 417 418 The diffusion algorithm visits each pixel in the image (in the right order!), 419 quantizes the current pixel and measures the quantization error. Note that the 420 quantization error is signed, i.e. it can be negative if the quantization made 421 the pixel brighter than the original brightness value. We then add fractions of 422 the quantization error to neighboring pixels as specified by the matrix. Rinse 423 and repeat. 424 425 Error diffusion visualized step by step. 426 427 This animation is supposed to visualize the algorithm, but won’t be able to 428 show that the dithered result resembles the original. 4×4 pixels are hardly 429 enough do diffuse and average out quantization errors. But it does show that if 430 a pixel is made brighter during quantization, neighboring pixels will be made 431 darker to make up for it (and vice-versa). 432 433 [dark-simple2d] [light-simple2d] Simple 2D Error Diffusion Dithering. 434 435 However, the simplicity of the diffusion matrix is prone to generating 436 patterns, like the line-like patterns you can see in the test images above. 437 438 Floyd-Steinberg 439 440 Floyd-Steinberg is arguably the most well-known error diffusion algorithm, if 441 not even the most well-known dithering algorithm. It uses a more elaborate 442 diffusion matrix to distribute the quantization error to all directly 443 neighboring, unvisited pixels. The numbers are carefully chosen to prevent 444 repeating patterns as much as possible. 445 446 116⋅(∗7351)\frac{1}{16} \cdot \left(\begin{array} {} & * & 7 \\ 3 & 5 & 1 \\ \ 447 end{array} \right) 161⋅(3∗571) 448 449 Diffusion matrix by Robert W. Floyd and Louis Steinberg. 450 451 Floyd-Steinberg is a big improvement as it prevents a lot of patterns from 452 forming. However, larger areas with little texture can still end up looking a 453 bit unorganic. 454 455 [dark-floydsteinberg] [light-floydsteinberg] Floyd-Steinberg Error Diffusion 456 Dithering. 457 458 Jarvis-Judice-Ninke 459 460 Jarvis, Judice and Ninke take an even bigger diffusion matrix, distributing the 461 error to more pixels than just immediately neighboring ones. 462 463 148⋅(∗753575313531)\frac{1}{48} \cdot \left(\begin{array} {} & {} & * & 7 & 5 \ 464 \ 3 & 5 & 7 & 5 & 3 \\ 1 & 3 & 5 & 3 & 1 \\ \end{array} \right) 481⋅⎝⎛3153∗ 465 75753531⎠⎞ 466 467 Diffusion matrix by J. F. Jarvis, C. N. Judice, and W. H. Ninke of Bell Labs. 468 469 Using this diffusion matrix, patterns are even less likely to emerge. While the 470 test images still show some line like patterns, they are much less distracting 471 now. 472 473 [dark-jarvisjudiceninke] [light-jarvisjudiceninke] Jarvis’, Judice’s and 474 Ninke’s dithering. 475 476 Atkinson Dither 477 478 Atkinson dithering was developed at Apple by Bill Atkinson and gained notoriety 479 on on early Macintosh computers. 480 481 18⋅(∗111111)\frac{1}{8} \cdot \left(\begin{array}{} & * & 1 & 1 \\ 1 & 1 & 1 & 482 \\ & 1 & & \\ \end{array} \right) 81⋅⎝⎛1∗11111⎠⎞ 483 484 Diffusion matrix by Bill Atkinson. 485 486 It’s worth noting that the Atkinson diffusion matrix contains six ones, but is 487 normalized using 18\frac{1}{8}81, meaning it doesn’t diffuse the entire error 488 to neighboring pixels, increasing the perceived contrast of the image. 489 490 [dark-atkinson] [light-atkinson] Atkinson Dithering. 491 492 Riemersma Dither 493 494 To be completely honest, the Riemersma dither is something I stumbled upon by 495 accident. I found an [24]in-depth article while I was researching the other 496 dithering algorithms. It doesn’t seem to be widely known, but I really like the 497 way it looks and the concept behind it. Instead of traversing the image 498 row-by-row it traverses the image with a [25]Hilbert curve. Technically, any 499 [26]space-filling curve would do, but the Hilbert curve came recommended and is 500 [27]rather easy to implement using generators. Through this it aims to take the 501 best of both ordered dithering and error diffusion dithering: Limiting the 502 number of pixels a single pixel can influence together with the organic look 503 (and small memory footprint). 504 505 [hilbertcurve] Visualization of the 256x256 Hilbert curve by making pixels 506 brighter the later they are visited by the curve. 507 508 The Hilbert curve has a “locality” property, meaning that the pixels that are 509 close together on the curve are also close together in the picture. This way we 510 don’t need to use an error diffusion matrix but rather a diffusion sequence of 511 length nnn. To quantize the current pixel, the last nnn quantization errors are 512 added to the current pixel with weights given in the diffusion sequence. In the 513 article they use an exponential falloff for the weights — the previous pixel’s 514 quantization error getting a weight of 1, the oldest quantization error in the 515 list a small, chosen weight rrr. This results in the following formula for the 516 iiith weight: 517 518 weight[i]=r−in−1\text{weight}[i] = r^{-\frac{i}{n-1}} weight[i]=r−n−1i 519 520 The article recommends r=116r = \frac{1}{16}r=161 and a minimum list length of 521 n=16n = 16n=16, but for my test image I found r=18r = \frac{1}{8}r=81 and n= 522 32n = 32n=32 to be better looking. 523 524 [dark-riemersma] [light-riemersma] 525 526 Riemersma dither with r=18r = \frac{1}{8}r=81 and n=32n = 32n=32. 527 528 The dithering looks extremely organic, almost as good as blue noise dithering. 529 At the same time it is easier to implement than both of the previous ones. It 530 is, however, still an error diffusion dithering algorithm, meaning it is 531 sequential and not suitable to run on a GPU. 532 533 💛 Blue noise, Bayer & Riemersma 534 535 As a 3D game, Obra Dinn had to use ordered dithering to be able to run it as a 536 shader. It uses both Bayer dithering and blue noise dithering which I also 537 think are the most aesthetically pleasing choices. Bayer dithering shows a bit 538 more structure while blue noise looks very natural and organic. I am also 539 particularly fond of the Riemersma dither and I want to explore how it holds up 540 when there are multiple colors in the palette. 541 542 Obra Dinn uses blue noise dithering for most of the environment. People and 543 other objects of interest are dithered using Bayer, which forms a nice visual 544 contrast and makes them stand out without breaking the games overall aesthetic. 545 Again, more on his reasoning as well his solution to handling camera movement 546 in his [28]forum post. 547 548 If you want to try different dithering algorithms on one of your own images, 549 take a look at my [29]demo that I wrote to generate all the images in this blog 550 post. Keep in mind that these are not the fastest. If you decide to throw your 551 20 megapixel camera JPEG at this, it will take a while. 552 553 Note: It seems I am hitting a de-opt in Safari. My blue noise generator 554 takes ~30 second in Chrome, but takes >20 minutes Safari. It is 555 considerably quicker in Safari Tech Preview. 556 557 I am sure this super niche, but I enjoyed this rabbit hole. If you have any 558 opinions or experiences with dithering, I’d love to hear them. 559 560 Thanks & other sources 561 562 Thanks to [30]Lucas Pope for his games and the visual inspiration. 563 564 Thanks to [31]Christoph Peters for his excellent [32]article on blue noise 565 generation. 566 567 [33]← Back to home [surma] 568 569 Surma 570 571 DX at Shopify. Web Platform Advocate. 572 Craving simplicity, finding it nowhere. 573 “A bit of a ‘careless eager student’ archetype” according to HN. 574 Internetrovert 🏳️🌈 He/him. 575 576 [34] twitter [35] mastodon [36] github [37] instagram [38] keybase [39] podcast 577 [40] rss [41]Licenses 578 579 References: 580 581 [1] https://surma.dev/ 582 [2] https://obradinn.com/ 583 [3] https://twitter.com/dukope 584 [4] https://papersplea.se/ 585 [5] https://unity.com/ 586 [6] https://squoosh.app/ 587 [7] https://forums.tigsource.com/index.php?topic=40832.msg1363742#msg1363742 588 [8] https://surma.dev/things/ditherpunk/dark-hires.jpg 589 [9] https://surma.dev/things/ditherpunk/light-hires.jpg 590 [10] https://surma.dev/lab/ditherpunk/lab 591 [11] https://developer.mozilla.org/en-US/docs/Web/API/ImageData 592 [12] https://surma.dev/lab/ditherpunk 593 [13] https://surma.dev/lab/ditherpunk/lab 594 [14] https://en.wikipedia.org/wiki/SRGB 595 [15] https://en.wikipedia.org/wiki/Bayer_filter 596 [16] https://en.wikipedia.org/wiki/Demosaicing 597 [17] https://en.wikipedia.org/wiki/Ordered_dithering#Pre-calculated_threshold_maps 598 [18] http://ulichney.com/ 599 [19] https://surma.dev/things/ditherpunk/bluenoise-1993.pdf 600 [20] https://en.wikipedia.org/wiki/Gaussian_blur 601 [21] https://surma.dev/things/ditherpunk/bluenoise-1993.pdf 602 [22] https://en.wikipedia.org/wiki/Cooley%E2%80%93Tukey_FFT_algorithm#Data_reordering,_bit_reversal,_and_in-place_algorithms 603 [23] https://twitter.com/DasSurma/status/1341203941904834561 604 [24] https://www.compuphase.com/riemer.htm 605 [25] https://en.wikipedia.org/wiki/Hilbert_curve 606 [26] https://en.wikipedia.org/wiki/Space-filling_curve 607 [27] https://twitter.com/DasSurma/status/1343569629369786368 608 [28] https://forums.tigsource.com/index.php?topic=40832.msg1363742#msg1363742 609 [29] https://surma.dev/lab/ditherpunk/lab 610 [30] https://twitter.com/dukope 611 [31] https://twitter.com/momentsincg 612 [32] http://momentsingraphics.de/BlueNoise.html 613 [33] https://surma.dev/ 614 [34] https://twitter.com/dassurma 615 [35] https://mastodon.social/@surma 616 [36] https://github.com/surma 617 [37] https://instagram.com/dassurma 618 [38] https://keybase.io/surma 619 [39] https://http203.libsyn.com/ 620 [40] https://surma.dev/index.xml 621 [41] https://surma.dev/licenses/