Any3DAny3D
·Any3D Team

From Blender to Production: An End-to-End Compression Walkthrough

3d-compressionpipelinetexture-compressionvertex-compressiongltf

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 metricValue
Blender source file~120MB (including un-exported high-poly)
Exported GLB (float32 + PNG)~50MB
Vertex count~180k
Maps6 × 4096×4096 (albedo, normal, roughness, metallic, AO, emissive)
VRAM usage (6 fully decompressed)~520MB
GoalFile ≤ 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: Automatic or JPEG (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:

ToolStrengthWeaknessGood for
gltf-transformAll-rounder, textures+vertices in one go; API-able, scriptableTop ratio below dedicated toolsRecommended main tool for most scenarios
gltfpackPro at vertex compression, native MeshOptWeak texture compressionVertex-dense, fine MeshOpt control
toktxMost professional texture tool, most parametersOnly textures, not whole modelsFine-tuning a single texture
gltf-pipelineVeteran, supports DracoInactive maintenance, few featuresExisting Draco legacy projects
Online tools (gltf.report)Zero installNot for automation/bulkExperiments, 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
StageFile sizeVRAMChange
Baseline50MB~520MB
Step 1 de-dup45MB~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.

StageFile sizeVRAMChange
Step 145MB~520MB
Step 2 + MeshOpt30MB~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.

StageFile sizeVRAMChange
Step 230MB~520MB
Step 3 + KTX26MB~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.

StageFile sizeVRAMChange
Step 36MB~70MB
Step 4 + simplify 0.54.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):

StepFile sizeVRAMCumulative reduction
Baseline (float32 + PNG)50MB~520MB
+ de-dup & weld45MB~520MB-10%
+ MeshOpt vertices30MB~520MB-40%
+ KTX2 textures6MB~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 --srgb on 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-ratio set 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 fallback field of KHR_texture_basisu for a safety net.

Series cheat sheet

The essence of all six articles condensed into one table—bookmark it.

Size composition

ComponentShareOptimization tool
Texture maps70-85%KTX2 (biggest gain)
Vertex data10-20%MeshOpt / quantization / Draco
Animation data0-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

ScenarioRecommendation
Zero-dependency, simplestPlain quantization (KHR_mesh_quantization)
Balanced web first choiceMeshOpt
Max ratio, can wait for decodeDraco
Mini Program / package-sensitivePlain quantization / MeshOpt; avoid Draco

Texture compression selection

Map typeRecommended encoding
albedo / emissive (color)KTX2 ETC1S
normal / roughness / metallic / AO (data)KTX2 UASTC
Desktop web, chasing download speedWebP / 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

PlatformTextureVertex
Desktop webWebP / KTX2MeshOpt
Mobile webKTX2 mandatoryMeshOpt
VRKTX2 mandatoryMeshOpt + LOD
Mini ProgramKTX2 / WebPMeshOpt / quantization
Large sceneKTX2 mandatoryMeshOpt + Draco + LOD

Series recap

Six articles done; strung together they form a complete chain:

  1. Why so heavy: nail down size composition and the VRAM truth
  2. Three weapons of vertex compression: principles and choice of quantization, MeshOpt, Draco
  3. The texture VRAM problem: why PNG/JPG are guilty in the GPU's eyes
  4. KTX2 in practice: ETC1S/UASTC selection, toolchain, engine loading
  5. Selection guide: a decision framework by platform/scenario
  6. 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.

Support Us