wandres.dev
PBR II · MeshPhysicalMaterial a fondo

clearcoat: la capa de laca que va encima de todo

Qué simula físicamente una segunda capa especular, por qué tiene su propio F0 y su propia normal, y cuándo un clearcoat resuelve algo que roughness no puede.

⏱ 17 min

Pintura de coche, fibra de carbono, madera barnizada, una superficie mojada, un plástico lacado: todos son lo mismo estructuralmente, una capa transparente y lisa sobre otra capa que tiene su propio color y su propia rugosidad. Un MeshStandardMaterial no puede representar eso con ningún valor de roughness, porque solo tiene una superficie. clearcoat añade la segunda.

🎯 Al terminar esta lección sabrás
  • Explicar qué añade clearcoat al cálculo de iluminación y con qué parámetros fijos.
  • Distinguir los casos que necesitan clearcoat de los que se resuelven con roughness.
  • Usar clearcoatNormalMap para el efecto de piel de naranja.
  • Estimar el coste de la capa y decidir si compensa.

Dos lóbulos, no uno

Con clearcoat activado, el shader evalúa un segundo lóbulo especular GGX completo, con su propia rugosidad y su propia normal, y lo suma encima del material base. En r184 la función se llama BRDF_GGX_Clearcoat y usa la misma tríada de distribución, Fresnel y visibilidad que la capa principal.

Lo interesante son los parámetros que no puedes tocar:

material.clearcoatF0 = vec3( 0.04 );
material.clearcoatF90 = 1.0;

El F0 de la capa está fijado en 0.04, es decir, un dieléctrico con índice de refracción 1.5. Y tiene sentido: un barniz, una laca o una película de agua son todos dieléctricos comunes, y ninguno tiene reflectancia tintada. La capa siempre refleja blanco, con la intensidad del Fresnel, sea cual sea el color de lo que hay debajo. Esa es exactamente la razón por la que un coche rojo tiene reflejos blancos y no rojos.

clearcoat va de cero a uno y controla cuánto pesa esa capa. clearcoatRoughness controla lo pulida que está, y sufre el mismo recorte por abajo que la rugosidad principal: se fuerza a un mínimo de 0.0525 y se le suma la rugosidad geométrica derivada de la variación de la normal en pantalla.

import * as THREE from 'three';

// Pintura de coche: base rugosa con color, capa de barniz muy pulida.
const pintura = new THREE.MeshPhysicalMaterial({
  color: 0xb4162c,
  metalness: 0,
  roughness: 0.45,
  clearcoat: 1.0,
  clearcoatRoughness: 0.03
});

// Fibra de carbono: base metalica y oscura bajo la misma laca.
const fibra = new THREE.MeshPhysicalMaterial({
  color: 0x1a1a1a,
  metalness: 1,
  roughness: 0.35,
  clearcoat: 1.0,
  clearcoatRoughness: 0.05
});

// Superficie mojada: la capa es agua, mas fina y menos pulida.
const mojado = new THREE.MeshPhysicalMaterial({
  color: 0x6b5a45,
  metalness: 0,
  roughness: 0.85,
  clearcoat: 0.6,
  clearcoatRoughness: 0.15
});

La combinación característica es siempre la misma: base rugosa, capa pulida. Si pones las dos pulidas, no distinguirás el clearcoat de un simple roughness bajo, y habrás pagado un lóbulo de más para nada.

Por qué roughness no puede hacer esto

La pregunta razonable es por qué no basta con bajar roughness. La respuesta es que una superficie con una sola rugosidad produce un solo brillo, y estos materiales producen dos a la vez: uno ancho y difuso que viene de la base, y otro estrecho y nítido que viene de la laca. Los dos coexisten en el mismo píxel.

Míralo en una pintura de coche real bajo una lámpara: hay un reflejo especular pequeño y muy definido de la propia lámpara, y alrededor un brillo amplio y suave que sigue la curvatura de la chapa. Con roughness solo puedes tener uno de los dos.

