Note / Real-time 3D

Why is my Character Creator GLB so big?

Every optimization guide tells you to compress your textures. For a rigged character that advice is close to useless, and the settings most people reach for next will quietly break the face.

Masterwork Studio  /  9 August 2026  /  measured, not estimated
Short answer

It is the morph targets. On a raw Character Creator 4 export they are roughly 96% of the file, and the textures you were about to compress are a rounding error. Draco will not help you, and quantization will break your shape keys.

Where the bytes actually are

A Character Creator 4 export ships 400 or more facial morphs. Each one stores a POSITION delta and a NORMAL delta, per affected vertex, as uncompressed float32. That is the whole story of the file size.

Here are two characters we measured, at opposite ends of the scale.

Two rigged characters, measured from the shipped files.
AssetMorph targetsRaw exportShippedReduction
CC4 avatar, full facial rig400+236 MB24 MB90%
Stylized low-poly character234.18 MB2.26 MB46%

The second character is the more useful one to look at, because it is small enough to break down completely:

4.18 MB export, 21,810 vertices, 43,528 triangles, 22 morph targets, 65-bone skin.
ComponentSizeShare
Morph targets3.3 MB81%
Geometry, Draco compressed510 KB12%
Textures, WebP, 18 maps302 KB7%
JSON100 KB2%

Twenty-three blend shapes cost six times what all the geometry and textures cost together. This character has 23. A CC4 export has more than 400.

Why Draco does not save you

The standard advice is Draco, usually stated with confidence. It is good advice for a prop and it does almost nothing for a character, for a reason that is easy to verify.

Draco does not compress morph targets. In Blender's glTF exporter, io_scene_gltf2/io/exp/draco.py:146 touches primitive.targets only to set preserve_triangle_order. Draco covers the base mesh attributes and nothing else.

It is slightly worse than neutral. Preserving triangle order to keep the morph targets valid makes Draco compress the base mesh a little less well than it otherwise would.

So on the character above, Draco is working on the 510 KB slice while the 3.3 MB slice sits untouched.

The settings that break the face

Having found that Draco does not help, the natural next move is to reach for something stronger. Each of these produced a broken deliverable for us, and none of them failed loudly.

Quantization destroys morph deltas on small-scale models

This is the worst one, because the file loads fine and looks fine until a shape key is activated.

The character was authored small, with a bounding box around 0.017 units. Running quantize() scaled the morph POSITION deltas by roughly 40,000 times, turning a 0.04 unit displacement into 1,580 units. Activate any shape key and the vertices are thrown to infinity. The symptom people report is "my shape keys stopped working", which sounds like an exporter problem and is not.

Setting quantizationVolume: 'mesh' makes it worse in a second way: it clones the shared skin per mesh, in our case turning one skin into nine. Using 'scene' keeps the single skin and still corrupts the morphs. There is no safe setting here. Do not quantize a character.

Meshopt breaks Blender round-trip

EXT_meshopt_compression produces a file Blender refuses, with "Extension EXT_meshopt_compression is not available on this addon version". If the client ever needs to open the asset, this rules meshopt out regardless of what it saves.

WebP textures render white in some viewers

EXT_texture_webp leaves the material bindings intact but viewers without support cannot load the image, so meshes arrive untextured and white. Fine if you control the runtime. Not fine for a deliverable that gets opened in whatever the client has.

Pruning deletes textures that look constant

The default prune() collapses near-solid textures into baseColorFactor and removes them. On our model it ate the eyebrow texture and turned the brows into solid dark red. The fix is keepSolidTextures: true.

What actually works

This pipeline is lossless to functionality. Morphs stay bit-exact, the file still imports into Blender, and it loads in any viewer.

  1. Strip the vertex colors. CC4 writes COLOR_0 and COLOR_1 that stylized web work does not use.
  2. Weld, dedup, and prune gently, restricting prune to geometry property types and keeping solid textures.
  3. Drop the morph NORMAL deltas and keep the POSITION deltas. This is the single biggest win, roughly halving the file on its own.
  4. Prune accessors again afterwards. Detaching the normal deltas does not shrink anything. The orphaned accessors sit in the buffer until a second pass removes them, which is easy to miss because the first pass reports success.
  5. Leave the textures as JPEG or PNG, and skip quantize, meshopt and WebP entirely.

That took the CC4 avatar from 236 MB to 24 MB with the morphs intact.

Fewer materials means smaller file

This one is not widely known and it is worth more than most compression settings.

Every material primitive carries every morph target. A mesh split across nine materials stores all its blend shapes nine times.

Splitting the head into its own object, so the shape keys ride only the head's five primitives instead of all nine, took the character from 4.18 MB to 2.26 MB. No decimation, no texture change: vertex and triangle totals were identical before and after, and the textures stayed at 301.8 KB.

The same split fixed a bug we had been chasing separately. Fourteen visemes were all reporting an identical maximum delta of 0.16418, which turned out to be stray body vertices being dragged by every facial shape. Moving the head to its own object removed them, and the deltas dropped to a sane 0.008 to 0.03.

Delete the textures that are not textures

Ten of that character's eighteen exported maps were single-valued. Every metallic map was solid black, every roughness map but one was solid white, and they were shipping as 2048 and 4096 pixel PNGs.

Detect them by reading the pixel buffer and testing for zero range, then unlink the node and write the constant into the Principled socket instead. The exporter emits metallicFactor and roughnessFactor rather than an image. It is provably lossless and it removed five of the six ORM textures, landing the whole texture payload at 302 KB.

Use exact peak-to-peak, not standard deviation, for the constancy test. Float32 std over a 4096 pixel square image accumulates enough error to return a spurious nonzero and wrongly reject a genuinely constant map.

Also check for duplicate image datablocks pointing at the same file. A Body_Base_Color.png and a .001 of it are bit identical and get exported twice.

How to prove you did not break it

Compression damage to morph targets is invisible until someone animates the face, which is usually after delivery. Run this before you ship.

How these were measured

Byte sizes are the shipped files, not what a modelling package reports. Component splits were taken from the glTF buffer views, so the morph share is the actual bytes attributed to morph accessors rather than an estimate.

The two characters are different pipelines on purpose. The CC4 avatar was compressed after export with gltf-transform. The stylized character was exported from Blender 5.1 with the exporter doing the work, Draco and WebP enabled. Both are production deliverables rather than test scenes, which is also why neither is named here.

One exporter setting is worth stating because it silently ruins the file: export_apply must stay off. Applying modifiers on export destroys shape keys.

If you are sizing 3D assets for the web more generally, the companion note covers what a GLB should weigh when it is not a character, and why texture count predicts file size better than triangle count.

How big should a GLB file be?