wandres.dev
ENTORNOS E IBL · HDRI, envMap y la luz realista

Entornos procedurales: RoomEnvironment y fromScene

Cómo generar un entorno sin descargar un solo byte, qué contiene exactamente RoomEnvironment, y cuándo un entorno construido a mano supera a un HDRI descargado.

⏱ 17 min

No hace falta descargar un HDRI para tener iluminación basada en imagen. PMREMGenerator.fromScene() acepta cualquier escena de Three.js, la renderiza en seis direcciones desde un punto y produce el mismo PMREM que produciría un fichero. Con eso, un entorno de estudio deja de ser un asset de varios megabytes y pasa a ser doscientas líneas de geometría que se ejecutan en unos pocos milisegundos. RoomEnvironment, que es exactamente eso, es el entorno por defecto de la mitad de los ejemplos oficiales y del visor de modelos de Google.

🎯 Al terminar esta lección sabrás
  • Generar un entorno con fromScene y entender el papel de sigma, near y far.
  • Describir qué geometría y qué materiales contiene RoomEnvironment y por qué esos.
  • Construir un entorno propio con paneles emisivos para un caso concreto.
  • Decidir entre entorno procedural y HDRI con criterios de peso, control y realismo.

fromScene, y qué hace de verdad

La firma completa en r184 es:

fromScene( scene, sigma = 0, near = 0.1, far = 100, options = {} )
// options: { size = 256, position = origen }

Internamente crea una PerspectiveCamera de 90 grados y aspecto 1, la coloca en position y renderiza la escena seis veces, una por cara del cubo, directamente sobre el atlas CubeUV. Después, si sigma es mayor que cero, aplica el desenfoque gaussiano esférico, y finalmente ejecuta la cadena de prefiltrado GGX.

Los cuatro parámetros hacen cosas concretas y ninguno es decorativo:

sigma es un desenfoque previo, en radianes, aplicado antes del prefiltrado por rugosidad. Sirve para suavizar los bordes duros de una escena hecha de cajas: sin él, el reflejo de un panel rectangular en un metal pulido se ve como un rectángulo perfecto con esquinas afiladas, que delata que el entorno es geometría. El valor que usan los ejemplos oficiales es 0.04, y es un buen punto de partida.

near y far son los planos de la cámara cúbica. Si tu geometría de entorno está a 30 unidades y far vale 100, todo bien; si está a 300, se recorta. Los valores por defecto sirven para escalas de habitación.

options.size es la resolución del cubemap, 256 por defecto. Subirlo solo tiene sentido si el entorno tiene detalle fino que quieras ver en un reflejo especular.

options.position decide desde dónde se captura, y es lo que permite generar un entorno correcto para un objeto que no está en el origen.

import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';

const pmrem = new THREE.PMREMGenerator( renderer );
const habitacion = new RoomEnvironment();

scene.environment = pmrem.fromScene( habitacion, 0.04 ).texture;

// Los dos son desechables: su informacion ya vive en el PMREM.
habitacion.dispose();
pmrem.dispose();

Cero bytes descargados. En un portátil normal, la generación completa cuesta del orden de veinte milisegundos.

Qué hay dentro de RoomEnvironment

Merece la pena abrirlo porque es un manual de cómo se construye un entorno de estudio, y porque su implementación revela cómo funciona el truco.

La escena contiene una caja grande con side: BackSide que hace de habitación, seis cajas como muebles para dar variación en los reflejos, una PointLight de 900 de potencia arriba, y seis planos emisivos que hacen de fuentes: dos grandes a la izquierda simulando ventanas, uno a la derecha, uno frontal, uno trasero y uno cenital.

La parte instructiva son los materiales de esos planos:

// createAreaLightMaterial en RoomEnvironment.js, r184:
const material = new MeshLambertMaterial( {
  color: 0x000000,
  emissive: 0xffffff,
  emissiveIntensity: intensity   // 17, 20, 43, 50 o 100 segun el panel
} );

Negro difuso y emisivo blanco con intensidades muy por encima de 1. Eso es precisamente lo que hace que el resultado sea un entorno de rango alto: al renderizar a un render target de HalfFloatType, un emissiveIntensity de 100 produce un valor de 100 en el buffer, no un blanco recortado. Cuando el PMREM convoluciona esa escena, esos paneles dominan la energía igual que el sol domina un HDRI de exterior. Sin valores por encima de 1, el entorno procedural sería plano y no serviría.

Los valores de intensidad —50, 50, 17, 43, 20, 100— no son aleatorios: reproducen la iluminación de un estudio con una fuente principal fuerte, un relleno lateral, un contraluz suave y un cenital dominante. Es un montaje de fotografía traducido a código.

ℹ️
Los otros dos entornos de los addons

Junto a RoomEnvironment hay ColorEnvironment, que genera un entorno de color plano —útil como relleno neutro—, y DebugEnvironment, que pinta cada dirección de un color distinto y sirve para verificar orientaciones y rotaciones. Los tres viven en three/addons/environments/.

Construir el tuyo

El patrón es directo: una Scene, geometría con materiales emisivos de intensidad alta, y fromScene. Aquí un estudio de producto de tres puntos:

