From Blender to Production: An End-to-End Compression Walkthrough
By here in the series, tools, principles, and selection have all been covered. In this final article we string everything into a runnable pipeline: starting from a real Blender model, compress step by step, recording file size, VRAM, and load time at each step, and finally see whether it can shrink from a 50MB heavyweight to a 5MB model that opens instantly on a phone.
Target reader: someone who has read the previous five and is ready to actually do it. No new concepts here—only copyable flows, commands, and scripts.
Starting point: a real PBR model
We use a very typical e-commerce display model as the sample: a high-detail product model with full PBR maps.
| Initial metric | Value |
|---|---|
| Blender source file | ~120MB (including un-exported high-poly) |
| Exported GLB (float32 + PNG) | ~50MB |
| Vertex count | ~180k |
| Maps | 6 × 4096×4096 (albedo, normal, roughness, metallic, AO, emissive) |
| VRAM usage (6 fully decompressed) | ~520MB |
| Goal | File ≤ 5MB, VRAM controllable, instant open on mobile |
A 50MB file and 520MB VRAM—this model crashes for sure on mobile. Let's go step by step.
Step 0: export correctly from Blender
The first gate of compression is actually export; many people bleed out here.
Key settings when exporting glTF from Blender:
- Format:
glTF Binary (.glb)(single file, easy to transfer) - Geometry: check
Normals,Tangents(PBR normal maps need tangents) - UV: make sure it's exported (on by default)
- Textures:
AutomaticorJPEG(the texture format here doesn't matter; we'll re-compress later—but make sure they're exported) - Compression: do not check Blender's built-in mesh compression; we'll use more specialized tools
- Transform:
+Y Up(glTF standard) - Data: check only what you need (don't export animation, cameras, lights if not needed, to reduce size)
After export, model.glb: 50MB, 6 PNG maps, float32 vertices. That's our baseline.
The first common pitfall is right here: Blender by default exports unused meshes and hidden helper objects too. Before exporting, run
File > Clean Up > Purge Orphans, and in the outliner select only the objects to export.
The end-to-end pipeline, full picture
Draw the whole line so you have a mental map:
Blender source file
│ export .glb (float32 + PNG) 50MB
▼
[1] De-dup + weld duplicate vertices (gltf-transform) ~45MB
│
[2] Vertex compression: MeshOpt (gltfpack / gltf-transform) ~30MB
│
[3] Texture compression: PNG → KTX2 (ETC1S/UASTC) ~6MB
│
[4] (optional) Geometry simplification LOD (simplify) ~4-5MB
▼
Final model-final.glb ~5MB
│
Engine load (Three.js / Babylon.js) → runtime transcode → ship
The numbers for each step are tracked live in the tables below.
Toolchain: which to pick
There are several compression tools; here's a comparison so you don't pick wrong:
| Tool | Strength | Weakness | Good for |
|---|---|---|---|
| gltf-transform | All-rounder, textures+vertices in one go; API-able, scriptable | Top ratio below dedicated tools | Recommended main tool for most scenarios |
| gltfpack | Pro at vertex compression, native MeshOpt | Weak texture compression | Vertex-dense, fine MeshOpt control |
| toktx | Most professional texture tool, most parameters | Only textures, not whole models | Fine-tuning a single texture |
| gltf-pipeline | Veteran, supports Draco | Inactive maintenance, few features | Existing Draco legacy projects |
| Online tools (gltf.report) | Zero install | Not for automation/bulk | Experiments, one-off tasks |
Main recommendation: run the whole flow with gltf-transform; use gltfpack for vertex detail and toktx for single-texture tuning as needed. All steps below are based on gltf-transform.
Step 1: de-dup + weld
Models often have duplicate vertices, unused nodes, and materials. Clean once first.
gltf-transform optimize model.glb step1.glb --weld --prune
| Stage | File size | VRAM | Change |
|---|---|---|---|
| Baseline | 50MB | ~520MB | — |
| Step 1 de-dup | 45MB | ~520MB | -5MB (VRAM unchanged, because textures are still there) |
VRAM barely moved—that's expected. De-dup mainly saves vertices and structure; textures are the VRAM heavyweight.
Step 2: vertex compression MeshOpt
gltf-transform optimize step1.glb step2.glb --meshopt --weld --prune
--meshopt quantizes vertices to 16-bit and applies MeshOpt lossless encoding, automatically adding the EXT_meshopt_compression extension.
| Stage | File size | VRAM | Change |
|---|---|---|---|
| Step 1 | 45MB | ~520MB | — |
| Step 2 + MeshOpt | 30MB | ~520MB | -15MB (vertex part) |
VRAM still ~520MB? Right—because vertices are a small share of VRAM (10-20%); cutting them has limited VRAM impact. The real VRAM monster is textures, handled next.
Step 3: texture compression PNG → KTX2
This step is the best ROI.
gltf-transform optimize step2.glb step3.glb \
--texture-compress basisu \
--meshopt --weld --prune
--texture-compress basisu auto-detects each map: color maps (albedo, emissive) → ETC1S; data maps (normal, roughness, metallic, AO) → UASTC.
| Stage | File size | VRAM | Change |
|---|---|---|---|
| Step 2 | 30MB | ~520MB | — |
| Step 3 + KTX2 | 6MB | ~70MB | -24MB file / -450MB VRAM |
This step is the turning point of the whole pipeline:
- File drops from 30MB to 6MB
- VRAM drops from 520MB to ~70MB—because the six 4096 maps go from "raw decompressed pixels" to "block-compressed," each from ~87MB down to ~11-14MB
VRAM drops an order of magnitude—that's the key to whether it runs on mobile.
Step 4: (optional) geometry simplification
If you want even smaller and the scene allows lowering vertex precision, add geometry simplification.
gltf-transform optimize step3.glb final.glb \
--texture-compress basisu \
--meshopt \
--simplify --simplify-ratio 0.5 \
--weld --prune
--simplify-ratio 0.5 means keep about 50% of the vertices.
| Stage | File size | VRAM | Change |
|---|---|---|---|
| Step 3 | 6MB | ~70MB | — |
| Step 4 + simplify 0.5 | 4.5MB | ~70MB | -1.5MB (VRAM roughly unchanged) |
Simplification mostly saves file size; VRAM impact is small. The cost is reduced model detail—visible up close. E-commerce product pages usually shouldn't over-simplify; buildings/large scenes are a great fit.
Full effect-tracking table
Stack the four steps for the full picture (based on the sample above; numbers are illustrative of magnitude):
| Step | File size | VRAM | Cumulative reduction |
|---|---|---|---|
| Baseline (float32 + PNG) | 50MB | ~520MB | — |
| + de-dup & weld | 45MB | ~520MB | -10% |
| + MeshOpt vertices | 30MB | ~520MB | -40% |
| + KTX2 textures | 6MB | ~70MB | -88% file / -87% VRAM |
| + geometry simplify (0.5) | 4.5MB | ~70MB | -91% file |
Conclusion: texture compression delivers the vast majority of the size and VRAM gains. Vertex compression is the icing; texture compression is the rescue. This fully matches the first article's thesis—textures are 80% of size, so optimizing them has the highest return.
One-command version: lazy one-shot compression
If you don't want to go step by step, do all optimizations at once:
gltf-transform optimize model.glb model-final.glb \
--texture-compress basisu \
--meshopt \
--simplify --simplify-ratio 0.5 \
--weld --prune
This one command = de-dup + weld + vertex MeshOpt + texture KTX2 + geometry simplification. It's enough for 90% of scenarios; the step-by-step is mainly to understand and tune.
Automation script: reusable
Encapsulate the flow into a script you can drop into your build. This script outputs a different version per target platform and prints the effect at each step.
// scripts/compress-model.mjs
import { optimize } from "@gltf-transform/functions";
import { NodeIO } from "@gltf-transform/core";
import { KHRONOS_EXTENSIONS } from "@gltf-transform/extensions";
import { filesize } from "filesize";
const io = new NodeIO().registerExtensions(KHRONOS_EXTENSIONS);
// Compression strategies per platform
const PROFILES = {
mobile: {
textureCompression: "basisu",
meshCompression: "meshopt",
simplify: { ratio: 0.5 },
weld: true,
prune: true,
},
vr: {
textureCompression: "basisu",
meshCompression: "meshopt",
// Close-up VR viewing, no simplify
simplify: null,
weld: true,
prune: true,
},
desktop: {
textureCompression: "webp",
meshCompression: "meshopt",
simplify: null,
weld: true,
prune: true,
},
};
async function compress(inputPath, profileName) {
const cfg = PROFILES[profileName];
const doc = await io.read(inputPath);
const before = Buffer.byteLength(await io.writeBinary(doc), "utf8");
await optimize(doc, {
textureCompression: cfg.textureCompression,
meshCompression: cfg.meshCompression,
simplify: cfg.simplify ?? undefined,
weld: cfg.weld,
prune: cfg.prune,
});
const bytes = await io.writeBinary(doc);
const outPath = inputPath.replace(/\.glb$/, `-${profileName}.glb`);
await io.write(outPath, doc);
const after = bytes.byteLength;
console.log(
`${profileName.padEnd(8)} ${filesize(before)} → ${filesize(after)} ` +
`(${Math.round((1 - after / before) * 100)}% smaller) → ${outPath}`
);
}
// Usage: node scripts/compress-model.mjs path/to/model.glb
const input = process.argv[2];
for (const profile of Object.keys(PROFILES)) {
await compress(input, profile);
}
Drop it into your project:
node scripts/compress-model.mjs public/models/model.glb
# mobile 50MB → 4.5MB (91% smaller) → model-mobile.glb
# vr 50MB → 6.2MB (87% smaller) → model-vr.glb
# desktop 50MB → 11MB (78% smaller) → model-desktop.glb
The frontend just loads the matching version per device.
Don't want to build this pipeline yourself? Any3D's online tool does all the above in one shot—upload a GLB and it automatically runs texture KTX2 + vertex MeshOpt and outputs compressed versions per platform, saving you the trouble of installing a local toolchain.
Common pitfalls FAQ
Model turns black / textures don't show after compression
- 99% it's color space: a color map missed sRGB. In Three.js,
texture.colorSpace = THREE.SRGBColorSpace. - Forgot
--srgbon color maps with toktx.
Lighting looks wrong after compressing normal maps
- Normal map used ETC1S; switch to UASTC.
- Normal map is DirectX style (green channel down); the engine wants OpenGL style, so flip the G channel.
Mobile load stuck on first paint
- Check whether you're loading the Draco decoder wasm (extra request). Prefer MeshOpt on mobile.
- KTX2 transcoder path misconfigured; transcode fails and falls back to CPU decode.
Compressed file got bigger instead
- The map is too small (< 128px); KTX2 isn't worth it—block compression has fixed overhead.
- The model was already compressed once; a second pass has no gain (even negative).
Model breaks apart after simplification
--simplify-ratioset too low; raise to 0.7-0.8.- Simplification is friendly to hard surfaces (machinery, architecture) and prone to breaking organic curves (characters).
KTX2 fails to load in some browsers
- Old Safari / old WebView don't support it. Provide a PNG/WebP fallback, or use the
fallbackfield ofKHR_texture_basisufor a safety net.
Series cheat sheet
The essence of all six articles condensed into one table—bookmark it.
Size composition
| Component | Share | Optimization tool |
|---|---|---|
| Texture maps | 70-85% | KTX2 (biggest gain) |
| Vertex data | 10-20% | MeshOpt / quantization / Draco |
| Animation data | 0-15% | Reduce keyframes / compress |
| Other | < 2% | De-dup |
VRAM formula
Traditional-format VRAM = width * height * 4 bytes * 1.333 (with mipmaps)
KTX2 block-compressed VRAM ≈ above / 4 (ETC1S) or / 2 (UASTC)
Vertex compression selection
| Scenario | Recommendation |
|---|---|
| Zero-dependency, simplest | Plain quantization (KHR_mesh_quantization) |
| Balanced web first choice | MeshOpt |
| Max ratio, can wait for decode | Draco |
| Mini Program / package-sensitive | Plain quantization / MeshOpt; avoid Draco |
Texture compression selection
| Map type | Recommended encoding |
|---|---|
| albedo / emissive (color) | KTX2 ETC1S |
| normal / roughness / metallic / AO (data) | KTX2 UASTC |
| Desktop web, chasing download speed | WebP / AVIF |
| Small maps (< 128px) | Keep PNG, don't KTX2 |
One-shot command
# Full optimization (texture + vertex + simplify)
gltf-transform optimize model.glb model-final.glb \
--texture-compress basisu --meshopt \
--simplify --simplify-ratio 0.5 --weld --prune
Platform cheat sheet
| Platform | Texture | Vertex |
|---|---|---|
| Desktop web | WebP / KTX2 | MeshOpt |
| Mobile web | KTX2 mandatory | MeshOpt |
| VR | KTX2 mandatory | MeshOpt + LOD |
| Mini Program | KTX2 / WebP | MeshOpt / quantization |
| Large scene | KTX2 mandatory | MeshOpt + Draco + LOD |
Series recap
Six articles done; strung together they form a complete chain:
- Why so heavy: nail down size composition and the VRAM truth
- Three weapons of vertex compression: principles and choice of quantization, MeshOpt, Draco
- The texture VRAM problem: why PNG/JPG are guilty in the GPU's eyes
- KTX2 in practice: ETC1S/UASTC selection, toolchain, engine loading
- Selection guide: a decision framework by platform/scenario
- This article: end-to-end pipeline, from Blender to production
The core is one sentence: think through the bottleneck first (download/VRAM/frame rate), then pick tools; texture compression has the biggest return, vertex compression is the icing; different platforms have different fates—don't apply a blanket rule.
Following this article's script and cheat sheet, taking your model from 50MB to 5MB and VRAM from 520MB to 70MB should be a reproducible path. The rest is just doing it.