wandres.dev
CARGAR Y OPTIMIZAR ASSETS · Draco, Meshopt y gltf-transform

Meshopt frente a Draco: dos filosofías de compresión

Cómo funciona EXT_meshopt_compression con cuantización más filtros de bytes, por qué su decodificador cabe en 15 KB, y el criterio para elegir entre los dos compresores.

⏱ 17 min

Draco optimiza el tamaño; meshopt optimiza el tiempo total. Esa es la diferencia de fondo entre los dos y explica todo lo demás: por qué meshopt comprime menos, por qué descomprime entre diez y veinte veces más rápido, por qué su decodificador ocupa quince kilobytes en vez de trescientos, y por qué en la mayoría de las escenas reales el fichero que llega antes a la pantalla es el de meshopt aunque sea más grande.

🎯 Al terminar esta lección sabrás
  • Describir las dos etapas de meshopt y en qué se diferencian del enfoque de Draco.
  • Explicar por qué la descompresión de meshopt es paralelizable y la de Draco no.
  • Comparar los dos con números de tamaño y de tiempo sobre un caso concreto.
  • Aplicar el criterio de elección y saber cuándo no comprimir en absoluto.

Cómo funciona meshopt

EXT_meshopt_compression viene de la biblioteca meshoptimizer, de Arseny Kapoulkine, y su diseño parte de una premisa distinta: la geometría, después de cuantizar, es un flujo de bytes con mucha estructura, y esa estructura se puede explotar con transformaciones baratísimas.

Etapa uno: cuantización. Igual que Draco. Posiciones a enteros de 14 o 16 bits, normales a 8 o 10, UVs a 12. Se apoya en KHR_mesh_quantization, y la conversión de vuelta a float la hace el hardware al leer el atributo, sin coste alguno. Esta etapa por sí sola reduce entre 2 y 3 veces.

Etapa dos: codificación de bytes. Los datos cuantizados pasan por dos codificadores muy simples. El vertex codec explota que los vértices vecinos tienen valores parecidos: guarda diferencias y agrupa los bytes por posición dentro del valor, de forma que los bytes altos, que casi siempre son iguales, se comprimen a casi nada. El index codec explota que los triángulos consecutivos comparten vértices y codifica la conectividad con un esquema de bytes.

Ninguna de las dos etapas hace codificación de entropía real ni recorridos de grafo. Ese es el motivo de todo lo demás.

Las consecuencias

El decodificador es diminuto. meshopt_decoder.module.js con su WASM ronda los 15 KB. El de Draco son unos 300 KB entre el wrapper y el .wasm. Sobre un modelo de 500 KB, esa diferencia de 285 KB se come buena parte de la ventaja de compresión de Draco.

La descompresión es rapidísima. El decodificador de meshopt procesa del orden de mil millones de bytes por segundo. En cifras comparables a las de la lección anterior:

Vértices Draco Meshopt
10 000 ~8 ms ~0.5 ms
100 000 ~60 ms ~4 ms
1 000 000 ~600 ms ~35 ms

No reordena los vértices. Meshopt cuantiza y codifica en el orden que le llegue, así que preserva los índices. Y como gltfpack reordena primero para la caché post-transformada de la GPU, el modelo comprimido llega ya optimizado para dibujarse rápido, no solo para descargarse pequeño.

Se puede combinar con compresión de transporte. El flujo de bytes que produce meshopt tiene todavía redundancia que gzip o Brotli aprovechan. Un .glb con meshopt servido con Brotli se acerca bastante al tamaño de Draco, y esa compresión la hace el servidor y la deshace el navegador en código nativo, gratis. Draco, en cambio, ya ha exprimido la entropía y apenas se comprime más.

La comparación completa

Sobre un modelo de arquitectura de 180 000 triángulos con geometría desnuda de 12 MB:

Sin comprimir Draco Meshopt Meshopt + Brotli
Geometría en disco 12.0 MB 1.1 MB 2.4 MB 1.5 MB
Decodificador a descargar 0 300 KB 15 KB 15 KB
Total primera visita 12.0 MB 1.4 MB 2.4 MB 1.5 MB
Tiempo de descompresión 0 ~110 ms ~7 ms ~7 ms
Preserva orden de vértices no

