El camino del color, del fichero al píxel
El espacio de trabajo de Three.js, outputColorSpace, y qué ocurre exactamente en cada etapa desde que se lee un téxel hasta que se escribe un píxel.
El camino completo
Cinco etapas y solo dos decisiones tuyas: la de la izquierda, declarar de dónde viene cada dato, y la del final, declarar a dónde va la imagen. Todo lo de en medio lo hace Three.js sin que tengas que intervenir.
El espacio de trabajo
ColorManagement es un objeto singleton con dos propiedades que gobiernan el sistema entero:
const ColorManagement = { enabled: true, workingColorSpace: LinearSRGBColorSpace, // ...};workingColorSpace es el espacio en el que ocurre todo el cálculo: la BRDF, las sumas de luces, las mezclas alfa, la interpolación de vértices, la generación de mipmaps que hace la GPU cuando la textura declara su formato. Es lineal por la razón que ya conoces, porque la aritmética de la luz solo tiene sentido sobre cantidades proporcionales a la energía.
Los dos métodos que hacen el trabajo se llaman, desde la revisión 177, colorSpaceToWorking y workingToColorSpace. Los nombres antiguos, toWorkingColorSpace y fromWorkingColorSpace, siguen existiendo pero emiten un aviso de obsolescencia. Si mantienes código de hace un par de años, ese aviso es lo que verás.
enabled es el interruptor general. Ponerlo a false desactiva todas las conversiones a la vez, y solo existe por compatibilidad con proyectos anteriores a la revisión 152, cuando la gestión de color se volvió automática. Desactivarlo hoy es garantizar que ninguna textura ni ningún color se interpreten correctamente.
outputColorSpace
Al final del fragment shader, después del mapeo de tonos, el color pasa por una última conversión que lo lleva del espacio de trabajo al espacio del destino. Ese destino lo declara el renderer:
import * as THREE from 'three';const renderer = new THREE.WebGLRenderer({ antialias: true });renderer.outputColorSpace = THREE.SRGBColorSpace; // ya es el valor por defectoEs el valor correcto para una pantalla normal y no hay que tocarlo. El único otro valor con sentido es LinearSRGBColorSpace, y solo si la salida no va a una pantalla sino a otro proceso que espera valores lineales.
Merece la pena entender por qué esta conversión existe y por qué no es opcional. El framebuffer de la página tiene ocho bits por canal. Escribir en él valores lineales gastaría casi todos los códigos en el rango alto y produciría banding severo en las sombras, exactamente el problema que la codificación sRGB vino a resolver. La conversión de salida no es un ajuste estético: es lo que hace que ocho bits basten.
La lista mínima de configuración
Reduciendo todo el nivel a lo imprescindible, un proyecto con la gestión de color correcta tiene exactamente esto:
import * as THREE from 'three';const renderer = new THREE.WebGLRenderer({ antialias: true });renderer.outputColorSpace = THREE.SRGBColorSpace; // defecto, explicito por claridadrenderer.toneMapping = THREE.ACESFilmicToneMapping; // este NO es el defectorenderer.toneMappingExposure = 1.0;const loader = new THREE.TextureLoader();const albedo = loader.load('/tex/color.jpg');albedo.colorSpace = THREE.SRGBColorSpace; // colorconst normales = loader.load('/tex/normal.png'); // dato: nadaconst rugosidad = loader.load('/tex/rough.png'); // dato: nadaCuatro líneas de configuración y una decisión por textura. Lo único que no viene bien por defecto es el mapeo de tonos, que arranca en NoToneMapping, y ese es el tema de la lección siguiente.
Un último apunte de higiene: si cargas modelos glTF, no tienes que hacer nada de la parte de texturas, porque el formato lo declara y el cargador lo respeta. Toda esta lección aplica a las texturas que cargas tú y a los colores que escribes tú, que en una escena real suelen ser bastantes más de los que parece.