Gama vs linear
Por que as cores guardadas não são quantidades de luz, como decodificar e codificar sRGB e por que iluminação e mesclagem pertencem aos valores lineares.
Como funciona
A luz se soma. Duas lâmpadas iguais dão o dobro da luz de uma, e um pixel metade coberto por uma forma branca recebe metade da luz de um pixel totalmente coberto. A matemática dos shaders supõe esse tipo de número, proporcional à quantidade de luz: um valor linear. Os números guardados nas imagens e mandados para as telas não são lineares. Eles são codificados com uma curva, e confundir os dois é um dos motivos mais comuns para um shader ficar lamacento, escuro ou duro.
Por que as cores são codificadas
Um canal de 8 bits tem 256 degraus. A visão humana é muito mais sensível às diferenças entre tons escuros do que entre
tons claros, então espalhar esses degraus por igual sobre a quantidade de luz desperdiçaria a maioria nas altas luzes e
deixaria faixas visíveis nas sombras. O padrão sRGBA codificação padrão das cores de tela. Os valores guardados seguem uma curva próxima de uma potência 1/2.2 da luz, então precisam ser decodificados antes de iluminar ou mesclar. sRGB A codificação padrão das cores de tela. Os valores guardados seguem uma curva próxima de uma potência 1/2.2 da luz, então precisam ser decodificados antes de iluminar ou mesclar.linear = ((c + 0.055) / 1.055)^2.4 for c > 0.04045
A curva exata do sRGB é um trecho reto curto perto do preto e uma curva de potência acima dele:
vec3 srgbToLinear(vec3 c) { // decodifica: valor guardado para quantidade de luz
vec3 lo = c / 12.92;
vec3 hi = pow((c + 0.055) / 1.055, vec3(2.4));
return mix(hi, lo, lessThanEqual(c, vec3(0.04045)));
}
vec3 linearToSrgb(vec3 l) { // codifica: quantidade de luz para valor guardado
l = max(l, 0.0); // pow é indefinido abaixo de zero
vec3 lo = l * 12.92;
vec3 hi = 1.055 * pow(l, vec3(1.0 / 2.4)) - 0.055;
return mix(hi, lo, lessThanEqual(l, vec3(0.0031308)));
}
O mix com um vetor booleano escolhe componente a componente: o segundo argumento onde a condição é verdadeira, o
primeiro onde é falsa. pow(c, vec3(2.2)) e pow(l, vec3(1.0 / 2.2)) são aproximações próximas, um pouco erradas perto
do preto.
A regra
Decodifique o que entra, faça as contas em linear, codifique uma vez no fim.
- Decodifique cores feitas por pessoas: texturas de imagem, cores escolhidas num editor, códigos hex de um design.
- Calcule em linear: iluminação, mesclagem, mistura, desfoque, média, fade.
- Codifique a cor final uma vez, quando ela é escrita na tela.
Quebrar a regra tem sintomas reconhecíveis. Um gradiente misturado entre cores codificadas afunda escuro e apagado no
meio: misturar vermelho e verde codificados pela metade dá vec3(0.5, 0.5, 0.0), um verde-oliva fraco, enquanto
misturar os valores lineares e codificar o resultado dá mais ou menos vec3(0.74, 0.74, 0.0), um amarelo limpo.
Desfoques escurecem os detalhes claros, a luz cai rápido demais em volta de uma lâmpada e bordas suavizadas parecem
finas e serrilhadas.
Dados já são lineares
Só as cores são codificadas. Um mapa de normais, um mapa de rugosidade ou de altura, uma textura de ruído, uma máscara
ou um flow map guardam números, e decodificá-los como sRGB entorta esses números. As engines pedem isso por textura (a
dica source_color do Godot, a caixa sRGB da Unity, a opção sRGB da Unreal), e no WebGL2 isso é o formato da textura.
Deixe a GPU fazer
As conversões são baratas, mas raramente você precisa escrevê-las. Uma textura guardada como SRGB8_ALPHA8 é
decodificada pela GPU em toda leitura, antes da filtragem, então texture() devolve valores lineares. Um framebuffer
num formato sRGB codifica na escrita. O canvas padrão do WebGL2 não tem formato sRGB, então um shader WebGL2 escrito à
mão chama algo como linearToSrgb por conta própria antes de escrever a saída. Os cards deste site seguem a regra: os
controles de cor chegam decodificados, o efeito trabalha com valores lineares e o código em volta codifica uma vez.
Precisão é o preço de trabalhar em linear. Valores lineares guardados numa textura de 8 bits mostram faixas visíveis nos tons escuros, porque são justamente os degraus que o sRGB foi feito para proteger. Guarde os resultados intermediários em alvos de ponto flutuante ou de 16 bits, e deixe o armazenamento de 8 bits para cores codificadas.