wandres.dev
PBR I · La teoría del renderizado físico

Metalness es binario: los valores intermedios que arruinan una escena

Por qué un material es conductor o no lo es, qué material imposible construyes con metalness a 0.5, y la lista de comprobación de una escena que parece de 2005.

⏱ 20 min

De todos los deslizadores de un material PBR, metalness es el único que no es un deslizador: es un interruptor con una rampa dibujada encima. La física no admite grados intermedios entre conductor y aislante en un mismo punto de la superficie, y cada vez que dejas ese valor en medio estás describiendo una sustancia que no existe. Es, con diferencia, la causa número uno de que una escena web parezca de hace veinte años.

🎯 Al terminar esta lección sabrás
  • Explicar por qué la conductividad eléctrica determina si hay componente difusa.
  • Calcular qué material produce exactamente un valor de metalness intermedio.
  • Enumerar los tres casos en los que un valor intermedio sí está justificado.
  • Aplicar una lista de comprobación a una escena que “parece de videojuego antiguo”.

La física: conductores y aislantes

La diferencia entre un metal y un no metal, en lo que respecta a la luz, es la disponibilidad de electrones libres. En un conductor los hay, y responden inmediatamente al campo electromagnético de la onda incidente: la parte de la luz que penetra en la superficie se absorbe en una distancia del orden de decenas de nanómetros, mucho menor que la longitud de onda visible. Nada regresa. Por eso un metal no tiene componente difusa: no hay luz que haya viajado bajo la superficie y salido de nuevo.

En un dieléctrico no hay electrones libres. La luz penetra, rebota entre partículas de pigmento, pierde selectivamente las longitudes de onda que esas partículas absorben, y una fracción sale por la superficie en una dirección esencialmente aleatoria. Eso es el color difuso, y por eso el color de un plástico rojo está en su difuso mientras que su reflejo es blanco.

De ahí salen las dos consecuencias numéricas que ya has visto:

  • Un metal tiene F0 alto, entre 0.5 y 1.0 según el material, y tintado.
  • Un dieléctrico tiene F0 bajo, entre 0.02 y 0.05 en la práctica, y gris.

Un punto de una superficie es una cosa o la otra. No existe una sustancia que sea medio conductora en el sentido óptico relevante aquí.

Qué construyes con metalness a 0.5

No hace falta discutirlo en abstracto, porque el shader es explícito. Con color igual a C, metalness igual a m y el ior por defecto:

difuso    = C * (1 - m)
F0        = mezcla(0.04, C, m)

Con m = 0.5 y un C de 0.8:

  • El difuso vale 0.4. La mitad del difuso de un plástico.
  • El F0 vale 0.42. Diez veces el de un dieléctrico, la mitad del de un metal, y tintado con el color base.

Ese material refleja como un metal a medias y dispersa como un plástico a medias, y el total no suma la energía de ninguno de los dos. Es más oscuro que el dieléctrico equivalente porque ha perdido la mitad de su difuso, y menos brillante que el metal equivalente porque su F0 es menor. No corresponde a ninguna sustancia y, lo que es peor, su aspecto depende mucho más de la iluminación concreta que el de cualquiera de los dos extremos, así que es el material que más te obligará a reajustar cuando cambies el entorno.

El síntoma es reconocible: superficies que parecen de plástico sucio con un reflejo apagado y coloreado. Es la textura visual del 3D de gama media de principios de siglo.

Los tres casos legítimos

Hay exactamente tres situaciones donde un valor intermedio es correcto, y las tres ocurren en una textura, no en un escalar del material.

Mezcla a nivel de texel. Un metal oxidado tiene zonas de metal desnudo y zonas de óxido, que es un dieléctrico. Si un texel cubre una zona con las dos cosas, su metalicidad promedio es intermedia, y eso es correcto porque describe una mezcla espacial por debajo de la resolución de la textura, no una sustancia intermedia.

Antialiasing del propio mapa. Los mipmaps de un mapa de metalicidad binario producen niveles con valores intermedios en las fronteras. Es inevitable y es preferible al aliasing.

