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.
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 fragmentoO programa da GPU que roda uma vez para cada pixel coberto por um triângulo e decide a cor dele. Shader de fragmento O programa da GPU que roda uma vez para cada pixel coberto por um triângulo e decide a cor dele. Operações por amostra As etapas fixas depois do shader de fragmento que decidem se a cor dele chega ao pixel, e como. Testes de tesoura, stencil e profundidade, depois o blending e as máscaras de escrita, configurados pelo host e não no GLSL.
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), ougl.SRC_ALPHA, gl.ONEpara 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
profundidadeUma imagem do tamanho do framebuffer que guarda, para cada pixel, a distância da superfície mais próxima desenhada até agora. O teste de profundidade a usa para esconder o que fica atrás. Buffer de profundidade Uma imagem do tamanho do framebuffer que guarda, para cada pixel, a distância da superfície mais próxima desenhada até agora. O teste de profundidade a usa para esconder o que fica atrás.
- desenhe todos os objetos opacos, com profundidade testada e escrita;
- ordene os objetos transparentes de trás para a frente;
- 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".