Saltar al contenido

El orden correcto de la cadena

Dónde va exactamente el tone mapping, dónde el antialiasing y por qué, con las reglas que nadie escribe, y la alternativa de pmndrs con su fusión de efectos.

La cadena canónica

En código:

JS
composer.addPass( new RenderPass( escena, camara ) );composer.addPass( ssaoPass );composer.addPass( bokehPass );composer.addPass( bloomPass );composer.addPass( outlinePass );composer.addPass( smaaPass );composer.addPass( new OutputPass() );

Ahora la justificación de cada posición, que es lo que de verdad importa.

Las cuatro reglas

Regla 1: el tone mapping va al final, siempre

Esta es la regla que no se negocia y la que más se incumple.

Todos los buffers del composer son de media precisión con espacio de color NoColorSpace. Es decir: valores lineales sin recortar, que pueden ser mayores que 1. El píxel del reflejo especular de una lámpara puede valer 40. Eso es correcto y es lo que hace posible que los efectos funcionen.

El tone mapping es la operación que comprime ese rango abierto al rango que una pantalla puede mostrar, y la conversión de espacio de color es la que aplica la curva sRGB para que los valores se perciban bien. Son las dos últimas operaciones de todo el pipeline de imagen, en Three.js y en cualquier motor.

OutputPass hace exactamente esas dos cosas, y las hace leyendo tres propiedades del renderer:

