4. Cargar modelos glTF y animaciones
Secciones
Ajustes de lectura
Ninguna voz para este idioma en este navegador.
Más voces
Código y términos técnicos en inglés
Desactivado: la voz de la página lo lee todo, sin pausa entre dos voces.
Ninguna voz para este idioma en este navegador.
Más voces
Más voces: Microsoft Edge ofrece voces naturales en línea, y Windows (Configuración › Hora e idioma › Voz), macOS y Android instalan otras.
Código: la página en src/04-gltf/main.ts, el script que escribe los modelos, scripts/make-models.ts, lo que construye el cargador en scripts/l04-gltf.ts, y los ejercicios en scripts/l04-exercises.ts.
glTF, el formato que espera three.js
Sección titulada «glTF, el formato que espera three.js»glTF 2.0 es un estándar de Khronos, como OpenGL y Vulkan, para enviar escenas 3D a una aplicación que las renderiza. Khronos lo llama “el JPEG del 3D”: un formato de entrega, no un formato de edición como un archivo de Blender. Un recurso glTF contiene un grafo de escena de nodos, mallas cuyos búferes corresponden directamente a búferes de la GPU, materiales metálico-rugosidad (el modelo de la lección 2), texturas, cámaras, skins y animaciones.
Viene en dos formas: un archivo JSON .gltf con archivos .bin e imágenes por separado, o un único binario .glb que los empaqueta juntos. Blender lo exporta, igual que la mayoría de las herramientas 3D y tiendas de recursos.
| MonoGame | WPF 3D | three.js | |
|---|---|---|---|
| Archivos de modelo | FBX o X, compilados en tiempo de compilación por la canalización de contenido a .xnb |
sin cargador: XAML o código, o una biblioteca de terceros | glTF 2.0, cargado en tiempo de ejecución por GLTFLoader |
| Lo que da una carga | un Model con mallas y huesos |
un Group con hijos Mesh, Object3D y Bone, más clips de animación |
|
| Animaciones | tu código, o una biblioteca | Storyboard y AnimationClock |
AnimationClip reproducido por AnimationMixer |
| Compresión | los formatos de la canalización | Draco o meshopt para la geometría, KTX2 para las texturas, cada uno con su propio decodificador |
Un modelo escrito por un script
Sección titulada «Un modelo escrito por un script»Para que cada byte sea reproducible, el curso no descarga ningún modelo: scripts/make-models.ts construye un metrónomo con primitivas de three.js y lo escribe con glTF Transform, una biblioteca para leer, editar y escribir glTF en Node.js. Un cuerpo ahusado, y un brazo formado por una varilla y una pesa que oscila de +25° a −25° y vuelve en un segundo.
Dos detalles del script importan para cualquier modelo:
- El pivote del brazo es el origen de su nodo. La geometría de la varilla se desplaza hacia arriba la mitad de su longitud, y el nodo del brazo está en la base de la varilla, delante del cuerpo, así que al rotar el nodo el brazo oscila alrededor de su base (líneas 54-57). Un modelo cuyas piezas giran alrededor del punto equivocado se exportó con los orígenes equivocados.
- Los colores de los materiales en glTF son lineales.
baseColorFactorcontiene valores lineales, así que el script primero convierte sus colores hexadecimales sRGB conTHREE.Color(línea 21);GLTFLoaderlee el factor comoLinearSRGBColorSpacey una textura de color base comoSRGBColorSpace(GLTFLoader.js, líneas 829-836), las reglas de la lección 3.
La animación es un canal glTF que apunta a la rotation del brazo con cinco cuaterniones, uno cada cuarto de segundo, interpolados linealmente (líneas 62-72):
// Una animación, "swing": rotación del brazo alrededor de Z, como cuaterniones (x, y, z, w), interpolados linealmenteconst angles = [0, 25, 0, -25, 0];const times = document.createAccessor('swing_times').setType(Accessor.Type.SCALAR).setArray(new Float32Array([0, 0.25, 0.5, 0.75, 1])).setBuffer(buffer);const rotations = document .createAccessor('swing_rotations') .setType(Accessor.Type.VEC4) .setArray(new Float32Array(angles.flatMap((deg) => new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), THREE.MathUtils.degToRad(deg)).toArray()))) .setBuffer(buffer);const sampler = document.createAnimationSampler().setInput(times).setOutput(rotations).setInterpolation('LINEAR');const channel = document.createAnimationChannel().setTargetNode(arm).setTargetPath('rotation').setSampler(sampler);document.createAnimation('swing').addSampler(sampler).addChannel(channel);El script escribe el modelo tres veces, tal cual y con cada compresión de geometría:
$ node scripts/make-models.tsmetronome.glb 58540 bytes, 24239 gzippedmetronome-draco.glb 10424 bytes, 7438 gzippedprune: Removed types... Accessor (12)prune: Removed types... Accessor (9)metronome-meshopt.glb 24616 bytes, 7526 gzippedLo que construye GLTFLoader
Sección titulada «Lo que construye GLTFLoader»GLTFLoader también funciona en Node.js, siempre que el archivo no tenga imágenes que decodificar ni geometría Draco, que DRACOLoader decodifica en Web Workers. scripts/l04-gltf.ts analiza el archivo sin comprimir e imprime el árbol:
$ node scripts/l04-gltf.tsGroup "metronome" Object3D "metronome_1" Mesh "body" — 48 vertices, MeshStandardMaterial "walnut" Object3D "arm" Mesh "rod" — 24 vertices, MeshStandardMaterial "brass" Mesh "weight" — 1225 vertices, MeshStandardMaterial "brass"asset: { generator: 'glTF-Transform v4.5.0', version: '2.0' }clip "swing": 1 s, tracks arm.quaternion (5 keys, QuaternionKeyframeTrack)...gltf.scenees unGroupque lleva el nombre de la escena glTF, y cada nodo glTF se convierte en unObject3D, o en unMeshcuando tiene una malla. El nodo raíz del script también se llamaba “metronome”;GLTFLoaderhace únicos los nombres, y lo renombra “metronome_1” (createUniqueName). El código que busca objetos congetObjectByNamedepende de esos nombres, así que acuérdalos con quien exporte el modelo.- Los materiales son
MeshStandardMaterial, con el nombre del material glTF. Extensiones comoKHR_materials_clearcoatlos convierten enMeshPhysicalMaterial. - La animación se convierte en un
AnimationClipengltf.animations, con una pista por propiedad animada, llamadanodo.propiedad:arm.quaternion. Un clip son datos, no está ligado a un objeto: los nombres de nodo en los nombres de las pistas lo conectan con una escena.
Reproducir la animación
Sección titulada «Reproducir la animación»AnimationMixer reproduce clips sobre los objetos que cuelgan de una raíz. clipAction(clip) devuelve una AnimationAction, el equivalente de un AnimationClock de WPF: tiene su propio tiempo, velocidad (timeScale), peso y modo de bucle, y varias acciones pueden mezclarse sobre los mismos objetos. Nada se mueve hasta que llamas a mixer.update(delta) con los segundos transcurridos desde el último fotograma, normalmente desde el bucle de animación y un Timer.
El mismo script avanza el mezclador a mano y lee el ángulo del brazo:
t = 0.000 s: arm at 0°t = 0.125 s: arm at 12.5°t = 0.250 s: arm at 25°t = 0.500 s: arm at 0°t = 0.750 s: arm at -25°t = 1.250 s: arm at 25°timeScale 2, then 0.125 s more: t = 0.500 s in the clip, arm at 0°meshopt weight position: Int16Array normalized true scale [ 0.09, 0.09, 0.09 ]- Entre dos claves, el ángulo se interpola: 12.5° a 0.125 s, a medio camino entre 0° y 25°. Las pistas de cuaterniones se interpolan con una interpolación esférica, que da el mismo ángulo intermedio para una rotación alrededor de un único eje.
- El clip se repite en bucle por defecto (
LoopRepeat): a 1.25 s, el brazo está donde estaba a 0.25 s.mixer.timesigue contando, y cada acción reinicia su propio tiempo. - Con
timeScale = 2, 0.125 s de tiempo real hacen avanzar la acción 0.25 s en el clip, de 0.25 a 0.5.
Geometría comprimida: Draco y meshopt
Sección titulada «Geometría comprimida: Draco y meshopt»El archivo sin comprimir ocupa 58 540 bytes, sobre todo datos de vértices en coma flotante. Dos extensiones de glTF lo comprimen, y ambas necesitan un decodificador en el navegador:
- Draco (
KHR_draco_mesh_compression), de Google, codifica la conectividad de una malla y sus atributos cuantizados en un flujo de bits compacto. Da los archivos más pequeños, y necesita un decodificador WebAssembly de unos cientos de kilobytes, que se ejecuta en Web Workers. - meshopt (
EXT_meshopt_compression), de meshoptimizer, reordena y codifica los búferes para que gzip o Brotli los compriman bien, y busca una decodificación rápida. Aquí viene conKHR_mesh_quantization, que guarda las posiciones y las normales como enteros pequeños en lugar de números en coma flotante. Su decodificador es un módulo JavaScript con WebAssembly incrustado.
| Archivo | Bytes | Con gzip |
|---|---|---|
metronome.glb |
58 540 | 24 239 |
metronome-draco.glb |
10 424 | 7 438 |
metronome-meshopt.glb |
24 616 | 7 526 |
En disco, Draco es 2.4 veces más pequeño que meshopt; sobre una conexión HTTP que comprime, están a menos de un 1 % de diferencia. Ese es el diseño de meshopt: deja el último paso al servidor web. La última línea de la salida del script muestra la cuantización: las posiciones de la pesa volvieron como un Int16Array normalizado, con una escala de 0.09 en el nodo para restaurar el radio de la esfera.
La página carga los tres archivos con un solo cargador (main.ts, líneas 25-44):
// Un solo cargador para los tres archivos: elige el decodificador que pide el extensionsRequired de cada archivoconst loader = new GLTFLoader().setDRACOLoader(new DRACOLoader()).setMeshoptDecoder(MeshoptDecoder);
const files = ['metronome.glb', 'metronome-draco.glb', 'metronome-meshopt.glb'];const mixers: THREE.AnimationMixer[] = [];const loaded: Record<string, unknown> = {};const models = await Promise.all(files.map((file) => loader.loadAsync(`generated/${file}`)));
models.forEach((gltf, i) => { gltf.scene.position.x = (i - 1) * 1.6; scene.add(gltf.scene); const mixer = new THREE.AnimationMixer(gltf.scene); mixer.clipAction(gltf.animations[0]).play(); mixers.push(mixer); let vertices = 0; gltf.scene.traverse((object) => { if ((object as THREE.Mesh).isMesh) vertices += (object as THREE.Mesh).geometry.getAttribute('position').count; }); loaded[files[i]] = { extensionsUsed: gltf.parser.json.extensionsUsed ?? [], vertices, clip: gltf.animations[0].name };});Sin el decodificador correspondiente, un archivo que declara la extensión en extensionsRequired no se carga, con THREE.GLTFLoader: setMeshoptDecoder must be called before loading compressed files (GLTFLoader.js, línea 1622) o THREE.GLTFLoader: No DRACOLoader instance provided. para Draco (línea 1951).

