Primeira Lição de Dieta para Modelos 3D: Os Três Golpes da Compressão de Vértices
No artigo anterior, abrimos um arquivo GLB e vimos que as texturas consomem 80% do volume, enquanto os vértices ocupam apenas 10-20%. Então, será que a compressão de vértices é irrelevante?
Muito pelo contrário. Quando a textura de um modelo já está comprimida com KTX2 e os vértices estão bem densos, os 20% restantes são os vértices — e esses 20% podem ser reduzidos pela metade ou até 90%. Mais importante ainda: a compressão de vértices é uma das poucas otimizações que custam quase zero e têm efeito imediato: adicione alguns comandos, troque um decodificador, e o arquivo emagrece.
Este artigo esclarece três coisas: como os dados de vértice realmente são; as personalidades de Quantização, MeshOpt e Draco; e uma conclusão para você evitar armadilhas — não existe solução "melhor", apenas a "mais adequada" para cada caso.
Qual o Tamanho de um Vértice?
Primeiro, vejamos o que um vértice contém. No glTF, cada vértice é composto por vários atributos:
| Atributo | Função | Precisão Padrão | Bytes por Vértice |
|---|---|---|---|
| position (posição) | Coordenadas no espaço | 3 × float32 | 12 |
| normal (normal) | Define a direção da iluminação | 3 × float32 | 12 |
| tangent (tangente) | Cálculo do mapa de normais | 4 × float32 | 16 |
| texcoord_0 (UV) | Coordenadas de amostragem | 2 × float32 | 8 |
| color (cor) | Coloração em nível de vértice | 4 × float32 | 16 |
Um vértice com todos os atributos PBR completos precisa de 48-64 bytes só para dados geométricos. Um modelo com 100 mil vértices significa 5-6MB apenas em vértices.
Note que quase tudo aqui usa float32 (ponto flutuante de 32 bits). Essa é a configuração padrão e também o ponto de partida para a compressão de vértices — porque a maioria dos atributos não precisa de precisão de 32 bits.
Primeiro Golpe: Quantização
A quantização é o princípio fundamental por trás de toda compressão de vértices; Draco e MeshOpt também a utilizam internamente.
A quantização (mapear números de ponto flutuante de alta precisão para inteiros de baixa precisão) funciona assim: para o número 3.14159265, você só precisa lembrar 3.14. Para um conjunto de coordenadas em um espaço, você não registra cada decimal com 32 bits; em vez disso, usa um inteiro de faixa menor.
Original: position.x = 1.234567 (float32, 4 bytes)
Quantizado: position.x = 1234 (int16, 2 bytes) + um scale/offset para restaurar
Comparação antes e depois da quantização:
| Atributo | Bytes float32 | Após quantização (16 bits) | Economia |
|---|---|---|---|
| position | 12 | 6 | 50% |
| normal | 12 | 6 (ou 4, com int8 + octahedral) | 50-67% |
| tangent | 16 | 4-8 | 50-75% |
| texcoord | 8 | 4 | 50% |
Para aquele vértice de 48-64 bytes, após a quantização, ele geralmente cai para 16-24 bytes, reduzindo o volume pela metade ou mais.
Quando Usar Quantização
- Você só quer reduzir o tamanho, sem precisar de taxa de compressão extrema
- Você quer zero dependência de decodificador — glTF quantizado usa a extensão padrão
KHR_mesh_quantization, suportada nativamente pelos principais engines, sem precisar de bibliotecas extras - A plataforma alvo é sensível ao tamanho do pacote (como mini-apps, onde um decodificador Draco extra custa dezenas de KB)
Quando Evitar
- O modelo é muito pequeno e os detalhes são o diferencial (como peças industriais em escala milimétrica). A quantização tende a falhar visivelmente em modelos pequenos — a textura até vai bem, mas um deslocamento de 0,1mm na posição do vértice é perceptível em close-ups.
Armadilha real de perda de precisão na quantização: Em uma cena de exibição de joias, após quantizar o modelo de anel para 16 bits, surgiram serrilhados nas bordas de metal em close-ups. O motivo não era a falta de vértices, mas sim o mundo coordenado pequeno demais — a faixa de 16 bits não era suficiente para expressar detalhes finos. A solução é reduzir a faixa de quantização (diminuir o bounding box do
position) ou usar maior profundidade de bits para modelos pequenos.
Segundo Golpe: MeshOpt
O MeshOpt é a extensão oficial do glTF EXT_meshopt_compression, com o objetivo de "boa taxa de compressão e decodificação rápida".
Ele primeiro quantiza os atributos (como acima) e depois usa uma técnica chamada LISS (codificação de entropia sem perdas) para comprimir ainda mais os inteiros quantizados sem perdas. Em outras palavras: quantização com perdas + codificação de entropia sem perdas = tamanho menor, qualidade igual à quantização.
- Taxa de compressão: 30-50% menor que apenas quantização
- Velocidade de decodificação: extremamente rápida, implementação pura em C/JS, decodifica dezenas de milhões de vértices por segundo em um único thread
- Tamanho do decodificador: pequeno (~20-30KB após gzip)
- Compatibilidade: suporte nativo em Three.js e Babylon.js, é um dos padrões de fato na web
Quando Usar MeshOpt
- Precisa de maior compressão, mas não aceita a decodificação mais lenta do Draco
- Foco em web, mobile e WebXR — a velocidade de decodificação impacta diretamente a experiência de carregamento inicial
- Modelos que precisam ser descomprimidos com frequência (como fases carregadas dinamicamente)
Quando Evitar
- Sua plataforma alvo não suporta
EXT_meshopt_compression(engines antigas e raras) - Você só precisa que "funcione" e não se importa com a diferença de 30% — nesse caso, a quantização pura é mais simples e tem uma dependência a menos
Terceiro Golpe: Draco
O Draco é a solução de compressão do Google, focada em "taxa de compressão extrema".
A diferença fundamental entre ele e os outros dois: o Draco altera a conectividade dos vértices (topologia). A quantização apenas muda a representação numérica de cada vértice; o MeshOpt adiciona codificação sem perdas por cima; mas o Draco reorganiza a malha triangular, expressando "quais vértices formam triângulos" de forma mais compacta.
- Taxa de compressão: a mais alta entre os três; modelos com vértices densos frequentemente alcançam redução de mais de 90%
- Velocidade de decodificação: a mais lenta entre os três, mas ainda rápida (apenas relativamente mais lenta)
- Tamanho do decodificador: maior (~100-200KB, geralmente precisa carregar wasm separadamente)
- Qualidade: ajustável, mas em taxas de compressão extremas pode haver deformações visíveis
Quando Usar Draco
- Modelos enormes, com vértices ultra densos (modelos escaneados com milhões de vértices, terrenos)
- Carregamento único, reutilização por longo tempo após descompressão (a decodificação mais lenta é aceitável)
- O tamanho do pacote não é o gargalo; a velocidade de download é
Quando Evitar
- Mobile + necessidade de carregamento inicial rápido — o decodificador e o modelo precisam ser baixados, o que acaba atrasando
- Ambientes rigorosos com tamanho de pacote, como mini-apps
- Modelos que precisam de animação de esqueleto (skinning) ou morph targets — o Draco tem suporte fraco para isso e pode causar problemas se configurado incorretamente
Comparação Final: Uma Tabela para Escolher
As taxas de compressão abaixo são referências de benchmarks da comunidade (avaliações do DeepKolos no Zhihu + discussões no Reddit r/threejs); podem variar entre modelos, mas a relação relativa é estável:
| Solução | Taxa de compressão (vs float32) | Velocidade de decodificação | Tamanho do decodificador | Com perdas? | Extensão glTF |
|---|---|---|---|---|---|
| Quantização pura | ~50% | Nativa, sem decodificação | 0 | Sim (precisão) | KHR_mesh_quantization |
| MeshOpt | ~25-35% | Muito rápida | ~25KB | Sim (precisão) | EXT_meshopt_compression |
| Draco | ~10-20% | Rápida (mais lenta dos três) | ~100-200KB | Sim (precisão + topologia) | KHR_draco_mesh_compression |
Compatibilidade de decodificador e plataforma:
| Plataforma | Quantização pura | MeshOpt | Draco |
|---|---|---|---|
| Web Desktop | ✅ Nativo | ✅ Nativo | ✅ Precisa decod. |
| Web Mobile | ✅ Nativo | ✅ Nativo | ⚠️ Decod. pesado |
| WebXR/VR | ✅ Nativo | ✅ Recomendado | ⚠️ Cautela |
| Mini-apps (WeChat) | ✅ Recomendado | ✅ Recomendado | ❌ Evitar |
Resumo em uma frase: Para simplicidade e zero dependências → quantização pura; para equilíbrio → MeshOpt; para taxa de compressão extrema e se puder esperar → Draco.
Na Prática: Usando gltfpack para Quantização e MeshOpt
O gltfpack é a ferramenta oficial do glTF; um comando resolve quantização e MeshOpt.
Primeiro, instale (os binários podem ser baixados do gltfpack release):
# Quantiza model.glb para 16 bits e aplica compressão MeshOpt
gltfpack -i model.glb -o model-packed.glb -cc
# -cc = compress (adiciona EXT_meshopt_compression sobre a quantização padrão)
Parâmetros comuns:
# Apenas quantização, sem MeshOpt (mais leve, zero dependência de decodificador)
# O gltfpack já quantiza vértices para 16 bits por padrão (KHR_mesh_quantization), sem parâmetros extras
gltfpack -i model.glb -o model-quant.glb
# Quantização + MeshOpt
gltfpack -i model.glb -o model-meshopt.glb -cc
# Com muitos vértices, pode simplificar (reduz o número de vértices, altera o modelo)
gltfpack -i model.glb -o model-simplify.glb -cc -si 0.5
# -si 0.5 significa simplificar para cerca de 50% dos vértices
Sobre o
-cc: é o interruptor "compress", que adicionaEXT_meshopt_compressionpor cima. Sem-cc, o gltfpack já faz quantização por padrão — ou seja, o comandogltfpack -i in.glb -o out.glbjá é "quantização pura, zero dependência de decodificador". (-vé o interruptor de verbose/log detalhado, não confunda.)
Efeito típico (um modelo PBR de 5MB com 120 mil vértices, apenas referência):
| Processamento | Tamanho do arquivo | Descrição |
|---|---|---|
| Original (float32) | 5.0MB | Linha de base |
| Quantização pura (padrão) | 2.6MB | Redução pela metade, sem diferença visual significativa |
MeshOpt (-cc) | 1.7MB | Mais 35% de economia, carregamento um pouco mais rápido |
Nota: a simplificação com
-sié uma operação com perdas que altera a geometria do modelo, diferente de compressão. A compressão tenta manter a fidelidade visual; a simplificação reduz detalhes ativamente. Ambas podem ser combinadas, mas depende se o cenário permite.
Armadilhas Comuns
- A direção da normal mudou após a quantização: provavelmente usou precisão muito baixa. Use pelo menos 16 bits para normal, ou codificação octaédrica de 8 bits.
- Materiais sumiram após decodificar com Draco: o Draco comprime apenas a malha; materiais e texturas precisam ser tratados separadamente. Ao carregar, configure o decodificador Draco e a extensão KHR corretamente.
- Draco não carrega em mini-apps: o wasm do decodificador pode ter restrições em certos runtimes; trocar para MeshOpt geralmente resolve.
- Modelo "deriva" após quantização: quando o modelo está muito longe da origem das coordenadas, 16 bits não são suficientes para expressar coordenadas grandes + pequenos detalhes. A solução é mover o modelo para perto da origem antes de quantizar, ou aumentar a profundidade de bits.
Próximos Passos
Vértices comprimidos, mas não comemore ainda — como dissemos, as texturas ocupam 80% do volume do modelo. No próximo artigo, vamos mudar de campo e ver por que PNG/JPG tradicionais são um "devorador de memória" aos olhos da GPU, e como os formatos de textura nativos da GPU resolvem esse problema.