JS
this.uniforms[ 'toneMappingExposure' ].value = renderer.toneMappingExposure;if ( this._outputColorSpace !== renderer.outputColorSpace || this._toneMapping !== renderer.toneMapping ) {  this._outputColorSpace = renderer.outputColorSpace;  this._toneMapping = renderer.toneMapping;  this.material.defines = {};  if ( ColorManagement.getTransfer( this._outputColorSpace ) === SRGBTransfer ) this.material.defines.SRGB_TRANSFER = '';  if ( this._toneMapping === LinearToneMapping ) this.material.defines.LINEAR_TONE_MAPPING = '';  else if ( this._toneMapping === ACESFilmicToneMapping ) this.material.defines.ACES_FILMIC_TONE_MAPPING = '';  // ... el resto de modos  this.material.needsUpdate = true;}

Lee del renderer, así que basta con configurar renderer.toneMapping y renderer.outputColorSpace como siempre y OutputPass se adapta solo.

¿Qué pasa si lo pones antes del bloom? Que la imagen llega al bloom ya comprimida al rango de 0 a 1 y ya codificada en sRGB. El umbral de brillo deja de poder distinguir entre un blanco de papel y un sol, porque los dos valen 1. El desenfoque hace medias entre valores codificados, que es matemáticamente incorrecto. Y el resultado es el bloom lavado y sucio que se reconoce de lejos.

¿Y si lo omites? La imagen sale oscura y desaturada, porque los valores lineales llegan a pantalla sin la curva sRGB. Es el síntoma clásico de “monté el composer y ahora todo se ve mal”.

Regla 2: el antialiasing después de todo lo que crea bordes

Un antialiasing por imagen busca discontinuidades y las suaviza. Si lo ejecutas antes de un efecto que introduce bordes nuevos —un contorno, un resplandor con corte duro, una viñeta abrupta—, esos bordes nuevos salen sin tratar y se ven con escalones justo al lado de los que sí suavizaste. Por eso el antialiasing va después de bloom, de contornos y de cualquier efecto que dibuje geometría o siluetas.

La otra mitad de la regla es el espacio de color, y aquí Three.js sí documenta el contrato de forma explícita, en dos ficheros que se responden entre sí.

SMAAPass dice que opera en linear-srgb y debe ejecutarse antes de OutputPass.

OutputPass dice que un pass que requiera entrada en sRGB, como FXAA, debe seguir a OutputPass en la cadena.

Es la única pareja del catálogo con el orden relativo escrito, y la razón es que si te equivocas no obtienes un error sino un antialiasing que funciona mal sin que sea evidente por qué: los algoritmos de detección de bordes calibran sus umbrales sobre diferencias de luminancia percibida, y esas diferencias son distintas antes y después de aplicar una curva.

Hay una tensión honesta que conviene conocer. Ejecutar SMAA sobre valores HDR sin recortar no es ideal: un píxel de valor 40 junto a uno de valor 1 es una diferencia enorme que puede desbordar la heurística de bordes, y el resultado son puntos brillantes aislados que el antialiasing no consigue suavizar. La solución industrial estándar es aplicar el antialiasing después del tone mapping. La implementación de Three.js no lo permite para SMAA, y esa es una de las razones para mirar la alternativa de la última sección.

Regla 3: los efectos que leen profundidad, pronto

SSAOPass y BokehPass reconstruyen información tridimensional del buffer de profundidad. Cuanto antes vayan, más se parece la imagen sobre la que operan a la escena original, y menos posibilidad hay de que un efecto anterior haya introducido cambios que descuadren la correspondencia entre color y profundidad.

Dentro de esos dos, el orden entre sí sigue la lógica óptica: la oclusión ambiental es una propiedad de la iluminación de la escena y va primero; la profundidad de campo es un efecto de la lente y va después, porque en una cámara real la luz ya iluminada es la que atraviesa el objetivo. Y el bloom va después de los dos, porque es un fenómeno del sensor y de la difusión interna del objetivo: un punto de luz desenfocado por la profundidad de campo debe resplandecer con su forma desenfocada, no con la enfocada.

Regla 4: el color grading, entre el tone mapping y el antialiasing

Si añades un ajuste de color —curvas, LUT, saturación, viñeteo— la posición depende de en qué espacio esté pensado. Un LUT calibrado sobre imagen final, que es lo habitual porque es lo que producen las herramientas de color, va después de OutputPass. Un ajuste físico, como una corrección de exposición, va antes, porque opera sobre radiancia.

Un viñeteo, que es geométrico y no tiene semántica de color, funciona en cualquiera de los dos sitios; después queda algo más contrastado.

La alternativa: postprocessing de pmndrs

Todo lo anterior describe una arquitectura con un problema estructural: cada efecto es un pase completo de pantalla. Como viste, el coste dominante del post-procesado es el ancho de banda, y una cadena de seis passes mueve la imagen doce veces por el bus de memoria.

La librería postprocessing de pmndrs ataca exactamente eso. Su idea central es separar el concepto de pass del de efecto: los efectos no son passes, son fragmentos de shader que un EffectPass fusiona en un único programa. Su README lo dice así:

Texto
This library provides an EffectPass which automatically organizes and mergesany given combination of effects. This minimizes the amount of renderoperations and makes it possible to combine many effects without theperformance penalties of traditional pass chaining. Additionally, everyeffect can choose its own blend function.

En código:

JS
import { BloomEffect, EffectComposer, EffectPass, RenderPass } from 'postprocessing';const composer = new EffectComposer( renderer );composer.addPass( new RenderPass( escena, camara ) );composer.addPass( new EffectPass( camara, new BloomEffect() ) );

Un EffectPass con cinco efectos dentro cuesta un pase, no cinco. Ese es todo el argumento, y es un argumento fuerte: en móvil y en GPU integrada la diferencia es del orden de dos a tres veces.

Su modelo de color también es distinto y conviene conocerlo antes de mezclar conceptos. Por defecto usa buffers de byte sin signo en sRGB, no de media precisión lineales, y hay que pedir HDR explícitamente:

JS
const composer = new EffectComposer( renderer, { frameBufferType: HalfFloatType } );

Y su documentación recomienda dejar renderer.toneMapping en NoToneMapping y aplicar el tone mapping con un ToneMappingEffect al final de la cadena. Es lo contrario de lo que hace OutputPass, que lee el ajuste del renderer. Mezclar las dos convenciones es la fuente más común de imágenes mal expuestas al migrar.

La restricción práctica que hay que verificar siempre es la dependencia de versión. En su versión 6.39.1, la declaración es:

JSON
"peerDependencies": { "three": ">= 0.168.0 < 0.185.0" }

Es decir, r184 entra por los pelos: es la última versión soportada por ese rango. Antes de adoptarla en un proyecto que actualiza Three.js con frecuencia, comprueba el rango de la versión concreta que vas a instalar.

Three.js addons pmndrs postprocessing
Coste con muchos efectos un pase por efecto un pase por EffectPass
Buffers por defecto media precisión, lineal byte sin signo, sRGB
Tone mapping OutputPass lee del renderer ToneMappingEffect, renderer en NoToneMapping
Dependencias ninguna extra paquete aparte con rango de versión
Catálogo 30 passes efectos y passes propios, más amplio en algunos casos