Saltar al contenido

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.

La negociación

En la raíz del JSON hay dos arrays de cadenas:

JSON
{  "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:

JS
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:

JS
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.

JS
// 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.