Pular para o conteúdo
Renderização · SaídaIntermediário

Banding e dithering

Por que degradês suaves se partem em faixas com 8 bits, e como um pouco de ruído antes do arredondamento as esconde.

Demo ao vivo em breve

Como funciona

A tela recebe 8 bits por canal: 256 valores possíveis. Na maior parte do tempo, isso sobra. Aí aparece um degradê escuro e lento, um céu noturno ou uma vinheta, e ele se parte em faixas visíveis. Isso é banding.

Os números explicam. Digamos que um céu vá de 0.10 a 0.18 (em sRGB codificado) ao longo de uma tela de 1440 pixels de largura. Essa faixa contém só uns 20 dos 256 valores, então o degradê vira 20 tiras chapadas de uns 70 pixels de largura. Cada degrau é minúsculo, mas o olho é afinado para achar bordas e exagera cada um até virar uma linha.

O banding é pior onde poucos valores cobrem uma área grande: tons escuros, degradês lentos, neblina e céus, e imagens cujas etapas intermediárias foram guardadas com pouca precisão. A compressão de vídeo piora mais ainda.

Duas correções

Mais bits onde der. Render targets intermediários em RGBA16F não têm banding; valores lineares guardados num RGBA8 simples têm muito banding nos escuros. Mas a tela continua recebendo 8 bits, então a última etapa sempre arredonda.

Dithering na última etapa. Antes de o valor ser arredondado para 8 bits, some um deslocamento minúsculo que muda de pixel para pixel, de até meio degrau para cada lado. Onde o valor real fica a 30% do caminho entre dois níveis, cerca de 30% dos pixels agora arredondam para cima e o resto para baixo. Vistos da distância normal, os vizinhos voltam à média real. As bordas duras das faixas viram uma granulação fina e uniforme, que o olho ignora.

Onde somar

Faça o dithering na precisão que vai cortar o valor, logo antes do corte: depois do tone mapping e depois da codificação sRGB, como a última coisa da passada final. Um degrau de 8 bits é 1.0 / 255.0:

color = linearToSrgb(color);
color += (noise - 0.5) / 255.0; // noise em 0..1, diferente por pixel
fragColor = vec4(color, 1.0);

Fazer o dithering antes da codificação, em valores lineares, erra o tamanho do degrau. A codificação estica os escuros, então meio degrau de deslocamento em linear vira vários degraus de ruído visível perto do preto, e pouco demais nos claros.

Qual ruído

O deslocamento precisa de um valor por pixel. Três fontes comuns:

  • Ruído aleatório, de um hash da posição do pixel. Não precisa de textura. A granulação é irregular: aglomerados e buracos ao acaso.
  • Dithering ordenado, de uma pequena matriz que se repete (uma matriz de Bayer). Barato e totalmente previsível. A trama regular aparece se você olhar de perto, e é justamente o visual retrô que os efeitos de dither estilizado buscam.
  • Ruído azul (blue noise), de uma textura pré-calculada. Aleatório, mas sem os aglomerados, então dá a granulação mais fina e menos visível. A escolha de costume em produção.

Um dithering ordenado com uma matriz de Bayer 4×4:

const float BAYER4[16] = float[16](
     0.0,  8.0,  2.0, 10.0,
    12.0,  4.0, 14.0,  6.0,
     3.0, 11.0,  1.0,  9.0,
    15.0,  7.0, 13.0,  5.0
);

float bayer4(vec2 fragCoord) {
    ivec2 p = ivec2(fragCoord) & 3;              // posição dentro do bloco 4×4
    return (BAYER4[p.y * 4 + p.x] + 0.5) / 16.0; // 16 valores igualmente espaçados em 0..1
}

Passe gl_FragCoord.xy para ela e use o resultado como noise acima. A mesma função com um bit em vez de oito, dois níveis por canal em vez de 256, é o clássico dither de 1 bit: a técnica Posterize & dither transforma a correção num estilo.

No Godot

A engine tem uma opção de debanding para 3D (use_debanding no viewport, ou a configuração de projeto Use Debanding). Ela soma o dithering no fim do quadro por você. Nos seus próprios shaders de pós, some como acima, na última passada.

Animado ou parado

Um padrão parado pode parecer uma tela suja num movimento lento de câmera. Trocar o ruído a cada quadro o transforma numa cintilação que o olho tira na média, e o anti-aliasing temporal suaviza mais ainda. Para uma imagem parada, um padrão parado serve.

Leitura complementar