Kris

First Lesson in Model Slimming: Vertex Compression's Three Techniques

3D CompressionVertex CompressionMeshOptDracoglTF

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:

AttributePurposeDefault PrecisionBytes per Vertex
positionCoordinates in space3 × float3212
normalDetermines lighting direction3 × float3212
tangentNormal map calculations4 × float3216
texcoord_0 (UV)Texture sampling coordinates2 × float328
color (vertex)Vertex-level shading4 × float3216

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:

Attributefloat32 bytesQuantized (16-bit)Savings
position12650%
normal126 (or 4, with int8 + octahedral)50-67%
tangent164-850-75%
texcoord8450%

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:

SolutionCompression Ratio (vs float32)Decoding SpeedDecoder SizeLossy?glTF Extension
Pure Quantization~50%Native, no decoder needed0Yes (precision)KHR_mesh_quantization
MeshOpt~25-35%Extremely fast~25KBYes (precision)EXT_meshopt_compression
Draco~10-20%Fast (slowest of the three)~100-200KBYes (precision + topology)KHR_draco_mesh_compression

Decoder and Platform Compatibility:

PlatformPure QuantizationMeshOptDraco
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):

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

# 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):

ProcessingFile SizeDescription
Original (float32)5.0MBBaseline
Quantization only (default)2.6MBHalved, virtually no visual difference
MeshOpt (-cc)1.7MBAnother 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.

Support Us