Shadow acne y peter-panning: la causa geométrica y la cura
Por qué una superficie se sombrea a sí misma con bandas, por qué el bias constante despega la sombra del objeto, y cómo normalBias resuelve el problema atacando la causa en vez del síntoma.
La causa: un texel es un área, no un punto
Un shadow map tiene resolución finita. Cada texel cubre una porción de la escena que, proyectada sobre la superficie que ilumina, es un pequeño paralelogramo. Durante el primer pase, ese texel guarda un solo valor de profundidad: el que le tocó al rasterizar, correspondiente aproximadamente al centro del texel.
Durante el segundo pase, todos los fragmentos que caen dentro de ese paralelogramo se comparan contra ese mismo valor único. Y ahí está el problema: si la superficie está inclinada respecto a la dirección de la luz, la profundidad real varía a lo largo del paralelogramo. La mitad del texel queda por delante del valor guardado y pasa la prueba; la otra mitad queda por detrás y la suspende. Resultado: la superficie se declara en sombra a sí misma, en franjas alternas.
De aquí salen dos predicciones comprobables, y las dos se cumplen siempre:
El acné empeora con la inclinación. Si la superficie es perpendicular a los rayos, la profundidad no varía dentro del texel y no hay acné en absoluto. Cuanto más rasante la incidencia, mayor la variación y peor el artefacto. Por eso un suelo iluminado por un sol bajo se llena de bandas mientras que el mismo suelo con el sol en el cénit sale limpio.
El acné empeora con texels grandes. Duplicar mapSize reduce a la mitad el ancho del paralelogramo y por tanto a la mitad la variación de profundidad dentro de él. Y estrechar el frustum de la cámara de sombra hace exactamente lo mismo, gratis.
El error a corregir, en concreto, es la mitad de la variación de profundidad a lo ancho de un texel. Con t el tamaño de un texel proyectado sobre la superficie y θ el ángulo entre la normal y la dirección a la luz, ese error vale aproximadamente t * tan(θ). Fíjate en el tan: cuando θ se acerca a 90 grados, tiende a infinito. No existe una constante que cubra todos los ángulos.
Bias constante y su precio
La cura obvia es restar un margen antes de comparar:
sol.shadow.bias = -0.0005; // se RESTA de la profundidad del fragmentoCon eso, un fragmento tiene que estar más profundo que el texel por un margen para declararse en sombra, y el acné desaparece. Pero el mismo margen se aplica a los fragmentos que sí estaban legítimamente ocluidos: los que están a menos de ese margen por detrás de un oclusor real ahora pasan la prueba y se iluminan.
El síntoma es una franja iluminada entre el objeto y su sombra, como si el objeto levitase. Se llama peter-panning por la sombra de Peter Pan, que se le escapaba. Y el desplazamiento no es sutil: el margen se aplica en el espacio de profundidad normalizado de la cámara de sombra, así que un bias de -0.001 sobre un frustum de 500 unidades de fondo son medio metro de sombra perdida.
Tres cosas que hay que saber sobre bias en Three.js y que casi nunca se dicen:
- El signo es negativo. El valor por defecto es
0. Los valores que funcionan son negativos y del orden de-0.0001a-0.005. - La escala depende del frustum. El mismo
biasque va bien confar = 50es inservible confar = 500. Si cambias la cámara de sombra, tienes que recalibrar elbias. - No es lo mismo que el polygon offset del primer pase.
biasactúa en el segundo pase, sobre la lectura. No modifica lo que se escribió.
normalBias: mover el punto de muestreo, no la profundidad
normalBias ataca el problema por el otro lado, y es la mejor herramienta de las dos. En vez de falsear la profundidad, desplaza el punto que se usa para consultar el mapa a lo largo de la normal de la superficie:
sol.shadow.normalBias = 0.02; // en unidades de mundo, positivoLa documentación del código fuente lo dice literalmente: “Defines how much the position used to query the shadow map is offset along the object normal”. El punto consultado se levanta ligeramente sobre la superficie, y como el error de acné es precisamente un error tangencial —la profundidad varía a lo ancho del texel—, levantarse lo suficiente saca el punto de la zona ambigua.
Las ventajas frente a bias son concretas y las tres importan:
Está en unidades de mundo. normalBias = 0.02 significa dos centímetros si tu escena está en metros, y ese valor sigue siendo correcto si cambias el far del frustum. Es un número que puedes razonar.
Se adapta a la geometría. Como el desplazamiento va por la normal, las superficies rasantes —las que peor lo pasan— se desplazan en la dirección que más ayuda, mientras que las perpendiculares apenas se ven afectadas.
No produce peter-panning en la dirección de la luz. El desplazamiento es perpendicular a la superficie, no paralelo a los rayos, así que la sombra no se despega del contacto. Su artefacto propio es distinto: si te pasas, las sombras se deforman cerca de los bordes y las esquinas cóncavas pierden la sombra de contacto.
La alternativa sin bias: renderizar las caras traseras
Existe una solución al acné que no usa ningún bias, y en geometría cerrada es superior a las dos anteriores. Consiste en invertir el descarte de caras durante el primer pase: en vez de guardar la profundidad de la cara frontal del oclusor, guardar la de la trasera.
Como la cara trasera está por detrás de la frontal en el grosor del objeto, el margen de seguridad lo aporta la propia geometría, y es exactamente proporcional a lo que hace falta. El acné desaparece sin desplazar nada.
// Three.js aplica esto durante el pase de sombra si el material lo pide.material.shadowSide = THREE.BackSide;Por defecto shadowSide es null, lo que hace que Three.js deduzca el lado a partir de material.side. Ponerlo explícitamente a THREE.BackSide activa el truco.
Su precio es la restricción: solo funciona con geometría cerrada y con grosor. Un plano de una sola cara no tiene cara trasera útil; un muro modelado como un único quad se vuelve completamente transparente para las sombras. Por eso la estrategia mixta es la habitual: shadowSide: THREE.BackSide en los objetos sólidos, y un normalBias moderado global para el resto.