Pular para o conteúdo
Renderização · Profundidade e blendingIntermediário

Blending e transparência

Como a cor de um fragmento se combina com o que já está na tela, e os modos alfa, aditivo e pré-multiplicado construídos a partir disso.

Demo ao vivo em breve

Como funciona

Sem blending, um fragmento que passa nos testes substitui a cor do pixel. Vidro, fumaça, fogo, a borda suave de um sprite e o fade de uma interface precisam de outra coisa: uma mistura da cor nova com a que já está lá. Blending é essa mistura. Ele roda depois do shader de fragmento, entre as operações por amostra, e o shader nunca vê a cor que está atrás: ele só devolve uma cor e um alfa, e a configuração de blending decide o que fazer com eles.

A equação de blending

O blending trabalha com duas cores: a origem (source), o que o shader de fragmento acabou de devolver, e o destino (destination), o que o pixel guarda. Cada uma é multiplicada por um fator, e os dois produtos são combinados:

resultado = origem × fatorOrigem + destino × fatorDestino

O host escolhe os fatores com gl.blendFunc(srcFactor, dstFactor) e a operação com gl.blendEquation(): soma (o padrão), subtração, subtração invertida e, no WebGL2, também mínimo e máximo. gl.blendFuncSeparate() define fatores diferentes para os canais de cor e para o alfa.

Três modos cobrem a maioria dos efeitos

  • Alfa (“sobre”): gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA). O resultado é origem × α + destino × (1 − α): alfa 1 substitui, 0 deixa o pixel como está, 0,5 fica no meio do caminho. Vidro, sprites, interface com fade.
  • Aditivo: gl.blendFunc(gl.ONE, gl.ONE), ou gl.SRC_ALPHA, gl.ONE para escalar pelo alfa. A origem é somada ao que já está lá, então só consegue clarear. Fogo, faíscas, brilhos, lasers: coisas que emitem luz. Como a soma não liga para a ordem, camadas aditivas nunca precisam ser ordenadas; num alvo de 8 bits, porém, elas estouram em branco onde se acumulam.
  • Multiplicação: gl.blendFunc(gl.DST_COLOR, gl.ZERO). O resultado é origem × destino, então só consegue escurecer. Sombras pintadas como decalques, vidro colorido.

Alfa pré-multiplicado

No alfa “direto” (straight), a cor e a opacidade ficam separadas: um vermelho meio transparente é (1, 0, 0, 0.5). No alfa pré-multiplicado, a cor já vem multiplicada pelo alfa: o mesmo vermelho é (0.5, 0, 0, 0.5), e ele é misturado com gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA).

vec3 color = vec3(1.0, 0.45, 0.1);
float alpha = 0.6;
fragColor = vec4(color * alpha, alpha); // pré-multiplicado: combina com blendFunc(ONE, ONE_MINUS_SRC_ALPHA)

Parece um detalhe, mas resolve problemas reais:

  • A filtragem fica correta. Quando uma textura é filtrada ou usa mipmaps, um texel totalmente transparente ainda tem uma cor (muitas vezes preta ou branca) que vaza para os vizinhos no alfa direto, deixando franjas escuras ou claras em volta dos sprites. Pré-multiplicado, um texel transparente é (0, 0, 0, 0) e não soma nada.
  • Um modo faz os dois papéis. Uma saída com cor mas alfa 0 soma luz sem cobrir nada, então a mesma configuração de blending desenha fumaça (que cobre) e brasas (que somam) numa única chamada de desenho.
  • Camadas se combinam. Duas camadas pré-multiplicadas podem ser juntadas numa só primeiro e misturadas depois com o mesmo resultado, e é disso que as passadas de composição dependem.

O navegador compõe um canvas WebGL na página como pré-multiplicado por padrão (o atributo de contexto premultipliedAlpha). Um shader que escreve alfa direto num canvas transparente ganha bordas claras e erradas onde a página aparece por trás.

A ordem importa

Superfícies opacas podem ser desenhadas em qualquer ordem, porque o buffer de profundidade as ordena por pixel. O blending alfa não pode contar com isso: o “sobre” dá um resultado diferente conforme a camada que vem primeiro, e uma superfície transparente que escreve profundidade esconde qualquer superfície transparente desenhada atrás dela depois. A receita de sempre:

  1. desenhe todos os objetos opacos, com profundidade testada e escrita;
  2. ordene os objetos transparentes de trás para a frente;
  3. desenhe-os com blending ligado, profundidade testada mas não escrita (gl.depthMask(false)).

A ordenação é por objeto, então objetos que se cruzam, ou uma malha que se sobrepõe a si mesma, ainda podem sair errados. Quando a borda pode ser dura, um recorte por alfa evita o problema por completo: o shader descarta os fragmentos abaixo de um limiar e a superfície continua opaca, com profundidade escrita e sem depender de ordem, ao custo de bordas serrilhadas (ou do alpha-to-coverage do MSAA para suavizá-las).

No Godot e neste site

No Godot, um shader spatial escolhe o blending com render_mode blend_mix (o padrão), blend_add, blend_sub, blend_mul ou blend_premul_alpha. O blending só acontece em materiais transparentes: escrever ALPHA leva o material para o pipeline transparente, que o Godot ordena de trás para a frente e, por padrão, desenha sem escrever profundidade. Escrever ALPHA_SCISSOR_THRESHOLD faz um recorte em vez disso. Um shader canvas_item (2D) sempre mistura, com mix por padrão.

Neste site, o blend de uma passada no render.json é "opaque", "alpha" (origem × α + destino × (1 − α)) ou "additive" (destino + origem × α); os dois modos misturados têm a profundidade testada mas não escrita. Sprites sempre usam "alpha"; cards que brilham, como o Hologram, usam "additive".

Leitura complementar