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.
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 iluminascene.environment = null; // los metales salen negrosLa 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 fondoscene.backgroundRotation.y = Math.PI * 0.35; // gira el fondoscene.backgroundBlurriness = 0.4; // 0 a 1, desenfoca el fondobackgroundBlurriness 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.
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.