wandres.dev
PBR II · MeshPhysicalMaterial a fondo

iridescence y anisotropy: interferencia y metal cepillado

Dos capas que atacan límites distintos del modelo de microfacetas: una añade física ondulatoria y otra rompe la simetría del lóbulo especular.

⏱ 18 min

El arcoíris de una pompa de jabón y el brillo estirado de una sartén de acero cepillado son los dos fenómenos que más delatan a un motor que no los modela. Ninguno se puede aproximar tocando roughness o color, porque el primero no es reflexión sino interferencia de ondas y el segundo requiere que la rugosidad tenga dirección. Cada uno tiene su capa.

🎯 Al terminar esta lección sabrás
  • Explicar la interferencia de película fina y qué representan los nanómetros de iridescenceThicknessRange.
  • Configurar iridiscencia con y sin mapa de grosor.
  • Entender que la anisotropía deforma el lóbulo especular en el espacio tangente.
  • Anticipar la dependencia de la anisotropía respecto al mapeo UV.

Iridiscencia: interferencia, no reflexión

Cuando una película muy fina cubre una superficie, la luz se refleja dos veces: en la cara superior de la película y en la inferior. Los dos rayos vuelven a juntarse desfasados, y ese desfase depende del grosor de la película, del ángulo de incidencia y de la longitud de onda. Algunas longitudes se refuerzan y otras se cancelan, así que el color reflejado cambia con el ángulo.

Es física ondulatoria pura, y ninguna BRDF de microfacetas la contiene, porque el modelo de microfacetas es óptica geométrica: rayos que rebotan, sin fase. Por eso iridescence no es un efecto estético añadido, es un término que el modelo base no puede producir.

Tres propiedades la controlan y las tres tienen significado físico:

  • iridescence, de cero a uno, cuánto pesa la capa.
  • iridescenceIOR, el índice de refracción de la película, 1.3 por defecto.
  • iridescenceThicknessRange, un array de dos números en nanómetros, [100, 400] por defecto.

Esos nanómetros son literales: el rango de grosores de película que producen colores en el espectro visible está entre unos cien y unos ochocientos nanómetros, del orden de la longitud de onda de la luz. Si pones valores de orden mil o superiores, los ciclos de interferencia se aprietan tanto que el resultado se vuelve gris ruidoso, que es exactamente lo que pasa con una capa gruesa en la realidad.

import * as THREE from 'three';

// Pompa de jabon: base transmisiva, pelicula fina, grosor variable.
const pompa = new THREE.MeshPhysicalMaterial({
  transmission: 1,
  thickness: 0,
  roughness: 0.02,
  metalness: 0,
  iridescence: 1,
  iridescenceIOR: 1.33,
  iridescenceThicknessRange: [100, 700]
});

// Titanio anodizado: base metalica, pelicula de oxido delgada.
const anodizado = new THREE.MeshPhysicalMaterial({
  color: 0x8a8f96,
  metalness: 1,
  roughness: 0.25,
  iridescence: 0.8,
  iridescenceIOR: 2.4,      // el oxido tiene indice alto
  iridescenceThicknessRange: [200, 400]
});

Sin iridescenceThicknessMap, el grosor es constante y vale el máximo del rango, no la media. Eso importa: cambiar solo el primer número del array no hace nada si no hay mapa. Con iridescenceThicknessMap, el grosor se interpola entre los dos extremos usando el canal verde de la textura, y ahí es donde aparece el patrón de manchas de color que hace creíble una pompa o una mancha de aceite. iridescenceMap modula la intensidad con el canal rojo.

Anisotropía: el lóbulo deja de ser redondo

La rugosidad de un MeshStandardMaterial es un solo número, lo que implica que las microfacetas están distribuidas igual en todas las direcciones y el brillo es redondo. En muchos materiales reales no lo es: el metal cepillado, el vinilo, un disco de aluminio torneado, el pelo, la seda. Todos tienen surcos orientados, y su brillo se estira perpendicularmente a esos surcos.

