---
title: "Model Slimming: Vertex Compression's Three Techniques"
description: "How many bytes does vertex data actually take? Quantization, MeshOpt, and Draco - which tool fits which scenario? It's not about 'Draco is best,' but 'the right tool for the right job.'"
url: "https://any3d.cc/en-US/blog/vertex-compression-basics"
markdown: "https://any3d.cc/llm/blog/vertex-compression-basics.md"
date: "2026-06-26"
tags: ["3d-compression","vertex-compression","meshopt","draco","gltf"]
---

# Model Slimming: Vertex Compression's Three Techniques

In the previous article, we opened up a GLB file and saw that textures eat up 80% of the volume, while vertices only account for 10-20%. So is vertex compression irrelevant?

Far from it. When a model's textures are already compressed to KTX2 and the vertices are dense, the remaining 20% is vertices—and that 20% can be cut in half or even by 90%. More importantly, vertex compression is one of the few optimizations that is almost zero-cost and takes effect immediately: add a few commands, swap a decoder, and the file slims down.

This article covers three things: what vertex data actually looks like; the personalities of Quantization, MeshOpt, and Draco; and a conclusion that will save you from common pitfalls—**There is no 'best' solution, only the 'most suitable' one.**

## How Big Is a Vertex?

Let's first look at what's inside a vertex. In glTF, each vertex is made up of several attributes:

| Attribute       | Purpose                       | Default Precision | Bytes per Vertex |
| --------------- | ----------------------------- | ----------------- | ---------------- |
| position        | Coordinates in space          | 3 × float32       | 12               |
| normal          | Determines lighting direction | 3 × float32       | 12               |
| tangent         | Normal map calculations       | 4 × float32       | 16               |
| texcoord_0 (UV) | Texture sampling coordinates  | 2 × float32       | 8                |
| color (vertex)  | Vertex-level shading          | 4 × float32       | 16               |

A vertex with full PBR attributes can take **48-64 bytes** of geometric data. A model with 100,000 vertices means 5-6MB just for vertices.

Notice that almost everything uses `float32` (32-bit floating point). That's the default, and it's the window of opportunity for vertex compression—because most attributes don't need that level of precision.

## Technique 1: Quantization

Quantization is the underlying principle of all vertex compression; both Draco and MeshOpt use it internally.

**Quantization** (mapping high-precision floating-point numbers to low-precision integers) is essentially: you don't need to remember `3.14159265`; remembering `3.14` is enough. For a set of coordinates in a spatial range, instead of recording every decimal with 32 bits, you use a smaller-range integer.

```
Original:   position.x = 1.234567   (float32, 4 bytes)
Quantized:  position.x = 1234       (int16,   2 bytes)   + a scale/offset to reconstruct
```

Comparison before and after quantization:

| Attribute | float32 bytes | Quantized (16-bit)               | Savings |
| --------- | ------------- | -------------------------------- | ------- |
| position  | 12            | 6                                | 50%     |
| normal    | 12            | 6 (or 4, with int8 + octahedral) | 50-67%  |
| tangent   | 16            | 4-8                              | 50-75%  |
| texcoord  | 8             | 4                                | 50%     |

For a vertex that originally took 48-64 bytes, quantization can typically bring it down to **16-24 bytes**, cutting the volume in half or more.

### When to Use Quantization

- You only want to reduce size without needing extreme compression ratios.
- You want **zero decoder dependency**—a quantized glTF uses the standard `KHR_mesh_quantization` extension, and mainstream engines support it natively without needing an extra decoding library.
- The target platform is sensitive to package size (e.g., WeChat Mini Programs, where adding a Draco decoder costs dozens of KB).

### When Not to Use

- The model is very small and details are the selling point (e.g., millimeter-level industrial parts). Quantization can be particularly noticeable on small models—textures may look fine, but a vertex position offset of 0.1mm becomes visible in close-up shots.

> **Real-world pitfall with quantization precision loss**: In a jewelry display scene, a ring model quantized to 16 bits showed jagged edges on metal surfaces in close-up. The problem wasn't too few vertices, but that the world coordinate system was too small, so the 16-bit integer range wasn't fine enough. Solution: reduce the quantization range (shrink the `position` bounding box) or use a higher bit depth for small models.

## Technique 2: MeshOpt

MeshOpt is the glTF official extension `EXT_meshopt_compression`. Its positioning is "good compression ratio, blazing fast decoding."

It works by first quantizing attributes (same as above), then using a technique called **LISS (lossless entropy coding)** to further compress the quantized integers losslessly. In other words: **lossy quantization + lossless entropy coding = smaller size, same visual quality as quantization alone.**

- Compression ratio: **30-50% smaller** than pure quantization
- Decoding speed: extremely fast, pure C/JS implementation, decodes tens of millions of vertices per second in a single thread
- Decoder size: very small (about 20-30KB gzipped)
- Compatibility: natively supported by Three.js, Babylon.js, making it a de facto standard on the web

### When to Use MeshOpt

- You need higher compression but can't tolerate Draco's slower decoding.
- Primarily web-based, mobile, or WebXR—decoding speed directly affects first-load experience.
- Models need to be decompressed frequently (e.g., dynamically loaded levels).

### When Not to Use

- Your target platform doesn't support `EXT_meshopt_compression` (very few old engines).
- You just need "it works" and don't care about a 30% difference—then pure quantization is simpler and has one less dependency.

## Technique 3: Draco

Draco is Google's compression scheme, positioned as "extreme compression ratio."

