AmbientLight y HemisphereLight: las dos luces que no son luces
Por qué AmbientLight es físicamente falsa y qué le falta exactamente, cómo HemisphereLight aproxima el cielo y el suelo con una sola operación, y para qué sirve cada una en una escena moderna.
Las dos luces más baratas de Three.js comparten una propiedad incómoda: ninguna de las dos simula una fuente. No tienen posición útil, no proyectan sombra y no aparecen en ninguna ecuación de transporte de luz. Son rellenos: una forma barata de meter en la escena la energía que rebota en las paredes y que un renderizador en tiempo real no puede permitirse calcular. Entender qué falsifican exactamente es lo que te permite usarlas sin que la escena parezca de plástico.
- Explicar qué término de la ecuación de renderizado aproxima una
AmbientLighty qué información descarta. - Predecir por qué una escena iluminada solo con
AmbientLightpierde toda la forma de los objetos. - Calcular la contribución de una
HemisphereLighta partir de la normal de un fragmento. - Elegir entre relleno analítico y entorno precalculado con un criterio, no por costumbre.
Qué falsifica AmbientLight
La luz que llega a un punto de una superficie tiene dos orígenes: la que viene directamente de una fuente y la que ha rebotado al menos una vez en otra geometría. El segundo término, la iluminación indirecta, es el caro: exige resolver una integral sobre todo el hemisferio visible desde ese punto, con visibilidad incluida. Ningún renderizador forward en tiempo real lo hace.
AmbientLight sustituye esa integral entera por una constante:
const ambiente = new THREE.AmbientLight( 0x404040, 1.0 );
scene.add( ambiente );
Y en el shader se traduce, literalmente, a sumar ambientLightColor * diffuseColor al resultado, sin más. Sin producto escalar con la normal, sin atenuación, sin oclusión. Ahí están las tres mentiras, y merece la pena separarlas porque tienen consecuencias distintas.
No depende de la normal. Una cara que mira al cielo y otra que mira al suelo reciben lo mismo. Como el sombreado de una superficie curva es precisamente la variación del producto escalar a lo largo de la superficie, una esfera iluminada solo con ambiente se ve como un círculo plano de color uniforme. Es la prueba más rápida de que una escena está sobreiluminada con ambiente: si los objetos han perdido volumen, sobra ambiente.
No depende de la posición. El interior de una caja recibe tanto ambiente como su exterior. En la realidad, un rincón recibe mucha menos luz indirecta que una superficie abierta, porque su hemisferio visible está tapado. Esa diferencia es exactamente lo que captura un mapa de oclusión ambiental, y es también la razón por la que aoMap y AmbientLight van juntos: el aoMap de Three.js solo atenúa la contribución indirecta, es decir, el ambiente y el entorno. Un aoMap sin ambiente ni entorno no hace nada visible.
No tiene dirección. No hay reflejo especular posible. La AmbientLight no contribuye al término GGX de un MeshStandardMaterial; solo al difuso. Una escena metálica —metalness = 1, difuso nulo por definición— iluminada únicamente con ambiente sale negra, y es correcto que salga negra.
Elevar el suelo de la escena para que las zonas en sombra no sean negro absoluto, cuando no hay presupuesto para un entorno. Valores bajos —una intensidad de 0.2 a 0.4 con un color frío— y siempre acompañada de al menos una luz direccional que aporte la forma. Como iluminación principal no sirve para nada.
HemisphereLight: la primera mentira con gradiente
HemisphereLight corrige la mentira más dañina de las tres —la independencia de la normal— por un coste ridículo. Recibe dos colores, cielo y suelo, y mezcla entre ellos según hacia dónde mire la normal:
// Cielo azulado arriba, rebote calido de tierra abajo.
const hemi = new THREE.HemisphereLight( 0x87ceeb, 0xb97a57, 1.5 );
scene.add( hemi );
El cálculo en el fragment shader es de dos líneas. Con N la normal en espacio de vista y L el eje del hemisferio también en espacio de vista, el factor de mezcla es dot(N, L) * 0.5 + 0.5, y el color resultante es mix(groundColor, skyColor, factor). Es decir: una normal que apunta exactamente hacia arriba recibe el color del cielo puro, una que apunta hacia abajo recibe el del suelo puro, y una horizontal recibe la media exacta de los dos.
Ese dot es todo lo que hace falta para recuperar el volumen. Una esfera iluminada solo con una HemisphereLight ya tiene forma: se ve más clara arriba y más oscura abajo, que es justo lo que ocurre al aire libre en un día nublado.
HemisphereLight hereda de Light, que hereda de Object3D, así que tiene position. Pero lo que se usa no es la posición como punto: es el vector desde el origen hasta esa posición, normalizado, como eje del hemisferio. Por eso hemi.position.set( 0, 20, 0 ) y hemi.position.set( 0, 1, 0 ) dan exactamente el mismo resultado, y por eso hemi.position.set( 0, 0, 0 ) produce un eje degenerado. Si quieres un cielo inclinado —una puesta de sol que tiñe desde el horizonte— mueve la posición fuera del eje Y.
En términos de radiometría, una HemisphereLight es la aproximación de orden más bajo posible a un entorno: dos muestras direccionales interpoladas linealmente. Un entorno real, muestreado con armónicos esféricos de segundo orden, usa nueve coeficientes por canal y captura además la asimetría lateral y los gradientes cruzados. Pero esas dos muestras cuestan cero texturas, cero memoria de vídeo y tres operaciones de ALU, y en muchas escenas la diferencia perceptual frente a un entorno no justifica descargar cuatro megabytes de HDRI.
Cuándo bastan y cuándo no
El criterio no es estético, es de material. Estas dos luces solo alimentan el término difuso, así que su utilidad se derrumba en cuanto la escena tiene superficies reflectantes.
| Escena | Ambient + Hemisphere | Entorno IBL |
|---|---|---|
Estilizada, roughness alto, sin metales |
suficiente | innecesario |
| Producto con metal o barniz | inservible, sale negro o mate | obligatorio |
| Terreno exterior grande, materiales mates | suficiente y mucho más barato | caro sin ganancia clara |
| Interior con ventanas | insuficiente, no hay direccionalidad del rebote | necesario, o lightmaps |
Cuando la respuesta es «suficiente», la combinación que funciona casi siempre es esta:
// Relleno direccional barato: da volumen y color de rebote.
const hemi = new THREE.HemisphereLight( 0xb9d5ff, 0x6b4f3a, 1.2 );
scene.add( hemi );
// Fuente principal: da forma dura, especular y sombra.
const sol = new THREE.DirectionalLight( 0xfff2e0, 3.0 );
sol.position.set( 5, 10, 7 );
sol.castShadow = true;
scene.add( sol );
Dos luces, una sombra, ninguna textura de entorno. Es el punto de partida correcto para cualquier escena que tenga que funcionar en móvil, y a partir de ahí se añade solo lo que se justifique midiendo.
Hay un fallo silencioso que aparece en cuanto mezclas AmbientLight con un entorno, y casi nadie lo diagnostica bien. En un renderizador con energía conservada, la radiancia que sale de una superficie no puede superar a la que entra. Cuando pones scene.environment, Three.js integra ese entorno de forma coherente: la parte difusa viene del mip más rugoso del PMREM, la especular del mip correspondiente a la roughness del material, y el aoMap atenúa ambas. Si encima añades una AmbientLight, esa constante se suma fuera de ese sistema, en el término difuso directo. El resultado es una superficie que emite más energía de la que recibe, y visualmente se traduce en algo muy concreto: los rincones dejan de verse como rincones. El aoMap sigue atenuando el entorno correctamente, pero el ambiente uniforme rellena de vuelta lo que el aoMap acababa de quitar, y como no depende de la normal ni de la posición, lo rellena por igual en el rincón y en la superficie abierta. La gente entonces sube aoMapIntensity a 1.5 o 2, lo que produce halos negros donde el mapa tiene ruido y sigue sin resolver el rincón. La regla es simple y no admite excepciones prácticas: si hay scene.environment, no hay AmbientLight. Si el entorno es demasiado oscuro, se sube scene.environmentIntensity, que multiplica dentro del sistema y respeta la oclusión.