MSAA frente a supersampling: dónde está exactamente el ahorro
Qué multiplica el multimuestreo y qué no, por qué el fragment shader se ejecuta una vez por píxel y no una vez por muestra, y las construcciones de WGSL que rompen esa propiedad.
El multimuestreo tiene fama de ser supersampling con otro nombre, y esa confusión es la razón por la que mucha gente lo descarta pensando que cuadruplica el coste. No lo hace, y la diferencia no es un detalle de implementación sino la idea central de la técnica: se muestrea la geometría cuatro veces y se sombrea una. Entender exactamente qué está multiplicado por cuatro y qué no explica de golpe por qué MSAA es asequible, por qué solo arregla los bordes de la geometría y por qué hay cuatro líneas de WGSL que lo convierten silenciosamente en supersampling.
- Explicar el origen del aliasing de borde en términos de muestreo de una señal.
- Distinguir la frecuencia de muestreo de la visibilidad de la frecuencia de sombreado.
- Justificar por qué las posiciones de muestra no forman una rejilla alineada.
- Identificar las construcciones de WGSL que fuerzan la ejecución por muestra.
De dónde sale el borde escalonado
Un rasterizador decide si un píxel pertenece a un triángulo evaluando una función binaria en el centro del píxel: dentro o fuera. Esa función es un escalón, un escalón tiene contenido en todas las frecuencias, y muestrear una señal de frecuencia infinita en una rejilla regular produce aliasing por el teorema del muestreo. No es un defecto del algoritmo: es lo que ocurre siempre que se muestrea sin filtrar antes.
Lo correcto sería integrar la cobertura sobre el área del píxel y obtener un valor continuo entre cero y uno. Eso es caro: exige conocer la geometría exacta del recorte del triángulo contra el cuadrado del píxel. Todo el antialiasing práctico es una aproximación de esa integral, y las dos familias históricas se diferencian en dónde ponen el esfuerzo.
El supersampling aumenta la frecuencia de muestreo de todo. Renderiza a una resolución mayor —cuatro veces el número de píxeles para un factor de dos en cada eje— y promedia al final. Es exacto en el sentido de que aproxima bien la integral, y multiplica por cuatro absolutamente todo: la rasterización, el número de ejecuciones del fragment shader, la memoria y el ancho de banda.
El multimuestreo aumenta la frecuencia de muestreo solo de la visibilidad. Y ahí está toda la técnica.
Lo que se multiplica y lo que no
Con sampleCount: 4, cada píxel tiene cuatro posiciones de muestra y cuatro valores almacenados de color y de profundidad. Lo que el hardware hace por cada triángulo que toca un píxel es:
- Calcular la cobertura: cuáles de las cuatro posiciones caen dentro del triángulo. Esto se hace cuatro veces por píxel, en función fija.
- Ejecutar la prueba de profundidad para cada muestra cubierta, contra su propio valor almacenado. Cuatro veces por píxel, también en función fija.
- Ejecutar el fragment shader una sola vez para ese triángulo en ese píxel.
- Escribir el color resultante en todas las muestras que hayan pasado los dos filtros anteriores.
El punto 3 es la técnica entera. Las etapas caras de un renderizador moderno —muestreo de texturas, evaluación del modelo de iluminación, bucles sobre luces, ramas— viven en el fragment shader, y ese shader se ejecuta el mismo número de veces con y sin multimuestreo. Lo que se multiplica por cuatro es hardware de función fija barato, más el almacenamiento.
Al terminar el pass, la operación de resolución promedia las cuatro muestras de cada píxel y produce la imagen final de una muestra por píxel. En el interior de un triángulo, las cuatro muestras tienen exactamente el mismo color, así que el promedio no cambia nada. En un borde, unas muestras vienen de un triángulo y otras del que hay detrás, y el promedio produce el valor intermedio que suaviza el escalón. Se antialiasean los bordes de la geometría y solo eso, porque la geometría es lo único que se ha muestreado más veces.
La consecuencia se enuncia en una frase que conviene memorizar: MSAA es supersampling de la visibilidad, no del sombreado. Todo lo que sabe de esta técnica y todo lo que no puede hacer se deduce de ahí.
Las posiciones de muestra no están en una rejilla
Si las cuatro muestras de un píxel formaran una rejilla de dos por dos alineada con la pantalla, un borde casi horizontal cruzaría a la vez las dos muestras de arriba o las dos de abajo, y solo produciría tres niveles distintos de cobertura: cero, mitad y uno. El hardware usa en cambio una rejilla rotada: cuatro puntos dispuestos de forma que ninguno comparta ni fila ni columna con otro.
Con esa disposición, un borde casi horizontal los cruza de uno en uno y produce cinco niveles: cero, un cuarto, la mitad, tres cuartos y uno. Los bordes casi horizontales y casi verticales son, con diferencia, los que más aparecen en imágenes de arquitectura, interfaces y objetos fabricados, así que optimizar para ellos es exactamente lo que hay que hacer. Es la razón por la que 4 muestras en rejilla rotada se ven claramente mejor que renderizar al doble de resolución en cada eje, pese a que ambos usan cuatro muestras por píxel.
WebGPU no expone las posiciones concretas —son propiedad del hardware— pero sí garantiza el número: sampleCount admite 1 o 4, y ningún otro valor. No hay 2x ni 8x ni 16x. Esa restricción sorprende a quien viene de las APIs nativas y es deliberada: cuatro muestras están disponibles en prácticamente todo el hardware con el mismo patrón de calidad, y ocho no.
Toda la economía de MSAA descansa en que el fragment shader se ejecuta una vez por píxel. Hay construcciones del lenguaje que rompen esa propiedad, y cuando lo hacen el shader pasa a ejecutarse una vez por muestra: has convertido tu antialiasing barato en supersampling completo y con él el coste de sombreado por cuatro. La primera es @builtin(sample_index). Leer qué muestra estás sombreando solo tiene sentido si estás sombreando una muestra, así que su mera presencia fuerza la ejecución por muestra. La segunda es el calificador de interpolación sample en cualquier entrada, escrito como @interpolate(perspective, sample): pide explícitamente que los atributos se interpolen en la posición de cada muestra, lo que exige una ejecución por muestra. La tercera es escribir @builtin(sample_mask) como salida cuando el valor depende de algo evaluado por muestra. Y la cuarta, la traicionera, es @interpolate(perspective, centroid). Esta no fuerza la ejecución por muestra —el sombreado sigue siendo uno por píxel— pero mucha gente la confunde con sample porque las dos aparecen juntas en la documentación, y quien quería centroid y escribe sample no ve ninguna diferencia visual, solo un renderizador cuatro veces más lento en fragmento. Merece la pena saber para qué existe centroid, porque resuelve un problema real: en un píxel parcialmente cubierto, el centro del píxel puede caer fuera del triángulo, y evaluar ahí los atributos interpolados los extrapola más allá de sus vértices. Con coordenadas de textura eso muestrea fuera del recorte y produce píxeles de basura en los bordes de los atlas; con un factor de mezcla puede sacarlo del rango cero-uno y disparar un sqrt de un número negativo. centroid desplaza el punto de evaluación al centroide del área cubierta, que sí está dentro, y cuesta cero. La regla operativa: usa centroid en los atributos que indexan atlas o alimentan funciones con dominio acotado, no lo uses en el resto porque introduce una pequeña discontinuidad, y no escribas sample nunca a menos que sepas exactamente que quieres pagar supersampling. Y si sospechas que alguien lo ha escrito en tu código, búscalo antes de perfilar nada más: la diferencia es un factor de cuatro en la etapa más cara del pipeline.
Lo que esto implica antes de seguir
Tres conclusiones que preparan el resto del nivel.
El coste de MSAA en sombreado no es cero, pero no crece con el número de muestras: crece con el número de triángulos que tocan cada píxel, porque el shader se ejecuta una vez por triángulo y por píxel. Una escena con geometría densa de triángulos subpíxel se acerca al coste del supersampling sin ninguna de sus ventajas, y ese es el motivo por el que el multimuestreo escala mal con el nivel de detalle exagerado.
El coste en memoria sí es lineal en el número de muestras, sin matices, y es el que decide en muchos dispositivos.
Y todo lo que produzca variación de alta frecuencia dentro del sombreado de un píxel —una textura con detalle fino, un reflejo especular estrecho, una silueta recortada con alfa— es invisible para el multimuestreo, porque el multimuestreo no toca la frecuencia de sombreado. Eso ocupa su propia lección y es la crítica más importante que se le puede hacer a la técnica.