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

scene.environment frente a scene.background, y los seis mandos

Por qué iluminar y ver el fondo son dos cosas independientes, qué controla cada una de las seis propiedades de Scene, y cómo rotar el entorno sin girar la escena.

⏱ 16 min

Scene tiene dos propiedades que aceptan una textura de entorno y seis que las modulan. La confusión más común de todo este nivel es tratarlas como una sola: asignar el HDRI a background y esperar que ilumine, o asignarlo a environment y preguntarse por qué el fondo sigue negro. Son sistemas completamente independientes, con formatos distintos, costes distintos y controles distintos, y separarlos bien es lo que permite el truco que usa toda la producción seria: iluminar con un fichero minúsculo y mostrar otro grande.

🎯 Al terminar esta lección sabrás
  • Distinguir qué consume scene.environment y qué consume scene.background.
  • Usar environmentIntensity y environmentRotation para ajustar la iluminación sin tocar materiales.
  • Aplicar backgroundBlurriness, backgroundIntensity y backgroundRotation de forma coherente con el entorno.
  • Saber cuándo anular el entorno global en un material concreto y cómo.

Dos sistemas que no se hablan

scene.environment es una fuente de iluminación. Se consume en el fragment shader de cada material físico, mediante dos lecturas del PMREM: una para el difuso y otra para el especular. Espera una textura en formato CubeUVReflectionMapping, es decir, la salida de un PMREMGenerator. No se dibuja en ninguna parte.

scene.background es lo que se pinta donde no hay geometría. Acepta un Color, una textura plana, un CubeTexture o una equirectangular con su mapping puesto. No ilumina nada en absoluto.

// Iluminar y ver son decisiones separadas. Esto es perfectamente valido:
scene.environment = pmrem.fromEquirectangular( hdrPequeno ).texture;
scene.background = new THREE.Color( 0x101014 );   // fondo liso, iluminacion de HDRI

// Y esto tambien, aunque es lo que casi nadie quiere:
scene.background = hdrGrande;                     // se ve el HDRI, no ilumina
scene.environment = null;                         // los metales salen negros

La documentación de Scene.environment añade una restricción que conviene tener presente: “it’s not possible to overwrite an existing texture assigned to the envMap material property”. El entorno de escena es un valor por defecto, no un forzado. Si un material tiene su propio envMap, gana el del material. Eso es exactamente lo que quieres cuando un objeto necesita reflejar algo distinto —un espejo con un CubeCamera, por ejemplo—: le pones su envMap y el resto de la escena sigue con el global.

Los tres mandos del entorno

environmentIntensity y environmentRotation son de las adiciones más útiles de los últimos años porque sustituyen operaciones que antes había que hacer objeto a objeto.

// Sube o baja TODA la iluminacion de entorno, sin tocar materiales.
scene.environmentIntensity = 1.6;

// Gira el entorno sin girar la escena. Es un Euler, en radianes.
scene.environmentRotation.y = Math.PI * 0.35;

environmentIntensity vale 1 por defecto y multiplica la contribución del entorno. Antes de que existiera había que recorrer la escena poniendo material.envMapIntensity en cada material, lo cual además rompía en cuanto cargabas un glTF nuevo. Sigue existiendo envMapIntensity por material y sigue siendo útil para excepciones —bajarle el entorno solo al suelo, por ejemplo—, pero el mando global es este.

environmentRotation es el que cambia el flujo de trabajo. Un HDRI trae el sol donde lo traiga, y lo que quieres es que el reflejo especular caiga donde a ti te interese en la composición. Antes había que rotar la escena entera o editar el HDRI en Photoshop. Ahora es una línea. Y como es un Euler, se puede animar:

// Animar la rotacion del entorno da una sensacion muy convincente
// de luz cambiante, y cuesta cero: no regenera el PMREM.
function animate( t ) {
  scene.environmentRotation.y = t * 0.00005;
  renderer.render( scene, camera );
}

Ese detalle es importante: rotar el entorno no recalcula el PMREM. La rotación se aplica como una matriz en el shader al muestrear. Es gratis.

Los tres mandos del fondo

Simétricos, pero con un extra que no tiene el entorno.

