3DAssets.dev

three.js guide

How to load a free GLB model in three.js

A working GLTFLoader setup, straight from a CDN URL with no build step, plus animation, sensible scaling and the four mistakes that account for most blank screens.

3DAssets.dev4 min read

Loading a model in three.js is genuinely a five-line job, and most of the difficulty people hit is in the four lines around it. Here is the whole thing, with the parts that actually go wrong called out.

The minimal version

GLTFLoader is not in the three.js core bundle; it lives in the addons directory. Import it, point it at a URL, add the result to your scene:

1import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
2
3const loader = new GLTFLoader()
4loader.load('https://cdn.3dassets.dev/assets/28312/v1/model.glb', (gltf) => {
5 scene.add(gltf.scene)
6})

That URL is a real model, a young oak, and you can paste that snippet as it stands. Every asset page on the site generates this snippet with its own URL already in it, so in practice you copy rather than type.

A .glb is the binary form of glTF with everything packed into one file, textures included. There is no second request, no material wiring and no texture path to fix up.

The version that also handles failure

The three-argument form gives you progress and, more usefully, errors:

1loader.load(
2 'https://cdn.3dassets.dev/assets/28312/v1/model.glb',
3 (gltf) => {
4 scene.add(gltf.scene)
5 },
6 (event) => {
7 console.log(`${((event.loaded / event.total) * 100).toFixed(0)}% loaded`)
8 },
9 (error) => {
10 console.error('Model failed to load', error)
11 },
12)

If you skip the error callback, a failed load is silent and you spend twenty minutes looking at your camera setup. Add it before you need it.

Loading from a CDN rather than your own server

Passing a remote URL straight to GLTFLoader works as long as the host sends the right CORS headers. Models on cdn.3dassets.dev allow GET from any origin, and each URL is immutable: the bytes behind it never change, so you can cache it hard and never think about invalidation.

Hotlinking is fine for prototypes, demos and production alike. Copy the file into your own build when you need it available offline, or when you want it bundled with the rest of your assets for a single deployment artefact.

To get the browser fetching the model in parallel with your JavaScript instead of after it, preload:

1<link rel="preload" as="fetch" crossorigin href="https://cdn.3dassets.dev/assets/28312/v1/model.glb">

Animated models

Models with moving parts carry their animation clips in the same file. The model below is a wooden supply crate that opens and closes. Play a clip with an AnimationMixer, and update it from your render loop:

1import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
2import { AnimationMixer } from 'three'
3
4let mixer
5
6const loader = new GLTFLoader()
7loader.load('https://cdn.3dassets.dev/assets/27447/v1/model.glb', (gltf) => {
8 scene.add(gltf.scene)
9 mixer = new AnimationMixer(gltf.scene)
10 mixer.clipAction(gltf.animations[0]).play()
11})
12
13// Call from your render loop; deltaSeconds is time since the previous frame.
14function updateAssetAnimation(deltaSeconds) {
15 mixer?.update(deltaSeconds)
16}

The forgettable half is the update call. A mixer that is never updated produces a model frozen in its first frame, which looks exactly like a model with no animation at all.

Clips are named rather than numbered, and the names are published on each asset, so you can pick one deliberately instead of taking index zero:

1const open = gltf.animations.find((clip) => clip.name === 'open')
2mixer.clipAction(open).play()

The vocabulary is small on purpose: open and close come as a pair, and spin, roll, idle and similar say what the motion is. A crate reports open and close; a ceiling fan reports spin.

Scale, orientation and pivots

Models on 3DAssets.dev are authored in metres with +Y up, which is what three.js expects, and they sit on the ground plane rather than floating around their own centre. So a five-metre tree arrives five units tall and its base is at y = 0. You place it with a position, not a scale factor.

This matters more than it sounds. If you find yourself writing model.scale.set(0.01, 0.01, 0.01) to make something fit, the usual cause is not the model: it is a camera set up for a scene measured in centimetres, or two assets from unrelated sources with different conventions. Every asset publishes its bounding box and its size in metres in the API, so you can check rather than guess.

To fit a camera to whatever you just loaded, measure it:

1import { Box3, Vector3 } from 'three'
2
3const box = new Box3().setFromObject(gltf.scene)
4const size = box.getSize(new Vector3())
5const centre = box.getCenter(new Vector3())
6camera.position.set(centre.x, centre.y + size.y * 0.5, centre.z + size.length())
7camera.lookAt(centre)

Why your model is invisible

In rough order of how often each one is the answer:

  • No light. Every material except MeshBasicMaterial needs one. An ambient light plus a directional light is enough to prove the model is there.
  • Camera position. At the wrong scale you are either inside the model or a kilometre away. Use the bounding box above rather than nudging numbers.
  • The load failed. Check the console and the network tab. A 404 or a CORS error is a completely different problem from a rendering one, and without an error callback it looks identical.
  • Metallic material, no environment. A physically based material with high metalness and nothing to reflect renders close to black. Give the scene an environment map. Three's own RoomEnvironment is generated locally and needs no external file, which is the sensible default:
1import { PMREMGenerator } from 'three'
2import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'
3
4const pmrem = new PMREMGenerator(renderer)
5scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture

Reach for a downloaded HDR only when you specifically want that lighting. A preset that fetches a large HDR from a third-party CDN on every page load is a slow first paint and a dependency on somebody else's uptime.

No decoders required

Geometry here is compressed with KHR_mesh_quantization and textures are re-encoded to WebP with EXT_texture_webp. Both are read natively by three.js, so the plain loader above is the whole setup. There is no DRACOLoader to configure, no decoder path to host and no KTX2Loader needing a renderer handed to it before it will work.

Next

If you are working in React rather than plain three.js, the same models load through drei with rather less ceremony: see free 3D models in React Three Fiber. If you would rather have an assistant find and place the models for you, connect it to the catalogue first.

Common questions

How do I load a GLB file in three.js?
Import GLTFLoader from three/addons/loaders/GLTFLoader.js, create one, and call load() with the file URL and a callback that adds gltf.scene to your scene. A GLB is self-contained, with its textures embedded, so there is nothing else to fetch and no material setup to write.
Can I load a 3D model from a CDN URL instead of hosting the file?
Yes, provided the host sends CORS headers. Models on cdn.3dassets.dev allow GET from any origin and their URLs are immutable, so you can pass one straight to GLTFLoader in production and cache it aggressively. Hosting the file yourself is still worth it if you need it inside an offline build.
Why is my GLB model invisible or black in three.js?
Almost always one of four things: the model has no light on it and its material is not MeshBasicMaterial, the camera is inside the model or too far away for its scale, the load failed with a CORS or 404 error visible in the console, or the renderer has no environment map so a metallic material has nothing to reflect and renders black.
Do I need a Draco or KTX2 decoder for these models?
No. Models on 3dassets.dev use KHR_mesh_quantization for geometry and EXT_texture_webp for textures, both of which three.js supports natively. A plain GLTFLoader with no decoder configuration reads them, which is one fewer script to host and one fewer thing to get wrong.

Keep reading

React guide

Free 3D models in React Three Fiber

useGLTF, preloading, Suspense boundaries and animation hooks, with the patterns that keep a React scene fast once you are drawing the same model two hundred times.

AI setup

Give your AI assistant a 3D model library

An AI coding assistant can write a three.js scene in seconds and then fill it with grey cubes, because it has no models and no way to find any. Connecting it to a catalogue fixes that in one line.