Tone mapping: las seis curvas y cuál elegir
Qué se hace con los valores que superan el uno, cómo se comporta cada operador de Three.js r184, y por qué el ACES de Three.js no es el ACES de referencia.
El problema del recorte
El valor por defecto de renderer.toneMapping es NoToneMapping, que no hace nada: los valores por encima de uno se recortan al escribir en el framebuffer.
El recorte tiene dos efectos, y el segundo es el feo. El primero es obvio: todo lo que supere el uno se convierte en el mismo blanco, así que un foco potente y otro diez veces más potente producen la misma mancha plana, sin ninguna información de forma dentro.
El segundo es el desplazamiento de tono. Los tres canales se recortan por separado, y una luz cálida no llega al uno en los tres a la vez: primero satura el rojo, después el verde, y al final el azul. Así que a medida que la intensidad sube, el color va del naranja al amarillo y de ahí al blanco, pasando por tonos que no estaban en la escena. Es el aspecto quemado y sucio que reconoces de cualquier render sin mapeo de tonos.
Una curva de mapeo de tonos resuelve las dos cosas: comprime de forma continua, así que sigue habiendo diferencia entre un cinco y un veinte, y lo hace teniendo en cuenta los tres canales juntos.
Los seis operadores
Three.js r184 trae seis curvas más el modo personalizado. Estos son sus comportamientos reales, leídos de sus implementaciones.
LinearToneMapping multiplica por la exposición y recorta. Es útil como control de exposición cuando quieres el aspecto de recorte pero con una intensidad ajustable. No resuelve ninguno de los dos problemas.
ReinhardToneMapping aplica la función clásica, el valor dividido entre uno más el valor. Comprime todo el rango infinito al intervalo de cero a uno de forma continua, así que nada llega nunca al blanco puro. El resultado conserva muchísimo detalle en las luces y a cambio desatura y aplana la imagen: las escenas Reinhard tienen un aspecto grisáceo y de bajo contraste característico.
CineonToneMapping implementa el operador filmico de Jim Hejl y Richard Burgess-Dawson, una aproximación de la respuesta de una película fotográfica. Da mucho contraste y hunde los negros, con un aspecto cinematográfico algo agresivo. Fíjate en que su implementación termina con una potencia de 2,2: es que el operador original devuelve valores ya codificados y hay que deshacerlo para que la codificación de salida los vuelva a aplicar.
ACESFilmicToneMapping es el más usado y el que la mayoría de escenas debería usar por defecto. Transforma al espacio de color AP1 de ACES, aplica un ajuste racional que aproxima la cadena de referencia de la Academia, y vuelve. Produce una curva en S con hombro suave en las luces y pie en las sombras, y desplaza los tonos altos hacia el ámbar, lo que resulta muy agradable y muy reconocible.
AgXToneMapping viene de Blender, a través de Filament. Trabaja en primarios Rec.2020, codifica en logaritmo entre unos menos 12,5 y unos más 4 EV respecto al gris medio, aplica un sigmoide y vuelve. Su característica distintiva es que los tonos muy brillantes tienden a blanco conservando el matiz durante mucho más rango que ACES, en lugar de virar. El resultado es menos contrastado, más neutro y más fotográfico. Es la mejor elección cuando la escena tiene fuentes muy intensas y no quieres el viraje cálido.
NeutralToneMapping implementa el mapeador neutro del grupo 3D Commerce de Khronos. Está diseñado con un objetivo muy concreto: que el color de un producto se parezca lo más posible al color real del producto. Deja los valores intactos por debajo de un umbral cercano a 0.76 y solo comprime a partir de ahí, con una desaturación leve. Si estás haciendo un configurador o una ficha de producto donde el cliente compara el color con una foto del catálogo, es el operador correcto y ACES no lo es.
import * as THREE from 'three';const renderer = new THREE.WebGLRenderer({ antialias: true });renderer.toneMapping = THREE.ACESFilmicToneMapping;renderer.toneMappingExposure = 1.0;toneMappingExposure multiplica el color antes de la curva, así que es un control de exposición fotográfica real, no un brillo aplicado al final. Duplicarlo equivale a abrir un paso de diafragma. Y el mapeo de tonos ocurre antes de la codificación al espacio de salida, en ese orden y nunca al revés.
Cómo elegir
Cuatro preguntas resuelven la decisión.
¿La fidelidad del color es un requisito? Comercio electrónico, configuradores, catálogos. Entonces NeutralToneMapping.
¿Hay fuentes muy intensas en cuadro? Neones, soles, emisivos fuertes. Entonces AgXToneMapping, que aguanta mucho más rango sin virar.
¿Buscas un aspecto cinematográfico y contrastado? Entonces ACESFilmicToneMapping, que es además lo que espera ver la mayoría de la gente porque es lo que usan los motores de videojuego.
¿Estás renderizando datos, un diagrama o interfaz? Entonces NoToneMapping es defendible, porque una curva altera los colores que has elegido a propósito. Y recuerda que material.toneMapped = false saca un material concreto de la curva sin afectar al resto, aunque esa bandera se ignora en cuanto hay post-proceso.
Lo que no es defensable es dejar NoToneMapping en una escena PBR con entorno. Toda la información de rango alto que aporta el HDRI se recorta en el uno, y has pagado la iluminación basada en imagen para tirar la mitad de su rango a la basura.