Its fundamental difference from the previous two: **Draco changes the connectivity of vertices (topology).** Quantization only changes the numerical representation of each vertex, MeshOpt adds lossless encoding on top, but Draco reorganizes the triangle mesh to express "which vertices form triangles" in a more compact way.

- Compression ratio: highest of the three; dense vertex models often achieve **over 90% reduction**
- Decoding speed: slowest of the three, but still fast (just relatively slower)
- Decoder size: larger (about 100-200KB, usually requires a separate wasm load)
- Visual quality: adjustable, but extreme compression ratios can cause visible distortion

### When to Use Draco

- Models are extremely large with very dense vertices (million-vertex scanned models, terrains).
- One-time load, reused for a long time (slower decoding is acceptable).
- Package size isn't the bottleneck, but download speed is.

### When Not to Use

- Mobile + fast first-screen required—downloading both the decoder and the model can actually slow things down.
- Environments with strict package size constraints (like some mini programs).
- Models need skinned animation or morph targets—Draco has weaker support for these, and misconfiguration can cause issues.

## Putting Them All Together: A Selection Table

The compression ratios below are based on community benchmarks (DeepKolos' review on Zhihu (a Chinese Q&A platform) and discussions on Reddit r/threejs). Results vary by model, but relative relationships are stable:

| Solution          | Compression Ratio (vs float32) | Decoding Speed              | Decoder Size | Lossy?                     | glTF Extension               |
| ----------------- | ------------------------------ | --------------------------- | ------------ | -------------------------- | ---------------------------- |
| Pure Quantization | ~50%                           | Native, no decoder needed   | 0            | Yes (precision)            | `KHR_mesh_quantization`      |
| MeshOpt           | ~25-35%                        | Extremely fast              | ~25KB        | Yes (precision)            | `EXT_meshopt_compression`    |
| Draco             | ~10-20%                        | Fast (slowest of the three) | ~100-200KB   | Yes (precision + topology) | `KHR_draco_mesh_compression` |

Decoder and Platform Compatibility:

| Platform      | Pure Quantization | MeshOpt        | Draco                |
| ------------- | ----------------- | -------------- | -------------------- |
| Desktop Web   | ✅ Native         | ✅ Native      | ✅ Requires decoder  |
| Mobile Web    | ✅ Native         | ✅ Native      | ⚠️ Heavy decoder     |
| WebXR/VR      | ✅ Native         | ✅ Recommended | ⚠️ Use caution       |
| Mini Programs | ✅ Recommended    | ✅ Recommended | ❌ Avoid if possible |

**One-line summary**: Want peace of mind with zero dependencies → Pure Quantization; want balance → MeshOpt; want extreme compression ratio and can afford the decoding cost → Draco.

## Hands-on: Quantization and MeshOpt with gltfpack

`gltfpack` is an official glTF tool that handles both quantization and MeshOpt in a single command.

Install first (binaries available from [gltfpack releases](https://github.com/zeux/meshoptimizer/releases)):

```bash
# Quantize model.glb to 16-bit and add MeshOpt compression
gltfpack -i model.glb -o model-packed.glb -cc

# -cc = compress (adds EXT_meshopt_compression on top of default quantization)
```

Common parameters:

```bash
# Quantize only, no MeshOpt (lightest, zero decoder dependencies)
# gltfpack quantizes vertices to 16-bit by default (KHR_mesh_quantization), no extra flags needed
gltfpack -i model.glb -o model-quant.glb

# Quantize and enable MeshOpt
gltfpack -i model.glb -o model-meshopt.glb -cc

# When vertex count is huge, simplify geometry simultaneously (reduces vertices, alters model)
gltfpack -i model.glb -o model-simplify.glb -cc -si 0.5
# -si 0.5 means simplify to ~50% vertices
```

> Note on `-cc`: it is the "compress" flag that adds `EXT_meshopt_compression`. **Without `-cc`, gltfpack defaults to pure quantization**—meaning `gltfpack -i in.glb -o out.glb` is already "pure quantization with zero decoder dependencies." (`-v` is for verbose logging, don't confuse them.)

Typical results (based on a 5MB, 120k-vertex PBR model, for reference):

| Processing                  | File Size | Description                                |
| --------------------------- | --------- | ------------------------------------------ |
| Original (float32)          | 5.0MB     | Baseline                                   |
| Quantization only (default) | 2.6MB     | Halved, virtually no visual difference     |
| MeshOpt (`-cc`)             | 1.7MB     | Another 35% saved, slightly faster loading |

> Caution: `-si` simplification is a **lossy operation that modifies model geometry**, which is different from compression. Compression preserves visual fidelity as much as possible, while simplification actively removes details. They can be combined, but ensure your scenario allows it.

## Common Pitfalls

- **Normals point in the wrong direction after quantization**: Usually caused by insufficient precision. Use at least 16-bit for normals, or 8-bit with octahedral encoding.
- **Missing materials after Draco decoding**: Draco only compresses mesh geometry; materials and textures must be handled separately. Make sure both Draco decoder and KHR extensions are configured when loading.
- **Draco fails to load in mini programs / restricted environments**: The wasm decoder may have execution restrictions in some runtime sandboxes. Switching to MeshOpt usually resolves the issue.
- **Model "drifts" after quantization**: When a model is far from the origin, 16-bit precision cannot span large coordinates while keeping small details. Solution: translate model to origin before quantizing, or increase bit depth.

## Next Steps

Vertices are compressed, but don't celebrate just yet—as mentioned earlier, textures take up 80% of a model's volume. In the next article, we'll shift battlegrounds to explore why traditional PNG/JPG textures are memory hogs in the GPU's eyes, and how GPU-native texture formats solve this problem.
