La cámara de sombra: el ajuste que lo cambia todo
Cómo el frustum de la cámara de la luz determina la resolución efectiva de cada sombra, qué cámara usa cada tipo de luz, y por qué apretarlo mejora más que duplicar el tamaño del mapa.
La cuenta que gobierna todo
Para una DirectionalLight, la cámara de sombra es una OrthographicCamera con estos valores por defecto:
// Lo que crea DirectionalLightShadow en su constructor:new THREE.OrthographicCamera( -5, 5, 5, -5, 0.5, 500 );Un volumen de 10 por 10 unidades de sección y 499.5 de profundidad. Con el mapSize por defecto de 512 por 512, cada texel cubre:
ancho del frustum / ancho del mapa = 10 / 512 = 0.0195 unidadesCasi dos centímetros si tu escena está en metros. Eso es aceptable para un objeto de mesa, y catastrófico para un exterior. Si estiras el frustum a 200 unidades para cubrir un terreno sin tocar la resolución:
200 / 512 = 0.39 unidadesCasi cuarenta centímetros por texel. La sombra de una farola desaparece en el ruido de la cuantización. Y esta es la relación que hay que interiorizar: la calidad de la sombra es el cociente entre el ancho del frustum y el ancho del mapa, y los dos términos son igual de potentes. Reducir el frustum a la mitad vale exactamente lo mismo que duplicar el mapa, pero el primero no cuesta memoria ni ancho de banda.
const sol = new THREE.DirectionalLight( 0xffffff, 3 );sol.position.set( 8, 12, 6 );sol.castShadow = true;// Abrazar la escena: si todo cabe en un cubo de 20 unidades centrado// en el origen, la diagonal proyectada nunca supera 14.2 por lado.const s = 15;sol.shadow.camera.left = -s;sol.shadow.camera.right = s;sol.shadow.camera.top = s;sol.shadow.camera.bottom = -s;sol.shadow.camera.near = 1;sol.shadow.camera.far = 40;sol.shadow.mapSize.set( 2048, 2048 );// Obligatorio tras tocar el frustum de una camara ortografica.sol.shadow.camera.updateProjectionMatrix();scene.add( sol );La llamada a updateProjectionMatrix() no es opcional. Modificar left, right, top, bottom, near o far de una cámara no recalcula nada por sí solo; hay que pedirlo explícitamente. Es el olvido que produce el clásico «he cambiado los valores y no pasa nada».
Cada luz tiene su cámara y no todas se tocan igual
| Luz | Clase de sombra | Cámara | Qué puedes tocar |
|---|---|---|---|
DirectionalLight |
DirectionalLightShadow |
OrthographicCamera(-5, 5, 5, -5, 0.5, 500) |
los seis planos, a mano |
SpotLight |
SpotLightShadow |
PerspectiveCamera(50, 1, 0.5, 500) |
near y focus; el resto es derivado |
PointLight |
PointLightShadow |
PerspectiveCamera(90, 1, 0.5, 500) |
near y far |
La SpotLight es el caso interesante porque su cámara se recalcula sola en cada frame. Su updateMatrices hace esto:
// SpotLightShadow.updateMatrices, textualmente:const fov = RAD2DEG * 2 * light.angle * this.focus;const aspect = ( this.mapSize.width / this.mapSize.height ) * this.aspect;const far = light.distance || camera.far;Traducido: el campo de visión sale del angle del foco, y el far sale de su distance si es distinto de cero. Poner foco.shadow.camera.fov = 30 a mano no sirve de nada, porque se sobrescribe en el siguiente frame. Lo único que controlas de verdad son near —que sí se respeta y es el ajuste de precisión que ya viste— y focus, ese multiplicador que estrecha el frustum dentro del cono para no malgastar los texels de las esquinas.
Para PointLight, la cámara es de 90 grados y aspecto 1 porque son las seis caras de un cubo; ahí no hay nada que tocar salvo near y far. Y el far importa: por defecto vale 500, lo que reparte la precisión del buffer sobre un rango absurdo para una bombilla de habitación. Bajarlo a la distancia real de la pared más lejana es de las mejoras más rápidas que existen.
Visualizar el frustum es el primer paso, siempre
Casi cualquier problema de sombras se diagnostica en diez segundos si ves la caja. CameraHelper dibuja el frustum exacto de cualquier cámara, incluida la de una sombra:
const helperSombra = new THREE.CameraHelper( sol.shadow.camera );scene.add( helperSombra );// Si mueves la luz, hay que actualizarlo:function animate() { sol.shadow.camera.updateProjectionMatrix(); helperSombra.update(); renderer.render( scene, camera );}Con la caja visible, los tres diagnósticos más comunes son inmediatos:
Un objeto no proyecta sombra. Está fuera de la caja. O tiene castShadow = false.
La sombra aparece y desaparece al mover la cámara. El frustum es más pequeño que la escena y el objeto entra y sale por el plano far o por un lateral.
La sombra es un borrón. La caja es enorme comparada con el objeto. Aprieta.
Para ver el contenido del mapa en sí —lo que de verdad se ha renderizado en el primer pase— existe ShadowMapViewer en three/addons/utils/ShadowMapViewer.js, que pinta el shadow map en una esquina de la pantalla. Es lo que quieres cuando sospechas que un objeto se está saltando el primer pase.