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.
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.
- Explicar por qué la conductividad eléctrica determina si hay componente difusa.
- Calcular qué material produce exactamente un valor de
metalnessintermedio. - 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
F0alto, entre0.5y1.0según el material, y tintado. - Un dieléctrico tiene
F0bajo, entre0.02y0.05en 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
F0vale0.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.
Cuando una escena PBR se ve mal, casi siempre es una combinación de estas seis cosas, y en este orden de frecuencia:
metalnessintermedio. Comprueba que todos tus materiales tienen0o1. Recorre la escena conscene.traversey saca por consola los que estén en medio.- No hay entorno. Sin
scene.environmentla integral indirecta es cero. UnRoomEnvironmentpasado porPMREMGeneratorcuesta cero bytes y cambia la escena por completo. roughnessen0o en1. Casi nada en el mundo real está en los extremos. Los valores interesantes están entre0.15y0.7, y variarlos con una textura importa más que acertar el escalar.- Colores base fuera de rango. Blancos puros, negros puros, saturaciones máximas. Nada de eso existe.
- Espacio de color mal marcado. Una textura de color sin
SRGBColorSpacesale lavada; un mapa de datos marcado como sRGB sale mal sin dar ningún error. - 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.
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.