Con Brotli activado en el servidor —que deberías tener de todas formas— los tamaños quedan prácticamente empatados y meshopt gana en tiempo por quince a uno. Sin Brotli, Draco gana en tamaño por casi el doble.

El criterio de elección

Usa meshopt cuando tienes Brotli o gzip en el servidor, que es el caso normal; cuando la escena tiene muchas mallas y quieres que aparezcan rápido; cuando vas a manipular la geometría por código y necesitas índices estables; o cuando el objetivo es móvil, donde el tiempo de CPU es el recurso escaso.

Usa Draco cuando el modelo es muy grande —por encima de veinte megabytes de geometría—, cuando no controlas la compresión del servidor, o cuando el ancho de banda del usuario es el cuello de botella dominante y el tiempo de CPU no importa. El caso canónico es un escaneado 3D de alta densidad que se carga una vez.

No comprimas cuando la geometría total baja de unos 300 KB. El decodificador y la latencia extra no se amortizan.

Y hay una combinación que no existe: no se pueden usar los dos a la vez sobre la misma malla. Son extensiones mutuamente excluyentes en la codificación de los bufferViews. Sí puedes, en cambio, combinar cualquiera de las dos con KTX2 para las texturas, y de hecho es lo normal.

Los comandos

Con gltf-transform:

# Meshopt, con cuantizacion por defecto
gltf-transform meshopt modelo.glb salida.glb --level high

# Draco
gltf-transform draco modelo.glb salida.glb \
  --method edgebreaker \
  --quantize-position 14 --quantize-normal 10 --quantize-texcoord 12

Con gltfpack, que es la herramienta nativa de meshoptimizer y se instala con npm install --global gltfpack:

# -c aplica meshopt; -cc usa el modo de mayor compresion
gltfpack -i entrada.gltf -o salida.glb -cc

# Anadiendo compresion de texturas a KTX2 en la misma pasada
gltfpack -i entrada.gltf -o salida.glb -cc -tc

Esos son exactamente los flags que el ejemplo oficial webgl_loader_gltf_compressed documenta en su código para generar coffeemat.glb: “gltfpack -i coffeemat/scene.gltf -o coffeemat.glb -cc -tc”, con el resultado usando EXT_meshopt_compression para geometría y KHR_texture_basisu para texturas.

💡
gltfpack hace más que comprimir

Por defecto también fusiona mallas que comparten material, reordena índices para la caché de vértices, elimina nodos vacíos y simplifica jerarquías. Con -si 0.5 además decima la malla al 50 % de triángulos usando el simplificador de meshoptimizer, que preserva las UVs y los bordes. Es un pipeline entero en un comando.

La compresión de geometría casi nunca es tu problema, y medirlo antes ahorra el 90 % del trabajo

Hay un sesgo muy fuerte en la comunidad de Three.js hacia optimizar la geometría, probablemente porque es la parte que se siente «tuya» y porque Draco tiene números espectaculares que dan gusto enseñar. Los datos de proyectos reales apuntan en otra dirección. En un modelo de producto o de arquitectura descargado de cualquier biblioteca, el reparto típico del peso es 80 a 90 % texturas, 10 a 20 % geometría. Comprimir la geometría un 90 % ahorra entre un 9 y un 18 % del total; redimensionar las texturas de 4K a 1K y pasarlas a KTX2 ahorra más del 80 %. Y en VRAM el desequilibrio es todavía más extremo, porque la geometría comprimida se descomprime a su tamaño original en memoria mientras que las texturas KTX2 siguen comprimidas. El orden correcto de trabajo, entonces, empieza siempre por el mismo comando: gltf-transform inspect modelo.glb, que imprime una tabla con el desglose de bytes por mallas y por texturas y te dice en treinta segundos dónde está tu problema. Después, en este orden: eliminar lo que no se usa con prune y dedup, que en modelos de biblioteca suele quitar un tercio del peso sin tocar nada visible; redimensionar texturas según la distancia real de visionado; comprimir texturas a KTX2; reducir primitivas con join y palette; y solo entonces comprimir la geometría. Cada paso de esa lista tiene mejor relación entre esfuerzo y ahorro que el siguiente, y la gente empieza casi siempre por el último.