
Threejs Agents Model Optimizer
- 20 installs
- 11 repo stars
- Updated July 8, 2026
- openaec-foundation/three.js-claude-skill-package
Helps with ai & agent building tasks.
About
threejs-agents-model-optimizer is a Claude Code skill in the AI & Agent Building category.
- threejs-agents-model-optimizer
- AI & Agent Building
- AI-coding skill
Threejs Agents Model Optimizer by the numbers
- 20 all-time installs (skills.sh)
- +2 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #10,459 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/three.js-claude-skill-package --skill threejs-agents-model-optimizerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 20 |
|---|---|
| repo stars | ★ 11 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/three.js-claude-skill-package ↗ |
What it does
Helps with ai & agent building tasks.
Files
threejs-agents-model-optimizer
Quick Reference
Optimization Pipeline Overview
INPUT (.glb/.gltf)
|
v
[1. ASSESS] ── polygon count, texture sizes, file size, draw calls
|
v
[2. MESH OPTIMIZE] ── dedup, flatten, join, weld, simplify
|
v
[3. TEXTURE OPTIMIZE] ── resize, KTX2 compress, atlas
|
v
[4. COMPRESS] ── Draco mesh compression, quantize attributes
|
v
[5. VALIDATE] ── visual diff, file size check, runtime test
|
v
OUTPUT (optimized .glb)Target File Size Budgets
| Platform | Max File Size | Max Polygons | Max Texture Size |
|---|---|---|---|
| Mobile web | 2 MB | 50K triangles | 1024x1024 |
| Desktop web | 5 MB | 200K triangles | 2048x2048 |
| Desktop app | 20 MB | 500K triangles | 4096x4096 |
| Hero asset (single) | 1 MB | 30K triangles | 1024x1024 |
Tool Selection
| Tool | Best For | Install |
|---|---|---|
gltf-transform | Full pipeline, scriptable, Node.js API | npm i @gltf-transform/cli |
gltfpack | Fast one-shot compression | npm i -g gltfpack |
| Blender | Manual mesh editing, UV repacking | Blender 3.6+ |
Critical Warnings
NEVER skip the assessment step -- optimizing blindly wastes time and can produce worse results than the original.
NEVER apply Draco compression AND meshopt compression to the same file -- they are mutually exclusive. Choose ONE.
NEVER use KTX2 UASTC for diffuse color maps on mobile -- UASTC textures are 8-16 bytes/texel in VRAM. ALWAYS use ETC1S for color maps on mobile targets.
NEVER simplify meshes below 10% of original without visual validation -- aggressive simplification destroys silhouettes and UV mapping.
ALWAYS validate optimized models visually before shipping -- automated metrics cannot catch all visual artifacts.
ALWAYS keep the original unoptimized model in version control -- optimization is lossy and irreversible.
---
Step 1: Assessment
Before optimizing, ALWAYS gather these metrics:
# Using gltf-transform CLI
npx gltf-transform inspect model.glb
# Key output to check:
# - Mesh count and total triangle count
# - Texture count, dimensions, and format
# - Total file size (uncompressed)
# - Accessor count (indicates potential deduplication)
# - Animation track countAssessment Decision Tree
File size > target budget?
├── YES: Textures > 50% of file size?
│ ├── YES → Start with texture optimization (Step 3)
│ └── NO → Start with mesh optimization (Step 2)
└── NO: Draw calls > 50?
├── YES → Merge meshes (join/flatten)
└── NO → Only apply compression (Step 4)Polygon Count Guidelines
| Asset Type | Target Triangles | Notes |
|---|---|---|
| Background prop | 100-500 | Minimal detail |
| Mid-ground object | 1K-5K | Visible but not hero |
| Hero/focus object | 10K-50K | High detail, close-up |
| Character | 15K-30K | With LOD chain |
| Full scene | 100K-300K | All objects combined |
| Architectural (BIM) | 200K-500K | Simplified from CAD |
---
Step 2: Mesh Optimization
gltf-transform Mesh Pipeline
ALWAYS run these operations in this order:
# 1. Remove duplicate accessors and unused data
npx gltf-transform dedup input.glb deduped.glb
# 2. Flatten node hierarchy (removes empty nodes)
npx gltf-transform flatten deduped.glb flat.glb
# 3. Join meshes sharing the same material
npx gltf-transform join flat.glb joined.glb
# 4. Weld vertices (merge vertices within tolerance)
npx gltf-transform weld joined.glb welded.glb --tolerance 0.0001
# 5. Simplify mesh (reduce triangle count)
npx gltf-transform simplify welded.glb simplified.glb \
--ratio 0.5 \
--error 0.001
# 6. Remove unused resources
npx gltf-transform prune simplified.glb output.glbCombined Pipeline (Single Command)
npx gltf-transform optimize input.glb output.glb \
--compress draco \
--texture-compress ktx2Mesh Simplification Settings
| Quality Level | Ratio | Error Tolerance | Use Case |
|---|---|---|---|
| Minimal | 0.75 | 0.0005 | Subtle reduction, preserves detail |
| Moderate | 0.50 | 0.001 | Balanced quality/size |
| Aggressive | 0.25 | 0.005 | LOD1/LOD2 generation |
| Extreme | 0.10 | 0.01 | LOD3, distant objects only |
Weld Settings
| Tolerance | Effect |
|---|---|
| 0.0001 | Conservative -- merges only nearly-identical vertices |
| 0.001 | Standard -- good for most models |
| 0.01 | Aggressive -- may cause visible seams on hard edges |
---
Step 3: Texture Optimization
Texture Decision Tree
Texture type?
├── Color/Diffuse (baseColorTexture)
│ ├── Mobile → ETC1S (KTX2), max 1024px
│ └── Desktop → UASTC (KTX2), max 2048px
├── Normal map
│ └── ALWAYS UASTC (KTX2) — ETC1S causes visible artifacts on normals
├── ORM (occlusion/roughness/metallic)
│ └── ETC1S (KTX2) — perceptual quality less critical
├── Emissive
│ └── ETC1S (KTX2) — unless HDR, then keep as-is
└── HDR environment
└── Keep as .hdr or .exr — do NOT KTX2 compressKTX2 Compression Commands
# ETC1S: smaller file, lower quality (color maps, ORM)
npx gltf-transform ktx2 input.glb output.glb \
--slots "baseColorTexture,emissiveTexture,occlusionTexture" \
--filter "baseColorTexture=etc1s" \
--quality 128
# UASTC: larger file, higher quality (normal maps)
npx gltf-transform ktx2 input.glb output.glb \
--slots "normalTexture" \
--filter "normalTexture=uastc"KTX2: ETC1S vs UASTC
| Property | ETC1S | UASTC |
|---|---|---|
| File size | Very small (6-8x smaller) | Moderate (2-4x smaller) |
| VRAM usage | Small | Large (8-16 bytes/texel) |
| Quality | Good for color | Near-lossless |
| Decode speed | Fast | Requires transcoding |
| Best for | Color maps, ORM, emissive | Normal maps, detail textures |
Texture Resize
# Resize all textures to max 1024x1024
npx gltf-transform resize input.glb output.glb --width 1024 --height 1024
# Resize only textures larger than 2048px
npx gltf-transform resize input.glb output.glb --width 2048 --height 2048---
Step 4: Mesh Compression
Draco Compression
# Default Draco compression
npx gltf-transform draco input.glb output.glb
# Draco with quality settings
npx gltf-transform draco input.glb output.glb \
--quantize-position 14 \
--quantize-normal 10 \
--quantize-texcoord 12 \
--quantize-color 8Draco Quantization Bits
| Attribute | Default | High Quality | Aggressive |
|---|---|---|---|
| Position | 14 bits | 16 bits | 11 bits |
| Normal | 10 bits | 12 bits | 8 bits |
| TexCoord | 12 bits | 14 bits | 10 bits |
| Color | 8 bits | 10 bits | 6 bits |
Higher bits = better quality, larger file. 14-bit position is sufficient for most models.
Quantize Without Draco
# Quantize attributes (reduces file size without Draco dependency)
npx gltf-transform quantize input.glb output.glbLoading Draco-Compressed Models in Three.js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const dracoLoader = new DRACOLoader();
// ALWAYS set the decoder path — Draco uses WASM
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/');
dracoLoader.preload();
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);
gltfLoader.load('model-draco.glb', (gltf) => {
scene.add(gltf.scene);
});
// ALWAYS dispose when done
dracoLoader.dispose();Loading KTX2-Compressed Textures
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
const ktx2Loader = new KTX2Loader();
// ALWAYS set the transcoder path — KTX2 uses WASM basis_transcoder
ktx2Loader.setTranscoderPath('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/basis/');
ktx2Loader.detectSupport(renderer);
const gltfLoader = new GLTFLoader();
gltfLoader.setKTX2Loader(ktx2Loader);---
Step 5: Validation
Validation Checklist
After optimization, ALWAYS verify:
1. File size -- meets target budget from Step 1 2. Visual quality -- load in Three.js viewer, compare to original 3. Bounding box -- same dimensions as original (no scale errors) 4. Materials -- all textures load, no missing maps 5. Animations -- all clips play correctly (if applicable) 6. Performance -- measure draw calls, frame time
# Compare file sizes
ls -la original.glb optimized.glb
# Validate GLTF structure
npx gltf-transform inspect optimized.glb
# Check for errors
npx gltf-transform validate optimized.glb---
LOD Generation
LOD Chain Strategy
| LOD Level | Screen Coverage | Triangle Ratio | Distance |
|---|---|---|---|
| LOD0 | >30% screen | 1.0 (full) | Near |
| LOD1 | 10-30% screen | 0.5 | Medium |
| LOD2 | 3-10% screen | 0.25 | Far |
| LOD3 | <3% screen | 0.10 | Very far |
Generating LODs with gltf-transform
# Generate LOD chain
npx gltf-transform simplify model.glb lod0.glb --ratio 1.0
npx gltf-transform simplify model.glb lod1.glb --ratio 0.5 --error 0.001
npx gltf-transform simplify model.glb lod2.glb --ratio 0.25 --error 0.005
npx gltf-transform simplify model.glb lod3.glb --ratio 0.10 --error 0.01Three.js LOD Implementation
import * as THREE from 'three';
const lod = new THREE.LOD();
// ALWAYS add LODs from highest to lowest detail
lod.addLevel(meshLOD0, 0); // distance 0 = closest
lod.addLevel(meshLOD1, 10); // switch at 10 units
lod.addLevel(meshLOD2, 30); // switch at 30 units
lod.addLevel(meshLOD3, 80); // switch at 80 units
scene.add(lod);
// In render loop — ALWAYS call update for LOD switching
lod.update(camera);---
gltfpack Alternative
For quick one-shot optimization without a pipeline:
# Install
npm install -g gltfpack
# Optimize with meshopt compression
gltfpack -i input.glb -o output.glb -cc -tc
# Flags:
# -cc mesh compression (meshopt)
# -tc texture compression (KTX2/BasisU)
# -si N simplify to ratio (0.0-1.0)
# -tp texture power-of-two resizeNEVER mix gltfpack meshopt output with gltf-transform Draco -- the compression formats are incompatible.
Loading Meshopt-Compressed Models
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
const gltfLoader = new GLTFLoader();
gltfLoader.setMeshoptDecoder(MeshoptDecoder);---
Blender Export Optimization
When exporting from Blender to glTF:
| Setting | Recommended Value | Why |
|---|---|---|
| Format | glTF Binary (.glb) | Single file, smaller |
| Apply Modifiers | ON | Bakes procedural geometry |
| Compression | OFF | Compress with gltf-transform after |
| Textures | Include | Embedded in .glb |
| Limit to | Selected Objects | Avoid exporting hidden/unused |
| +Y Up | ON | Three.js convention |
NEVER enable Draco in Blender's exporter -- Blender's Draco implementation is outdated. ALWAYS compress with gltf-transform or gltfpack after export.
---
Node.js Scripting with gltf-transform API
import { NodeIO } from '@gltf-transform/core';
import { dedup, flatten, join, weld, simplify,
textureCompress, draco, prune, quantize } from '@gltf-transform/functions';
import draco3d from 'draco3dgltf';
import sharp from 'sharp';
const io = new NodeIO()
.registerExtensions(KHRONOS_EXTENSIONS)
.registerDependencies({
'draco3d.decoder': await draco3d.createDecoderModule(),
'draco3d.encoder': await draco3d.createEncoderModule(),
});
const document = await io.read('input.glb');
// ALWAYS run transforms in dependency order
await document.transform(
dedup(),
flatten(),
join(),
weld({ tolerance: 0.0001 }),
simplify({ ratio: 0.5, error: 0.001 }),
textureCompress({ targetFormat: 'ktx2' }),
draco(),
prune(),
quantize(),
);
await io.write('output.glb', document);---
Reference Links
- references/methods.md -- gltf-transform API and Three.js loader methods
- references/examples.md -- Complete optimization pipeline examples
- references/anti-patterns.md -- Common optimization mistakes
Official Sources
- https://gltf-transform.dev/
- https://threejs.org/docs/#examples/en/loaders/GLTFLoader
- https://threejs.org/docs/#examples/en/loaders/DRACOLoader
- https://threejs.org/docs/#examples/en/loaders/KTX2Loader
- https://threejs.org/docs/#api/en/objects/LOD
- https://github.com/zeux/meshoptimizer
anti-patterns.md -- threejs-agents-model-optimizer
Anti-Pattern 1: Mixing Draco and Meshopt Compression
WRONG:
# Compress with Draco first
npx gltf-transform draco model.glb draco-model.glb
# Then try to add meshopt on top
gltfpack -i draco-model.glb -o final.glb -ccWHY: Draco and meshopt are mutually exclusive mesh compression formats. They use different GLTF extensions (KHR_draco_mesh_compression vs EXT_meshopt_compression). Applying both corrupts the file or silently drops one format. The loader will fail or produce garbage geometry.
CORRECT:
# Choose ONE compression format
npx gltf-transform draco model.glb output.glb
# OR
gltfpack -i model.glb -o output.glb -cc---
Anti-Pattern 2: Optimizing Without Assessment
WRONG:
# Just blindly apply everything
npx gltf-transform optimize model.glb output.glb --compress draco --texture-compress ktx2
# "It's smaller, ship it!"WHY: Without knowing the original polygon count, texture sizes, and file size breakdown, you cannot make informed decisions. A model with 500 triangles and 40MB of textures needs texture optimization, not mesh simplification. Blindly simplifying wastes time and may degrade quality on the wrong axis.
CORRECT:
# ALWAYS assess first
npx gltf-transform inspect model.glb
# Read the output, then decide which optimizations to apply---
Anti-Pattern 3: ETC1S for Normal Maps
WRONG:
npx gltf-transform ktx2 model.glb output.glb \
--filter "normalTexture=etc1s"WHY: ETC1S is a lossy format optimized for perceptual color quality. Normal maps contain mathematical direction data, not perceptual color. ETC1S compression introduces block artifacts that cause visible lighting errors: faceted shading, banding on smooth surfaces, and sparkle artifacts.
CORRECT:
# ALWAYS use UASTC for normal maps
npx gltf-transform ktx2 model.glb output.glb \
--filter "normalTexture=uastc" \
--filter "baseColorTexture=etc1s"---
Anti-Pattern 4: Joining Animated Meshes
WRONG:
# Model has animated doors and windows
npx gltf-transform join building.glb joined.glb
# Now doors and windows are merged into walls -- animations breakWHY: The join command merges meshes that share materials into single meshes. If those meshes have independent animations (skeletal or morph target), joining them destroys the animation targets. The merged mesh cannot be animated per-part.
CORRECT:
# Flatten and dedup, but skip join for animated models
npx gltf-transform dedup building.glb step1.glb
npx gltf-transform flatten step1.glb step2.glb
# Do NOT join if parts are independently animated
npx gltf-transform weld step2.glb output.glb---
Anti-Pattern 5: Draco Compression in Blender Export
WRONG:
Blender Export Settings:
Format: glTF Binary (.glb)
Compression: ✓ Draco <-- enabling thisWHY: Blender's built-in Draco encoder uses an older version and produces suboptimal compression ratios. More importantly, it prevents further pipeline optimization -- gltf-transform cannot modify Draco-compressed meshes without first decompressing them, which adds complexity and may introduce rounding errors.
CORRECT:
Blender Export Settings:
Format: glTF Binary (.glb)
Compression: ✗ None <-- keep this off
# Then compress with gltf-transform
npx gltf-transform draco exported.glb final.glb---
Anti-Pattern 6: Over-Simplification Without Visual Check
WRONG:
# Simplify to 5% -- maximum compression!
npx gltf-transform simplify character.glb output.glb \
--ratio 0.05 --error 0.02
# Ship without looking at itWHY: Extreme simplification (below 10% of original) destroys silhouettes, UV mapping, and surface detail. Characters lose fingers, architectural models lose window frames, mechanical parts lose functional geometry. The --error tolerance alone cannot prevent all visual artifacts because it measures geometric deviation, not perceptual quality.
CORRECT:
# Simplify conservatively
npx gltf-transform simplify character.glb output.glb \
--ratio 0.5 --error 0.001
# ALWAYS load the result in a Three.js viewer and compare side-by-side
# If more reduction is needed, decrease ratio incrementally (0.4, 0.3, ...)---
Anti-Pattern 7: Forgetting Decoder Setup at Runtime
WRONG:
const loader = new GLTFLoader();
// Loading a Draco-compressed model without setting up DRACOLoader
const gltf = await loader.loadAsync('model-draco.glb');
// Error: "No DRACOLoader instance provided"WHY: Draco and KTX2 compressed assets require WASM decoders at runtime. The GLTFLoader does not include these by default. Without the decoder, loading fails with a cryptic error or silently produces empty geometry.
CORRECT:
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/draco/');
dracoLoader.preload();
const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath('/basis/');
ktx2Loader.detectSupport(renderer);
const loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);
loader.setKTX2Loader(ktx2Loader);---
Anti-Pattern 8: Not Disposing Decoders
WRONG:
function loadModel(url) {
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/draco/');
const loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);
return loader.loadAsync(url);
// dracoLoader WASM instance leaks every call
}WHY: DRACOLoader and KTX2Loader instantiate WASM modules that consume significant memory. Creating new instances per load without disposing them causes memory leaks. The WASM modules are never garbage collected.
CORRECT:
// Create ONCE, reuse, dispose when done
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/draco/');
dracoLoader.preload();
const loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);
// Load multiple models with the same loader instance
await loader.loadAsync('model1.glb');
await loader.loadAsync('model2.glb');
// Dispose when ALL loading is complete
dracoLoader.dispose();---
Anti-Pattern 9: UASTC on Mobile for Color Maps
WRONG:
npx gltf-transform ktx2 model.glb output.glb \
--filter "baseColorTexture=uastc"
# Deploy to mobile webWHY: UASTC textures are 8-16 bytes per texel in GPU VRAM. A single 2048x2048 UASTC texture uses 32-64MB of VRAM. Mobile GPUs typically have 1-4GB of shared VRAM. Using UASTC for color maps on mobile causes out-of-memory crashes, texture thrashing, and severe frame drops.
CORRECT:
# Use ETC1S for color maps on mobile -- 10-20x smaller VRAM footprint
npx gltf-transform ktx2 model.glb output.glb \
--filter "baseColorTexture=etc1s" \
--quality 128---
Anti-Pattern 10: Discarding the Original Model
WRONG:
# Optimize in-place, overwriting the original
npx gltf-transform optimize model.glb model.glb --compress draco
# Original is gone foreverWHY: All optimization operations are lossy. Mesh simplification removes geometry. Texture compression reduces quality. Draco quantization rounds vertex positions. If a bug is found, visual quality needs improvement, or a different optimization strategy is needed, the original model is required. Without it, re-optimization compounds losses.
CORRECT:
# ALWAYS write to a new file
npx gltf-transform optimize model.glb model-optimized.glb --compress draco
# Keep originals in version control or asset storage
# Use a naming convention: model.glb (original), model-web.glb (optimized)---
Anti-Pattern 11: Wrong Transform Order
WRONG:
# Compress BEFORE simplifying
npx gltf-transform draco model.glb step1.glb
npx gltf-transform simplify step1.glb step2.glb --ratio 0.5
# Draco decompression + re-compression introduces double quantization errorsWHY: Draco compression quantizes vertex positions. Simplifying after Draco means the simplifier works on already-quantized (less precise) data, producing worse results. Additionally, the output would need to be re-compressed, applying quantization twice and compounding precision loss.
CORRECT:
# ALWAYS follow this order:
# 1. dedup → 2. flatten → 3. join → 4. weld → 5. simplify
# → 6. texture optimize → 7. compress (Draco) → 8. prune → 9. quantize
npx gltf-transform dedup model.glb step1.glb
npx gltf-transform simplify step1.glb step2.glb --ratio 0.5
npx gltf-transform draco step2.glb output.glbexamples.md -- threejs-agents-model-optimizer
Example 1: Full Web Optimization Pipeline (CLI)
Optimize a 50MB architectural model to under 5MB for desktop web delivery.
# Step 1: Assess the model
npx gltf-transform inspect building.glb
# Output shows: 450K triangles, 12 textures at 4096x4096, 48MB total
# Step 2: Resize textures to 2048 max
npx gltf-transform resize building.glb step2.glb \
--width 2048 --height 2048
# Step 3: Deduplicate shared data
npx gltf-transform dedup step2.glb step3.glb
# Step 4: Flatten empty nodes
npx gltf-transform flatten step3.glb step4.glb
# Step 5: Join meshes by material
npx gltf-transform join step4.glb step5.glb
# Step 6: Weld close vertices
npx gltf-transform weld step5.glb step6.glb --tolerance 0.001
# Step 7: Simplify mesh to 50%
npx gltf-transform simplify step6.glb step7.glb \
--ratio 0.5 --error 0.001
# Step 8: Compress textures to KTX2
npx gltf-transform ktx2 step7.glb step8.glb
# Step 9: Apply Draco compression
npx gltf-transform draco step8.glb step9.glb
# Step 10: Prune unused data
npx gltf-transform prune step9.glb building-optimized.glb
# Verify result
npx gltf-transform inspect building-optimized.glb
# Expected: ~225K triangles, KTX2 textures, ~3-5MB---
Example 2: One-Command Optimization
For quick optimization without manual steps:
npx gltf-transform optimize input.glb output.glb \
--compress draco \
--texture-compress ktx2---
Example 3: Mobile-Optimized Asset
Strict optimization for mobile web (target: <2MB, <50K triangles).
# Aggressive texture resize
npx gltf-transform resize model.glb step1.glb \
--width 1024 --height 1024
# Aggressive mesh simplification
npx gltf-transform simplify step1.glb step2.glb \
--ratio 0.25 --error 0.005
# ETC1S compression for small file size
npx gltf-transform ktx2 step2.glb step3.glb \
--filter "baseColorTexture=etc1s" \
--quality 128
# Draco with aggressive quantization
npx gltf-transform draco step3.glb mobile-model.glb \
--quantize-position 11 \
--quantize-normal 8 \
--quantize-texcoord 10---
Example 4: LOD Chain Generation
Generate 4 LOD levels from a single high-poly model.
# LOD0: Full quality (just optimize, no simplification)
npx gltf-transform dedup hero.glb lod0.glb
npx gltf-transform weld lod0.glb lod0.glb --tolerance 0.0001
# LOD1: 50% triangles
npx gltf-transform simplify hero.glb lod1.glb \
--ratio 0.5 --error 0.001
# LOD2: 25% triangles
npx gltf-transform simplify hero.glb lod2.glb \
--ratio 0.25 --error 0.005
# LOD3: 10% triangles
npx gltf-transform simplify hero.glb lod3.glb \
--ratio 0.10 --error 0.01
# Compress all LODs
for f in lod0.glb lod1.glb lod2.glb lod3.glb; do
npx gltf-transform draco "$f" "compressed-$f"
doneLoading LOD Chain in Three.js
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/draco/');
dracoLoader.preload();
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);
const lod = new THREE.LOD();
const lodFiles = [
{ file: 'compressed-lod0.glb', distance: 0 },
{ file: 'compressed-lod1.glb', distance: 15 },
{ file: 'compressed-lod2.glb', distance: 40 },
{ file: 'compressed-lod3.glb', distance: 100 },
];
// Load all LOD levels
const loadPromises = lodFiles.map(({ file, distance }) =>
gltfLoader.loadAsync(file).then((gltf) => {
lod.addLevel(gltf.scene, distance);
})
);
await Promise.all(loadPromises);
scene.add(lod);
// LOD updates automatically if lod.autoUpdate is true (default)---
Example 5: Node.js Scripted Pipeline
Programmatic optimization for build scripts or CI/CD.
import { NodeIO } from '@gltf-transform/core';
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
import {
dedup, flatten, join, weld, simplify,
textureCompress, draco, prune, quantize
} from '@gltf-transform/functions';
import draco3d from 'draco3dgltf';
async function optimizeModel(inputPath, outputPath, options = {}) {
const {
simplifyRatio = 0.5,
simplifyError = 0.001,
maxTextureSize = 2048,
useDraco = true,
useKTX2 = true,
} = options;
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
if (useDraco) {
io.registerDependencies({
'draco3d.decoder': await draco3d.createDecoderModule(),
'draco3d.encoder': await draco3d.createEncoderModule(),
});
}
const document = await io.read(inputPath);
const transforms = [
dedup(),
flatten(),
join(),
weld({ tolerance: 0.0001 }),
];
if (simplifyRatio < 1.0) {
transforms.push(simplify({ ratio: simplifyRatio, error: simplifyError }));
}
if (useKTX2) {
transforms.push(textureCompress({ targetFormat: 'ktx2' }));
}
if (useDraco) {
transforms.push(draco());
}
transforms.push(prune(), quantize());
await document.transform(...transforms);
await io.write(outputPath, document);
console.log(`Optimized: ${inputPath} -> ${outputPath}`);
}
// Usage
await optimizeModel('assets/building.glb', 'dist/building.glb', {
simplifyRatio: 0.5,
useDraco: true,
useKTX2: true,
});---
Example 6: Three.js Runtime with All Decoders
Complete loader setup supporting Draco, KTX2, and Meshopt compressed models.
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
function createOptimizedLoader(renderer) {
// Draco decoder
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/');
dracoLoader.preload();
// KTX2 transcoder
const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath(
'https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/basis/'
);
ktx2Loader.detectSupport(renderer);
// GLTF loader with all decoders
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);
gltfLoader.setKTX2Loader(ktx2Loader);
gltfLoader.setMeshoptDecoder(MeshoptDecoder);
return {
loader: gltfLoader,
dispose() {
dracoLoader.dispose();
ktx2Loader.dispose();
},
};
}
// Usage
const renderer = new THREE.WebGLRenderer();
const { loader, dispose } = createOptimizedLoader(renderer);
const gltf = await loader.loadAsync('optimized-model.glb');
scene.add(gltf.scene);
// ALWAYS dispose decoders when no longer needed
dispose();---
Example 7: gltfpack Quick Optimization
One-command optimization using gltfpack (meshopt-based).
# Basic optimization with meshopt compression
gltfpack -i model.glb -o optimized.glb -cc -tc
# With simplification to 50%
gltfpack -i model.glb -o optimized.glb -cc -tc -si 0.5
# With texture power-of-two resize
gltfpack -i model.glb -o optimized.glb -cc -tc -tp
# Maximum compression
gltfpack -i model.glb -o optimized.glb -cc -tc -si 0.5 -tpLoading in Three.js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
const loader = new GLTFLoader();
loader.setMeshoptDecoder(MeshoptDecoder);
const gltf = await loader.loadAsync('optimized.glb');
scene.add(gltf.scene);---
Example 8: Batch Optimization Script
Optimize all GLB files in a directory.
#!/bin/bash
INPUT_DIR="./assets/models"
OUTPUT_DIR="./dist/models"
mkdir -p "$OUTPUT_DIR"
for file in "$INPUT_DIR"/*.glb; do
filename=$(basename "$file")
echo "Optimizing: $filename"
npx gltf-transform optimize "$file" "$OUTPUT_DIR/$filename" \
--compress draco \
--texture-compress ktx2
# Report size reduction
original=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file")
optimized=$(stat -f%z "$OUTPUT_DIR/$filename" 2>/dev/null || stat -c%s "$OUTPUT_DIR/$filename")
ratio=$(echo "scale=1; $optimized * 100 / $original" | bc)
echo " $original -> $optimized bytes (${ratio}%)"
donemethods.md -- threejs-agents-model-optimizer
gltf-transform CLI Commands
inspect
npx gltf-transform inspect <input>Returns mesh count, triangle count, texture dimensions, accessor count, file size breakdown. ALWAYS run before optimizing.
dedup
npx gltf-transform dedup <input> <output>Removes duplicate accessors, textures, and materials. Lossless operation. ALWAYS run first in any pipeline.
flatten
npx gltf-transform flatten <input> <output>Flattens the node hierarchy by applying parent transforms to children. Removes empty/intermediate nodes. Reduces draw call overhead.
join
npx gltf-transform join <input> <output>Merges meshes that share the same material into single draw calls. Reduces CPU overhead from draw call submission.
NEVER join meshes that need independent animation or interaction -- joining makes them inseparable.
weld
npx gltf-transform weld <input> <output> [--tolerance <float>]| Parameter | Default | Description |
|---|---|---|
--tolerance | 0.0001 | Vertex merge distance threshold |
Merges vertices within the specified tolerance. Reduces vertex count without changing geometry shape.
simplify
npx gltf-transform simplify <input> <output> --ratio <float> --error <float>| Parameter | Default | Description |
|---|---|---|
--ratio | 0.5 | Target ratio of original triangle count (0.0 - 1.0) |
--error | 0.001 | Maximum allowed geometric error |
--lock-border | false | Prevent simplification of mesh boundaries |
Uses meshoptimizer's simplification algorithm. The --error parameter is the maximum geometric deviation normalized to the mesh bounding box.
draco
npx gltf-transform draco <input> <output> [options]| Parameter | Default | Description |
|---|---|---|
--quantize-position | 14 | Position quantization bits (1-16) |
--quantize-normal | 10 | Normal quantization bits (1-16) |
--quantize-texcoord | 12 | Texture coordinate quantization bits (1-16) |
--quantize-color | 8 | Vertex color quantization bits (1-16) |
--quantize-generic | 12 | Generic attribute quantization bits (1-16) |
Applies Draco mesh compression. Requires DRACOLoader at runtime in Three.js.
ktx2
npx gltf-transform ktx2 <input> <output> [options]| Parameter | Default | Description |
|---|---|---|
--slots | all | Comma-separated texture slots to compress |
--filter | auto | Per-slot compression: etc1s or uastc |
--quality | 128 | ETC1S quality (1-255) |
--effort | 0 | UASTC effort level (0-5) |
Compresses textures to KTX2 format using Basis Universal. Requires KTX2Loader at runtime.
quantize
npx gltf-transform quantize <input> <output>Reduces attribute precision (e.g., float32 to int16) without Draco. Smaller file size, no runtime decoder needed.
prune
npx gltf-transform prune <input> <output>Removes unreferenced accessors, textures, materials, and meshes. ALWAYS run as the last step before compression.
resize
npx gltf-transform resize <input> <output> --width <int> --height <int>Resizes all textures that exceed the specified dimensions. Maintains aspect ratio.
optimize (combined)
npx gltf-transform optimize <input> <output> [--compress draco|meshopt] [--texture-compress ktx2|webp]Convenience command that runs dedup, flatten, join, weld, prune, and optional compression in a single pass.
---
Three.js Loader API
DRACOLoader
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';| Method | Signature | Description |
|---|---|---|
setDecoderPath | (path: string): this | Path to Draco WASM decoder files |
setDecoderConfig | (config: object): this | Decoder configuration |
preload | (): this | Begin loading the decoder module |
dispose | (): void | Release decoder resources |
KTX2Loader
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';| Method | Signature | Description |
|---|---|---|
setTranscoderPath | (path: string): this | Path to Basis Universal transcoder WASM |
detectSupport | (renderer: WebGLRenderer): this | Detect GPU compressed format support |
dispose | (): void | Release transcoder resources |
GLTFLoader Integration
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';| Method | Signature | Description |
|---|---|---|
setDRACOLoader | (loader: DRACOLoader): this | Enable Draco decompression |
setKTX2Loader | (loader: KTX2Loader): this | Enable KTX2 texture decoding |
setMeshoptDecoder | (decoder: object): this | Enable meshopt decompression |
load | (url, onLoad, onProgress, onError): void | Load GLTF/GLB file |
loadAsync | (url, onProgress): Promise<GLTF> | Promise-based loading |
THREE.LOD
const lod = new THREE.LOD();| Method | Signature | Description |
|---|---|---|
addLevel | (object: Object3D, distance: number, hysteresis?: number): this | Add a LOD level |
getCurrentLevel | (): number | Get current active level index |
getObjectForDistance | (distance: number): Object3D | Get object at distance |
update | (camera: Camera): void | Update LOD selection based on camera |
autoUpdate | boolean (property) | Auto-update in render loop (default: true) |
The hysteresis parameter (r160+) prevents flickering at LOD boundaries by requiring objects to move beyond the threshold by this fraction before switching.
---
gltf-transform Node.js API
Core Classes
import { NodeIO, WebIO, Document } from '@gltf-transform/core';| Class | Description |
|---|---|
NodeIO | Read/write GLTF in Node.js (filesystem + network) |
WebIO | Read/write GLTF in browser (fetch-based) |
Document | In-memory GLTF document graph |
Transform Functions
import {
dedup, flatten, join, weld, simplify,
prune, quantize, draco, meshopt,
textureCompress, textureResize
} from '@gltf-transform/functions';ALWAYS call document.transform() with transforms in dependency order:
await document.transform(
dedup(), // 1. Remove duplicates
flatten(), // 2. Flatten hierarchy
join(), // 3. Merge meshes
weld(options), // 4. Merge vertices
simplify(options), // 5. Reduce triangles
textureResize({ size: [2048, 2048] }), // 6. Resize textures
textureCompress({ targetFormat: 'ktx2' }), // 7. Compress textures
draco(), // 8. Compress mesh
prune(), // 9. Remove unused
quantize(), // 10. Reduce precision
);