Color o dato: la regla que evita el error silencioso
Qué texturas llevan SRGBColorSpace y cuáles no llevan nada, qué se ve exactamente cuando te equivocas en cada dirección, y cómo auditarlo por código.
La regla, en una pregunta
¿Lo que guarda esa textura es algo que verías con los ojos?
Si la respuesta es sí, es color, y lleva texture.colorSpace = THREE.SRGBColorSpace. Si la respuesta es no, si lo que guarda es un número que alimenta un cálculo, es dato, y no se le pone nada: el valor por defecto de Texture.colorSpace es NoColorSpace, que es exactamente lo correcto.
Aplicada al catálogo completo:
| textura | tipo | colorSpace |
|---|---|---|
map |
color | SRGBColorSpace |
emissiveMap |
color | SRGBColorSpace |
sheenColorMap |
color | SRGBColorSpace |
specularColorMap |
color | SRGBColorSpace |
matcap |
color | SRGBColorSpace |
lightMap |
color | SRGBColorSpace si es una imagen sRGB |
normalMap |
dato | ninguno |
bumpMap |
dato | ninguno |
roughnessMap |
dato | ninguno |
metalnessMap |
dato | ninguno |
aoMap |
dato | ninguno |
alphaMap |
dato | ninguno |
displacementMap |
dato | ninguno |
clearcoatMap y su rugosidad |
dato | ninguno |
clearcoatNormalMap |
dato | ninguno |
transmissionMap y thicknessMap |
dato | ninguno |
iridescenceMap y su grosor |
dato | ninguno |
anisotropyMap |
dato | ninguno |
sheenRoughnessMap |
dato | ninguno |
specularIntensityMap |
dato | ninguno |
La proporción cuenta la historia: de veinte mapas, cinco son color. La regla por defecto es “no tocar”, y la excepción es marcar sRGB.
Hay dos casos que merecen matiz. Un lightMap guarda radiancia, así que conceptualmente es color; si viene de un PNG creado por una herramienta de horneado, lleva sRGB, y si viene de un fichero de rango alto tipo EXR o HDR, ya es lineal y no se marca. Y un gradientMap de material toon es una tabla de consulta sobre la que el argumento se puede sostener en las dos direcciones; lo pragmático es tratarlo como color si lo pintaste a ojo y como dato si lo generaste numéricamente, y en cualquier caso el efecto de equivocarse es solo que las bandas caen en sitios ligeramente distintos.
Qué se ve exactamente al equivocarse
Los dos errores tienen firmas visuales distintas y reconocibles. Conocerlas convierte media hora de depuración en diez segundos.
Textura de color sin marcar. Los valores codificados se usan como si ya fueran lineales, y como la codificación sRGB sube los valores respecto a su equivalente lineal, todo sale más claro de lo que debería, con poco contraste y aspecto lavado. Los tonos medios se van hacia arriba, los negros dejan de ser negros y la imagen parece velada. Es el aspecto característico del 3D web mal configurado, y la reacción instintiva, bajar la intensidad de las luces, lo empeora porque aplana todavía más el contraste.
Textura de datos marcada como color. Los valores se decodifican y bajan, mucho más en la zona media que en los extremos. El efecto depende del mapa:
- Un
normalMapmarcado como sRGB es el caso grave. El0.5neutro de las componentes X e Y se convierte en0.214, que remapeado al rango de menos uno a uno da-0.57. Es decir, una zona plana deja de ser plana y pasa a tener normales fuertemente inclinadas hacia la misma esquina en toda la superficie. El resultado no parece un error de textura: parece que la luz viene de otro sitio, o que el material tiene un relieve raro y sucio. No hay ningún aviso. - Un
roughnessMapmarcado como sRGB baja todos los valores, así que el material sale mucho más pulido y brillante de lo que pintaste. - Un
aoMapmarcado como sRGB oscurece de más: la oclusión se aplica con mucha más fuerza y el objeto parece manchado. - Un
metalnessMapmarcado como sRGB baja los grises intermedios, lo que en un mapa que debería ser binario apenas se nota, porque el cero y el uno se conservan. Es el único caso donde el error es casi inocuo.
La auditoría
Como el error es silencioso, la forma sensata de defenderse es comprobarlo por código. Esta función recorre la escena y avisa de las dos incoherencias:
import * as THREE from 'three';const MAPAS_DE_COLOR = new Set([ 'map', 'emissiveMap', 'sheenColorMap', 'specularColorMap', 'matcap']);const MAPAS_DE_DATOS = new Set([ 'normalMap', 'bumpMap', 'roughnessMap', 'metalnessMap', 'aoMap', 'alphaMap', 'displacementMap', 'clearcoatMap', 'clearcoatRoughnessMap', 'clearcoatNormalMap', 'transmissionMap', 'thicknessMap', 'iridescenceMap', 'iridescenceThicknessMap', 'anisotropyMap', 'sheenRoughnessMap', 'specularIntensityMap']);export function auditarEspaciosDeColor(raiz) { const problemas = []; raiz.traverse((objeto) => { const materiales = Array.isArray(objeto.material) ? objeto.material : (objeto.material ? [objeto.material] : []); for (const material of materiales) { for (const clave of MAPAS_DE_COLOR) { const t = material[clave]; if (t && t.isTexture && t.colorSpace !== THREE.SRGBColorSpace) { problemas.push(`${objeto.name || objeto.type}.${clave} deberia ser SRGBColorSpace`); } } for (const clave of MAPAS_DE_DATOS) { const t = material[clave]; if (t && t.isTexture && t.colorSpace === THREE.SRGBColorSpace) { problemas.push(`${objeto.name || objeto.type}.${clave} NO deberia ser SRGBColorSpace`); } } } }); return problemas;}// Uso tras cargar un modelo.const fallos = auditarEspaciosDeColor(scene);if (fallos.length) console.warn(fallos.join('\n'));Con modelos glTF esta auditoría no debería sacar nada: el formato declara qué texturas son de color y GLTFLoader marca cada una correctamente. Los avisos aparecen cuando cargas texturas sueltas con TextureLoader, que es cuando la decisión es tuya, y cuando alguien sustituye una textura de un modelo cargado por otra propia.