Saltar al contenido

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

Shell
npm install --global @gltf-transform/cligltf-transform inspect salon.glb

La salida, resumida:

Texto
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 MB

El 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

Shell
gltf-transform prune salon.glb p1.glb --keep-attributes falsegltf-transform dedup p1.glb p2.glb

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

Shell
gltf-transform resize p2.glb p3.glb --width 1024 --height 1024

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

Shell
gltf-transform palette p3.glb p4.glb --min 5gltf-transform join p4.glb p5.glb

palette 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

Shell
# 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 18

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

Shell
gltf-transform weld p7.glb p8.glbgltf-transform reorder p8.glb p9.glbgltf-transform meshopt p9.glb p10.glb --level high

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

Shell
#!/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.