scene.backgroundIntensity = 0.8;              // atenua el fondo
scene.backgroundRotation.y = Math.PI * 0.35;  // gira el fondo
scene.backgroundBlurriness = 0.4;             // 0 a 1, desenfoca el fondo

backgroundBlurriness solo funciona cuando el fondo es un mapa de entorno, y lo que hace por dentro es leer de un nivel más alto de la cadena de mips. Su uso canónico es el render de producto: un fondo desenfocado da profundidad de campo y contexto sin competir con el objeto.

💡
Si giras uno, gira el otro

environmentRotation y backgroundRotation son propiedades separadas y no se sincronizan. Si rotas solo el entorno, los reflejos dejarán de corresponder con lo que se ve de fondo, y el ojo lo detecta enseguida aunque no sepa decir qué falla. Cuando quieras rotar de verdad, copia el mismo Euler en las dos: scene.backgroundRotation.copy( scene.environmentRotation ).

El patrón de producción

Juntando las dos lecciones anteriores con esta, el montaje que resuelve bien el caso general es este:

import * as THREE from 'three';
import { HDRLoader } from 'three/addons/loaders/HDRLoader.js';

async function montarEntorno( renderer, scene ) {
  const pmrem = new THREE.PMREMGenerator( renderer );
  pmrem.compileEquirectangularShader();

  // 1. Iluminacion: HDR de 1K, ~4 MB. Se descarta tras generar el PMREM.
  const hdr = await new HDRLoader().loadAsync( '/env/loft_1k.hdr' );
  hdr.mapping = THREE.EquirectangularReflectionMapping;
  const envTarget = pmrem.fromEquirectangular( hdr );
  scene.environment = envTarget.texture;
  hdr.dispose();
  pmrem.dispose();

  // 2. Fondo: WebP de 4K en sRGB, ~600 KB. Se ve pero no ilumina.
  const fondo = await new THREE.TextureLoader().loadAsync( '/env/loft_4k.webp' );
  fondo.mapping = THREE.EquirectangularReflectionMapping;
  fondo.colorSpace = THREE.SRGBColorSpace;
  scene.background = fondo;

  // 3. Ajuste artistico, sin tocar ningun material.
  scene.environmentIntensity = 1.0;
  scene.backgroundIntensity = 0.9;
  scene.backgroundBlurriness = 0.25;
  scene.environmentRotation.y = 0.6;
  scene.backgroundRotation.copy( scene.environmentRotation );

  // Devuelve el target para poder liberarlo al cambiar de entorno.
  return envTarget;
}

Cuatro megabytes y medio de descarga total, contra los treinta o cuarenta de un HDRI de 4K que hiciera las dos cosas, y con mejor resultado en las dos.

Un fondo no se ve nunca reflejado, y esa incoherencia es la que delata el truco

El patrón de los dos ficheros funciona porque los dos representan lo mismo, y en cuanto dejan de representar lo mismo el ojo lo caza sin que sepas explicarlo. El caso donde se rompe con más frecuencia no es el que la gente teme —la resolución distinta, que es irrelevante— sino el espacio de color y la exposición. El fondo WebP es una imagen sRGB con el rango ya recortado y una curva tonal aplicada por quien la exportó; el HDR es radiancia lineal. Si tu toneMapping es ACESFilmicToneMapping, esa curva se aplica también al fondo, porque el fondo pasa por el mismo pipeline de salida, y el resultado es que el fondo sale doblemente comprimido: apagado, lavado y con menos contraste que el HDRI del que salió. La solución no es desactivar el mapeo tonal, que destrozaría los objetos, sino exportar el fondo sin curva tonal, en lineal, y dejar que el pipeline se la aplique una sola vez. En la práctica eso significa generar el fondo desde el mismo .hdr con un script —gltf-transform no sirve aquí, pero oiiotool o incluso un canvas de tres líneas con la textura HDR renderizada a pantalla completa y capturada con renderer.domElement.toDataURL() sí—, en vez de descargar un JPEG cualquiera del mismo sitio. Y hay un segundo desajuste, más sutil: backgroundIntensity y environmentIntensity no están vinculados, así que si subes uno para compensar y el otro no, los reflejos de la escena tendrán una exposición distinta a la del fondo que supuestamente reflejan. En una escena de producto sobre fondo desenfocado no se nota; en un exterior con un coche cromado, es lo primero que se ve.