Página en vivo: ejecutarla aquí
Abrir a pantalla completa. Pruébalo: ábrela a pantalla completa con la pestaña Red de las herramientas de desarrollo: los tres archivos .glb y el decodificador Draco llegan con su tamaño, los números que el ejercicio 3 pide medir.
$ node scripts/probe.mjs 04-gltf{ "backend": "WebGPU", "gpu": "nvidia, blackwell", "frame": 4, "render": { "calls": 23, "drawCalls": 10, "triangles": 6709 }, ... "loaded": { "metronome.glb": { "extensionsUsed": [], "vertices": 1297, "clip": "swing" }, "metronome-draco.glb": { "extensionsUsed": [ "KHR_draco_mesh_compression" ], "vertices": 1287, "clip": "swing" }, "metronome-meshopt.glb": { "extensionsUsed": [ "EXT_meshopt_compression", "KHR_mesh_quantization" ], "vertices": 1295, "clip": "swing" } }, "dracoArmAngle": 12.5}- Los tres metrónomos se ven iguales, y el brazo de la copia Draco está a 12.5° después de tres fotogramas de 0.125 s: 0.375 s dentro del clip, de vuelta de 25° hacia 0°.
- 10 llamadas de dibujo: 3 mallas por modelo y la pasada de salida. Cada modelo tiene 16 + 12 + 2 208 = 2 236 triángulos, y 3 × 2 236 + 1 = 6 709. Las 23 llamadas son las de la lección 3: 17 para el PMREM del entorno, y 2 por fotograma.
- El número de vértices difiere, y el ejercicio 1 averigua adónde fueron los vértices.
Lo que pesan los decodificadores
Sección titulada «Lo que pesan los decodificadores»La compilación de producción muestra la otra cara del compromiso:
$ npx vite build...dist/assets/draco_wasm_wrapper-fZCQGLGb.js 58.45 kBdist/assets/draco_wasm_wrapper-DxJM36Ib.js 58.76 kBdist/assets/draco_decoder-Z1_iN-Ht.wasm 192.42 kB │ gzip: 64.11 kBdist/assets/draco_decoder-C32yEggz.wasm 285.74 kB │ gzip: 89.66 kBdist/assets/draco_decoder-fzg4nYZr.js 719.41 kB...dist/assets/04-gltf-CCDSPzCq.js 80.55 kB │ gzip: 24.11 kB...Desde r185, DRACOLoader no necesita setDecoderPath: localiza los decodificadores incluidos en el paquete three con new URL(…, import.meta.url) (DRACOLoader.js, líneas 16-24); el DRACOLoader de r184 no tiene esa URL, y el de r180 empieza con un decoderPath vacío. Vite ve las cinco URL en el módulo y copia los cinco archivos en la compilación, 1.31 MB, aunque un navegador con WebAssembly solo carga dos de ellos: el envoltorio y el decodificador de 286 kB (líneas 381-402). El decodificador más pequeño, solo para glTF, de 192 kB, se usa cuando pasas la constante exportada DRACO_GLTF_CONFIG a setDecoderPath, y el draco_decoder.js de 719 kB es una alternativa para navegadores sin WebAssembly. El decodificador de meshopt no tiene archivo propio: está dentro del fragmento de 80.55 kB de la página, con GLTFLoader y DRACOLoader.
Para este modelo, Draco ahorra 88 bytes respecto a meshopt en la transferencia, y cuesta unos 90 kB de WebAssembly comprimido con gzip en la primera carga. Draco compensa en mallas grandes, como objetos escaneados o terrenos; para la mayoría de las escenas, meshopt con gzip es la mejor opción por defecto, y además comprime animaciones y datos de instancias, algo que Draco no hace.
Texturas: KTX2
Sección titulada «Texturas: KTX2»La geometría suele ser la parte más pequeña de un modelo: una textura de 2048 por 2048 ocupa unos 16 MB de memoria de GPU una vez decodificada, más con mipmaps, sea cual sea el tamaño de su PNG. Los archivos KTX2 con compresión Basis Universal (KHR_texture_basisu) permanecen comprimidos en la GPU: KTX2Loader los transcodifica a un formato que la GPU admita, como BC7 en las GPU de escritorio o ASTC en los teléfonos. El cargador necesita sus archivos de transcodificación y que el renderizador detecte los formatos: new KTX2Loader().setTranscoderPath(…).detectSupport(renderer), y luego gltfLoader.setKTX2Loader(ktx2Loader). El metrónomo del curso no tiene texturas, así que esta parte no se mide aquí (por verificar).
En GuitarAlchemist/ga
Sección titulada «En GuitarAlchemist/ga»GA carga modelos glTF en varios componentes, y cada uno ilustra un punto de esta lección en el commit 05c8eda:
- Una promesa que el cargador no cumple.
Guitar3D.tsxanuncia “GLTF/GLB model loading with KTX2 texture compression support” y un “Meshopt decoder for optimized geometry” en su cabecera (líneas 1-12), pero crea unnew GLTFLoader()sin más (línea 189), sinsetKTX2LoadernisetMeshoptDecoder. Una guitarra exportada con meshopt o KTX2 fallaría con los errores citados arriba. - Decodificadores desde una CDN.
Ocean.tsxcarga los decodificadores de Draco desde los servidores de Google,setDecoderPath('https://www.gstatic.com/draco/v1/decoders/')(línea 339). Con r180, hace falta una ruta. Eso hace que la página dependa de un tercero en tiempo de ejecución, y de una versión del decodificador que three.js no eligió. Tras actualizar a r185 o posterior, quitar la línea permite que Vite empaquete el decodificador que corresponde al cargador. - Errores silenciados.
DemerzelFaceOverlay.tsxreemplazaconsole.errormientras se carga su modelo de rostro, para ocultar los errores de KTX2 (líneas 173-180), y lo restaura después. Las texturas KTX2 del modelo se omiten, algo que el comentario acepta porque el componente reemplaza los materiales; cualquier otro componente que registre un error que mencione KTX2 durante ese tiempo queda silenciado también.setKTX2Loadereliminaría los errores en lugar de ocultarlos. - Un mezclador sin nada que reproducir.
HandAnimationTest.tsxcrea unAnimationMixerpara el modelo de la mano (línea 231) y lo actualiza en cada fotograma (línea 157), pero nunca llama aclipAction: un mezclador sin acciones no mueve nada. La página mueve ella misma los huesos de la mano, asignando sus rotaciones (línea 389).
Experimento GA
Sección titulada «Experimento GA»El experimento 6 de la lección 14 comprime una textura de madera de diapasón a KTX2: Basis LZ la entrega en 145 kB en lugar de 1.2 MB, y una madera procedural en TSL no descarga nada. Las texturas de madera generadas con ComfyUI llegarán pronto; mientras tanto, el experimento usa una textura escrita por un script.
Puntos clave
Sección titulada «Puntos clave»- glTF 2.0 es el formato de entrega en torno al cual está construido three.js: un árbol de nodos, materiales metálico-rugosidad con factores de color lineales, y animaciones como pistas de fotogramas clave.
GLTFLoaderdevuelvegltf.scene, unGroup, ygltf.animations; los nombres pueden volverse únicos, y el código que busca objetos por nombre depende de ellos.AnimationMixer.update(delta)hace avanzar el reloj;clipActiondevuelve la acción que guarda el tiempo, la velocidad, el modo de bucle y el peso.- Draco da los archivos más pequeños y un decodificador grande; meshopt da archivos que se comprimen bien con gzip y un decodificador pequeño. Mide los dos, en la transferencia.
- Desde r185,
DRACOLoaderencuentra sus decodificadores por sí solo, y un empaquetador los copia todos en la compilación.
Ejercicios
Sección titulada «Ejercicios»- El modelo sin comprimir tiene 1 297 vértices, la copia Draco 1 287 y la copia meshopt 1 295, y sin embargo los tres metrónomos se ven iguales y tienen el mismo número de triángulos. ¿Adónde fueron los vértices? Compruébalo con
scripts/l04-exercises.ts, que decodifica ambas copias en Node.js con glTF Transform. - ¿Dónde está el brazo a 0.875 s? Luego reproduce el clip una sola vez, con
LoopOnceyclampWhenFinished = true, durante 1.5 s: ¿dónde está el brazo, cuánto valeaction.time, y sigue en marcha la acción? - Una página muestra un único modelo escaneado de 3 MB, y otra muestra 40 modelos pequeños de 50 kB cada uno. ¿Qué compresión elegirías para cada una, y qué medirías antes de decidir?
Solución 1
$ node scripts/l04-exercises.tsmetronome.glb body 48 vertices, 16 triangles | rod 24 vertices, 12 triangles | weight 1225 vertices, 2208 trianglesmetronome-draco.glb body 40 vertices, 16 triangles | rod 24 vertices, 12 triangles | weight 1223 vertices, 2208 trianglesmetronome-meshopt.glb body 48 vertices, 16 triangles | rod 24 vertices, 12 triangles | weight 1223 vertices, 2208 triangles...Dos causas. La pesa, una SphereGeometry de 48 segmentos, tiene 49 vértices en sus filas superior e inferior, pero los triángulos de cada polo solo usan 48 de ellos: los vértices 48 y 1176 no pertenecen a ningún triángulo. Los dos codificadores descartan los vértices que ningún triángulo usa, ya que reconstruyen los búferes a partir del índice. El cuerpo pierde 8 más solo en la copia Draco: el draco() de glTF Transform primero suelda los vértices que tienen los mismos atributos (draco.ts, línea 71), y el cuerpo se había hecho no indexado, con una copia de cada esquina por triángulo. meshopt() reordena los vértices pero no los suelda.
Solución 2
t = 0.875 s: arm at -12.5°LoopOnce, clampWhenFinished, after 1.5 s: arm at 0°, action.time 1, running false, finished events 1A 0.875 s, el brazo está a medio camino entre −25° a 0.75 s y la vuelta a 0° a 1 s. Reproducida una sola vez, la acción se detiene al final del clip: su tiempo se queda en 1, isRunning() es false, y el mezclador emite un evento finished, que es donde una página empezaría la siguiente animación. clampWhenFinished conserva la última pose; sin él, la documentación dice que la acción se desactiva al terminar y deja de afectar al objeto. Este clip termina en 0°, donde empezó, así que la diferencia no se nota aquí: pruébalo con un clip que termine en otra posición.
Solución 3
Para el modelo escaneado de 3 MB, Draco: la geometría densa e irregular es donde su codificación gana más, y descargar una vez un decodificador de unos 90 kB con gzip es poco al lado de los megabytes ahorrados. Para los 40 modelos pequeños, meshopt: los archivos se comprimen con gzip casi tan bien como los de Draco, el decodificador ya está en el paquete JavaScript, y busca una decodificación rápida, lo que importa cuando llegan 40 archivos a la vez. Mide el tamaño de transferencia con la compresión real del servidor, el tiempo desde la petición hasta el primer fotograma, y el tiempo de decodificación en un teléfono lento, no el tamaño en disco. Esta respuesta está razonada a partir de las mediciones de esta lección sobre un modelo pequeño, no medida en esas dos páginas (por verificar).
Fuentes
Sección titulada «Fuentes»- Khronos: especificación de glTF 2.0,
KHR_draco_mesh_compression,EXT_meshopt_compression,KHR_mesh_quantization,KHR_texture_basisu - Manual de three.js: Loading 3D models; documentación:
GLTFLoader,DRACOLoader,KTX2Loader,AnimationMixer,AnimationAction - Código fuente de three.js en r186:
GLTFLoader.js,DRACOLoader.js - glTF Transform 4.5.0:
dracoymeshopt - Draco, Google; meshoptimizer