Recubrimientos muy finos. Pintura metálica, polvo metálico sobre plástico, capas de unas pocas micras. Aquí la respuesta correcta suele ser otra: usa metalness a uno con la geometría adecuada, o pasa a MeshPhysicalMaterial con clearcoat, que es lo que de verdad modela una capa sobre otra.

Fuera de esos tres, la regla es dura y sin excepciones útiles: el metalness escalar de un material es 0 o es 1.

Los valores reales de los metales

Como el F0 de un metal es su color base, poner un metal correcto es cuestión de escribir el color medido. Estos son valores publicados en las referencias estándar de la industria, expresados como componentes lineales:

metal R G B
aluminio 0.913 0.921 0.925
plata 0.972 0.960 0.915
oro 1.000 0.766 0.336
cobre 0.955 0.638 0.538
cromo 0.550 0.556 0.554
hierro 0.562 0.565 0.578
níquel 0.660 0.609 0.526
titanio 0.542 0.497 0.449

Fíjate en dos cosas. Todos son claros: el más oscuro, el titanio, ronda 0.5, y no hay ningún metal por debajo de eso. Y solo el oro y el cobre tienen un tinte fuerte; el resto son prácticamente grises. Si tu “oro” tiene un color base marrón oscuro, no es oro, es plástico marrón con metalness mal puesto.

Para escribir esos valores en Three.js hay que tener cuidado con el espacio de color, porque son lineales y setHex interpreta sRGB:

import * as THREE from 'three';

// Correcto: valores lineales, espacio explicito.
const oro = new THREE.MeshStandardMaterial({
  metalness: 1,
  roughness: 0.25
});
oro.color.setRGB(1.000, 0.766, 0.336, THREE.LinearSRGBColorSpace);

// Incorrecto: 0xffc355 es "el mismo" color pero en sRGB,
// y al decodificarlo sale mucho mas oscuro de lo que toca.
const oroMalo = new THREE.MeshStandardMaterial({
  color: 0xffc355,
  metalness: 1,
  roughness: 0.25
});

Color.setRGB interpreta sus componentes en el espacio de trabajo, que es LinearSRGBColorSpace; pasarle el cuarto argumento explícito lo deja documentado y a prueba de cambios. setHex y setStyle, en cambio, interpretan sRGB por defecto, que es lo correcto para colores elegidos en un selector pero no para valores medidos.

La lista de comprobación de la escena que parece de 2005

Cuando una escena PBR se ve mal, casi siempre es una combinación de estas seis cosas, y en este orden de frecuencia:

  1. metalness intermedio. Comprueba que todos tus materiales tienen 0 o 1. Recorre la escena con scene.traverse y saca por consola los que estén en medio.
  2. No hay entorno. Sin scene.environment la integral indirecta es cero. Un RoomEnvironment pasado por PMREMGenerator cuesta cero bytes y cambia la escena por completo.
  3. roughness en 0 o en 1. Casi nada en el mundo real está en los extremos. Los valores interesantes están entre 0.15 y 0.7, y variarlos con una textura importa más que acertar el escalar.
  4. Colores base fuera de rango. Blancos puros, negros puros, saturaciones máximas. Nada de eso existe.
  5. Espacio de color mal marcado. Una textura de color sin SRGBColorSpace sale lavada; un mapa de datos marcado como sRGB sale mal sin dar ningún error.
  6. Sin mapeo de tonos. Con NoToneMapping, todo lo que pase de uno se recorta a blanco plano y la imagen pierde el rango alto entero.

Las dos últimas no son de material sino de gestión de color, no producen ningún mensaje de error, y son las más difíciles de diagnosticar precisamente por eso. Tienen su propio nivel.

⚔️ Reto práctico

Escribe una función de auditoría que recorra la escena con scene.traverse, y para cada material con isMeshStandardMaterial avise por consola si metalness no es exactamente 0 ni 1 y no hay metalnessMap, si roughness es 0 o 1, o si algún canal del color base supera 0.94 o baja de 0.02. Ejecútala sobre un modelo glTF descargado de cualquier repositorio público y cuenta cuántos avisos salen.