Pular para o conteúdo
Fundamento · Matemática de corIniciante

RGB(A), luminância e modos de mesclagem

Cor como um vec3 ou vec4, o que o alfa significa, o quanto uma cor parece clara e a matemática por trás de multiplicar, screen, overlay e somar.

Como funciona

Para um shader, uma cor é um vetor: vermelho, verde e azul num vec3, de 0 (nada) a 1 (cheio), mais um quarto valor opcional, o alfa, num vec4. Tudo de vetores vale aqui. A aritmética funciona canal a canal, .rgb e .a são swizzles, e o mix mistura duas cores tão fácil quanto dois números.

Aritmética como luz

Cada operação básica tem uma leitura física, o que facilita escolher:

vec3 base  = vec3(0.8, 0.6, 0.4);
vec3 tint  = base * vec3(1.0, 0.5, 0.5); // multiplicar: um filtro colorido; só escurece
vec3 lit   = base + vec3(0.2);           // somar: mais luz por cima; só clareia
vec3 dim   = base * 0.5;                 // escalar: a mesma cor, metade da luz
vec3 blend = mix(base, vec3(0.1, 0.2, 0.9), 0.3); // 30% do caminho até o azul

Multiplicar por branco (vec3(1.0)) não muda nada e por preto dá preto, e é por isso que texturas e iluminação se combinam multiplicando. Valores acima de 1 são normais dentro de um shader, como luz forte que ainda não foi ajustada para a tela; a saída corta em 1, a não ser que esteja renderizando num alvo de ponto flutuante. Valores negativos não têm sentido como cor e costumam vir de uma subtração ou de um filtro de nitidez que precisa de um max(c, 0.0).

Alfa

O alfa é o quarto canal. Por convenção ele significa cobertura ou opacidade: 1 é sólido, 0 é invisível. O shader só o escreve na saída; se ele faz alguma coisa depende da mesclagem (blending), configurada fora do shader. Com a mesclagem desligada, o alfa não mistura este desenho com o que já está lá; ele só é guardado no destino. A maioria dos destinos o ignora, mas um canvas WebGL com o padrão alpha: true o usa para se compor sobre a página, então escreva 1.0 nele a menos que queira que a página apareça por trás.

Há dois jeitos de guardar uma cor com alfa. O alfa direto (straight) guarda a cor como ela é, e a opacidade à parte. O alfa pré-multiplicado guarda a cor já multiplicada pelo alfa, então um vermelho meio transparente é vec4(0.5, 0.0, 0.0, 0.5). Pôr uma cor sobre um fundo (“over”) fica assim:

vec3 overStraight = src.rgb * src.a + dst.rgb * (1.0 - src.a); // alfa direto
vec3 overPremul   = src.rgb +         dst.rgb * (1.0 - src.a); // pré-multiplicado

Cores pré-multiplicadas filtram e desfocam certo (um texel totalmente transparente não contribui com nada, seja qual for a cor guardada nele) e conseguem expressar brilho aditivo (alfa 0 com cor acima de 0). Misturar as duas convenções sem querer dá franjas escuras ou claras em volta das bordas suaves.

Luminância

Os três canais não parecem igualmente claros. O olho é mais sensível ao verde e menos ao azul, então o verde cheio parece muito mais claro que o azul cheio. A luminância pesa os canais por essa sensibilidade; para as primárias do sRGB e do Rec. 709, usadas pelas telas, os pesos são:

float luminance(vec3 c) { return dot(c, vec3(0.2126, 0.7152, 0.0722)); }

Os pesos somam 1, então o branco continua 1. A luminância é o jeito certo de deixar uma cor cinza (vec3(luminance(c))), de ajustar a saturação (mix(vec3(luminance(c)), c, amount): 0 é cinza, 1 é o original, acima de 1 fica mais viva), de escolher as áreas claras para um brilho ou de rodar um detector de bordas num canal só em vez de três. Esses pesos valem para valores lineares; a próxima página, sobre gama, explica a diferença.

Modos de mesclagem

Os editores de imagem dão nome a um punhado de jeitos de combinar uma cor de base a com uma camada b. Cada um é uma linha de matemática canal a canal, com valores de 0 a 1:

  • Multiplicar a * b: escurece, como sobrepor dois slides. O branco é neutro.
  • Screen 1.0 - (1.0 - a) * (1.0 - b): clareia, o inverso de multiplicar. O preto é neutro.
  • Somar a + b: clareia e pode passar de 1; brilhos, fogo e luz são somados.
  • Overlay: multiplica onde a base é escura e faz screen onde ela é clara, o que aumenta o contraste. O cinza 50% é neutro.

O overlay tem um desvio por canal, que step e mix escrevem sem if:

vec3 overlay(vec3 a, vec3 b) {
    vec3 dark  = 2.0 * a * b;                        // multiplicar, dobrado
    vec3 light = 1.0 - 2.0 * (1.0 - a) * (1.0 - b);  // screen, dobrado
    return mix(dark, light, step(0.5, a));          // escolhe por canal pela base
}

Para aplicar qualquer modo com força parcial, misture da base até o resultado: mix(a, overlay(a, b), opacity).

As cores nos cards deste site seguem a mesma matemática. Uma cor que você escolhe nos controles de um card é escrita como você a vê e chega ao shader já convertida para valores lineares, o que faz valerem os pesos de luminância e a aritmética de luz acima.

Leitura complementar