threejs-agents-model-optimizer

Installation
SKILL.md

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 count

Assessment 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.glb

Combined Pipeline (Single Command)

npx gltf-transform optimize input.glb output.glb \
  --compress draco \
  --texture-compress ktx2

Mesh 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 compress

KTX2 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 8

Draco 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.glb

Loading 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.01

Three.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 resize

NEVER 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

Official Sources

Related skills
Installs
9
GitHub Stars
1
First Seen
Apr 1, 2026