Three.js From Zero · Article s6-10
S6-10 Production Asset Pipeline
The Production Asset Pipeline
Artist hits "Export" in Blender. Web app ships the model. In between: 8 steps that turn 50MB of raw artistry into a 2MB cached asset optimized for first-paint. The full recipe.
The pipeline end-to-end
Artist exports from Blender
Apply modifiers. Principled BSDF materials. +Y Up on. Bake All Actions on. Draco checkbox on (even though we'll re-run gltf-transform).
File → Export → glTF 2.0 → output.glb
Validate
Before ANY further processing, check the file is correct.
gltf-transform validate output.glb # or: npx gltf-validator output.glb
If validation fails, go back to step 1. Don't proceed.
Inspect
Know what you're dealing with.
gltf-transform inspect output.glb
Look at: total size, triangle count, texture sizes, extensions used.
Clean
Remove cruft.
gltf-transform optimize output.glb cleaned.glb \ --prune --dedup --weld
Usually 10-30% smaller with no quality loss.
Resize oversized textures
Artists love shipping 8K textures for a 50-pixel-on-screen prop. Downscale.
gltf-transform resize cleaned.glb resized.glb \ --width 2048 --height 2048 \ --filter lanczos
Compress geometry
Pick Draco (smaller) or Meshopt (faster decode).
# Option A: Draco gltf-transform draco resized.glb geo.glb # Option B: Meshopt gltf-transform meshopt resized.glb geo.glb --compression medium
Compress textures
UASTC for normals, ETC1S for everything else.
gltf-transform uastc geo.glb --filter "*normal*" gltf-transform etc1s geo.glb --filter "*baseColor*,*roughness*,*metallic*"
End-product: final.glb.
Ship
Upload to CDN. Three.js consumes with DRACOLoader + KTX2Loader wired:
const loader = new GLTFLoader()
.setDRACOLoader(draco)
.setKTX2Loader(ktx2)
.setMeshoptDecoder(MeshoptDecoder);
loader.load('https://cdn.example.com/final.glb', g => scene.add(g.scene));
2. One-script automation
#!/usr/bin/env bash
set -e
IN=$1
OUT=$2
npx gltf-transform validate "$IN"
npx gltf-transform optimize "$IN" tmp1.glb
npx gltf-transform resize tmp1.glb tmp2.glb --width 2048 --height 2048
npx gltf-transform meshopt tmp2.glb tmp3.glb --compression medium
npx gltf-transform uastc tmp3.glb tmp4.glb --filter "*normal*"
npx gltf-transform etc1s tmp4.glb "$OUT" --filter "*"
rm tmp*.glb
echo "Done: $OUT"
echo "Before/after:"
ls -lh "$IN" "$OUT"
Save as pipeline.sh, run ./pipeline.sh raw.glb final.glb.
3. CI integration
# .github/workflows/assets.yml
on: push
jobs:
build-assets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: |
for f in raw/*.glb; do
./pipeline.sh "$f" "public/assets/$(basename $f)"
done
- uses: actions/upload-artifact@v4
with:
name: optimized-assets
path: public/assets/
4. Loading strategy
Your app now:
- Has DRACOLoader, KTX2Loader, MeshoptDecoder wired (S6-01).
- Uses LoadingManager for progress (S6-09).
- Streams near-player, unloads far (S6-09).
- Uses addressables instead of hardcoded URLs (S6-09).
5. A representative win
| Step | Size | % of raw |
|---|---|---|
| Raw Blender export | 14.5 MB | 100% |
| + validate (no change) | 14.5 MB | 100% |
| + optimize (prune, dedup, weld) | 11.2 MB | 77% |
| + resize 8K → 2K textures | 6.1 MB | 42% |
| + meshopt geometry | 4.3 MB | 30% |
| + UASTC normals | 2.8 MB | 19% |
| + ETC1S base/rough/metal | 1.4 MB | 10% |
10× smaller. Same visual output. That's a 2-second load become 0.2-second.
6. Common pipeline mistakes
- Skipping validate. Compression amplifies broken files.
- UASTC on ALL textures. Tripled size vs ETC1S where the quality isn't needed.
- Running both Draco + Meshopt. Pick one per file (they're alternative encodings).
- Shipping 8K textures. Mobile: 2K max. Desktop: 4K max.
- Not caching. Every reload re-fetches. Use HTTP cache headers +
THREE.Cache.enabled = true.
7. The monitoring loop
Ship with Lighthouse + RUM:
- Time to Largest Contentful Paint.
- Total transfer size.
- GPU memory used.
- Frames dropped during asset load.
Iterate pipeline settings based on real user data.
8. Season 6 recap
glTF internals. Draco. KTX2. Meshopt. gltf-transform. LOD tools. Blender export. Atlasing. Streaming. Full pipeline.
Ten articles covering the boring half of shipping 3D apps. The half that decides whether your app loads at all.
Season 7 next: UI & UX in 3D. HUDs. Worldspace UI. 3D menus. Gestures. Accessibility. The human interface to the render.
9. Takeaways
- Pipeline is 8 deterministic steps: validate, inspect, optimize, resize, compress-geo, compress-tex, ship.
- Automate in a shell script, run in CI.
- 10× size reduction with zero visible quality loss.
- Always monitor real-world load times.
- Iterate pipeline settings per asset.
Concept article — the pipeline is an offline script. All intermediate demos are in S6-01 through S6-09.