Poner a dieta un modelo, lección 1: las tres armas de la compresión de vértices
En el artículo anterior abrimos un archivo GLB y vimos que las texturas devoran el 80 % del tamaño, mientras que los vértices solo representan el 10-20 %. ¿Es entonces irrelevante la compresión de vértices?
Todo lo contrario. Cuando las texturas de un modelo ya están comprimidas a KTX2 y los vértices son densos, ese 20 % restante son los vértices, y ese 20 % se puede recortar a la mitad, o incluso un 90 %. Más aún, la compresión de vértices es una de las pocas optimizaciones casi gratuitas y de efecto inmediato: unos cuantos comandos, cambiar un decoder, y el archivo adelgaza.
Este artículo aclara tres cosas: cómo es realmente la información de vértices; el temperamento de cada uno de los tres enfoques (cuantización, MeshOpt, Draco); y una conclusión que te ahorrará dolores — no existe la solución «la mejor», solo la «más adecuada».
Qué tamaño tiene un vértice
Primero, qué hay dentro de un vértice. En glTF, cada vértice se compone de varios atributos:
| Atributo | Propósito | Precisión por defecto | Bytes por vértice |
|---|---|---|---|
| position (posición) | Coordenadas del vértice en el espacio | 3 × float32 | 12 |
| normal (normal) | Determina la dirección de iluminación | 3 × float32 | 12 |
| tangent (tangente) | Cálculo de normal maps | 4 × float32 | 16 |
| texcoord_0 (UV) | Coordenadas de muestreo de textura | 2 × float32 | 8 |
| color (color de vértice) | Sombreado por vértice | 4 × float32 | 16 |
Un vértice con el conjunto completo de atributos PBR ocupa 48-64 bytes solo en geometría. Un modelo de 100.000 vértices son 5-6 MB solo en vértices.
Casi todo aquí usa float32 (coma flotante de 32 bits). Es la configuración por defecto y también la superficie de ataque de la compresión de vértices — porque la gran mayoría de atributos no necesitan en absoluto precisión de 32 bits.
Arma uno: Cuantización
La cuantización es el principio subyacente de toda la compresión de vértices; Draco y MeshOpt la usan internamente.
Cuantización (mapear floats de alta precisión a enteros de baja precisión) se resume en: para el float 3.14159265, recordar 3.14 basta. Para un conjunto de coordenadas en un espacio, en lugar de registrar con precisión cada decimal en 32 bits, se usa un entero de rango menor para representarlo.
Original: position.x = 1.234567 (float32, 4 bytes)
Cuantizado: position.x = 1234 (int16, 2 bytes) + un scale/offset para restaurar
Antes vs. después:
| Atributo | bytes float32 | Cuantizado (16 bits) | Ahorro |
|---|---|---|---|
| position | 12 | 6 | 50 % |
| normal | 12 | 6 (o 4, con int8 + octahedral) | 50-67 % |
| tangent | 16 | 4-8 | 50-75 % |
| texcoord | 8 | 4 | 50 % |
Para ese vértice de 48-64 bytes, la cuantización lo comprime básicamente a 16-24 bytes, más que reduciéndolo a la mitad.
Cuándo usar cuantización
- Solo quieres reducir tamaño y no necesitas la máxima razón de compresión
- Quieres cero dependencias de decoder — un glTF cuantizado usa la extensión estándar
KHR_mesh_quantization, soportada nativamente por los motores principales, sin necesitar una librería de decoder extra - Tu plataforma objetivo es sensible al tamaño del paquete (p. ej. WeChat Mini Programs, donde incluir un decoder de Draco cuesta decenas de KB)
Cuándo no usarla
- El modelo es diminuto y el detalle es el argumento de venta (p. ej. piezas industriales a nivel milimétrico). La cuantización se delata sobre todo en modelos pequeños — las texturas pueden aguantar, pero un desplazamiento de vértice de 0,1 mm es visible en un close-up.
El coste real de la pérdida de precisión: en una escena de joyería, un modelo de anillo se cuantizó a 16 bits y el borde metálico mostró aliasing en close-ups. La causa no eran pocos vértices; el espacio mundo era demasiado pequeño para que los enteros de 16 bits lo expresaran con suficiente finura. La solución es reducir el rango de cuantización (achicar el bounding box de
position) o subir la profundidad de bits en modelos pequeños.
Arma dos: MeshOpt
MeshOpt es la extensión oficial de glTF EXT_meshopt_compression, posicionada como «razón de compresión decente, decodificación ultrarrápida».
Lo que hace: primero cuantiza los atributos (igual que arriba), luego aplica una técnica llamada codificación de entropía (lossless, sin pérdida) para recomprimir sin pérdida los enteros cuantizados. Es decir: cuantización con pérdida + codificación de entropía sin pérdida = menor tamaño, misma calidad que la cuantización sola.
- Razón de compresión: otro 30-50 % más pequeño que la cuantización sola
- Velocidad de decodificación: extremadamente rápida, pura C/JS, decenas de millones de vértices por segundo en un hilo
- Tamaño del decoder: minúsculo (~20-30 KB tras gzip)
- Compatibilidad: soporte nativo de Three.js y Babylon.js, un estándar de facto en la web
Cuándo usar MeshOpt
- Necesitas mayor razón de compresión pero no puedes aceptar la decodificación más lenta de Draco
- Web/móvil/WebXR en el centro — la velocidad de decodificación afecta directamente al first paint
- El modelo se decodifica con frecuencia (p. ej. niveles cargados dinámicamente)
Cuándo no usarlo
- Tu plataforma ni siquiera reconoce
EXT_meshopt_compression(raro, motores viejos) - Solo necesitas «que funcione» y no te importa un 30 % de diferencia — entonces la cuantización pura es más simple y tiene una dependencia menos
Arma tres: Draco
Draco es la solución de compresión de Google, posicionada para «máxima razón de compresión».
La diferencia fundamental con los otros dos: Draco cambia la conectividad (topología) de los vértices. La cuantización solo cambia la representación numérica de cada vértice; MeshOpt añade codificación sin pérdida; Draco reorganiza la malla de triángulos y expresa «qué vértices forman triángulos» de forma más compacta.
- Razón de compresión: la más alta de las tres, a menudo 90 %+ de reducción en modelos densos en vértices
- Velocidad de decodificación: la más lenta de las tres, pero en términos absolutos sigue siendo rápida
- Tamaño del decoder: mayor (~100-200 KB, normalmente cargado como wasm aparte)
- Calidad: ajustable, pero en razones extremas se ve deformación visible
Cuándo usar Draco
- Modelos extremadamente grandes y superdensos en vértices (scans de millones de vértices, terreno)
- Carga única, reutilizado mucho tiempo tras la decodificación (decodificación más lenta aceptable)
- El tamaño del paquete no es el cuello de botella, la velocidad de descarga sí
Cuándo no usarlo
- Móvil + necesitas un first paint rápido — hay que descargar tanto el decoder como el modelo, lo que frena
- Entornos estrictos de tamaño como Mini Programs
- Modelos que necesitan animación esquelética, morph targets — el soporte de Draco para esto es débil y la mala configuración causa problemas
Los tres juntos: una tabla de selección
Las razones de abajo referencian benchmarks de la comunidad (tests de DeepKolos + discusiones en Reddit r/threejs). Los modelos varían, pero las relaciones relativas son estables:
| Opción | Razón de compresión (vs float32) | Velocidad decode | Tamaño decoder | ¿Con pérdida? | Extensión glTF |
|---|---|---|---|---|---|
| Cuantización pura | ~50 % | Nativa, sin decode | 0 | Sí (precisión) | KHR_mesh_quantization |
| MeshOpt | ~25-35 % | Extremadamente rápida | ~25 KB | Sí (precisión) | EXT_meshopt_compression |
| Draco | ~10-20 % | Rápida (la más lenta de las tres) | ~100-200 KB | Sí (precisión + topología) | KHR_draco_mesh_compression |
Decoders y compatibilidad de plataforma:
| Plataforma | Cuantización pura | MeshOpt | Draco |
|---|---|---|---|
| Web escritorio | ✅ Nativa | ✅ Nativa | ✅ Necesita config decoder |
| Web móvil | ✅ Nativa | ✅ Nativa | ⚠️ Decoder pesado |
| WebXR/VR | ✅ Nativa | ✅ Recomendado | ⚠️ Con cautela |
| WeChat Mini Program | ✅ Recomendado | ✅ Recomendado | ❌ Evitar si posible |
Resumen de una línea: fácil y sin dependencias → cuantización pura; equilibrado → MeshOpt; máxima razón y se puede esperar → Draco.
Práctica: cuantización y MeshOpt con gltfpack
gltfpack es la herramienta oficial de glTF; un comando gestiona cuantización y MeshOpt.
Primero instala (binarios en gltfpack releases):
# Cuantizar model.glb a 16 bits y añadir compresión MeshOpt
gltfpack -i model.glb -o model-packed.glb -cc
# -cc = compress (añade EXT_meshopt_compression sobre la cuantización por defecto)
Parámetros comunes:
# Solo cuantizar, sin MeshOpt (lo más ligero, cero dependencia de decoder)
# gltfpack cuantiza los vértices a 16 bits (KHR_mesh_quantization) por defecto,
# así que no hace falta flag extra
gltfpack -i model.glb -o model-quant.glb
# Cuantizar y activar MeshOpt
gltfpack -i model.glb -o model-meshopt.glb -cc
# Con muchísimos vértices, puedes también simplificar (reduce el nº de vértices, altera el modelo)
gltfpack -i model.glb -o model-simplify.glb -cc -si 0.5
# -si 0.5 significa simplificar a aproximadamente el 50 % de los vértices
Sobre
-cc: es el conmutador «compress» que aplica ademásEXT_meshopt_compression.Sin-cc, gltfpack cuantiza por defecto — es decir,gltfpack -i in.glb -o out.glbpor sí solo ya es «cuantización pura, cero dependencia de decoder». (-ves el flag de log verboso — no los confundas.)
Resultados típicos (un modelo PBR de 5 MB, 120k vértices, solo referencia):
| Tratamiento | Tamaño archivo | Notas |
|---|---|---|
| Original (float32) | 5.0 MB | Base |
| Cuantización pura (por defecto) | 2.6 MB | Reducido a la mitad, sin diferencia visible |
MeshOpt (-cc) | 1.7 MB | Otro 35 % menos, carga algo más rápida |
Nota: la simplificación con
-sies una operación con pérdida que altera la geometría; no es lo mismo que compresión. La compresión intenta preservar la fidelidad visual; la simplificación elimina detalles activamente. Ambos se pueden apilar, pero depende de si la escena lo permite.
Trampas comunes
- Dirección de la normal cambiada tras cuantizar: normalmente precisión demasiado baja. Usa al menos 16 bits para normales, o codificación octahedral de 8 bits.
- Materiales perdidos tras decodificar Draco: Draco solo comprime mallas; los materiales y texturas deben tratarse aparte. Al cargar hay que configurar tanto el decoder de Draco como las extensiones KHR.
- Draco no carga en un Mini Program: el wasm del decoder está restringido en algunos runtimes; cambiar a MeshOpt suele arreglarlo.
- El modelo «deriva» tras cuantizar: cuando el modelo está lejos del origen, la precisión de 16 bits no puede expresar a la vez coordenadas grandes y detalle pequeño. Solución: mover el modelo cerca del origen antes de cuantizar, o aumentar la profundidad de bits.
Siguiente paso
Los vértices están comprimidos — no celebres demasiado pronto. Como se ha dicho, las texturas son el 80 % del tamaño del modelo. A continuación cambiamos de campo de batalla y vemos por qué el PNG/JPG tradicional es un «tragón de VRAM» a los ojos de la GPU, y cómo los formatos de textura nativos de GPU lo resuelven.