anisotropy introduce una segunda rugosidad. En el shader:

material.alphaT = mix( pow2( material.roughness ), 1.0, pow2( material.anisotropy ) );
material.anisotropyT = tbn[ 0 ] * anisotropyV.x + tbn[ 1 ] * anisotropyV.y;
material.anisotropyB = tbn[ 1 ] * anisotropyV.x - tbn[ 0 ] * anisotropyV.y;

La rugosidad a lo largo de la bitangente sigue siendo la del material; la rugosidad a lo largo de la tangente crece hacia uno según la anisotropía. Con anisotropy: 1, alphaT vale uno, es decir, rugosidad máxima en una dirección y la original en la otra: el brillo se convierte en una línea.

anisotropyRotation gira esa dirección en radianes, medidos desde la tangente y en sentido antihorario. anisotropyMap permite variarla por texel, y su empaquetado es peculiar: los canales rojo y verde llevan la dirección codificada de menos uno a uno, y el canal azul lleva la intensidad de cero a uno.

const acero = new THREE.MeshPhysicalMaterial({
  color: 0xb8bcc2,
  metalness: 1,
  roughness: 0.35,
  anisotropy: 0.9,
  anisotropyRotation: Math.PI / 2   // surcos girados 90 grados
});
La dirección de la anisotropía la decide tu mapeo UV, no el mundo

Las dos direcciones del cálculo, tbn[0] y tbn[1], son la tangente y la bitangente de la superficie, y esas se derivan del mapeo UV de la malla. Consecuencia: la orientación de los surcos sigue al desenvolvido, no a la geometría. Si el UV de una pieza cilíndrica está desenrollado en vertical y el de la pieza de al lado en horizontal, el mismo material con la misma anisotropyRotation producirá brillos perpendiculares en las dos, sin que hayas tocado nada.

Peor todavía: en las costuras del desenvolvido la tangente cambia de golpe, y ahí verás una línea donde el brillo salta. Y si la malla no trae atributo de tangentes, Three.js las deriva en el fragment shader a partir de las derivadas de la posición y de la UV, lo que en zonas con UV distorsionado da tangentes inestables y brillos que tiemblan al mover la cámara.

La regla operativa: si vas a usar anisotropía, el desenvolvido tiene que estar hecho pensando en la dirección del cepillado, y conviene exportar tangentes desde la herramienta de modelado en lugar de dejar que se calculen. Es el único parámetro de material de Three.js cuyo resultado depende directamente de una decisión que se tomó en el software de modelado.

Cuándo merecen la pena

iridescence es un efecto de alta visibilidad y coste moderado: dos evaluaciones de Fresnel de película fina en lugar de una de Schlick, más el muestreo del mapa de grosor si lo hay. Cuando el material lo pide, no hay sustituto; cuando no lo pide, se nota como un tinte irreal. Los casos legítimos son pocos y muy identificables: pompas, manchas de aceite o gasolina, nácar, élitros de insecto, metales anodizados, revestimientos ópticos, y ciertos plásticos holográficos.

anisotropy es más barata pero más exigente con los assets, por lo del desenvolvido. Los casos son metal cepillado o torneado, vinilo, pelo y tela de fibra alineada. Fuera de ahí, un normalMap con surcos finos produce un efecto parecido a distancia media y no depende de las tangentes.

Las dos comparten una característica con clearcoat y transmission: sus setters incrementan la versión del material al cruzar el cero, así que animarlas desde apagadas fuerza una recompilación. Y las dos requieren un entorno decente para verse, porque su efecto es sobre todo sobre la reflexión especular, no sobre el difuso.

⚔️ Reto práctico

Aplica el material anodizado a un toro y anima iridescenceThicknessRange moviendo el segundo valor de 100 a 800 nanómetros. Cuenta cuántos ciclos completos de color recorre la superficie y compáralo con lo que predice la interferencia: cada vez que el grosor aumenta en media longitud de onda dividida por el índice, el color completa un ciclo.