Skip to content
Three.js 3D Models Loading — Complete Guide to GLTF, GLB, OBJ, FBX & Draco Compression

Three.js 3D Models Loading — Complete Guide to GLTF, GLB, OBJ, FBX & Draco Compression

DodaTech Updated Jun 6, 2026 12 min read

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

Merging 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

  1. Apply Scale in Blender (Ctrl+A) — non-uniform scales corrupt normals
  2. File → Export → glTF 2.0 (.glb)
  3. Settings: +Y Up, Include Selected Objects, Draco compression if desired
  4. 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

Why is my model not showing up?
Most common causes: (1) wrong file path, (2) CORS blocking — use a local server, (3) missing Draco decoder, (4) model positioned off-screen — check position and size with Box3.
GLTF vs GLB — which should I use?
Use .glb for simplicity (one file). Use .gltf when you want the browser to cache textures and .bin separately.
How do I reduce model file size?
Enable Draco compression during export, reduce texture resolution to 1024×1024, remove unused vertex attributes, and merge static meshes.
Can I animate a loaded model?
Yes. GLTF and FBX support animation clips. Use THREE.AnimationMixer with clipAction() and update it each frame.
How do I make my model clickable?
Use Raycaster with intersectObjects(model.children, true) for recursive child checking.
Why does my model look different from Blender?
GLTF export only supports Principled BSDF. Custom node groups, normal map strength > 1, and certain texture setups don’t translate. Use the glTF-specific material workflow in Blender.

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

TopicDescriptionLink
AnimationPlaying model animations with AnimationMixerhttps://tutorials.dodatech.com/frontend/libraries/threejs/threejs-animation/
Lights & ShadowsLighting imported models properlyhttps://tutorials.dodatech.com/frontend/libraries/threejs/threejs-lights-shadows/
Interactivity & UIRaycasting on loaded models, click detectionhttps://tutorials.dodatech.com/frontend/libraries/threejs/threejs-interactivity-ui/
ParticlesParticle systems for atmospherehttps://tutorials.dodatech.com/frontend/libraries/threejs/threejs-particles/
Geometries & MaterialsUnderstanding PBR materials for exporthttps://tutorials.dodatech.com/frontend/libraries/threejs/threejs-geometries-materials/
HTMLServing files locallyHTML
Node.jsRunning gltf-transform CLINode.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