Three.js 3D Models Loading — Complete Guide to GLTF, GLB, OBJ, FBX & Draco Compression
Loading external 3D models transforms Three.js from a geometry playground into a serious 3D application framework. Real-world projects use models created in Blender, Fusion 360, Maya, or Cinema 4D — exported to formats Three.js can consume.
What You’ll Learn
By the end of this tutorial, you’ll load GLTF/GLB models with Draco compression, handle OBJ/FBX formats, use LoadingManager for progress tracking, optimize models for performance, auto-center imported models, and build a complete 3D Model Viewer with controls, wireframe toggle, and loading progress bar.
Why Model Loading Matters
Hard-coding geometries with BoxGeometry and SphereGeometry works for prototypes. Real applications need complex models — characters, vehicles, buildings, products. At DodaTech’s Durga Antivirus Pro, the 3D threat dashboard loads custom GLB models representing server racks and network nodes, each with embedded metadata for threat levels. The ability to load, traverse, and interact with external models is what makes Three.js production-ready.
flowchart LR
A[3D Modeling Tool] --> B[Export Format]
B --> C{Choose Format}
C --> D[GLTF/GLB: Recommended]
C --> E[OBJ: Legacy, no animation]
C --> F[FBX: Animation support]
D --> G[Add Draco Compression]
G --> H[Three.js Loader]
H --> I[LoadingManager: Progress]
I --> J[Scene Integration]
J --> K[3D Model Viewer Project]
style D fill:#44aa88,color:#fff,stroke:none
Import Paths — Getting This Right First
Three.js loaders live under three/examples/jsm/loaders/. This path changed in recent versions — wrong import paths are the #1 loading mistake.
// ✅ Correct (modern Three.js r125+)
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
// ❌ Wrong (old path — no longer works)
import { GLTFLoader } from "three/examples/js/loaders/GLTFLoader.js";
// Using import maps (CDN examples in this tutorial):
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
// three/addons/ maps to three/examples/jsm/ via import map
GLTF / GLB — The Recommended Format
GLTF is the “JPEG of 3D” — compact, self-contained, and widely supported. .glb is the binary version (single file). .gltf is JSON and may reference external .bin and texture files.
Basic Load
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
var loader = new GLTFLoader();
loader.load("models/scene.glb", function(gltf) {
scene.add(gltf.scene);
}, undefined, function(error) {
console.error("Failed to load model:", error);
});Why this works: The loader fetches the file, parses the GLTF JSON structure, creates Three.js objects (meshes, materials, textures) automatically, and returns the result in a gltf object.
What’s Inside the gltf Object
loader.load("models/scene.glb", function(gltf) {
gltf.scene; // Root THREE.Group — add this to your scene
gltf.scenes; // Array of scenes (usually one)
gltf.animations; // Array of THREE.AnimationClip (if model has animations)
gltf.cameras; // Cameras defined in the source file
gltf.asset; // Metadata (generator, version, copyright)
});Traversing the Loaded Model
After loading, you often need to modify materials or find specific objects inside the model:
gltf.scene.traverse(function(child) {
if (child.isMesh) {
child.material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
child.castShadow = true;
child.receiveShadow = true;
}
if (child.isMesh && child.name === "Wheel_FL") {
// Found the front-left wheel — attach custom logic here
wheelMeshes.push(child);
}
});Why traverse? A GLTF model is a tree of groups and meshes. traverse() walks through every node. Without it, you’d need to know the exact hierarchy to access child meshes.
Draco Compression — 10-20× Smaller
Draco compresses geometry data dramatically but requires a decoder at runtime.
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";
var loader = new GLTFLoader();
var dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("https://www.gstatic.com/draco/versioned/decoders/1.5.6/");
loader.setDRACOLoader(dracoLoader);
loader.load("models/compressed_model.glb", function(gltf) {
scene.add(gltf.scene);
});Common Draco mistakes:
- Missing decoder path — the decoder WASM files must be served. Use the Google CDN above or copy from
node_modules/three/examples/jsm/libs/draco/ - Wrong decoder version — match your Three.js Draco decoder version
- CORS — if the decoder is on a different origin, you need proper COOP/COEP headers or host on the same origin
OBJ + MTL — Legacy Format
OBJ is an older, text-based format. Materials are often in a separate .mtl file. No animation support.
import { OBJLoader } from "three/addons/loaders/OBJLoader.js";
import { MTLLoader } from "three/addons/loaders/MTLLoader.js";
// Load materials first, then geometry
var mtlLoader = new MTLLoader();
mtlLoader.load("models/model.mtl", function(materials) {
materials.preload();
var objLoader = new OBJLoader();
objLoader.setMaterials(materials);
objLoader.load("models/model.obj", function(root) {
scene.add(root);
});
});Limitations: OBJ does not support animation, PBR materials, or cameras. Prefer GLTF for new projects.
FBX — Animation Support
FBX is common in game pipelines and supports skeletons, morph targets, and animation.
import { FBXLoader } from "three/addons/loaders/FBXLoader.js";
var loader = new FBXLoader();
loader.load("models/character.fbx", function(object) {
scene.add(object);
});Playing FBX Animations
var mixer;
loader.load("models/character.fbx", function(object) {
scene.add(object);
mixer = new THREE.AnimationMixer(object);
var action = mixer.clipAction(object.animations[0]); // First animation clip
action.play();
});
function animate() {
requestAnimationFrame(animate);
var delta = clock.getDelta();
if (mixer) mixer.update(delta);
renderer.render(scene, camera);
}LoadingManager — Progress Tracking
For realistic loading experiences (progress bars, loading screens):
var manager = new THREE.LoadingManager();
manager.onStart = function(url, itemsLoaded, itemsTotal) {
console.log("Started loading " + url + " (" + itemsLoaded + "/" + itemsTotal + ")");
};
manager.onProgress = function(url, itemsLoaded, itemsTotal) {
var progress = (itemsLoaded / itemsTotal) * 100;
updateProgressBar(progress); // Update UI
};
manager.onError = function(url) {
console.error("Failed to load " + url);
};
manager.onLoad = function() {
console.log("All assets loaded!");
hideLoadingScreen();
};
var gltfLoader = new GLTFLoader(manager);
var textureLoader = new THREE.TextureLoader(manager);Model Optimization
Draco Compression (From Blender)
Apply Draco during export or use gltf-transform CLI:
npx gltf-transform draco input.glb output.glbMerging Geometry (Fewer Draw Calls)
import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
var geometries = [];
model.traverse(function(child) {
if (child.isMesh) geometries.push(child.geometry);
});
var merged = mergeGeometries(geometries, false);
var mergedMesh = new THREE.Mesh(merged, new THREE.MeshStandardMaterial());Level of Detail (LOD)
var lod = new THREE.LOD();
lod.addLevel(highPolyMesh, 0); // Full detail when close
lod.addLevel(mediumPolyMesh, 50); // Medium when 50+ units away
lod.addLevel(lowPolyMesh, 150); // Low when 150+ units away
scene.add(lod);
// In animation loop:
lod.update(camera);Auto-Center and Fit Imported Models
Imported models rarely land at the origin with the right size:
loader.load("models/scene.glb", function(gltf) {
var box = new THREE.Box3().setFromObject(gltf.scene);
var center = box.getCenter(new THREE.Vector3());
var size = box.getSize(new THREE.Vector3());
// Move model center to origin
gltf.scene.position.sub(center);
// Scale to fit in a 5-unit box
var maxDim = Math.max(size.x, size.y, size.z);
var scale = 5 / maxDim;
gltf.scene.scale.setScalar(scale);
scene.add(gltf.scene);
});Raycasting on Loaded Models
Click detection works on all meshes inside a loaded group:
var raycaster = new THREE.Raycaster();
var pointer = new THREE.Vector2();
renderer.domElement.addEventListener("click", function(event) {
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
var intersects = raycaster.intersectObjects(model.children, true); // recursive!
if (intersects.length > 0) {
intersects[0].object.material.color.setHex(0xff0000);
}
});Blender → Three.js Pipeline
- Apply Scale in Blender (Ctrl+A) — non-uniform scales corrupt normals
- File → Export → glTF 2.0 (.glb)
- Settings: +Y Up, Include Selected Objects, Draco compression if desired
- Use Principled BSDF in Blender (maps to PBR in Three.js)
Common Mistakes
1. Wrong import path for loaders
The #1 issue. Modern Three.js uses three/examples/jsm/loaders/ or three/addons/loaders/. The old three/examples/js/ path is gone.
2. Missing Draco decoder files
Draco-compressed models need a decoder at runtime. Without the decoder WASM files, loading silently fails. Always verify the decoder path.
3. CORS blocking model loading
Opening HTML from file:// blocks cross-origin requests. Always use a local server: npx serve . or python -m http.server.
4. Model loaded but off-screen
Imported models are rarely at the origin or correctly sized. Auto-center and scale after loading using Box3.
5. Not disposing loaders after use
Long-lived applications should call loader.dispose() when loaders are no longer needed to free memory.
6. Forgetting recursive flag in raycasting
raycaster.intersectObjects(model.children, false) only checks direct children. Use true for recursive traversal into nested groups.
7. Using non-PBR materials in Blender
GLTF export only supports Principled BSDF. Custom node groups, Cycles materials, or Eevee-specific setups won’t translate. Use the glTF-specific material workflow.
Practice Questions
Q1: Why is GLTF/GLB the recommended format for Three.js? A: It’s compact, self-contained (especially GLB), supports PBR materials, animation, cameras, and is designed for real-time rendering. It’s the “JPEG of 3D.”
Q2: What does the Draco decoder do? A: It decompresses Draco-encoded geometry at runtime. Draco can reduce file sizes by 10-20×, but the decoder must be available as WebAssembly files served alongside your app.
Q3: Why might a loaded model appear black? A: Most likely: the model uses materials that didn’t export correctly (e.g., Cycles materials instead of Principled BSDF), or there are no lights in the scene. Check both the export settings and scene lighting.
Q4: How do you handle models that load at the wrong position or scale?
A: Use THREE.Box3().setFromObject(model) to compute the bounding box, then center and scale the model accordingly using position.sub(center) and scale.setScalar(targetSize / maxDim).
Q5: What’s the difference between .glb and .gltf?
A: .glb is a single binary file containing everything (geometry, textures, materials). .gltf is a JSON file that references external .bin and texture files. GLB is simpler; GLTF allows the browser to cache assets separately.
Challenge: Find a free GLB model online (e.g., from Poly Pizza or Sketchfab), load it into Three.js, add raycasting so clicking any part highlights it, and add a toggle button that switches between the original texture and a solid red color.
FAQ
Try It Yourself
Copy this complete HTML file — it includes the full 3D Model Viewer with loading progress, auto-rotation, wireframe toggle, and fallback geometry if the model fails to load.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>3D Model Viewer — Three.js</title>
<style>
body { margin: 0; overflow: hidden; font-family: system-ui, sans-serif; }
#info { position: absolute; top: 16px; left: 50%; transform: translateX(-50%);
background: rgba(0,0,0,0.7); color: #fff; padding: 8px 20px;
border-radius: 8px; pointer-events: none; z-index: 10; font-size: 14px; }
#progress-container { position: absolute; top: 0; left: 0; width: 100%; height: 4px;
background: #333; z-index: 20; }
#progress-bar { width: 0%; height: 100%; background: #00bcd4; transition: width 0.3s; }
#controls { position: absolute; bottom: 24px; left: 50%; transform: translateX(-50%);
display: flex; gap: 12px; z-index: 10; }
#controls button { background: rgba(0,0,0,0.7); color: #fff;
border: 1px solid rgba(255,255,255,0.2); padding: 10px 20px;
border-radius: 8px; cursor: pointer; font-size: 14px; }
#controls button:hover { background: rgba(0,0,0,0.9); }
</style>
</head>
<body>
<div id="info">Drag to rotate · Scroll to zoom</div>
<div id="progress-container"><div id="progress-bar"></div></div>
<div id="controls">
<button id="btn-autorotate">Toggle Auto-Rotate</button>
<button id="btn-wireframe">Toggle Wireframe</button>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111122);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(5, 5, 10);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.autoRotate = false;
controls.autoRotateSpeed = 2.0;
controls.target.set(0, 0, 0);
const ambientLight = new THREE.AmbientLight(0x404060, 0.5);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 2.5);
dirLight.position.set(8, 12, 6);
dirLight.castShadow = true;
scene.add(dirLight);
const fillLight = new THREE.DirectionalLight(0x4488ff, 0.8);
fillLight.position.set(-4, 2, 6);
scene.add(fillLight);
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(20, 20),
new THREE.ShadowMaterial({ color: 0x111122, opacity: 0.4 })
);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -1.5;
ground.receiveShadow = true;
scene.add(ground);
const progressBar = document.getElementById('progress-bar');
const manager = new THREE.LoadingManager();
manager.onProgress = (url, loaded, total) => {
progressBar.style.width = `${(loaded / total) * 100}%`;
};
manager.onLoad = () => {
progressBar.style.width = '100%';
setTimeout(() => { document.getElementById('progress-container').style.opacity = '0'; }, 500);
};
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/');
const gltfLoader = new GLTFLoader(manager);
gltfLoader.setDRACOLoader(dracoLoader);
let model = null;
gltfLoader.load(
'https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf',
(gltf) => {
model = gltf.scene;
model.traverse((child) => { if (child.isMesh) { child.castShadow = true; child.receiveShadow = true; } });
scene.add(model);
},
undefined,
(err) => {
console.warn('Could not load external model, creating placeholder.', err);
const geo = new THREE.TorusKnotGeometry(1, 0.35, 128, 32);
const mat = new THREE.MeshStandardMaterial({ color: 0x4488ff, metalness: 0.7, roughness: 0.2 });
model = new THREE.Mesh(geo, mat);
model.castShadow = true;
scene.add(model);
progressBar.style.width = '100%';
setTimeout(() => { document.getElementById('progress-container').style.opacity = '0'; }, 500);
}
);
let wireframeMode = false;
document.getElementById('btn-wireframe').addEventListener('click', () => {
wireframeMode = !wireframeMode;
scene.traverse((child) => {
if (child.isMesh && child.material) {
if (Array.isArray(child.material)) child.material.forEach(m => m.wireframe = wireframeMode);
else child.material.wireframe = wireframeMode;
}
});
});
document.getElementById('btn-autorotate').addEventListener('click', () => {
controls.autoRotate = !controls.autoRotate;
});
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>What’s Next
| Topic | Description | Link |
|---|---|---|
| Animation | Playing model animations with AnimationMixer | https://tutorials.dodatech.com/frontend/libraries/threejs/threejs-animation/ |
| Lights & Shadows | Lighting imported models properly | https://tutorials.dodatech.com/frontend/libraries/threejs/threejs-lights-shadows/ |
| Interactivity & UI | Raycasting on loaded models, click detection | https://tutorials.dodatech.com/frontend/libraries/threejs/threejs-interactivity-ui/ |
| Particles | Particle systems for atmosphere | https://tutorials.dodatech.com/frontend/libraries/threejs/threejs-particles/ |
| Geometries & Materials | Understanding PBR materials for export | https://tutorials.dodatech.com/frontend/libraries/threejs/threejs-geometries-materials/ |
| HTML | Serving files locally | HTML |
| Node.js | Running gltf-transform CLI | Node.js |
You now know how to load, optimize, and interact with external 3D models in Three.js. The same GLTF loading pipeline powers the network node visualization in Durga Antivirus Pro’s 3D threat dashboard.
What’s Next
Congratulations on completing this Threejs Models Loading tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro