alphaMode: componer el canvas con el resto de la página
Qué hace el compositor con el alfa del canvas, la diferencia entre opaque y premultiplied, qué es el alfa premultiplicado y cómo producirlo correctamente desde el shader.
El alfa del canvas es el terreno donde se juntan dos sistemas que no se conocen: tu pipeline de render, que produce cuatro números por píxel, y el compositor del navegador, que tiene que combinar ese resultado con el fondo de la página, con el texto que hay encima y con lo que dicta el CSS. alphaMode es el contrato entre los dos, y equivocarse produce halos oscuros, bordes sucios o un canvas que tapa lo que debería dejar ver.
- Explicar qué hace el compositor con cada modo de alfa.
- Definir alfa premultiplicado y por qué es la representación correcta para componer.
- Producir desde el shader los valores que cada modo espera.
- Diagnosticar halos y bordes sucios a partir del modo elegido.
Dos modos y lo que significan
alphaMode admite exactamente dos valores. No hay un tercero.
'opaque', el valor por defecto. El compositor ignora el canal alfa de la textura y trata el canvas como una superficie opaca. Lo que haya detrás en la página no se ve, pase lo que pase con el alfa que escriba tu shader. Es el modo correcto para una escena que ocupa su espacio, y es el más eficiente: el compositor puede saltarse la mezcla, y en algunos casos puede evitar dibujar lo que queda tapado.
'premultiplied'. El compositor usa el canal alfa para mezclar el canvas con lo que hay detrás, e interpreta los canales de color como ya multiplicados por el alfa. Es el modo para una escena que tiene que integrarse con la página: un objeto 3D flotando sobre el contenido, un efecto sobre una imagen de fondo, una capa que se desvanece.
La consecuencia inmediata, y la que produce el desconcierto más frecuente: con 'opaque', escribir alfa 0 no hace transparente nada. Muchísima gente escribe vec4f(color, 0.0) esperando ver a través del canvas, no ve nada, y concluye que la transparencia no funciona. El modo por defecto es opaco y hay que cambiarlo explícitamente.
Qué es el alfa premultiplicado
Un píxel semitransparente se puede representar de dos formas.
En alfa directo, los canales de color guardan el color del objeto y el alfa guarda su cobertura por separado. Un rojo al 50% de opacidad es (1, 0, 0, 0.5).
En alfa premultiplicado, los canales de color ya vienen multiplicados por el alfa. El mismo rojo al 50% es (0.5, 0, 0, 0.5).
La segunda forma parece redundante y es la correcta para componer, por tres razones que se ven mejor con un ejemplo.
La composición es una interpolación, y con alfa directo hace falta una multiplicación extra. Con premultiplicado, combinar una capa sobre un fondo es resultado = capa + fondo * (1 - alfa). Con alfa directo es resultado = capa * alfa + fondo * (1 - alfa). Una operación menos por píxel y por capa.
Encadenar composiciones funciona. Con premultiplicado, el resultado de componer dos capas es a su vez un valor premultiplicado válido que se puede componer con una tercera. Con alfa directo hay que dividir por el alfa para recuperar el color, lo que es inestable cuando el alfa es pequeño e imposible cuando es cero.
El filtrado no produce halos. Este es el importante y el que explica los bugs visuales. Al interpolar dos texels vecinos con alfa directo, uno rojo opaco (1,0,0,1) y otro totalmente transparente cuyo color nadie definió y suele ser negro (0,0,0,0), la media es (0.5, 0, 0, 0.5): un rojo oscurecido. Ese es el halo negro que aparece en el borde de las texturas con transparencia. Con premultiplicado, el rojo opaco es (1,0,0,1), el transparente es (0,0,0,0), la media es (0.5,0,0,0.5), y ese valor premultiplicado representa exactamente rojo puro al 50%. Sin halo.
El alfa premultiplicado no es una peculiaridad de WebGPU: es la representación que usan internamente prácticamente todos los compositores gráficos, desde el de tu sistema operativo hasta el del navegador. WebGPU no ofrece un modo de alfa directo para el canvas precisamente porque componer así obligaría a una conversión por píxel antes de entregar al compositor.
Producir los valores correctos
Con alphaMode: 'premultiplied', tu shader tiene que escribir color ya multiplicado por alfa. Escribir alfa directo produce un resultado más brillante de lo que debería, porque el compositor no vuelve a multiplicar.
@fragment
fn fs(entrada: Salida) -> @location(0) vec4f {
let color = calcularColor(entrada); // rgb sin premultiplicar
let a = calcularAlfa(entrada);
return vec4f(color * a, a); // premultiplicado
}
Si además dibujas objetos transparentes unos sobre otros dentro de la escena, el estado de mezcla del pipeline tiene que ser coherente con esa representación. La mezcla clásica «sobre» con valores premultiplicados es esta:
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: { module, entryPoint: 'vs' },
fragment: {
module,
entryPoint: 'fs',
targets: [{
format: FORMATO_CANVAS,
blend: {
color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
},
}],
},
});
Fíjate en el srcFactor: 'one'. Con alfa directo sería 'src-alpha'; con premultiplicado el color ya viene multiplicado, así que multiplicarlo otra vez lo oscurecería. Es la diferencia de una palabra y produce un resultado visiblemente distinto.
Y un detalle que se olvida: el clearValue del pase también tiene que ser premultiplicado. Para empezar el fotograma con el canvas completamente transparente, el valor es { r: 0, g: 0, b: 0, a: 0 }. Si pones { r: 1, g: 1, b: 1, a: 0 } —blanco transparente en alfa directo— estás escribiendo un valor premultiplicado inválido, porque ningún canal puede superar al alfa, y el resultado depende de la implementación.
Diagnóstico
Cuatro síntomas y su causa.
«No veo a través del canvas». alphaMode está en 'opaque', que es el valor por defecto.
«Se ve a través pero los colores están lavados o demasiado brillantes». Estás escribiendo alfa directo con modo premultiplicado. Multiplica el color por el alfa en el shader.
«Los bordes de la geometría transparente tienen un halo oscuro». La mezcla está configurada para alfa directo mientras el shader produce premultiplicado, o al revés. Revisa que srcFactor sea 'one'.
«Funciona pero el rendimiento bajó al activar la transparencia del canvas». Es esperable: 'premultiplied' obliga al compositor a mezclar el canvas con la página en cada fotograma, trabajo que con 'opaque' no existe. Si no necesitas ver a través, no lo actives.
Hay una confusión que cuesta días y que conviene tener separada desde el principio: alphaMode controla cómo se compone el canvas entero con la página. No tiene absolutamente nada que ver con dibujar objetos translúcidos dentro de la escena, que es un problema distinto —el del orden de dibujado y la mezcla por fragmento— y que se resuelve con el estado de blend del pipeline.
Lo que sí ocurre es que los dos problemas se enredan cuando quieres las dos cosas a la vez, y ahí aparece un caso que no tiene solución perfecta. Imagina una escena con un cristal translúcido, sobre un canvas también translúcido que deja ver la página. El cristal se mezcla con lo que hay detrás dentro de la escena, y el resultado se mezcla con la página. Pero el cristal debería dejar ver también la página, y no puede: durante el pase de render, la página no existe como información. Tu shader no tiene forma de leerla.
Las tres salidas posibles, ninguna gratis. Renderizar el fondo de la página dentro de la escena, como una textura, si es contenido que controlas. Es lo único que da un resultado correcto. Aceptar que los objetos translúcidos se mezclan solo con lo que hay dentro de la escena y que el canvas entero se compone después, que es lo que hace todo el mundo y suele ser suficiente. O usar 'opaque' y dibujar el fondo tú, que es lo más rápido y lo que deberías elegir si el fondo es un color plano o un degradado.
El error caro es descubrir esto tarde. Una maqueta con un canvas transparente sobre una página con contenido detrás parece funcionar hasta que alguien pide un objeto de cristal, y entonces la respuesta correcta es rediseñar la composición, no ajustar factores de mezcla.
Falta la parte dinámica: el ciclo del fotograma y el redimensionado.