A Arte do Dithering: Imagens Bonitas com Apenas Um Bit
Como algoritmos de dithering criam sombreamento com apenas pixels pretos e brancos. De Floyd-Steinberg ao blue noise, com implementações práticas.

O Return of the Obra Dinn, do Lucas Pope, é um dos jogos mais impressionantes visualmente já feitos, e usa exatamente duas cores: preto e branco. Um bit por pixel. Sem tons de cinza, sem gradientes, sem anti-aliasing. Cada pixel está ligado ou desligado. Mesmo assim, o jogo renderiza um mundo 3D completo, com personagens reconhecíveis, percepção de profundidade e iluminação atmosférica, tudo com dithering engenhoso.
Dithering é a técnica de organizar pixels pretos e brancos em padrões que enganam o olho e fazem ele ver tons intermediários. Existe desde os primórdios da computação gráfica e está em alta de novo, no desenvolvimento de jogos, no design web e no problema cada vez mais relevante de exibir imagens em telas e-ink. Os algoritmos por trás dele são elegantes, surpreendentemente variados e mais aplicáveis do que você imagina.
Por que o Dithering Funciona
Seu sistema visual não processa pixels individuais, ele integra regiões. Quando você olha para um padrão de pixels pretos e brancos alternados, não enxerga pontos isolados. Enxerga cinza. Uma região em que 25% dos pixels são brancos parece cinza de 25%. Uma região com 75% de brancos parece cinza de 75%. A densidade de pixels brancos numa área controla o brilho percebido.
Esse é o mesmo princípio por trás da meia-tinta (halftone) da impressão de jornais. Aqueles pontos de tamanho e espaçamento variados criam a ilusão de tom contínuo a partir de tinta preta pura sobre papel branco. O dithering é o equivalente digital: usa padrões de pixels no lugar do tamanho dos pontos.
O desafio não é criar escala de cinza, isso é simples. O desafio é fazer isso sem artefatos visíveis: nada de banding (transições abruptas entre padrões), nada de viés direcional (padrões que criam linhas visíveis) e nada de efeitos moiré (padrões de interferência entre o dithering e a grade do display).
Threshold Dithering: A Abordagem Ingênua
O dithering mais simples: compara o brilho de cada pixel com um limiar (geralmente 50%). Mais claro que o limiar vira branco. Mais escuro vira preto. Isso produz uma silhueta de alto contraste, sem nenhuma gradação. Útil para texto e line art, péssimo para fotos ou renderização 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: Padrões em Grade
O ordered dithering usa uma matriz de limiares (também chamada de matriz de Bayer) que varia o limiar ao longo de um padrão de bloco que se repete. Em vez de comparar todos os pixels com o mesmo limiar, cada pixel é comparado com um valor diferente da matriz, de acordo com sua posição.
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)
O ordered dithering produz um padrão de hachura cruzada muito característico, reconhecível de imediato. Em resoluções baixas, a estrutura regular da grade fica visível, dando à imagem uma qualidade mecânica e retrô. Em muitos contextos isso é, na prática, uma escolha estética: o padrão ordenado de uma matriz de Bayer 8×8 tem um visual distinto que muitos jogos e artistas usam de propósito.
A matriz de Bayer é construída com cuidado para que cada nível de limiar adicione novos pixels em posições o mais espaçadas possível dos existentes. Isso minimiza aglomerações e dá a distribuição mais uniforme de pontos em cada nível de brilho. Também é extremamente rápido, com uma comparação por pixel e sem dependência de vizinhos, o que o torna adequado para renderização em tempo real.
Error Diffusion: Floyd-Steinberg e Companhia
O error diffusion adota uma abordagem completamente diferente. Em vez de usar uma matriz de limiares fixa, ele processa os pixels sequencialmente e distribui o 'erro' (a diferença entre o valor original do pixel e o resultado quantizado, preto ou branco) para os vizinhos que ainda não foram processados.
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))
O Floyd-Steinberg produz resultados bem mais naturais que o ordered dithering. A distribuição dos pixels não tem padrão de grade visível e lida muito bem com gradientes suaves. Os pontos se agrupam de forma orgânica, criando um visual mais próximo da meia-tinta fotográfica de impressão.
A desvantagem: ele é sequencial. Cada pixel depende do erro dos pixels processados antes, então não dá para paralelizar facilmente. Isso o torna mais lento que o ordered dithering e mais difícil de implementar em pipelines de renderização baseados em shaders. Para imagens pré-processadas, isso não importa. Para renderização 3D em tempo real a 60 fps, é um problema.
Blue Noise Dithering: O Melhor dos Dois Mundos
O blue noise dithering é o estado da arte atual para muitas aplicações. Ele usa uma matriz de limiares como o ordered dithering (então é rápido e paralelizável), mas os valores da matriz são organizados para produzir uma distribuição de blue noise: os pontos são colocados de forma aleatória, porém com uma restrição de espaçamento mínimo, evitando tanto as aglomerações do ruído branco quanto os padrões de grade do ordered dithering.
O termo 'blue noise' vem do processamento de sinais. O blue noise concentra a maior parte da energia em altas frequências, como a luz azul. Em termos espaciais, isso significa que o padrão não tem estrutura em larga escala (nenhuma grade visível, nenhum viés direcional), mas os pontos são distribuídos uniformemente em escala local (sem aglomerados, sem vazios). É o padrão que você obteria jogando dardos num alvo com a restrição de que cada dardo precisa estar a uma distância mínima de todos os outros.
Gerar uma boa matriz de limiares de blue noise é surpreendentemente difícil. É um problema de otimização que exige iterações até que o espectro de frequências corresponda ao perfil desejado. Mas, depois que você tem a matriz, usá-la é tão rápido quanto o dithering de Bayer: uma comparação por pixel.
Abordagem do Obra Dinn: Dithering Mapeado em Esfera
O dithering de Return of the Obra Dinn é especialmente engenhoso porque resolve um problema exclusivo da renderização 3D: a estabilidade temporal. Quando um padrão de dithering comum é aplicado no espaço da tela, mover a câmera faz o padrão 'deslizar'. Ele continua fixo na tela enquanto a geometria 3D se move por baixo. Isso cria um efeito de cintilação extremamente distrativo.
A solução de Pope: mapear o padrão de dithering no espaço 3D em vez de no espaço da tela. O padrão é projetado numa esfera ao redor da câmera e, para cada pixel, amostrado de acordo com a direção de visão. Quando a câmera gira, o padrão gira junto. Quando objetos se movem no espaço do mundo, o dithering deles fica estável, porque é derivado da posição no mundo, e não da posição na tela.
Essa é uma técnica que você pode adaptar para qualquer aplicação 3D que use dithering ou efeitos de meia-tinta. A ideia central: se você quer estabilidade temporal, seus valores de limiar devem ser função de algo estável (posição no mundo, direção de visão), e não de algo que muda a cada frame (coordenadas da tela).
Aplicações Práticas Além da Estética Retrô
Dithering não serve só para a estética retrô de jogos. Ele tem aplicações práticas reais no desenvolvimento moderno.
- Telas e-ink. E-readers e sinalização e-ink têm capacidade de escala de cinza limitada (tipicamente 16 níveis). O dithering é essencial para exibir fotos e gradientes nesses dispositivos. A escolha do algoritmo afeta diretamente a legibilidade e a qualidade da imagem.
- Redução do tamanho de arquivo. Uma imagem de 1 bit com dithering ocupa 1/8 do tamanho bruto de uma imagem em escala de cinza de 8 bits. Para entrega web de imagens decorativas, PNGs com dithering podem ser bem menores que alternativas em escala de cinza. Alguns designers web usam imagens com dithering de propósito, por motivos estéticos e de performance.
- Quantização de cores. Quando você reduz uma imagem colorida a uma paleta limitada (GIF, PNG indexado ou displays com paleta restrita), o dithering evita o banding que surge quando gradientes suaves são mapeados para valores de cor discretos. O Floyd-Steinberg no espaço de cor é como os codificadores de GIF lidam com isso.
- Banding de gradientes na renderização. Render targets de baixa precisão (8 bits por canal) podem produzir banding visível em gradientes suaves. Adicionar dithering no espaço da tela (mesmo apenas ±0,5 de um valor) quebra o banding sem ruído visível. A maioria dos game engines modernos faz isso automaticamente.
- Impressão térmica e de recibos. Impressoras térmicas são inerentemente de 1 bit: cada ponto é aquecido (preto) ou não. O dithering converte fotos e gráficos para uma forma imprimível. A escolha do algoritmo determina se a imagem do seu recibo será reconhecível ou um borrão.
Implementando Dithering em um Shader
Para aplicações em tempo real, o ordered dithering e o blue noise dithering podem ser implementados de forma eficiente como fragment shaders.
// 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);
}
No caso do blue noise, normalmente você pré-computa a textura de ruído e a amostra no shader. A textura pode ser repetida em blocos, então uma textura de blue noise de 64×64 ou 128×128 cobre a tela inteira quando repetida. O custo por pixel é uma amostra de textura e uma comparação, essencialmente gratuito em GPUs modernas.
Escolhendo o Algoritmo Certo
Cada abordagem de dithering tem um caso de uso em que é a melhor escolha.
- Threshold: Texto, line art e gráficos de alto contraste, onde o detalhe importa mais que a faixa tonal.
- Ordered (Bayer): Renderização em tempo real quando você quer uma estética retrô/meia-tinta deliberada. Rápido, paralelizável, amigável para GPU.
- Floyd-Steinberg: Processamento de imagens estáticas onde a qualidade importa mais. Ideal para fotos e imagens com gradientes suaves.
- Blue noise: Renderização em tempo real quando você quer resultados naturais sem padrões visíveis. A melhor escolha de uso geral para aplicações modernas.
- Esférico/espaço do mundo: Renderização 3D onde a estabilidade temporal é crítica. Essencial para renderização 3D de 1 bit, como no Obra Dinn.
Dithering é um daqueles temas em que a simplicidade do conceito ('organizar pontos para parecerem sombreamento') esconde a profundidade das escolhas de implementação. Cada algoritmo representa um trade-off diferente entre qualidade, velocidade e caráter visual. E numa era em que telas e-ink, conexões de baixa banda e estéticas retrô são todas relevantes, entender esses trade-offs é mais prático do que nunca.


