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

HDR e tone mapping

Mantenha a luz acima de 1 durante todo o quadro e depois comprima-a no que uma tela consegue mostrar.

Demo ao vivo em breve

Como funciona

A luz real cobre uma faixa enorme. Neve sob sol forte é dezenas de milhares de vezes mais clara que o interior de um quarto à noite, e o filamento de uma lâmpada é mais claro ainda. Uma tela mostra uma faixa de talvez mil para um, e uma imagem de 8 bits guarda de 0 a 1. As contas de sombreamento não ligam para esse limite: uma luz forte numa superfície branca dá 4.0 com facilidade, e um letreiro emissivo pode dar 20.0.

Se esses valores vão direto para um destino RGBA8, tudo acima de 1 é cortado em 1. Os brilhos viram manchas brancas chapadas, sem forma. Cores fortes também mudam de matiz ao serem cortadas: um laranja intenso (3.0, 1.2, 0.2) vira (1.0, 1.0, 0.2), um amarelo, porque o vermelho e o verde batem no teto.

A renderização em high dynamic range (HDR) resolve isso em duas partes: guardar os valores reais pelo maior tempo possível e depois mapeá-los para a tela de propósito, uma vez, no fim.

Guardar os valores: destinos float

A cena é renderizada num render target RGBA16F, que guarda valores até uns 65 000. Assim o bloom encontra o que é claro de verdade (qualquer coisa acima de 1), efeitos de lente trabalham com intensidades reais e nada foi jogado fora ainda. No WebGL2, renderizar num destino float exige a extensão EXT_color_buffer_float.

“Renderização HDR” aqui quer dizer a faixa interna. É uma coisa diferente de um monitor HDR; numa tela comum o resultado ainda precisa caber em 0–1.

Exposição

Uma câmera escolhe quanta luz chega ao sensor; um renderizador faz o mesmo com uma multiplicação. A exposição costuma ser dada em stops, e cada stop dobra a luz:

color *= exp2(exposureStops); // +1 é o dobro do brilho, -1 a metade

Tone mapping

Um tone mapper é uma curva que recebe de 0 a infinito e devolve de 0 a 1. Ela quase não mexe nos valores escuros e médios e dobra os claros para que se aproximem de 1 sem nunca chegar lá. O mais simples é o de Reinhard:

vec3 reinhard(vec3 c) {
    return c / (1.0 + c);
}

0.1 fica em cerca de 0.09, 1.0 vira 0.5 e 10.0 vira 0.91: os brilhos mantêm um degradê em vez de estourar. Os defeitos aparecem rápido, porém. Os tons médios escurecem (1.0 cai pela metade), então as cenas ficam chapadas e acinzentadas, e, aplicado a cada canal separadamente, ele puxa cores saturadas para o branco numa velocidade diferente por canal.

Curvas de produção se esforçam mais:

  • Curvas fílmicas acrescentam um “pé” que aprofunda os pretos e um “ombro” que arredonda os brilhos com suavidade, como um filme fotográfico.
  • ACES vem da indústria do cinema: contraste forte, e valores muito claros perdem saturação.
  • AgX mantém o matiz das cores mais estável conforme elas clareiam, dessaturando-as rumo ao branco de forma controlada.
  • Khronos PBR Neutral é feito para renders de produto: cores-base entre cerca de 0.08 e 0.8 passam quase sem mudança, e só os brilhos são comprimidos, sem desvio de matiz.

Não existe uma curva certa. É uma escolha artística, feita uma vez para o quadro inteiro.

Onde ele fica

O tone mapping fica no fim da cadeia de pós-processamento, e a ordem em volta dele é rígida:

  1. Sombreie a cena em HDR linear.
  2. Rode os efeitos que precisam do brilho real: bloom, lens flares, motion blur.
  3. Aplique a exposição e depois o tone mapper. O resultado volta a ser linear, mas agora em 0–1.
  4. Faça a correção de cor sobre a imagem mapeada.
  5. Codifique para sRGB e escreva na tela.

Juntando tudo na última passada:

vec3 hdr = texture(uScene, vUv).rgb;
vec3 mapped = reinhard(hdr * exp2(uExposure));
fragColor = vec4(linearToSrgb(mapped), 1.0);

Fazer o tone mapping antes do bloom deixa o bloom sem nada acima de 1 para trabalhar. Codificar antes do tone mapping aplica a curva aos números errados.

No Godot

O recurso Environment define o tone mapper: Linear (sem curva: valores acima de 1 estouram), Reinhard, Filmic, ACES e AgX, com ajustes de exposição e de ponto branco. O glow, o bloom do Godot, roda sobre a imagem HDR antes dele. Um shader spatial pode escrever ALBEDO ou EMISSION acima de 1 e a engine cuida do resto.

Neste site

O quadro de preview não faz tone mapping: ele limita a cor final a 0–1 e a codifica. Um card que gera valores acima de 1, como o Neon bloom, faz o tone mapping na sua própria última passada, então a curva faz parte do código que você copia.

Leitura complementar