L'arte del dithering: immagini bellissime con un solo bit
Come gli algoritmi di dithering creano le sfumature con soli pixel bianchi e neri, dal Floyd-Steinberg al blue noise, con implementazioni pratiche.

Return of the Obra Dinn di Lucas Pope è uno dei giochi visivamente più sorprendenti mai realizzati, e usa esattamente due colori: nero e bianco. Un bit per pixel. Niente grigi, niente gradienti, niente anti-aliasing. Ogni pixel è acceso o spento. Eppure in qualche modo il gioco riesce a rendere un mondo 3D completo, con personaggi riconoscibili, percezione della profondità e illuminazione atmosferica, il tutto grazie a un dithering ingegnoso.
Il dithering è la tecnica che consiste nel disporre pixel bianchi e neri in modo da indurre l'occhio a vedere sfumature intermedie. Esiste fin dai primi giorni della computer grafica e sta vivendo una nuova stagione: nello sviluppo di videogiochi, nel web design e nel problema sempre più attuale della visualizzazione di immagini sugli schermi e-ink. Gli algoritmi alla base sono eleganti, sorprendentemente vari e più applicabili di quanto si pensi.
Perché il dithering funziona
Il sistema visivo non elabora i singoli pixel: integra intere regioni. Quando guardi un pattern di pixel bianchi e neri alternati, non vedi i singoli punti: vedi grigio. Una zona in cui il 25% dei pixel è bianco appare come un grigio al 25%. Una zona con il 75% di pixel bianchi appare come un grigio al 75%. La densità di pixel bianchi in un'area controlla la luminosità percepita.
È lo stesso principio alla base della stampa a mezzitoni dei giornali. Quei punti di dimensione e spaziatura variabili creano l'illusione di un tono continuo partendo da semplice inchiostro nero su carta bianca. Il dithering è l'equivalente digitale: usa pattern di pixel invece delle dimensioni dei punti.
La sfida non è creare la scala di grigi, che è semplice. La sfida è farlo senza artefatti visibili: niente banding (transizioni brusche tra un pattern e l'altro), niente bias direzionali (pattern che generano linee visibili) e niente effetti moiré (figure di interferenza tra il dithering e la griglia del display).
Threshold dithering: l'approccio ingenuo
Il dithering più semplice: si confronta la luminosità di ogni pixel con una soglia (di solito il 50%). Se è più chiaro della soglia diventa bianco, altrimenti nero. Il risultato è una silhouette ad alto contrasto senza alcuna gradazione. Utile per testo e disegni al tratto, pessimo per fotografie o rendering 3D.
import numpy as np
from PIL import Image
def threshold_dither(image, threshold=128):
"""Binary threshold — no dithering at all, really."""
gray = np.array(image.convert('L'), dtype=float)
return Image.fromarray((gray > threshold).astype(np.uint8) * 255)
Ordered dithering: pattern su griglia
L'ordered dithering usa una matrice di soglie (chiamata anche matrice di Bayer) che varia la soglia lungo un pattern a piastrelle ripetute. Invece di confrontare ogni pixel con la stessa soglia, ciascuno viene confrontato con un valore diverso della matrice in base alla sua posizione.
def ordered_dither(image, matrix_size=4):
"""Ordered dithering with Bayer matrix."""
gray = np.array(image.convert('L'), dtype=float)
h, w = gray.shape
# 4×4 Bayer matrix (values 0-15, normalized to 0-255)
bayer_4x4 = np.array([
[ 0, 8, 2, 10],
[12, 4, 14, 6],
[ 3, 11, 1, 9],
[15, 7, 13, 5]
]) * (255 / 16)
# Tile the matrix across the image
threshold = np.tile(bayer_4x4, (h // 4 + 1, w // 4 + 1))[:h, :w]
return Image.fromarray((gray > threshold).astype(np.uint8) * 255)
L'ordered dithering produce un caratteristico motivo a tratteggio incrociato, immediatamente riconoscibile. A bassa risoluzione la struttura a griglia regolare è visibile e dà all'immagine un aspetto meccanico, retrò. In molti contesti è in realtà una scelta estetica: il pattern ordinato di una matrice di Bayer 8×8 ha un aspetto distintivo che molti giochi e artisti usano intenzionalmente.
La matrice di Bayer è costruita con cura in modo che ogni livello di soglia aggiunga nuovi pixel in posizioni il più possibile distanti da quelli già esistenti. Questo riduce gli agglomerati e garantisce la distribuzione più uniforme dei punti a ogni livello di luminosità. È inoltre estremamente veloce (un confronto per pixel, nessuna dipendenza dai vicini), il che la rende adatta al rendering in tempo reale.
Error diffusion: Floyd-Steinberg e compagni
Il dithering a diffusione dell'errore segue un approccio completamente diverso. Invece di usare una matrice di soglie fissa, elabora i pixel in sequenza e distribuisce l'«errore» (la differenza tra il valore originale del pixel e il risultato quantizzato, bianco o nero) ai pixel vicini non ancora elaborati.
def floyd_steinberg_dither(image):
"""Floyd-Steinberg error diffusion dithering."""
gray = np.array(image.convert('L'), dtype=float)
h, w = gray.shape
for y in range(h):
for x in range(w):
old_pixel = gray[y, x]
new_pixel = 255.0 if old_pixel > 128 else 0.0
gray[y, x] = new_pixel
error = old_pixel - new_pixel
# Distribute error to neighbors
# Floyd-Steinberg diffusion kernel:
# * 7/16
# 3/16 5/16 1/16
if x + 1 < w:
gray[y, x + 1] += error * 7 / 16
if y + 1 < h:
if x - 1 >= 0:
gray[y + 1, x - 1] += error * 3 / 16
gray[y + 1, x] += error * 5 / 16
if x + 1 < w:
gray[y + 1, x + 1] += error * 1 / 16
return Image.fromarray(gray.astype(np.uint8))
Il dithering Floyd-Steinberg produce risultati molto più naturali dell'ordered dithering. La distribuzione dei pixel non mostra alcuna griglia visibile e gestisce i gradienti morbidi in modo eccellente. I punti si aggregano in modo organico, con un aspetto più vicino alla stampa a mezzitoni fotografica.
Lo svantaggio: è sequenziale. Ogni pixel dipende dall'errore dei pixel già elaborati, quindi non è facile parallelizzarlo. Questo lo rende più lento dell'ordered dithering e più difficile da implementare nelle pipeline di rendering basate su shader. Per le immagini pre-elaborate non è un problema; per il rendering 3D in tempo reale a 60 fps, sì.
Blue noise dithering: il meglio dei due mondi
Il blue noise dithering è oggi lo stato dell'arte per molte applicazioni. Usa una matrice di soglie come l'ordered dithering (quindi è veloce e parallelizzabile), ma i valori della matrice sono disposti in modo da produrre una distribuzione di blue noise: i punti sono distribuiti in modo casuale ma con un vincolo di distanza minima, evitando sia gli agglomerati del white noise sia i pattern a griglia dell'ordered dithering.
Il termine «blue noise» viene dall'elaborazione dei segnali. Il blue noise concentra gran parte della sua energia alle alte frequenze, come la luce blu. In termini spaziali, questo significa che il pattern non ha struttura su larga scala (niente griglia visibile, niente bias direzionale), ma i punti sono distribuiti uniformemente a livello locale (niente agglomerati, niente vuoti). È il pattern che otterresti lanciando freccette su un bersaglio con il vincolo che ogni freccetta debba trovarsi a una distanza minima da tutte le altre.
Generare una buona matrice di soglie blue noise è sorprendentemente difficile: è un problema di ottimizzazione che richiede iterazioni finché lo spettro di frequenza non corrisponde al profilo desiderato. Ma una volta ottenuta la matrice, usarla è veloce quanto il dithering di Bayer: un confronto per pixel.
L'approccio di Obra Dinn: dithering mappato sulla sfera
Il dithering di Return of the Obra Dinn è particolarmente ingegnoso perché risolve un problema specifico del rendering 3D: la stabilità temporale. Quando un pattern di dithering standard viene applicato nello spazio schermo, muovere la camera fa sì che il pattern «nuoti»: resta fisso sullo schermo mentre la geometria 3D si muove sotto di esso. Ne risulta un effetto sfarfallio estremamente distraente.
La soluzione di Pope: mappare il pattern di dithering nello spazio 3D anziché nello spazio schermo. Il pattern viene proiettato su una sfera che circonda la camera, e per ogni pixel viene campionato in base alla direzione di vista. Quando la camera ruota, il pattern ruota con lei. Quando gli oggetti si muovono nel mondo, il loro dithering resta stabile perché deriva dalla posizione nel mondo e non da quella sullo schermo.
È una tecnica che puoi adattare a qualsiasi applicazione 3D che usi dithering o effetti a mezzitoni. L'idea chiave: se vuoi la stabilità temporale, i valori di soglia devono dipendere da qualcosa di stabile (posizione nel mondo, direzione di vista) e non da qualcosa che cambia a ogni frame (coordinate dello schermo).
Applicazioni pratiche oltre l'estetica retrò
Il dithering non serve solo per l'estetica retrò dei videogiochi. Ha applicazioni pratiche concrete nello sviluppo moderno.
- Display e-ink. Gli e-reader e la cartellonistica e-ink hanno una capacità di scala di grigi limitata (tipicamente 16 livelli). Il dithering è indispensabile per visualizzare fotografie e gradienti su questi dispositivi. La scelta dell'algoritmo incide direttamente sulla leggibilità e sulla qualità dell'immagine.
- Riduzione delle dimensioni dei file. Un'immagine dithered a 1 bit occupa 1/8 dello spazio grezzo di un'immagine in scala di grigi a 8 bit. Per la distribuzione web di immagini decorative, le PNG dithered possono essere molto più piccole delle alternative in scala di grigi. Alcuni web designer usano deliberatamente immagini dithered sia per ragioni estetiche sia per le prestazioni.
- Quantizzazione del colore. Quando riduci un'immagine a colori completi a una palette limitata (GIF, PNG indicizzate o display con palette ristretta), il dithering evita il banding che si verifica quando i gradienti morbidi vengono mappati su valori di colore discreti. Il Floyd-Steinberg applicato nello spazio colore è il modo in cui gli encoder GIF gestiscono questo problema.
- Banding nei gradienti durante il rendering. I render target a bassa precisione (8 bit per canale) possono produrre banding visibile nei gradienti morbidi. Aggiungere dithering nello spazio schermo (anche solo ±0,5 di un valore) spezza il banding senza rumore visibile. La maggior parte dei motori di gioco moderni lo fa automaticamente.
- Stampanti termiche e scontrini. Le stampanti termiche sono intrinsecamente a 1 bit: ogni punto viene riscaldato (nero) oppure no. Il dithering converte fotografie e grafica in una forma stampabile. La scelta dell'algoritmo determina se l'immagine dello scontrino è riconoscibile o diventa una macchia.
Implementare il dithering in uno shader
Per le applicazioni in tempo reale, ordered dithering e blue noise dithering possono essere implementati in modo efficiente come fragment shader.
// GLSL fragment shader for Bayer 8x8 ordered dithering
precision mediump float;
uniform sampler2D uTexture;
varying vec2 vTexCoord;
// Bayer 8x8 matrix lookup via bit manipulation (no texture needed)
float bayer8x8(vec2 pos) {
ivec2 p = ivec2(mod(pos, 8.0));
int index = p.x + p.y * 8;
// Bit-reversal trick for Bayer matrix values
int value = 0;
int x = p.x ^ p.y;
int y = p.y;
for (int i = 0; i < 3; i++) {
value = value * 4 + (x & 1) * 2 + (y & 1);
x >>= 1;
y >>= 1;
}
return float(value) / 64.0;
}
void main() {
vec4 color = texture2D(uTexture, vTexCoord);
float luminance = dot(color.rgb, vec3(0.299, 0.587, 0.114));
float threshold = bayer8x8(gl_FragCoord.xy);
float dithered = step(threshold, luminance);
gl_FragColor = vec4(vec3(dithered), 1.0);
}
Per il blue noise, di solito si precalcola la texture di rumore e la si campiona nello shader. La texture può essere ripetuta a piastrelle, quindi una texture blue noise da 64×64 o 128×128 copre tutto lo schermo quando viene ripetuta. Il costo per pixel è un campionamento di texture e un confronto: praticamente gratuito sulle GPU moderne.
Scegliere l'algoritmo giusto
Ogni approccio al dithering ha un caso d'uso in cui è la scelta migliore.
- Threshold: testo, disegni al tratto, grafica ad alto contrasto dove il dettaglio conta più della gamma tonale.
- Ordered (Bayer): rendering in tempo reale quando vuoi un'estetica retrò o a mezzitoni deliberata. Veloce, parallelizzabile, adatto alle GPU.
- Floyd-Steinberg: elaborazione di immagini statiche dove la qualità conta più di tutto. Ideale per fotografie e immagini con gradienti morbidi.
- Blue noise: rendering in tempo reale quando vuoi risultati dall'aspetto naturale senza pattern visibili. La scelta più versatile per le applicazioni moderne.
- Sferico/nello spazio del mondo: rendering 3D dove la stabilità temporale è fondamentale. Essenziale per il rendering 3D a 1 bit come quello di Obra Dinn.
Il dithering è uno di quegli argomenti in cui la semplicità del concetto («disponi dei punti perché sembrino ombreggiature») nasconde la profondità delle scelte implementative. Ogni algoritmo rappresenta un diverso compromesso tra qualità, velocità e carattere visivo. E in un'epoca in cui display e-ink, connessioni a bassa banda ed estetiche retrò sono tutti temi rilevanti, comprendere questi compromessi è più pratico che mai.


