Qué luz se puede precalcular y cuál no
La partición entre lo que depende de la vista y lo que no, entre lo estático y lo dinámico, y por qué el difuso indirecto es exactamente el término que se hornea.
Hornear luz es mover cálculo del tiempo de ejecución al tiempo de construcción. Suena a que todo se puede hornear y no es así: hay términos de la ecuación de renderizado que dependen de dónde esté el observador, y esos no se pueden guardar en una textura porque cambiarían en cuanto el usuario mueva la cámara. La partición no es arbitraria ni es cuestión de gusto: sale directamente de las variables de las que depende cada término.
- Separar los términos de la iluminación según dependan o no de la dirección de vista.
- Explicar por qué el difuso indirecto es el candidato natural al horneado y el especular no.
- Identificar qué elementos de una escena pueden considerarse estáticos a efectos de luz.
- Reconocer los tres artefactos típicos de mezclar geometría dinámica con luz horneada.
La partición por dependencia de la vista
La radiancia que sale de un punto se puede descomponer en cuatro términos según su origen y su comportamiento:
| Término | ¿Depende de la vista? | ¿Depende del movimiento? | ¿Horneable? |
|---|---|---|---|
| Difuso directo | no | sí, si la luz o el objeto se mueven | sí, si todo es estático |
| Difuso indirecto | no | sí, si algo se mueve | sí, es el caso ideal |
| Especular directo | sí | sí | no |
| Especular indirecto | sí | sí | no, pero se aproxima con IBL |
La columna que decide es la primera. Un BRDF lambertiano dispersa la luz por igual en todas las direcciones salientes, así que la radiancia difusa que sale de un punto es la misma vista desde donde sea. Es un número por punto de la superficie, y un número por punto de la superficie es exactamente lo que cabe en una textura. Eso es un lightmap.
El especular, en cambio, es un lóbulo alrededor de la dirección de reflexión: mueve la cámara y el brillo se desplaza sobre la superficie. Guardarlo en una textura congelaría el brillo en su sitio y el objeto parecería tener una mancha pintada. Por eso ningún flujo de horneado hornea el especular, y por eso una escena con lightmaps sigue necesitando scene.environment para que los materiales brillantes se comporten.
Una escena bien montada con horneado usa las dos cosas a la vez: el difuso indirecto viene de un lightmap, y el especular de un entorno IBL. No son alternativas. La confusión de tratarlas como excluyentes produce interiores con lightmaps preciosos y metales completamente negros.
Por qué el indirecto es el candidato perfecto
De los dos términos horneables, el que de verdad justifica todo el flujo de trabajo es el difuso indirecto, y por una razón económica: es el único de los cuatro que ningún renderizador en tiempo real puede calcular bien.
El difuso directo, si hace falta, sale de una DirectionalLight y un shadow map: cuesta un pase extra pero se puede. El especular directo es una evaluación de GGX. El especular indirecto tiene el PMREM. Pero el difuso indirecto —la luz que entra por una ventana, rebota en el suelo, tiñe el techo de amarillo y baja de nuevo— exige resolver el transporte global de luz, con múltiples rebotes e interreflexiones. Ningún motor web lo hace.
Y es justo el término que produce las señales visuales que el ojo asocia con «esto es un espacio real»: el color de rebote de las paredes, la penumbra suave en los rincones, la gradación de sombra bajo un mueble. Un renderizador sin ese término se ve plano y sin atmósfera, por muchas luces que le añadas.
El trazador de Blender, en cambio, sí lo calcula. Puede tardar cinco minutos por textura, pero eso da igual: se paga una vez, en tu ordenador, antes de publicar. Ese es todo el argumento del horneado: comprar tiempo offline para gastarlo en calidad online.
Qué cuenta como estático
Un lightmap es válido mientras nada de lo que contribuyó a él cambie. En concreto, tres cosas tienen que quedarse quietas:
La geometría que emite y la que recibe. Si un mueble se mueve, la sombra que proyectaba sigue pintada donde estaba y la que debería proyectar en su nueva posición no existe.
Las luces. Cambiar la hora del día invalida el lightmap entero. Si tu escena necesita un ciclo día-noche, la solución es hornear varios lightmaps e interpolar entre ellos, con el coste de memoria correspondiente.
Los materiales que participan en el rebote. Cambiar una pared de blanca a roja cambia el color del rebote sobre todo lo demás.
En la práctica, la escena se parte en dos categorías desde el principio del proyecto:
// Estatico: arquitectura, mobiliario fijo, terreno.
// Recibe su iluminacion del lightmap. No necesita luces analiticas.
paredes.material.lightMap = lightmapTextura;
paredes.material.lightMapIntensity = 1.0;
// Dinamico: personaje, puertas, objetos que el usuario mueve.
// No tiene lightmap. Se ilumina con IBL mas una luz con sombra.
personaje.castShadow = true;
personaje.receiveShadow = true;
Esa separación se decide antes de modelar, no después. Reasignar un objeto de una categoría a otra a mitad de proyecto implica rehacer UVs y volver a hornear.
Los tres artefactos de mezclar dinámico y horneado
Todos aparecen en el punto de contacto entre las dos categorías, y todos tienen la misma raíz: el lightmap no sabe que el objeto dinámico existe.
El personaje no recibe el rebote. El suelo está bañado de una luz amarilla preciosa que viene del lightmap, y el personaje que camina sobre él se ve gris, iluminado solo por el entorno. La mitigación estándar son las light probes: muestrear la iluminación indirecta en una rejilla de puntos y aplicar la más cercana al objeto dinámico. Three.js tiene LightProbe en el núcleo y LightProbeGenerator en three/addons/lights/LightProbeGenerator.js, que sabe construir una probe a partir de un cubemap.
El personaje no proyecta sombra sobre el lightmap. El lightmap es una textura fija; una sombra dinámica encima de él tiene que venir de un shadow map en tiempo real, lo que significa mantener una luz con sombra activa aunque toda la iluminación estática venga horneada. Es el motivo de que las escenas con lightmaps sigan teniendo una DirectionalLight: no ilumina, solo proyecta la sombra de lo que se mueve.
Doble contabilidad de la luz. El error más frecuente. Horneas el lightmap con el sol incluido y después dejas la DirectionalLight encendida en Three.js: la luz directa se suma dos veces y la escena sale lavada. Hay dos flujos válidos y hay que elegir uno: hornear solo el indirecto y dejar la luz directa en tiempo real, o hornear todo y apagar la luz salvo para proyectar sombras dinámicas, con intensity a cero.
Aquí hay un salto conceptual que decide si tu horneado se ve bien o se ve como una calcomanía. Cuando Blender hornea un pase de diffuse indirect, lo que guarda no es «el color final de esa superficie»: es la irradiancia que llega a ese punto, es decir, cuánta energía le entra, independientemente de qué color tenga la superficie. Y en el shader de Three.js, esa irradiancia se multiplica por el albedo del material antes de sumarse al resultado. La consecuencia práctica es enorme: si horneas correctamente la irradiancia, puedes cambiar la textura de color de la pared —de blanco a azul, de madera a mármol— y el lightmap sigue siendo válido, porque solo describe la distribución de luz, no el color. Si en cambio horneas el combined de Blender, que ya incluye el albedo, estás guardando color y luz mezclados: el lightmap deja de ser reutilizable, se multiplica por el albedo una segunda vez en el shader y la escena sale mucho más oscura y saturada de lo que debería. Ese doble producto del albedo es la causa número uno de «mi lightmap se ve sucio y oscuro», y no se arregla subiendo lightMapIntensity, porque el problema no es de escala sino de que el color está elevado al cuadrado. La regla, entonces: en Blender el pase que se hornea para un lightmap de Three.js es Diffuse con Direct y Color desactivados, dejando solo Indirect, o alternativamente el pase completo sobre materiales de albedo blanco puro. Cualquier otra cosa produce una textura que parece correcta en el visor de Blender y que está sistemáticamente mal en el navegador.