transmission, thickness e ior: vidrio de verdad
La diferencia entre opacity y transmission, qué hace el renderer para que un objeto vea lo que hay detrás, y cómo se configura un volumen con absorción.
opacity: 0.3 produce un objeto que deja pasar el color de lo que hay detrás sin desviarlo y que, además, pierde su reflexión especular en la misma proporción, cosa que el cristal real no hace: un cristal es transparente y a la vez muy reflectante. transmission es el mecanismo correcto, y su coste no es un lóbulo más, es una pasada de render entera.
- Distinguir
opacitydetransmissionen términos de qué hace cada uno con el especular. - Configurar un vidrio de pared fina y un volumen sólido, y saber cuál es cuál.
- Usar
attenuationColoryattenuationDistancepara dar color a un volumen. - Anticipar el coste de la pasada de transmisión y sus limitaciones.
Opacidad no es transmisión
opacity es una mezcla alfa: el color final es una interpolación entre lo que ya había en el buffer y lo que el material calcula. Todo el material se atenúa por igual, incluido su brillo especular. Un cristal con opacity: 0.1 casi no tiene reflejos, y un cristal real con esa transparencia los tiene enteros.
transmission modela otra cosa: la luz que atraviesa la superficie en lugar de reflejarse. El especular se mantiene intacto, porque lo que se transmite es justo lo que el Fresnel no reflejó. Un material con transmission: 1 sigue reflejando el cuatro por ciento en incidencia normal y casi el cien por cien en incidencia rasante, que es exactamente el comportamiento de una ventana.
La documentación es explícita en un punto: cuando transmission no es cero, opacity debe valer 1. Mezclar los dos mecanismos produce resultados sin sentido físico.
import * as THREE from 'three';
// Cristal de pared fina: una ventana, una copa, una burbuja.
const ventana = new THREE.MeshPhysicalMaterial({
transmission: 1,
thickness: 0, // pared fina: sin volumen
roughness: 0.02,
ior: 1.5,
metalness: 0,
opacity: 1
});
// Volumen solido: un prisma, una canica, una gota.
const canica = new THREE.MeshPhysicalMaterial({
transmission: 1,
thickness: 1.2, // en unidades del espacio de la malla
roughness: 0.05,
ior: 1.52,
attenuationColor: new THREE.Color(0x7fd1c1),
attenuationDistance: 0.6,
metalness: 0,
opacity: 1
});
Pared fina o volumen
thickness es la propiedad que decide entre dos modelos distintos, y su valor por defecto es 0.
Con thickness en cero, el material es de pared fina: la luz entra y sale por la misma superficie sin recorrer distancia interior. No hay refracción apreciable ni absorción. Es lo correcto para una ventana, una pompa, una lámina de plástico o el cristal de un reloj.
Con thickness mayor que cero, el material es un límite de volumen: la luz recorre esa distancia por dentro antes de salir, se desvía según el ior y se atenúa según el color y la distancia de atenuación. Es lo correcto para una canica, un prisma, un vaso lleno de líquido o una gota.
El valor de thickness está en las unidades del espacio de la malla, no en metros ni en píxeles. Para una esfera de radio uno, un thickness de dos es el diámetro completo; poner veinte no es “más sólido”, es un volumen veinte veces más largo, con la absorción correspondiente. Es el error más común: subir el grosor hasta que quede bien en vez de medirlo con la geometría.
thicknessMap guarda el grosor en el canal verde y multiplica al escalar, lo que permite que una malla con partes gruesas y finas se comporte correctamente en cada zona.
El color de un volumen no es su color base
En un material transmisivo, color casi no se ve, porque el difuso ha desaparecido: lo que había de difuso ahora se transmite. El color de un vidrio verde no está en color, está en la absorción del volumen.
attenuationColor es el color al que se convierte la luz blanca tras recorrer exactamente attenuationDistance unidades dentro del material. attenuationDistance vale Infinity por defecto, lo que significa ninguna absorción y un vidrio perfectamente incoloro.
La relación es exponencial, como manda la ley de Beer-Lambert: al doble de recorrido, el color se aplica dos veces. Por eso un objeto de vidrio coloreado se ve casi transparente en sus partes finas y muy saturado en las gruesas, y por eso este modelo produce cristal creíble mientras que teñir el color produce plástico.
Ajustar la pareja es cuestión de dos pasos. Primero elige el color de saturación máxima que quieres ver en la parte más gruesa del objeto. Después baja attenuationDistance hasta que la parte fina tenga el tinte que buscas: valores del orden de la mitad del tamaño del objeto suelen quedar bien, y valores muy pequeños producen un material casi opaco.
dispersion completa el conjunto: separa las longitudes de onda al refractar, que es el arcoíris de un prisma. Solo funciona con transmisión, y valores realistas están en el rango de cero a uno. Es de los pocos parámetros donde pasarse es inmediatamente visible y bastante feo.
Lo que hace el renderer por debajo
Un objeto transmisivo tiene que muestrear lo que hay detrás de él, y en un rasterizador eso no está disponible: cuando dibujas el cristal, el framebuffer contiene lo que ya se ha dibujado, no la escena completa.
La solución de Three.js es dibujar la escena dos veces. Antes de la pasada normal, el renderer hace una pasada de transmisión que dibuja la escena en un render target aparte. Ese target se le pasa al material en dos uniforms, transmissionSamplerMap y transmissionSamplerSize, y el shader lo muestrea desplazando la coordenada según el vector refractado. La rugosidad del material selecciona un nivel de mipmap más borroso de ese target, que es lo que produce el aspecto de vidrio esmerilado.
Esto explica de golpe todo el perfil de coste y todas las limitaciones.
El render target de transmisión contiene la escena dibujada sin los objetos transmisivos. La consecuencia es directa: dos vidrios superpuestos no se refractan mutuamente. Pon dos copas una detrás de otra y la de delante mostrará lo que hay al fondo como si la de atrás no existiera. Con una botella y su tapón, o con un vaso de paredes gruesas donde la pared trasera debería verse a través de la delantera, el artefacto es evidente y no tiene arreglo dentro de este modelo: no es un parámetro mal puesto, es la arquitectura de la técnica.
Las salidas reales son tres, y ninguna es gratis. Aceptarlo y componer la escena para que no se note, que es lo que hace el noventa por ciento de la producción. Sustituir el vidrio secundario por un MeshPhysicalMaterial sin transmisión y con envMap, que refleja pero no refracta. O irse a trazado de rayos, que en la web significa un render progresivo acumulado y no tiempo real. Antes de gastar horas ajustando ior y thickness para arreglar un artefacto de doble cristal, conviene saber que el artefacto no está en tus parámetros.
El coste, con números
La pasada de transmisión dibuja la escena una segunda vez. Si tu escena cuesta 400 llamadas de dibujo, con un solo objeto transmisivo pasa a costar unas 800. No importa cuántos objetos transmisivos haya: la pasada es una, así que el segundo cristal es casi gratis comparado con el primero. Esa es la asimetría clave: el primer material con transmisión es carísimo y los siguientes son baratos.
De ahí salen dos decisiones prácticas. Si solo necesitas un objeto de vidrio y estás justo de presupuesto, plantéate un MeshPhysicalMaterial sin transmisión, con roughness bajo, metalness cero y un buen envMap: refleja como el cristal y no refracta, y a cierta distancia nadie lo distingue. Y si ya has aceptado el coste de la pasada, no te cortes en usar transmisión en todos los objetos que la necesiten, porque el grueso ya lo has pagado.
Construye una canica con thickness igual al diámetro de la esfera y attenuationDistance igual al radio, y anima attenuationDistance de ese valor hasta la centésima parte. Observa el punto exacto en el que el vidrio deja de parecer vidrio y empieza a parecer plástico opaco, y anota qué relación entre grosor y distancia de atenuación marca esa frontera.