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

Saída em sRGB

Faça as contas em luz linear e codifique para sRGB uma única vez, no caminho para a tela.

Demo ao vivo em breve

Como funciona

O número que um shader de fragmento escreve não é a quantidade de luz que a tela emite. As telas esperam a entrada codificada com a curva sRGB, que gasta mais dos 256 degraus disponíveis nos tons escuros, onde o olho percebe diferenças pequenas, e menos nos claros. Um 0.5 guardado sai com cerca de 21% do brilho máximo, não 50%.

Isso é ótimo para armazenar e péssimo para fazer contas. Luz se soma: duas lâmpadas dão o dobro da luz de uma. Iluminação, blending, blur e todo mix entre cores supõem valores que se comportam assim, e valores codificados não se comportam. A média entre um preto e um branco codificados dá 0.5, que aparece como um cinza escuro; a média correta da luz é 0.5 em linear, que codificado vira cerca de 0.74.

Então um renderizador correto mantém dois tipos de número separados:

  • Valores lineares medem luz. Todo o sombreamento acontece neles.
  • Valores codificados em sRGB servem para armazenar e exibir: arquivos de imagem, seletores de cor e a tela final.

A regra é decodificar na entrada e codificar uma vez na saída.

A curva

O sRGB fica perto de uma potência de 2.2, com um trechinho reto perto do preto para a inclinação nunca ficar infinita. As duas direções, por canal (o alfa nunca é codificado):

vec3 srgbToLinear(vec3 c) {
    vec3 low  = c / 12.92;
    vec3 high = pow((c + 0.055) / 1.055, vec3(2.4));
    return mix(low, high, step(vec3(0.04045), c));
}

vec3 linearToSrgb(vec3 c) {
    vec3 low  = c * 12.92;
    vec3 high = 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055;
    return mix(low, high, step(vec3(0.0031308), c));
}

pow(c, vec3(1.0 / 2.2)) é um atalho comum. Fica perto, mas não é igual; ele clareia um pouco os tons mais escuros.

Decodificando as entradas

Cores que uma pessoa escolheu ou pintou estão em sRGB. O #808080 de um seletor de cor está codificado, e também quase toda foto e textura pintada à mão. Decodifique antes de usá-las nas contas.

Com texturas, a GPU faz isso por você. Uma textura criada como SRGB8_ALPHA8 é decodificada para linear a cada amostragem, antes da filtragem, então uma textura sRGB borrada ou com mipmaps continua tirando a média certa. Texturas de dados (normal maps, ruído, máscaras) não são cores e precisam ficar em RGBA8 simples: decodificá-las entorta os dados.

Codificando a saída

O OpenGL também sabe codificar na escrita. Quando o anexo de cor de um framebuffer é uma textura SRGB8_ALPHA8, o WebGL2 e o OpenGL ES 3.0 codificam cada valor escrito nela, e o blending acontece em linear antes da codificação. (O OpenGL de desktop pede também glEnable(GL_FRAMEBUFFER_SRGB).)

O canvas do WebGL2 é a exceção. O framebuffer padrão dele não tem modo sRGB: o que o último shader escreve aparece do jeito que está, e o navegador trata como já codificado. Então a última passada no WebGL2 precisa chamar linearToSrgb ela mesma, depois de qualquer tone mapping:

fragColor = vec4(linearToSrgb(clamp(color, 0.0, 1.0)), alpha);

Exatamente uma vez

Dois erros são comuns, e cada um tem uma cara:

  • Nunca codificado. Os valores lineares vão crus para a tela. A imagem fica escura e contrastada demais, e esferas iluminadas mostram uma borda dura onde a luz cai, em vez de uma transição suave.
  • Codificado duas vezes. A engine já codifica e o shader codifica de novo. A imagem fica lavada e leitosa, sem pretos de verdade.

Render targets intermediários devem guardar valores lineares em RGBA16F, ou em SRGB8_ALPHA8 se precisarem ser pequenos. Valores lineares num RGBA8 simples perdem os escuros: ganham 3 dos 256 degraus abaixo de 1% do brilho, onde a curva sRGB gasta uns 25, e as sombras ficam com banding.

No Godot

O renderizador 3D trabalha em linear e codifica no fim, então um shader spatial escreve ALBEDO linear. Uniforms de cor e texturas de cor marcados com source_color chegam decodificados. O renderizador 2D é diferente: a menos que o viewport ative o HDR 2D, ele trabalha direto em sRGB, e source_color não faz nada num shader canvas_item. É por isso que o template Godot deste site para cards 2D decodifica as cores e codifica o resultado à mão.

Leitura complementar