Por qué ordenar no resuelve la transparencia
La no conmutatividad del operador de composición, los tres casos en que ninguna ordenación por objeto es correcta, y qué se hace igualmente porque es lo mejor que hay.
El búfer de profundidad resolvió la visibilidad de los objetos opacos hace cincuenta años y lo hizo tan bien que hoy nadie piensa en ello. La transparencia sigue sin resolverse, y no por falta de ingenio: el consejo universal de «ordena de atrás a delante» no es una simplificación pedagógica de algo mejor, es una heurística que falla en casos que aparecen en cualquier escena real. Merece la pena entender exactamente por qué falla, porque de ahí sale el criterio para elegir entre las técnicas caras que vienen después.
- Demostrar que el operador de composición no es conmutativo y qué modos de mezcla sí lo son.
- Enumerar los tres casos en que ninguna ordenación por objeto produce el resultado correcto.
- Configurar correctamente la pasada de transparentes respecto al búfer de profundidad.
- Estimar el coste real de ordenar, incluido el que no aparece en el perfil de CPU.
La propiedad que falta
Componer una fuente sobre un destino es over(S, D) = S.rgb + D.rgb * (1 - S.a) con alfa premultiplicado. Aplica el operador dos veces, en los dos órdenes posibles, con dos fragmentos A y B sobre un fondo F:
A sobre (B sobre F) = A + (1 - A.a) * B + (1 - A.a)(1 - B.a) * F
B sobre (A sobre F) = B + (1 - B.a) * A + (1 - A.a)(1 - B.a) * F
El tercer término coincide. Los dos primeros no: en el primer caso B se atenúa por A y en el segundo al revés. El operador es asociativo pero no conmutativo, y por tanto el resultado depende del orden en que lleguen los fragmentos al mismo píxel. El rasterizador, que procesa triángulos en el orden en que se los das, convierte esa dependencia en una dependencia del orden de dibujado.
Compara con la profundidad. La prueba de profundidad calcula un mínimo, y min es conmutativo y asociativo. Por eso el búfer de profundidad da el mismo resultado dibujes en el orden que dibujes, y por eso el problema de la visibilidad opaca está cerrado y el de la transparencia no. No es que la transparencia sea más difícil de programar: es que su operador tiene una propiedad algebraica menos.
De ahí sale directamente el criterio para saber si un modo de mezcla necesita ordenación:
| Modo | Operador | Conmutativo | Necesita orden |
|---|---|---|---|
| Aditivo | suma | sí | no |
| Multiplicativo | producto | sí | no |
min y max |
mínimo y máximo | sí | no |
over |
composición | no | sí |
| Substractivo | resta | no | sí |
Ese cuadro es la primera decisión y la más rentable. Si el humo, el fuego y los destellos de tu escena son aditivos, no hay nada que ordenar en ellos y todo el resto de la lección no les aplica. La ordenación es un problema exclusivo del cristal, del agua, del follaje translúcido y de las interfaces con opacidad parcial.
Los tres casos en que ordenar no basta
El plan obvio es ordenar los objetos transparentes por distancia a la cámara y dibujarlos de atrás a delante. Falla de tres maneras distintas, y las tres aparecen en escenas normales.
Objetos que se intersecan. Dos planos de cristal que se cruzan en X no admiten ningún orden correcto: en la mitad izquierda de la pantalla uno está delante y en la mitad derecha lo está el otro. La ordenación es una relación total entre objetos, y la relación «está delante de» entre dos objetos que se intersecan simplemente no es una relación: depende del píxel. La única solución exacta pasa por partir la geometría por el plano del otro, que es lo que hacía un árbol BSP y lo que nadie hace ya en tiempo real porque implica reconstruir la geometría cuando algo se mueve.
Objetos que se solapan parcialmente sin intersecarse. Una cortina larga y una botella pequeña delante de ella. Ordenas por la distancia del centro del objeto y la cortina, cuyo centro está lejos, se dibuja primero: correcto. Ahora mueve la cámara para que un extremo de la cortina quede por delante de la botella: el centro de la cortina sigue estando más lejos, así que la ordenación no cambia, pero en la región del solapamiento el orden correcto se ha invertido. Ordenar por centro es una aproximación, y para objetos grandes o alargados la aproximación es sencillamente falsa. Ordenar por el punto más cercano tampoco arregla el caso general, solo lo mueve.
Ciclos. Tres objetos alargados dispuestos en aspa pueden cumplir que A esté delante de B, B delante de C y C delante de A. La relación deja de ser un orden parcial y ninguna permutación de la lista es correcta. Es el mismo ciclo que rompe el algoritmo del pintor, tiene ejemplos de tres polígonos que caben en un folio, y en escenas con vegetación o con partículas grandes ocurre continuamente.
Y hay un cuarto que no es un caso aparte sino el mismo problema una escala más abajo: un único objeto transparente cóncavo se solapa consigo mismo. Una copa, una botella, un coche con las lunas transparentes. Ordenar objetos no ayuda porque el problema está dentro de uno. La aproximación estándar es dibujarlo en dos pasadas, primero con cullMode: 'front' y después con cullMode: 'back', lo que ordena correctamente las caras traseras respecto a las delanteras para geometría convexa y falla otra vez si es cóncava.
Lo que se hace igualmente
Nada de lo anterior significa que no haya que ordenar. Significa que ordenar es una aproximación, y que conviene saberlo para no perseguir el último artefacto durante una tarde. El procedimiento estándar tiene cuatro piezas y todas importan.
Separar en dos listas. Opacos y transparentes son dos pasadas conceptuales distintas aunque compartan render pass. Los opacos van primero, ordenados aproximadamente de delante a atrás para aprovechar el rechazo temprano de profundidad; los transparentes después, ordenados de atrás a delante.
Probar profundidad sin escribirla. Es la configuración canónica y la razón es exacta: el objeto transparente tiene que ocultarse detrás de lo opaco, así que compara; pero si escribiera profundidad, taparía a los transparentes que vienen después y que están detrás de él, que es justo lo que no queremos.
const pipelineTransparente = device.createRenderPipeline({
label: 'cristal',
layout: layoutEscena,
vertex: { module, entryPoint: 'vs' },
fragment: { module, entryPoint: 'fs', targets: [{ format, blend: over }] },
primitive: { topology: 'triangle-list', cullMode: 'none' },
depthStencil: {
format: 'depth32float',
depthWriteEnabled: false, // clave: compara pero no escribe
depthCompare: 'less',
},
});
Las cuatro combinaciones de escritura y comparación de profundidad, y por qué esta es la de la transparencia, están en el depth buffer y sus formatos. Si la pasada de transparencias vive en un render pass propio, puedes además marcar el attachment con depthReadOnly: true y ahorrarte la escritura por completo.
Ordenar por algo barato y estable. La distancia al cuadrado desde la cámara al centro del volumen envolvente, sin raíz cuadrada. Y estable importa: si dos objetos empatan, el desempate debe ser determinista —por identificador, por ejemplo— o el orden bailará entre fotogramas y verás parpadeos.
No ordenar cuando no hace falta. Si el modo es aditivo, no ordenes. Si solo hay una capa de transparencia por píxel, no ordenes. Si el objeto es una interfaz plana en pantalla, el orden lo da la jerarquía y no la distancia.
Cuando alguien mide el coste de la transparencia ordenada, mide el sort y concluye que ordenar dos mil objetos cuesta décimas de milisegundo y no es problema. El coste real está en otro sitio y no aparece en esa medición. El bucle de dibujado de un renderizador rápido está ordenado por estado: por pipeline primero, luego por bind group de material, luego por buffers de geometría, precisamente para minimizar los cambios de estado. La ordenación por distancia impone un orden distinto e incompatible, y el segundo gana porque el primero es una optimización y el segundo es corrección. El resultado es que la lista de transparentes no se puede agrupar: cada objeto puede exigir su propio setPipeline y su propio setBindGroup, y un lote que en opacos habrías dibujado con un draw instanciado se convierte en cuarenta llamadas intercaladas con cambios de estado. He visto escenas donde mover un material de la lista de opacos a la de transparentes multiplicaba por seis el tiempo de CPU de ese material sin cambiar ni un píxel de su aspecto, simplemente porque dejaba de poder agruparse. De ahí salen dos consejos que parecen supersticiones de motor y no lo son. Uno: el alfa que vale 1 no es transparencia, y un material marcado como transparente «por si acaso» está pagando el precio entero sin usarlo; audita esa lista periódicamente. Y dos: cuando la ordenación por distancia y la agrupación por estado entran en conflicto, a veces la respuesta correcta es agrupar primero y ordenar dentro del grupo, aceptando artefactos entre grupos que no se solapan. Es incorrecto en el caso general y es indistinguible de lo correcto en casi todas las escenas, y esa es exactamente la clase de decisión que separa un renderizador que va a treinta fotogramas de uno que va a sesenta.
Construye la escena del ciclo: tres quads alargados y translúcidos dispuestos de forma que cada uno tape a otro por un extremo. Ordénalos por distancia del centro y gira la cámara despacio alrededor. Verás el momento exacto en que la ordenación cambia y la imagen salta, y verás también que no hay ninguna posición de la lista que sea correcta en todos los ángulos. Es el experimento que convierte esta lección en algo que ya no se olvida.