wandres.dev
TEXTURAS II · Los mapas de un material

roughnessMap, metalnessMap y el empaquetado ORM

Los dos mapas que hacen que un material deje de ser uniforme, los canales exactos que lee cada uno, y por qué en glTF viajan en la misma imagen.

⏱ 17 min

Un material con roughness escalar describe una superficie imposible: un objeto real tiene zonas pulidas, zonas rayadas, zonas con polvo y zonas con huellas, y esa variación es lo que el ojo usa para juzgar si algo es real. Los mapas de rugosidad y metalicidad son, con diferencia, los que más aportan a la credibilidad de un material, y su empaquetado en canales es una de las decisiones de diseño más rentables de glTF.

🎯 Al terminar esta lección sabrás
  • Asignar roughnessMap y metalnessMap sabiendo qué canal lee cada uno.
  • Evitar el error de multiplicación que deja el mapa de metalicidad sin efecto.
  • Explicar el empaquetado ORM y por qué ahorra más que un tercio.
  • Reconocer que estos mapas nunca llevan espacio de color sRGB.

Un canal cada uno, y no es casualidad

Las convenciones están fijadas en la documentación de MeshStandardMaterial y son estas:

mapa canal
aoMap rojo
roughnessMap verde
metalnessMap azul

Los tres son valores de un solo componente y los tres caben en canales distintos de una misma imagen RGB. Esa coincidencia no es fortuita: glTF define la textura de material metálico-rugoso con la rugosidad en el verde y la metalicidad en el azul precisamente para dejar el rojo libre, y define la textura de oclusión leyendo el rojo, de forma que las dos pueden ser el mismo fichero. El comentario del propio shader de Three.js lo dice sin rodeos, el canal rojo del mapa de oclusión es “compatible con una textura combinada OcclusionRoughnessMetallic”.

De ahí el nombre con el que se conoce en producción: ORM, por el orden de los canales.

import * as THREE from 'three';

const orm = new THREE.TextureLoader().load('/texturas/caja_orm.png');
// Es un mapa de datos: NADA de SRGBColorSpace.

const material = new THREE.MeshStandardMaterial({
  map: colorCaja,
  aoMap: orm,
  roughnessMap: orm,
  metalnessMap: orm,
  roughness: 1,     // imprescindible: los mapas multiplican
  metalness: 1
});

Sí, es la misma instancia de textura asignada tres veces. No se sube tres veces a la GPU ni ocupa memoria tres veces: Three.js identifica la textura por su objeto, así que es una sola subida, un solo sampler y una sola transformación UV. GLTFLoader hace exactamente esto al cargar un modelo con ORM.

El error que deja los mapas sin efecto

Fíjate en las dos últimas líneas del ejemplo. La documentación lo dice para los dos mapas con la misma frase: si el mapa está presente, ambos valores se multiplican.

Ahora recuerda los valores por defecto de MeshStandardMaterial:

this.roughness = 1.0;
this.metalness = 0.0;

roughness a uno es neutro para la multiplicación, así que un roughnessMap funciona sin tocar nada. Pero metalness a cero anula por completo el mapa: cualquier valor de la textura multiplicado por cero es cero, y el material entero se comporta como un dieléctrico. Asignas el mapa, no cambia nada, y no hay ningún aviso.

Es, con toda probabilidad, el error más frecuente al montar un material PBR a mano. La regla es corta: al asignar un metalnessMap, pon metalness: 1. Lo mismo aplica si has bajado roughness para un experimento anterior y luego asignas un roughnessMap.

El escalar sigue siendo útil como multiplicador global: roughness: 0.8 con un mapa te da una versión un veinte por ciento más pulida de todo el mapa, lo cual es una forma cómoda de ajustar sin repintar la textura. Pero es un ajuste, no el valor por defecto.

Qué guardar en cada mapa

Un mapa de rugosidad bien hecho cuenta la historia del objeto. Las zonas que se tocan mucho están más pulidas; las que acumulan polvo, más rugosas; los arañazos son líneas de rugosidad distinta a la base; el óxido es rugosidad alta con metalicidad baja. Un objeto con roughness uniforme parece salido de fábrica y esterilizado, y esa es exactamente la sensación de una escena que no acaba de funcionar.

El rango útil está entre 0.1 y 0.9 aproximadamente. Valores por debajo se recortan al mínimo interno de 0.0525; valores por encima de 0.9 producen superficies indistinguibles entre sí.

Un mapa de metalicidad, en cambio, quiere ser binario. Ya conoces el motivo: los valores intermedios describen materiales que no existen. La única excusa para un gris en un mapa de metalicidad es que ese téxel cubra físicamente una mezcla de metal desnudo y de recubrimiento. Un mapa de metalicidad con degradados suaves está mal hecho, y produce las costuras claras características del flujo metallic-roughness.

El ahorro del empaquetado es mucho mayor que un tercio

La cuenta ingenua dice que meter tres mapas en uno ahorra dos tercios de memoria. La cuenta real ahorra más, porque el coste de una textura no es solo su tamaño.

Tres texturas separadas de 1024 por 1024 significan tres subidas a la GPU, tres cadenas de mipmaps que generar, tres unidades de textura ocupadas en el shader, tres uniforms de transformación UV que actualizar por objeto y por fotograma, y sobre todo tres lecturas de memoria de vídeo desde posiciones distintas en cada fragmento. Esa última es la que importa: una lectura de textura que falla en caché cuesta cientos de ciclos, y tres lecturas dispersas fallan tres veces. Con ORM, un solo acceso trae los tres valores en la misma línea de caché.

Hay un límite práctico que también se alivia: renderer.capabilities.maxTextures te dice cuántas unidades de textura admite el fragment shader, y aunque el mínimo garantizado en WebGL2 es dieciséis, un MeshPhysicalMaterial con todas sus capas y todos sus mapas se acerca peligrosamente. Empaquetar canales es lo que permite que un material completo quepa.

Y una advertencia sobre el empaquetado: hazlo con una herramienta que entienda espacios de color, como gltf-transform. Si compones los tres canales en un editor de imagen tratándolos como una imagen normal, es fácil que el resultado se guarde con un perfil sRGB incrustado, y entonces cualquier pipeline que lo respete decodificará tus valores de rugosidad como si fueran color. La imagen se ve igual y los números están mal.

Nunca sRGB

Cierro con la regla que va a repetirse en el nivel siguiente y que aquí se aplica a los tres canales del ORM: son datos, no color. Un valor de rugosidad de 0.5 significa rugosidad media, no un gris medio percibido. Marcar la textura como SRGBColorSpace haría que Three.js aplicara la decodificación de sRGB y convirtiera ese 0.5 en 0.21, con lo que todo tu material saldría mucho más pulido de lo que pintaste.

El defecto de Texture.colorSpace es NoColorSpace, que es lo correcto para estos mapas, así que la regla operativa es simplemente no tocarlo. Los únicos mapas a los que hay que ponerles SRGBColorSpace explícitamente son los que representan color visible.