Hay un segundo efecto que tampoco es replicable: el clearcoat oscurece ligeramente la base, porque parte de la luz que iba a llegar a la capa inferior se refleja antes en la superior. Esa pérdida es lo que hace que una superficie mojada se vea más saturada y más oscura que la misma superficie seca, y sale sola del modelo.

La normal propia

clearcoatNormalMap es la propiedad menos usada y la que más cambia el resultado. Le da a la capa superior una normal independiente de la de la base, con su propia intensidad en clearcoatNormalScale, un Vector2.

Para qué sirve: una laca real no es perfectamente lisa, tiene ondulaciones de escala grande, lo que en pintura de coche se llama piel de naranja. Con una normal propia de baja frecuencia, los reflejos de la laca se ondulan mientras el color y el detalle de la base se mantienen quietos. Es el detalle que separa una pintura de coche creíble de una bola roja brillante.

El caso simétrico es igual de útil: fibra de carbono con un normalMap de trama en la base y sin clearcoatNormalMap, para que el tejido se vea en la base pero la laca refleje liso, como es en realidad.

const carbono = new THREE.MeshPhysicalMaterial({
  color: 0x101010,
  metalness: 1,
  roughness: 0.3,
  normalMap: tramaDeFibra,          // el tejido, solo en la base
  clearcoat: 1,
  clearcoatRoughness: 0.04,
  clearcoatNormalMap: pielDeNaranja, // ondulacion suave, solo en la laca
  clearcoatNormalScale: new THREE.Vector2(0.15, 0.15)
});
La capa no es un volumen y por eso no refracta

El modelo de clearcoat de Three.js, que es el de Burley y el que estandariza KHR_materials_clearcoat, añade un lóbulo especular pero no simula el paso de la luz por dentro de la capa. En una laca real, la luz que atraviesa el barniz se refracta al entrar y al salir, lo que desplaza ligeramente lo que ves de la base y aumenta su saturación. Aquí no ocurre nada de eso: la base se calcula como si la capa no estuviera, y solo se le suma el especular de la laca encima.

Hay dos consecuencias operativas. La primera es que clearcoat no tiene grosor: no existe una propiedad de espesor porque el modelo no la puede usar. Si necesitas una capa con volumen real, con absorción y refracción, lo que buscas es transmission con thickness, no clearcoat. La segunda es que puedes obtener un aspecto de laca acumulando clearcoat con metalness: 1, cosa que en un modelo con refracción real sería contradictoria, y de hecho es la receta estándar de la fibra de carbono y de la pintura metalizada.

El coste

Un lóbulo GGX más por luz, más un muestreo adicional del entorno prefiltrado para la reflexión indirecta de la capa, más una segunda normal que hay que interpolar y, si usas clearcoatNormalMap, una lectura de textura y una transformación al espacio tangente adicionales.

En la práctica, sobre un MeshStandardMaterial de referencia, activar clearcoat sube el coste del fragment shader alrededor de un tercio. No es catastrófico y es el más barato de todos los añadidos de MeshPhysicalMaterial, muy por debajo de transmission.

Dos avisos de rendimiento. El primero ya lo conoces: el setter de clearcoat incrementa la versión del material cuando el valor cruza el cero, así que animarlo desde 0 provoca una recompilación. Arranca en 0.001. El segundo es que clearcoat sin entorno se ve casi igual que sin clearcoat, porque lo que hace brillar la laca es sobre todo la reflexión del entorno, no las luces puntuales. Si vas a pagar la capa, asegúrate de que hay algo que reflejar.

⚔️ Reto práctico

Monta una fila de esferas con el mismo color base y roughness: 0.5, variando clearcoat de 0 a 1 en cinco pasos, todas bajo un RoomEnvironment. Después repite la fila con clearcoatRoughness variando y clearcoat fijo en uno. Identifica visualmente cuál de los dos parámetros cambia más el resultado y explica por qué mirando la fórmula de la BRDF.