sheen: por qué un tejido no es un MeshStandardMaterial
El lóbulo de retrodispersión que produce el halo de los bordes en terciopelo y satén, la distribución de Charlie, y el valor por defecto que hace que sheen parezca no funcionar.
El terciopelo tiene un comportamiento que ninguna BRDF de microfacetas puede producir: brilla más en los bordes, donde la superficie se aleja de la cámara, en lugar de brillar donde la geometría apunta hacia la luz. No es un truco de iluminación, es que la superficie no está hecha de microespejos sino de fibras verticales, y la luz rebota entre ellas hacia atrás. sheen añade un lóbulo específico para eso.
- Explicar qué fenómeno óptico modela
sheeny por qué GGX no lo produce. - Configurar los tres parámetros de
sheensin caer en el valor por defecto invisible. - Identificar qué tejidos necesitan
sheeny cuáles se resuelven conroughness. - Describir la compensación de energía que aplica Three.js al activar la capa.
Retrodispersión, no reflexión especular
Un tejido de fibras, como el terciopelo, el ante o el satén, no tiene una superficie en el sentido de una plancha con rugosidad. Tiene un bosque de fibras más o menos perpendiculares a la tela. Cuando la luz llega, rebota entre esas fibras varias veces y sale preferentemente hacia atrás, en dirección a la fuente, y también hacia los lados en incidencia rasante.
El efecto visible es un halo claro en la silueta del objeto, justo donde una BRDF de microfacetas produciría lo contrario. Es lo que hace que un sofá de terciopelo tenga los bordes de los cojines más claros que su centro, y por qué una tela así cambia de aspecto según en qué dirección la pases la mano.
Three.js lo implementa con una distribución específica llamada Charlie, de Estévez y Kulla, y una función de visibilidad de Neubelt pensada para el mismo caso. En el shader son D_Charlie y V_Neubelt, y la BRDF que las combina es BRDF_Sheen. Es un lóbulo aparte que se suma al material base; no lo sustituye.
Los tres parámetros y la trampa del defecto
sheen es la intensidad de la capa, de cero a uno. sheenColor es su tinte. sheenRoughness controla la anchura del halo.
La trampa está aquí:
this.sheenColor = new Color( 0x000000 ); // negro
this.sheenRoughness = 1.0;
this.sheen = 0;
sheenColor vale negro por defecto. El shader multiplica el lóbulo por ese color, así que poner sheen: 1 sin tocar nada más da exactamente cero de contribución. El material se recompila, pagas el coste, y no se ve absolutamente nada. Es la razón por la que medio internet cree que sheen no funciona.
La configuración mínima que sí hace algo:
import * as THREE from 'three';
// Terciopelo rojo: base oscura y saturada, halo claro y calido.
const terciopelo = new THREE.MeshPhysicalMaterial({
color: 0x6a0f1a,
roughness: 0.9,
metalness: 0,
sheen: 1.0,
sheenColor: new THREE.Color(0xd98a7a),
sheenRoughness: 0.25
});
// Saten: base mas clara, halo mas estrecho y mas brillante.
const saten = new THREE.MeshPhysicalMaterial({
color: 0x2a3f66,
roughness: 0.55,
metalness: 0,
sheen: 1.0,
sheenColor: new THREE.Color(0xbfd0ff),
sheenRoughness: 0.1
});
La receta que funciona casi siempre: base con roughness alto y color saturado, sheenColor claro y desaturado, sheenRoughness bajo. Un sheenRoughness alto ensancha tanto el lóbulo que deja de leerse como halo y se convierte en una subida general del brillo, que es indistinguible de haber aclarado el color base.
Los dos mapas asociados tienen canales distintos y conviene no confundirlos: sheenColorMap usa los canales RGB, y sheenRoughnessMap usa el canal alfa. Ese alfa no es un descuido, permite empaquetar el color y la rugosidad del sheen en una sola textura RGBA.
Qué tejidos lo necesitan
No todos. La pregunta discriminante es si el tejido tiene pelo o brillo direccional.
Lo necesitan: terciopelo, ante, pana, felpa, satén, seda, algunos tafetanes, y cualquier tela con fibra levantada. También, fuera del mundo textil, el polvo fino sobre una superficie y la pelusa de una fruta.
No lo necesitan: algodón mate, lino, lona, fieltro, denim. Todos esos son superficies rugosas normales y un MeshStandardMaterial con roughness alto y un buen normalMap de trama los resuelve mejor y más barato. Añadirles sheen los hace parecer sintéticos.
Three.js no se limita a sumar el lóbulo. En los tres términos afectados aplica un factor de compensación:
float sheenAlbedo = IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness );
float sheenEnergyComp = 1.0 - max3( material.sheenColor ) * sheenAlbedo;
diffuse *= sheenEnergyComp;Es decir, la energía que se lleva la capa de pelusa se resta del difuso y del especular de la base. Por eso al subir sheen la tela no se aclara en conjunto, se redistribuye: los bordes ganan y el centro pierde, que es exactamente lo que hace el terciopelo real.
La consecuencia práctica que no es obvia: un sheenColor blanco puro con sheen: 1 puede dejar el difuso de la base casi a cero, porque max3 toma el canal máximo del color y la compensación se acerca a uno. Si tu tela con sheen se ha vuelto plana y sin color, no es que el sheen sea demasiado fuerte, es que se ha comido la base. La solución es bajar la luminosidad de sheenColor, no bajar sheen.
El coste
sheen añade un lóbulo por luz directa, un término indirecto propio, y tres evaluaciones de IBLSheenBRDF, que es un ajuste polinómico con una exponencial. Es más caro que clearcoat en cuanto hay varias luces, porque el lóbulo de sheen se evalúa por luz igual que el principal, y encima la compensación de energía se calcula por luz también.
Como referencia práctica, sobre un MeshStandardMaterial con dos luces y entorno, activar sheen sube el coste del fragmento en torno a un cuarenta por ciento. Sigue siendo mucho más barato que transmission, que no compite en la misma liga.
La decisión razonable: sheen en el material protagonista, un sofá, una cortina, una prenda en primer plano; y MeshStandardMaterial con normalMap de trama para todo lo demás. En una escena de interior con quince objetos textiles, poner sheen en los quince es tirar rendimiento en superficies que ocupan cien píxeles.