function crearEstudio() {
  const env = new THREE.Scene();

  // Caja envolvente gris oscuro: da un rebote general y evita el negro.
  const caja = new THREE.Mesh(
    new THREE.BoxGeometry( 20, 12, 20 ),
    new THREE.MeshStandardMaterial( { color: 0x1b1b20, side: THREE.BackSide } )
  );
  env.add( caja );

  // MeshBasicMaterial no tiene emissive: su color ES la salida directa.
  // Color no recorta sus componentes, asi que setRGB por encima de 1
  // produce rango dinamico alto en el render target de HalfFloatType.
  function panel( w, h, r, g, b ) {
    const mat = new THREE.MeshBasicMaterial();
    mat.color.setRGB( r, g, b );
    return new THREE.Mesh( new THREE.PlaneGeometry( w, h ), mat );
  }

  const clave = panel( 8, 6, 60, 58, 55 );     // luz principal, izquierda
  clave.position.set( -7, 5, 4 );
  clave.lookAt( 0, 1, 0 );
  env.add( clave );

  const relleno = panel( 6, 5, 12, 14, 18 );   // relleno frio, derecha
  relleno.position.set( 7, 3, 3 );
  relleno.lookAt( 0, 1, 0 );
  env.add( relleno );

  const contra = panel( 5, 3, 30, 28, 26 );    // contraluz, detras y arriba
  contra.position.set( 0, 6, -8 );
  contra.lookAt( 0, 1, 0 );
  env.add( contra );

  return env;
}

const pmrem = new THREE.PMREMGenerator( renderer );
const estudio = crearEstudio();
scene.environment = pmrem.fromScene( estudio, 0.05 ).texture;

// Liberar la geometria y los materiales temporales.
estudio.traverse( ( o ) => {
  if ( o.isMesh ) { o.geometry.dispose(); o.material.dispose(); }
} );
pmrem.dispose();

La clave está en setRGB con valores muy por encima de 1. Color no recorta sus componentes, MeshBasicMaterial escribe su color tal cual, y el render target del PMREM es de HalfFloatType: la cadena entera preserva el rango alto. Ese es el mecanismo que hace que un entorno de tres planos ilumine como un HDRI y no como un difusor plano.

Cuándo procedural y cuándo HDRI

Criterio Procedural HDRI
Peso descargado 0 KB 1 a 15 MB
Tiempo de arranque ~20 ms de GPU descarga más 200-500 ms de CPU
Control artístico total y paramétrico fijo, o editar el fichero
Realismo del rebote limitado, geometría simple completo, capturado de la realidad
Fondo visible utilizable no, es geometría de caja
Cambio en tiempo real regenerar cuesta ms recargar cuesta MB

La regla que se deduce de la tabla es bastante nítida. Para render de producto sobre fondo liso, procedural gana casi siempre: no necesitas realismo de entorno, necesitas un reflejo controlado y creíble, y lo consigues sin descargar nada y pudiendo ajustar la posición de cada panel. Para una escena situada en un lugar real, HDRI gana siempre: el rebote de un bosque o de una calle tiene una complejidad que no vas a reproducir con cajas, y además querrás verlo de fondo.

Y hay un tercer camino, muy práctico, que combina los dos: cargar RoomEnvironment de inmediato para que la escena se vea correcta desde el primer frame, y sustituirla por el HDRI real cuando termine de descargarse. La transición es imperceptible si las exposiciones están calibradas y elimina el momento feo en el que todos los metales están negros.

fromScene ejecuta seis renders reales, con todo lo que eso arrastra

Es fácil pensar en fromScene como una utilidad barata y llamarla cada vez que algo cambia. No lo es, y el motivo está en el código: _sceneToCubeUV hace un bucle de seis iteraciones y en cada una llama a renderer.render( scene, cubeCamera ). Eso significa seis recorridos completos del grafo de escena, seis pasadas de frustum culling, seis ordenaciones de la cola de render y seis tandas de draw calls, con todos los cambios de estado que conlleve. Además, el método guarda y restaura el render target activo, el estado de xr.enabled, el toneMapping —que fuerza a NoToneMapping durante la captura, porque el entorno tiene que quedar en radiancia lineal— y el autoClear. Si tu escena de entorno tiene cien objetos, has ejecutado seiscientos draw calls, y a eso hay que sumarle la cadena de prefiltrado con sus 256 muestras por nivel. En un móvil, un fromScene sobre una escena moderadamente compleja se puede ir a más de cien milisegundos de bloqueo. De ahí salen dos reglas operativas. La primera: la escena de entorno tiene que ser deliberadamente pobre, unas pocas cajas y planos emisivos, nunca la escena real; RoomEnvironment tiene doce objetos y no es casualidad. La segunda: no regeneres el PMREM para cambiar la iluminación. Si lo que quieres es girar la luz, usa scene.environmentRotation, que es gratis; si lo que quieres es subirla o bajarla, usa scene.environmentIntensity, que también lo es. Regenerar solo tiene sentido cuando cambia la geometría del entorno, y en la práctica eso ocurre una vez por escena.