What’s in a model download
Each model downloads as a ZIP with the same prop in glb/, fbx/ and obj/ folders. For Three.js, use the GLB: one mesh with one material, the kit’s palette textures packed inside, and nothing else to load.
- Metres, +Y up. A 0.6 m crate is 0.6 units tall, so props from different kits share one scale.
- Base at the origin. The lowest point sits on y = 0 with the footprint centred, so
position.y = 0puts a prop on the ground. - Flat shading is baked in. The faces already have flat normals, so you don’t need
flatShading: true. - Glowing parts such as lamp glass use an emissive texture, which GLTFLoader reads. Add a bloom pass if you want them to bloom.
Load a model with an import map
This page loads three r170 from a CDN, no build step needed. Put the GLB in a models/ folder next to it. Browsers block loading files from a file:// page, so serve the folder with a local web server, for example python3 -m http.server.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Prop viewer</title>
<style>body { margin: 0; }</style>
<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>
</head>
<body>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xdfe8e4);
scene.add(new THREE.HemisphereLight(0xffffff, 0x667766, 2));
const sun = new THREE.DirectionalLight(0xffffff, 2.5);
sun.position.set(3, 6, 4);
scene.add(sun);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.05, 100);
camera.position.set(1.6, 1.2, 2.2);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0.3, 0);
controls.update();
// Metres, +Y up, base at the origin: the crate stands on y = 0.
new GLTFLoader().load('models/wooden-crate.glb', (gltf) => {
scene.add(gltf.scene);
});
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
renderer.setAnimationLoop(() => renderer.render(scene, camera));
</script>
</body>
</html>
With a bundler
Install three with npm and import GLTFLoader from three/addons. With Vite, files in public/ are served from the site root, so public/models/wooden-crate.glb loads as /models/wooden-crate.glb. Load each GLB once and clone it for every copy you place: clones share the geometry and the palette material.
// npm install three@0.170.0
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
export function placeProps(scene) {
loader.load('/models/wooden-crate.glb', (gltf) => {
const crate = gltf.scene;
crate.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
// Clones share the geometry and the palette material, so many copies stay cheap.
for (let i = 0; i < 5; i++) {
const copy = crate.clone();
copy.position.set(i * 0.9, 0, 0);
copy.rotation.y = Math.random() * Math.PI * 2;
scene.add(copy);
}
});
}
Shadows also need renderer.shadowMap.enabled = true and a light with castShadow = true.
Play sounds
Every sound pack has WAV masters and OGG copies. THREE.Audio plays them through the Web Audio API; use THREE.PositionalAudio instead, added to a prop, for sound that comes from a place in the scene.
const listener = new THREE.AudioListener();
camera.add(listener);
const coin = new THREE.Audio(listener);
new THREE.AudioLoader().load('sounds/coin.ogg', (buffer) => {
coin.setBuffer(buffer);
coin.setVolume(0.8);
});
// Browsers keep audio suspended until the player clicks, taps or presses a key.
window.addEventListener('pointerdown', () => {
listener.context.resume();
if (coin.buffer) {
if (coin.isPlaying) {
coin.stop();
}
coin.play();
}
});
Some Safari versions can’t play OGG. If you don’t want to handle that yourself, Howler.js takes both files and plays the first one the browser supports:
// npm install howler
import { Howl } from 'howler';
// Howler plays the first format the browser supports.
const coin = new Howl({ src: ['sounds/coin.ogg', 'sounds/coin.wav'], volume: 0.8 });
document.querySelector('#collect').addEventListener('click', () => coin.play());