Un pipeline real de optimización, con los números
Un modelo de 68 MB llevado a 3.4 MB paso a paso, con el comando exacto de cada etapa, lo que ahorra cada uno y cómo verificar que no has roto nada.
El punto de partida
npm install --global @gltf-transform/cligltf-transform inspect salon.glbLa salida, resumida:
scenes 1meshes 214 primitives, 486k vertices, 892k trianglesmaterials 47textures 31 (18x 4096², 9x 2048², 4x 1024²) -- 61.2 MBanimations 0extensions KHR_materials_specular, KHR_texture_transformtotal 68.4 MBEl diagnóstico está en la tercera línea: las texturas son el 89 % del peso. Dieciocho texturas a 4K en un interior que se ve desde tres metros. Y 214 primitivas, es decir 214 draw calls como suelo. La geometría, 7.2 MB, es el problema menor.
El pipeline, etapa por etapa
Etapa 1: quitar lo que no se usa
gltf-transform prune salon.glb p1.glb --keep-attributes falsegltf-transform dedup p1.glb p2.glbprune elimina nodos vacíos, materiales huérfanos, texturas no referenciadas y atributos de vértice que ningún material lee —el --keep-attributes false es lo que activa esto último y suele quitar segundos juegos de UVs y colores de vértice que nadie usa—. dedup fusiona recursos idénticos: dos texturas byte a byte iguales pasan a ser una.
68.4 MB → 51.1 MB. Un 25 % sin tocar nada visible. En modelos de biblioteca este primer paso casi siempre da un ahorro grande, porque acumulan restos de la exportación.
Etapa 2: redimensionar texturas
La pregunta previa: ¿cuántos píxeles de pantalla ocupa la textura más grande en la vista más cercana? En este interior, ninguna superficie ocupa más de 900 píxeles de alto. Con 1024 sobra.
gltf-transform resize p2.glb p3.glb --width 1024 --height 102451.1 MB → 12.8 MB. El ahorro más grande de todo el pipeline, y viene de un solo comando. Cada reducción a la mitad divide por cuatro.
Etapa 3: reducir primitivas
gltf-transform palette p3.glb p4.glb --min 5gltf-transform join p4.glb p5.glbpalette detecta materiales que solo se diferencian en el color base, los sustituye por uno solo con una textura de paleta diminuta y remapea las UVs. join fusiona primitivas que comparten material y no tienen transformación propia.
214 primitivas → 23 primitivas. 47 materiales → 9 materiales. El peso apenas cambia —12.8 MB → 12.6 MB—, pero los draw calls bajan un 89 %, que en móvil es lo que decide si la escena va a treinta o a sesenta.
Esta etapa va antes de comprimir por una razón concreta: fusionar mallas ya comprimidas obligaría a descomprimir y recomprimir, y join no lo hace.
Etapa 4: comprimir texturas
# Color y ORM en ETC1Sgltf-transform etc1s p5.glb p6.glb --slots "!normalTexture" --quality 200# Normales en UASTC, que tolera mal ETC1Sgltf-transform uastc p6.glb p7.glb --slots "normalTexture" --level 4 --zstd 1812.6 MB → 4.9 MB. Y lo que no se ve en el peso: la VRAM de texturas pasa de 112 MB a 28 MB.
Etapa 5: optimizar y comprimir geometría
gltf-transform weld p7.glb p8.glbgltf-transform reorder p8.glb p9.glbgltf-transform meshopt p9.glb p10.glb --level highweld fusiona vértices duplicados dentro de una tolerancia, lo que restaura el indexado que muchos exportadores destruyen. reorder reordena vértices e índices para la caché post-transformada de la GPU. meshopt cuantiza y codifica.
El orden de estos tres no es negociable: weld tiene que ir antes de reorder porque reordenar una malla sin indexar no sirve de nada, y los dos antes de meshopt porque la compresión congela el orden.
4.9 MB → 3.4 MB. Con Brotli en el servidor, el fichero que viaja son unos 2.9 MB.
El resultado
| Etapa | Comando | Tamaño | Δ |
|---|---|---|---|
| Original | — | 68.4 MB | |
| Limpieza | prune + dedup |
51.1 MB | -25 % |
| Redimensionado | resize 1024 |
12.8 MB | -75 % |
| Draw calls | palette + join |
12.6 MB | -2 % |
| Texturas | etc1s + uastc |
4.9 MB | -61 % |
| Geometría | weld+reorder+meshopt |
3.4 MB | -31 % |
De 68.4 a 3.4 MB: un factor de 20. Y las métricas que no están en la tabla:
| Antes | Después | |
|---|---|---|
| Draw calls | 214 | 23 |
| VRAM de texturas | 448 MB | 28 MB |
| Tiempo hasta el primer frame, portátil | 9.4 s | 1.1 s |
| Tiempo hasta el primer frame, móvil 4G | 71 s | 5.8 s |
La última fila es el argumento entero. Un modelo de 68 MB en una conexión móvil no es un modelo lento: es un modelo que nadie va a ver, porque el usuario se ha ido.
El script completo y la verificación
Encadenarlo todo
Encadenado, para meterlo en el package.json:
#!/usr/bin/env bashset -eIN="$1"; OUT="$2"; TMP=$(mktemp -d)gltf-transform prune "$IN" "$TMP/1.glb" --keep-attributes falsegltf-transform dedup "$TMP/1.glb" "$TMP/2.glb"gltf-transform resize "$TMP/2.glb" "$TMP/3.glb" --width 1024 --height 1024gltf-transform palette "$TMP/3.glb" "$TMP/4.glb" --min 5gltf-transform join "$TMP/4.glb" "$TMP/5.glb"gltf-transform etc1s "$TMP/5.glb" "$TMP/6.glb" --slots '!normalTexture' --quality 200gltf-transform uastc "$TMP/6.glb" "$TMP/7.glb" --slots 'normalTexture' --level 4 --zstd 18gltf-transform weld "$TMP/7.glb" "$TMP/8.glb"gltf-transform reorder "$TMP/8.glb" "$TMP/9.glb"gltf-transform meshopt "$TMP/9.glb" "$OUT" --level highgltf-transform inspect "$OUT"rm -rf "$TMP"Existe también gltf-transform optimize, que ejecuta una secuencia parecida en un solo comando. Es un buen punto de partida, pero no acierta con la resolución de las texturas —no puede saber a qué distancia se ve tu modelo— ni separa ETC1S de UASTC por slot. Para producción, el script explícito da mejor resultado.
Verificar que no has roto nada
Tres comprobaciones, en este orden:
Cuenta de triángulos idéntica. weld puede reducir vértices pero nunca triángulos. Si bajan, algo ha eliminado geometría y hay que revisar prune.
Comparación visual de capturas. Renderiza el antes y el después desde la misma cámara con la misma iluminación y compara las dos imágenes. Los tres fallos que aparecen aquí son facetado por cuantización agresiva, bandas en el sombreado por normales en ETC1S, y texturas borrosas por redimensionado excesivo.
Extensiones requeridas. gltf-transform inspect sobre el resultado tiene que listar EXT_meshopt_compression y KHR_texture_basisu en las extensiones, y tu cargador tiene que tener conectados los decodificadores correspondientes. Si te falta uno, el modelo no carga y el error es explícito.