El sistema de extensiones y las que de verdad importan
Cómo funcionan extensionsUsed y extensionsRequired, el catálogo de extensiones de material y de compresión, y qué hace GLTFLoader cuando encuentra una que no conoce.
El núcleo de glTF 2.0 es deliberadamente pequeño: geometría, jerarquía, animación y un modelo PBR metallic-roughness básico. Todo lo demás —el barniz de un coche, el cristal de una botella, la compresión que hace viable descargarlo— vive en extensiones. Ese diseño es el que ha permitido que un formato de 2017 siga siendo el vigente en 2026 sin romper compatibilidad ni una sola vez, y entender su mecánica de negociación es lo que te permite saber, antes de cargar un fichero, si se va a ver como debe.
- Distinguir
extensionsUseddeextensionsRequiredy predecir el comportamiento del cargador ante cada uno. - Identificar las extensiones de material que Three.js mapea a
MeshPhysicalMaterial. - Reconocer las tres extensiones de compresión y qué comprime cada una.
- Registrar un plugin propio en
GLTFLoaderpara una extensión no soportada.
La negociación
En la raíz del JSON hay dos arrays de cadenas:
{
"extensionsUsed": [ "KHR_materials_clearcoat", "KHR_texture_transform" ],
"extensionsRequired": [ "KHR_draco_mesh_compression" ]
}
extensionsUsed es informativo: «este fichero usa estas extensiones». Un cargador que no conozca alguna puede ignorarla y renderizar el resto correctamente, con una pérdida de fidelidad acotada. Un modelo con KHR_materials_clearcoat cargado sin soporte se ve sin barniz, pero se ve.
extensionsRequired es un contrato: «sin esto, el fichero no se puede interpretar». Un cargador que no soporte una extensión requerida debe negarse a cargar. Y tiene sentido: si la geometría viene comprimida con Draco y no sabes descomprimirla, los bufferViews no contienen vértices sino un blob opaco. No hay degradación posible.
La regla práctica que se deduce: las extensiones de apariencia van solo en used; las de codificación van también en required. Un exportador que meta KHR_materials_sheen en extensionsRequired está siendo innecesariamente restrictivo y rompe la carga en visores que se verían perfectamente bien sin el terciopelo.
El prefijo indica el estatus: KHR_ son ratificadas por Khronos, EXT_ están respaldadas por varios implementadores sin ratificación formal, y cualquier otro prefijo —ADOBE_, MSFT_, GOOGLE_— es de un solo vendedor.
Las extensiones de material
Son las más numerosas y en Three.js casi todas se traducen a propiedades de MeshPhysicalMaterial, que existe precisamente para cubrirlas.
| Extensión | Propiedad en Three.js | Qué modela |
|---|---|---|
KHR_materials_clearcoat |
clearcoat, clearcoatRoughness |
capa de barniz sobre el material |
KHR_materials_transmission |
transmission |
transparencia refractiva, cristal |
KHR_materials_volume |
thickness, attenuationColor |
absorción dentro del volumen |
KHR_materials_ior |
ior |
índice de refracción |
KHR_materials_specular |
specularIntensity, specularColor |
control del especular en dieléctricos |
KHR_materials_sheen |
sheen, sheenColor, sheenRoughness |
terciopelo, tela |
KHR_materials_iridescence |
iridescence, iridescenceIOR |
pompas de jabón, capas finas |
KHR_materials_anisotropy |
anisotropy, anisotropyRotation |
metal cepillado, pelo |
KHR_materials_emissive_strength |
emissiveIntensity |
emisión por encima de 1 |
KHR_materials_unlit |
MeshBasicMaterial |
sin iluminación, estilo plano |
Dos que no producen MeshPhysicalMaterial y conviene conocer:
KHR_texture_transform. Permite aplicar offset, rotación y escala a las UVs de una textura concreta sin duplicar el atributo de UVs. Es lo que hace posible un atlas donde cada material usa su región. Se traduce a texture.offset, texture.repeat y texture.rotation.
KHR_lights_punctual. Define luces —direccional, puntual y foco— dentro del glTF, con intensidades en las unidades fotométricas correctas: lux para direccionales, candelas para el resto. Es exactamente el modelo que Three.js adoptó en r155, y no es coincidencia: la convergencia hacia glTF fue una de las razones del cambio. Con esta extensión, un modelo puede traer su propia iluminación y verse igual en cualquier visor.
Las extensiones de compresión
Tres, y comprimen cosas distintas. Es importante no confundirlas porque se combinan.
KHR_draco_mesh_compression. Comprime geometría. Sustituye los bufferViews de los atributos por un blob Draco, que el cargador descomprime. Ratios de 5 a 10 veces sobre la geometría. Va siempre en extensionsRequired.
EXT_meshopt_compression. También comprime geometría, con otro algoritmo: cuantización más un codificador de bytes que se descomprime con un WASM diminuto. Ratios algo menores que Draco pero descompresión mucho más rápida. También va en required.
KHR_texture_basisu. Comprime texturas. Sustituye el PNG o JPEG por un fichero KTX2 con datos Basis Universal, que se transcodifican en el cliente al formato de bloque nativo de la GPU. Esto es cualitativamente distinto de las otras dos: no solo reduce la descarga, sino que la textura sigue comprimida en la VRAM.
Junto a ellas hay dos auxiliares que conviene reconocer:
KHR_mesh_quantization. Permite que los atributos usen tipos enteros en lugar de floats. Es lo que habilita la cuantización de meshopt y también se usa sola.
EXT_mesh_gpu_instancing. Un nodo puede declarar N instancias con sus matrices, y el cargador las convierte en un InstancedMesh. Para un bosque o una multitud, es la diferencia entre mil draw calls y uno.
Qué soporta GLTFLoader
GLTFLoader implementa directamente las extensiones de material listadas arriba, KHR_texture_transform, KHR_lights_punctual, KHR_mesh_quantization, EXT_mesh_gpu_instancing, EXT_texture_webp y EXT_texture_avif. Las tres de compresión requieren que le conectes el decodificador correspondiente:
const loader = new GLTFLoader()
.setDRACOLoader( dracoLoader ) // KHR_draco_mesh_compression
.setKTX2Loader( ktx2Loader ) // KHR_texture_basisu
.setMeshoptDecoder( MeshoptDecoder ); // EXT_meshopt_compression
Sin esa conexión, un fichero que las declare en extensionsRequired falla con un error explícito, que es el comportamiento correcto.
Para saber qué contiene un fichero antes de cargarlo, gltf-transform inspect lista las extensiones en su salida. Y en tiempo de ejecución, el objeto que devuelve el cargador trae la información del fichero:
const gltf = await loader.loadAsync( '/modelos/coche.glb' );
console.log( gltf.parser.json.extensionsUsed );
console.log( gltf.parser.json.extensionsRequired );
console.log( gltf.asset ); // { generator, version, copyright }
Registrar un plugin propio
Para una extensión que Three.js no implementa, GLTFLoader expone un sistema de plugins. Un plugin es un objeto con un name y una o varias de las funciones gancho que el parser invoca.
// Ejemplo minimo: leer una extension propia de nodo y guardar
// sus datos en userData para consumirlos despues.
loader.register( ( parser ) => ( {
name: 'MIEMPRESA_metadatos',
createNodeAttachment( nodeIndex ) {
const nodeDef = parser.json.nodes[ nodeIndex ];
const ext = nodeDef.extensions?.[ 'MIEMPRESA_metadatos' ];
if ( ! ext ) return null;
const marcador = new THREE.Object3D();
marcador.userData.metadatos = ext;
return Promise.resolve( marcador );
},
} ) );
Los ganchos disponibles cubren todos los puntos del parseo: loadMesh, loadMaterial, loadTexture, loadBufferView, createNodeMesh, createNodeAttachment, extendMaterialParams y afterRoot. El propio Three.js implementa sus extensiones con este mismo mecanismo, así que el sistema está bien probado.
Hay una extensión que ya no está en la tabla de arriba y cuya ausencia explica un problema recurrente. En los primeros años de glTF 2.0, Khronos ratificó dos modelos de material alternativos: el metallic-roughness del núcleo y KHR_materials_pbrSpecularGlossiness, que venía del flujo de trabajo de Unreal 3 y de Substance de la época. La coexistencia fue un error de diseño reconocido: dos formas de expresar lo mismo es exactamente lo que hundió a COLLADA. En 2021 la extensión pasó a archivada, Khronos recomendó formalmente migrar, y Three.js acabó retirando su soporte de GLTFLoader: en r184 ya no está. La consecuencia práctica es que un modelo exportado antes de 2021 —o por una herramienta que no se actualizó— puede cargar sin errores y verse completamente mal: como la extensión solo aparece en extensionsUsed y no en extensionsRequired, el cargador la ignora en silencio, se queda con el bloque pbrMetallicRoughness de respaldo si existe, y si no existe usa los valores por defecto, que son metalness 1 y roughness 1. El síntoma es inconfundible una vez lo conoces: todo el modelo se ve como metal rugoso gris, sin color, aunque las texturas estén ahí. Si te lo encuentras, la conversión es de un comando: gltf-transform metalrough entrada.glb salida.glb reescribe los materiales al modelo del núcleo, haciendo la conversión de specular-glossiness a metallic-roughness y regenerando las texturas que haga falta. La lección más general es que extensionsUsed sin extensionsRequired es una degradación silenciosa, y que por eso el primer paso ante cualquier modelo que se vea raro es imprimir esos dos arrays y comprobar qué se está ignorando.