three.js is a JavaScript 3D graphics library that creates and renders interactive 3D scenes in the browser using WebGL and WebGPU. It provides a complete scene graph, physically-based materials, dozens of geometry primitives, animation, audio, model loading, and post-processing — all with zero runtime dependencies.
This guide covers three.js v0.184.0 (revision r184).
1. Quick Start
Get a spinning cube on screen in under two minutes.
1.1 Minimal HTML Setup
Create a single HTML file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>three.js Quick Start</title>
<style>
body { margin: 0; }
canvas { display: block; }
</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.184.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.184.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
// Scene
const scene = new THREE.Scene();
// Camera
const camera = new THREE.PerspectiveCamera(
75, // field of view (degrees)
window.innerWidth / window.innerHeight, // aspect ratio
0.1, // near clipping plane
1000 // far clipping plane
);
camera.position.z = 5;
// Renderer
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Mesh = Geometry + Material
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// Animation loop
function animate() {
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
</script>
</body>
</html>
Open this file in any modern browser. You will see a rotating green cube.
Tip
Use
renderer.setAnimationLoop(callback)instead of manualrequestAnimationFramecalls. It handles the loop lifecycle automatically and is required for WebXR compatibility.
1.2 The Three Pillars
Every three.js application requires exactly three objects:
| Object | Class | Purpose |
|---|---|---|
| Scene | Scene |
Container for all 3D objects, lights, and cameras |
| Camera | PerspectiveCamera or OrthographicCamera |
Defines what portion of the scene is visible |
| Renderer | WebGLRenderer or WebGPURenderer |
Draws the scene to a <canvas> element |
The render call brings them together: renderer.render(scene, camera).
1.3 What Goes Into a Scene
Scene
├── Mesh (geometry + material)
├── Light (ambient, directional, point, spot, ...)
├── Group
│ ├── Mesh
│ └── Mesh
├── Camera
├── Sprite
├── Line / LineSegments
├── Points
└── Audio / PositionalAudio
All visible objects extend Object3D, which provides position, rotation, scale, visible, parent, children, and the add() / remove() methods that form the scene graph hierarchy.
2. Installation
2.1 npm (Recommended)
npm install three
Then import in your JavaScript or TypeScript modules:
import * as THREE from 'three';
Important
three.js is an ES module package (
"type": "module"in itspackage.json). Useimportsyntax, notrequire(). If your bundler or Node.js configuration expects CommonJS, you will need to configure it for ESM interop.
2.2 Package Exports
The three package exposes several entry points:
| Import path | What you get |
|---|---|
three |
Core library with WebGLRenderer |
three/webgpu |
WebGPURenderer + the full node/TSL system |
three/tsl |
Three Shading Language functions only |
three/addons |
All addons (controls, loaders, exporters, etc.) |
three/addons/controls/OrbitControls.js |
Individual addon files |
three/src/* |
Raw unbuilt source files |
// Import core + WebGPURenderer
import * as THREE from 'three/webgpu';
// Import a specific addon
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
2.3 CDN / Import Maps
For prototyping without a build tool, use an import map in your HTML:
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.184.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.184.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
</script>
Warning
Never load three.js from multiple sources (e.g., mixing a CDN copy with an npm copy). The library detects this and warns:
"WARNING: Multiple instances of Three.js being imported."Duplicate instances cause type checks to fail silently — materials, geometries, and textures from one instance are incompatible with renderers from another.
2.4 Development Server
If you clone the three.js repository for development or to run examples:
git clone https://github.com/mrdoob/three.js.git
cd three.js
npm install
npm start
This starts a dev server on port 8080 with live reloading. Other npm scripts:
| Command | Purpose |
|---|---|
npm run dev |
Same as npm start — build + dev server |
npm run build |
Rollup production build |
npm test |
Lint + unit tests |
npm run build-docs |
Generate JSDoc + LLM-friendly docs |
2.5 Browser Requirements
three.js requires WebGL 2 at minimum (WebGL 1 support was removed in r163).
| Feature | Browser support |
|---|---|
| WebGL 2 | Chrome 56+, Firefox 51+, Safari 15+, Edge 79+ |
| WebGPU | Chrome 113+, Firefox (behind flag), Safari (experimental) |
3. Configuration
three.js has no configuration files. All setup is programmatic through constructor parameters and object properties.
3.1 WebGLRenderer Options
const renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('myCanvas'), // existing canvas element
antialias: true, // smooth edges (default: false)
alpha: true, // transparent background (default: false)
premultipliedAlpha: true, // (default: true)
preserveDrawingBuffer: false, // set true for screenshots (default: false)
powerPreference: 'high-performance', // GPU hint: 'default', 'high-performance', 'low-power'
stencil: true, // stencil buffer (default: true)
depth: true, // depth buffer (default: true)
logarithmicDepthBuffer: false, // reduces z-fighting for large scenes
});
After construction, configure rendering behavior:
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
// Tone mapping for HDR content
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
// Color management
renderer.outputColorSpace = THREE.SRGBColorSpace;
// Shadows
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
3.2 WebGPURenderer Setup
import * as THREE from 'three/webgpu';
const renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// WebGPURenderer requires explicit initialization
await renderer.init();
renderer.setAnimationLoop(animate);
Important
WebGPURendererrequires callingawait renderer.init()before the first render. Calling.render()before.init()throws:"Renderer: .render() called before the backend is initialized. Use .init() first."
3.3 Scene Defaults
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222222); // solid color
// OR
scene.background = cubeTexture; // skybox (CubeTexture)
// OR
scene.background = equirectTexture; // equirectangular HDR
scene.fog = new THREE.Fog(0xcccccc, 10, 100); // linear fog: color, near, far
// OR
scene.fog = new THREE.FogExp2(0xcccccc, 0.02); // exponential fog: color, density
scene.environment = pmremTexture; // IBL for all PBR materials
4. Core Usage
4.1 Creating Scenes and Adding Objects
The Scene Graph
Scene extends Object3D. Every object added to the scene (or to a child of the scene) becomes part of a hierarchical tree. Transforms cascade: a child inherits its parent's position, rotation, and scale.
const scene = new THREE.Scene();
// Group objects for collective transforms
const carGroup = new THREE.Group();
carGroup.position.set(0, 0.5, 0);
scene.add(carGroup);
const body = new THREE.Mesh(
new THREE.BoxGeometry(2, 0.5, 1),
new THREE.MeshStandardMaterial({ color: 0x2244aa })
);
carGroup.add(body);
const wheel = new THREE.Mesh(
new THREE.CylinderGeometry(0.2, 0.2, 0.1, 16),
new THREE.MeshStandardMaterial({ color: 0x111111 })
);
wheel.position.set(-0.6, -0.3, 0.5);
wheel.rotation.z = Math.PI / 2;
carGroup.add(wheel);
// Rotating carGroup rotates both body and wheel together
carGroup.rotation.y = Math.PI / 4;
Object3D Properties
Every scene object inherits these from Object3D:
| Property | Type | Description |
|---|---|---|
position |
Vector3 |
Local position |
rotation |
Euler |
Local rotation in radians (order: 'XYZ' default) |
quaternion |
Quaternion |
Rotation as quaternion (synced with rotation) |
scale |
Vector3 |
Local scale |
visible |
boolean |
Whether to render this object and its children |
name |
string |
Human-readable name for lookups |
userData |
object |
Store custom data without subclassing |
layers |
Layers |
Bitmask layers for selective rendering/raycasting |
castShadow |
boolean |
Whether this object casts shadows |
receiveShadow |
boolean |
Whether this object receives shadows |
frustumCulled |
boolean |
Whether to skip rendering when outside camera frustum |
renderOrder |
number |
Override rendering order |
Key methods:
object.add(child); // add child to scene graph
object.remove(child); // remove child
object.getObjectByName('wheel'); // find descendant by name
object.traverse(callback); // walk all descendants
object.lookAt(x, y, z); // orient toward a point
object.localToWorld(vec3); // convert local → world coords
object.worldToLocal(vec3); // convert world → local coords
object.getWorldPosition(targetVec3); // get world-space position
object.getWorldQuaternion(targetQuat); // get world-space rotation
Note
lookAt()uses the object'supproperty (defaultVector3(0, 1, 0)) to determine which direction is "up." For cameras, this usually works as expected. For meshes, you may need to adjustupif the model's local orientation differs.
4.2 Geometries
Geometries define the shape of 3D objects as vertices, normals, and UVs stored in BufferAttribute arrays. All built-in geometries extend BufferGeometry.
Primitive Geometries
// Box: width, height, depth, widthSegments, heightSegments, depthSegments
new THREE.BoxGeometry(1, 1, 1, 1, 1, 1);
// Sphere: radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength
new THREE.SphereGeometry(1, 32, 16);
// Plane: width, height, widthSegments, heightSegments
new THREE.PlaneGeometry(10, 10, 1, 1);
// Circle: radius, segments, thetaStart, thetaLength
new THREE.CircleGeometry(1, 32);
// Cylinder: radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded
new THREE.CylinderGeometry(0.5, 0.5, 2, 32);
// Cone: radius, height, radialSegments, heightSegments, openEnded
new THREE.ConeGeometry(0.5, 2, 32);
// Torus: radius, tube, radialSegments, tubularSegments, arc
new THREE.TorusGeometry(1, 0.3, 16, 48);
// TorusKnot: radius, tube, tubularSegments, radialSegments, p, q
new THREE.TorusKnotGeometry(1, 0.3, 100, 16);
// Capsule: radius, length, capSegments, radialSegments
new THREE.CapsuleGeometry(0.5, 1, 10, 20);
// Ring: innerRadius, outerRadius, thetaSegments, phiSegments
new THREE.RingGeometry(0.5, 1, 32);
// Platonic solids: radius, detail
new THREE.DodecahedronGeometry(1, 0);
new THREE.IcosahedronGeometry(1, 0);
new THREE.OctahedronGeometry(1, 0);
new THREE.TetrahedronGeometry(1, 0);
Extruded and Lathed Geometries
// Extrude a 2D shape into 3D
const shape = new THREE.Shape();
shape.moveTo(0, 0);
shape.lineTo(1, 0);
shape.lineTo(1, 1);
shape.lineTo(0, 1);
shape.lineTo(0, 0);
const extrudeGeometry = new THREE.ExtrudeGeometry(shape, {
depth: 0.5,
bevelEnabled: true,
bevelThickness: 0.1,
bevelSize: 0.1,
bevelSegments: 3,
});
// Lathe: revolve a 2D profile around the Y axis
const points = [];
for (let i = 0; i < 10; i++) {
points.push(new THREE.Vector2(Math.sin(i * 0.2) * 0.5 + 0.5, i * 0.2));
}
const latheGeometry = new THREE.LatheGeometry(points, 32);
// Tube: extrude along a 3D curve
const curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(-1, 0, 0),
new THREE.Vector3(0, 1, 0),
new THREE.Vector3(1, 0, 0),
]);
const tubeGeometry = new THREE.TubeGeometry(curve, 64, 0.1, 8, false);
Custom BufferGeometry
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
-1, -1, 0,
1, -1, 0,
0, 1, 0,
]);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
// Optional: add normals, UVs, colors
const normals = new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1]);
geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
// Indexed geometry (shared vertices)
const indices = new Uint16Array([0, 1, 2]);
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
geometry.computeBoundingSphere();
Warning
BufferAttributerequires typed arrays (Float32Array,Uint16Array, etc.). Passing a regular JavaScript array triggers:"THREE.BufferAttribute: array should be a Typed Array."
Wireframe and Edge Views
// Show wireframe of any geometry
const wireframe = new THREE.WireframeGeometry(boxGeometry);
const line = new THREE.LineSegments(wireframe);
scene.add(line);
// Show only hard edges (angle threshold)
const edges = new THREE.EdgesGeometry(boxGeometry, 15); // 15-degree threshold
const edgeLine = new THREE.LineSegments(
edges,
new THREE.LineBasicMaterial({ color: 0x000000 })
);
scene.add(edgeLine);
4.3 Materials
Materials define how surfaces appear. All materials extend the Material base class.
Material Comparison
| Material | Lighting | Use case | Performance |
|---|---|---|---|
MeshBasicMaterial |
No | Unlit/flat color, wireframes, debugging | Fastest |
MeshLambertMaterial |
Diffuse only | Matte surfaces, low-poly styles | Fast |
MeshPhongMaterial |
Diffuse + specular | Shiny non-metallic surfaces | Medium |
MeshStandardMaterial |
PBR | Realistic surfaces (recommended default) | Medium-high |
MeshPhysicalMaterial |
Extended PBR | Glass, car paint, fabric, skin | Highest |
MeshToonMaterial |
Cel-shaded | Cartoon/stylized rendering | Medium |
MeshNormalMaterial |
Normal-colored | Debugging normals | Fast |
MeshDepthMaterial |
Depth-colored | Depth visualization | Fast |
MeshMatcapMaterial |
Baked matcap | Quick stylized look, no lights needed | Fast |
PBR Materials (MeshStandardMaterial)
const material = new THREE.MeshStandardMaterial({
color: 0xffffff, // base color (albedo)
metalness: 0.0, // 0 = dielectric, 1 = metal
roughness: 0.5, // 0 = mirror, 1 = fully rough
map: colorTexture, // albedo texture
normalMap: normalTexture, // surface detail via normals
roughnessMap: roughTex, // per-pixel roughness
metalnessMap: metalTex, // per-pixel metalness
aoMap: aoTexture, // ambient occlusion
aoMapIntensity: 1.0,
emissive: 0x000000, // self-illumination color
emissiveMap: emissiveTex,
emissiveIntensity: 1.0,
envMap: envTexture, // environment reflection (or use scene.environment)
envMapIntensity: 1.0,
displacementMap: dispTex, // vertex displacement
displacementScale: 0.1,
alphaMap: alphaTex, // per-pixel opacity
transparent: true,
opacity: 1.0,
side: THREE.DoubleSide, // FrontSide, BackSide, DoubleSide
flatShading: false,
wireframe: false,
});
MeshPhysicalMaterial (Advanced PBR)
Extends MeshStandardMaterial with additional properties:
const glassMaterial = new THREE.MeshPhysicalMaterial({
color: 0xffffff,
metalness: 0,
roughness: 0,
transmission: 1.0, // 0 = opaque, 1 = fully transparent glass
thickness: 0.5, // thickness for refraction
ior: 1.5, // index of refraction
clearcoat: 0.0, // clear lacquer layer (car paint)
clearcoatRoughness: 0.0,
sheen: 0.0, // fabric-like sheen
sheenRoughness: 0.5,
sheenColor: new THREE.Color(0xffffff),
iridescence: 0.0, // thin-film iridescence (soap bubbles)
iridescenceIOR: 1.3,
});
Line and Point Materials
// Lines
new THREE.LineBasicMaterial({ color: 0xff0000, linewidth: 1 });
new THREE.LineDashedMaterial({ color: 0xff0000, dashSize: 0.3, gapSize: 0.1 });
// Points
new THREE.PointsMaterial({ color: 0x00ff00, size: 0.1, sizeAttenuation: true });
// Sprites (always face camera)
new THREE.SpriteMaterial({ map: spriteTexture, color: 0xffffff });
Common Material Properties
All materials share these properties:
material.transparent = true; // enable alpha blending
material.opacity = 0.5; // global opacity (requires transparent: true)
material.side = THREE.DoubleSide; // render both faces
material.depthWrite = true; // write to depth buffer
material.depthTest = true; // test against depth buffer
material.visible = true; // skip rendering entirely
material.blending = THREE.NormalBlending; // blending mode
material.needsUpdate = true; // force shader recompilation after changing defines
ShaderMaterial (Custom Shaders)
const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uColor: { value: new THREE.Color(0xff0000) },
uTexture: { value: texture },
},
vertexShader: `
uniform float uTime;
varying vec2 vUv;
void main() {
vUv = uv;
vec3 pos = position;
pos.z += sin(pos.x * 5.0 + uTime) * 0.1;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
fragmentShader: `
uniform vec3 uColor;
uniform sampler2D uTexture;
varying vec2 vUv;
void main() {
vec4 texColor = texture2D(uTexture, vUv);
gl_FragColor = vec4(uColor * texColor.rgb, 1.0);
}
`,
transparent: false,
side: THREE.DoubleSide,
});
// Update in animation loop
material.uniforms.uTime.value = elapsedTime;
4.4 Textures
Loading Textures
const textureLoader = new THREE.TextureLoader();
// Basic loading
const texture = textureLoader.load('/textures/wood.jpg');
// With callbacks
const texture = textureLoader.load(
'/textures/wood.jpg',
(texture) => { console.log('Loaded'); }, // onLoad
(xhr) => { console.log(xhr.loaded / xhr.total * 100 + '%'); }, // onProgress
(err) => { console.error('Error:', err); } // onError
);
Texture Properties
texture.wrapS = THREE.RepeatWrapping; // horizontal: ClampToEdgeWrapping, RepeatWrapping, MirroredRepeatWrapping
texture.wrapT = THREE.RepeatWrapping; // vertical
texture.repeat.set(4, 4); // tile 4x4
texture.offset.set(0.5, 0.5); // UV offset
texture.rotation = Math.PI / 4; // rotate UVs
texture.center.set(0.5, 0.5); // rotation center
texture.magFilter = THREE.LinearFilter; // magnification filter
texture.minFilter = THREE.LinearMipmapLinearFilter; // minification filter
texture.generateMipmaps = true;
texture.colorSpace = THREE.SRGBColorSpace; // for color maps (albedo, emissive)
// Leave as LinearSRGBColorSpace for data maps (normal, roughness, metalness, ao)
texture.flipY = true; // flip vertically (default true for image loads)
Cube Textures (Skybox / Environment)
const cubeLoader = new THREE.CubeTextureLoader();
const envMap = cubeLoader.load([
'px.jpg', 'nx.jpg', // positive-x, negative-x
'py.jpg', 'ny.jpg', // positive-y, negative-y
'pz.jpg', 'nz.jpg', // positive-z, negative-z
]);
scene.background = envMap;
scene.environment = envMap; // applies as IBL to all PBR materials
LoadingManager
Track loading progress across multiple assets:
const manager = new THREE.LoadingManager();
manager.onStart = (url, loaded, total) => {
console.log(`Started loading: ${url}. ${loaded}/${total} files.`);
};
manager.onProgress = (url, loaded, total) => {
console.log(`Loading: ${loaded}/${total}`);
};
manager.onLoad = () => {
console.log('All assets loaded');
};
manager.onError = (url) => {
console.error('Error loading:', url);
};
const textureLoader = new THREE.TextureLoader(manager);
const gltfLoader = new GLTFLoader(manager);
// All loaders sharing this manager contribute to progress
4.5 Lighting
Without lights, only MeshBasicMaterial, MeshNormalMaterial, and MeshMatcapMaterial are visible. All other materials require at least one light.
Light Types
// Ambient: uniform illumination from all directions (no shadows)
const ambient = new THREE.AmbientLight(0x404040, 1.0); // color, intensity
scene.add(ambient);
// Hemisphere: sky color from above, ground color from below (no shadows)
const hemi = new THREE.HemisphereLight(0x87ceeb, 0x362907, 1.0);
scene.add(hemi);
// Directional: parallel rays (sun-like, casts shadows)
const dir = new THREE.DirectionalLight(0xffffff, 1.5);
dir.position.set(5, 10, 7);
scene.add(dir);
// Point: emits in all directions from a position (casts shadows)
const point = new THREE.PointLight(0xff9900, 2, 50, 2); // color, intensity, distance, decay
point.position.set(0, 3, 0);
scene.add(point);
// Spot: cone-shaped (casts shadows)
const spot = new THREE.SpotLight(0xffffff, 2);
spot.position.set(0, 10, 0);
spot.angle = Math.PI / 6; // cone angle
spot.penumbra = 0.3; // soft edge (0-1)
spot.decay = 2;
spot.distance = 50;
spot.target.position.set(0, 0, 0); // aim point
scene.add(spot);
scene.add(spot.target);
// RectArea: rectangular light (area light, no shadows, PhysicalMaterial only)
const rect = new THREE.RectAreaLight(0xffffff, 5, 4, 2); // color, intensity, width, height
rect.position.set(0, 5, 0);
rect.lookAt(0, 0, 0);
scene.add(rect);
Shadow Setup
Shadows require configuration on the renderer, the light, and each object:
// 1. Enable on renderer
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// 2. Enable on light
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048; // resolution
dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 50;
// For DirectionalLight, shadow camera is orthographic:
dirLight.shadow.camera.left = -10;
dirLight.shadow.camera.right = 10;
dirLight.shadow.camera.top = 10;
dirLight.shadow.camera.bottom = -10;
dirLight.shadow.bias = -0.001; // reduce shadow acne
// 3. Enable on objects
mesh.castShadow = true;
floor.receiveShadow = true;
Tip
Use
CameraHelperto visualize a shadow camera's frustum and debug shadow coverage:scene.add(new THREE.CameraHelper(dirLight.shadow.camera));
4.6 Cameras
PerspectiveCamera
const camera = new THREE.PerspectiveCamera(
75, // fov: vertical field of view in degrees
window.innerWidth / window.innerHeight, // aspect ratio
0.1, // near plane
1000 // far plane
);
camera.position.set(0, 5, 10);
camera.lookAt(0, 0, 0);
OrthographicCamera
const frustumSize = 10;
const aspect = window.innerWidth / window.innerHeight;
const camera = new THREE.OrthographicCamera(
-frustumSize * aspect / 2, // left
frustumSize * aspect / 2, // right
frustumSize / 2, // top
-frustumSize / 2, // bottom
0.1, // near
1000 // far
);
camera.position.set(5, 5, 5);
camera.lookAt(0, 0, 0);
CubeCamera (for dynamic reflections)
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256);
const cubeCamera = new THREE.CubeCamera(0.1, 100, cubeRenderTarget);
scene.add(cubeCamera);
const reflectiveMaterial = new THREE.MeshStandardMaterial({
envMap: cubeRenderTarget.texture,
metalness: 1,
roughness: 0,
});
// In animation loop: update before main render
cubeCamera.position.copy(reflectiveMesh.position);
cubeCamera.update(renderer, scene);
renderer.render(scene, camera);
4.7 Animation
AnimationMixer
three.js uses a mixer-based animation system. Models loaded from GLTF/FBX files typically include AnimationClip data.
let mixer;
const loader = new GLTFLoader();
loader.load('model.glb', (gltf) => {
const model = gltf.scene;
scene.add(model);
mixer = new THREE.AnimationMixer(model);
// Play the first animation clip
const clip = gltf.animations[0];
const action = mixer.clipAction(clip);
action.play();
// Play all animations
gltf.animations.forEach((clip) => {
mixer.clipAction(clip).play();
});
});
// In animation loop:
const clock = new THREE.Clock();
function animate() {
const delta = clock.getDelta();
if (mixer) mixer.update(delta);
renderer.render(scene, camera);
}
AnimationAction Controls
const action = mixer.clipAction(clip);
// Playback control
action.play();
action.stop();
action.reset();
action.paused = true;
// Speed and direction
action.timeScale = 1.0; // 1 = normal, 2 = double speed, -1 = reverse
action.setEffectiveTimeScale(0.5);
// Weight (for blending)
action.setEffectiveWeight(1.0);
// Loop modes
action.loop = THREE.LoopRepeat; // default: loops forever
action.loop = THREE.LoopOnce; // play once
action.loop = THREE.LoopPingPong; // forward then backward
action.clampWhenFinished = true; // hold last frame (with LoopOnce)
action.repetitions = 3; // play 3 times
// Crossfade between animations
const walkAction = mixer.clipAction(walkClip);
const runAction = mixer.clipAction(runClip);
walkAction.play();
walkAction.crossFadeTo(runAction, 0.5, true); // target, duration, warp
runAction.play();
AnimationClip Events
mixer.addEventListener('finished', (event) => {
console.log('Animation finished:', event.action.getClip().name);
});
mixer.addEventListener('loop', (event) => {
console.log('Animation looped:', event.action.getClip().name);
});
Programmatic Animation
For simple animations without clips, manipulate properties directly:
const clock = new THREE.Clock();
function animate() {
const elapsed = clock.getElapsedTime();
// Orbit
mesh.position.x = Math.cos(elapsed) * 3;
mesh.position.z = Math.sin(elapsed) * 3;
// Bob up and down
mesh.position.y = Math.sin(elapsed * 2) * 0.5 + 1;
// Continuous rotation
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
4.8 Loading 3D Models
GLTFLoader (Recommended Format)
glTF (.glb / .gltf) is the recommended format for three.js.
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.7/');
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);
gltfLoader.load(
'model.glb',
(gltf) => {
const model = gltf.scene;
scene.add(model);
// Access animations
console.log('Animations:', gltf.animations);
// Access cameras embedded in the file
console.log('Cameras:', gltf.cameras);
},
(xhr) => {
console.log((xhr.loaded / xhr.total * 100) + '% loaded');
},
(error) => {
console.error('Error loading model:', error);
}
);
Other Loaders
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
import { STLLoader } from 'three/addons/loaders/STLLoader.js';
import { PLYLoader } from 'three/addons/loaders/PLYLoader.js';
import { PCDLoader } from 'three/addons/loaders/PCDLoader.js';
import { ColladaLoader } from 'three/addons/loaders/ColladaLoader.js';
// OBJ
const objLoader = new OBJLoader();
objLoader.load('model.obj', (obj) => scene.add(obj));
// FBX
const fbxLoader = new FBXLoader();
fbxLoader.load('model.fbx', (fbx) => {
scene.add(fbx);
// FBX animations
if (fbx.animations.length > 0) {
const mixer = new THREE.AnimationMixer(fbx);
mixer.clipAction(fbx.animations[0]).play();
}
});
// STL (3D printing format, geometry only)
const stlLoader = new STLLoader();
stlLoader.load('model.stl', (geometry) => {
const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial());
scene.add(mesh);
});
KTX2Loader (Compressed Textures)
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath('https://unpkg.com/three@0.184.0/examples/jsm/libs/basis/');
ktx2Loader.detectSupport(renderer);
ktx2Loader.load('texture.ktx2', (texture) => {
material.map = texture;
material.needsUpdate = true;
});
4.9 Audio
three.js integrates the Web Audio API for 3D spatial audio.
// Listener attached to the camera
const listener = new THREE.AudioListener();
camera.add(listener);
// Non-positional (ambient) audio
const bgMusic = new THREE.Audio(listener);
const audioLoader = new THREE.AudioLoader();
audioLoader.load('music.mp3', (buffer) => {
bgMusic.setBuffer(buffer);
bgMusic.setLoop(true);
bgMusic.setVolume(0.5);
bgMusic.play();
});
// Positional (3D spatialized) audio
const sound = new THREE.PositionalAudio(listener);
audioLoader.load('engine.mp3', (buffer) => {
sound.setBuffer(buffer);
sound.setRefDistance(1);
sound.setLoop(true);
sound.play();
});
carMesh.add(sound); // attach to a mesh — sound follows the mesh in 3D space
// Audio analysis (frequency data)
const analyser = new THREE.AudioAnalyser(bgMusic, 256); // fftSize
// In animation loop:
const data = analyser.getAverageFrequency(); // returns 0-255
Note
Browsers require a user gesture (click, tap) before audio can play. Start audio playback inside a click event handler, not on page load.
4.10 Interaction and Raycasting
Raycaster
Raycaster projects a ray from one point in a direction and reports which objects it intersects.
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
window.addEventListener('click', (event) => {
// Convert mouse position to normalized device coordinates (-1 to +1)
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true); // recursive
if (intersects.length > 0) {
const hit = intersects[0];
console.log('Hit object:', hit.object.name);
console.log('Hit point:', hit.point); // Vector3 in world coords
console.log('Hit face normal:', hit.face.normal); // face normal
console.log('Distance:', hit.distance);
// Highlight the clicked object
hit.object.material.color.set(0xff0000);
}
});
Using Layers for Selective Raycasting
// Set objects to specific layers
clickableObject.layers.set(1); // only on layer 1
decorObject.layers.set(2); // only on layer 2
// Raycaster respects its own layers setting
raycaster.layers.set(1); // only test against layer-1 objects
OrbitControls
The most common way to let users navigate a 3D scene:
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // smooth camera movement
controls.dampingFactor = 0.05;
controls.enableZoom = true;
controls.enablePan = true;
controls.enableRotate = true;
controls.minDistance = 2; // min zoom
controls.maxDistance = 50; // max zoom
controls.maxPolarAngle = Math.PI / 2; // prevent going below ground
// In animation loop:
controls.update();
Other available controls:
| Control | Behavior |
|---|---|
OrbitControls |
Orbit around a target point |
MapControls |
Pan-centric (like a map editor) |
TrackballControls |
Unconstrained rotation (no gimbal lock) |
FlyControls |
Fly through the scene (6DOF) |
FirstPersonControls |
First-person camera |
PointerLockControls |
FPS-style mouse lock |
ArcballControls |
Arcball rotation |
DragControls |
Drag objects with the mouse |
TransformControls |
Move/rotate/scale gizmos |
5. Common Tasks
5.1 Responsive Resize
Handle window resize to prevent stretching:
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
For OrthographicCamera:
window.addEventListener('resize', () => {
const aspect = window.innerWidth / window.innerHeight;
camera.left = -frustumSize * aspect / 2;
camera.right = frustumSize * aspect / 2;
camera.top = frustumSize / 2;
camera.bottom = -frustumSize / 2;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
5.2 Loading and Displaying a GLTF Model
Complete example with lighting, controls, and animation:
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xdddddd);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 2, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
// Lights
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
dirLight.position.set(5, 10, 5);
dirLight.castShadow = true;
dirLight.shadow.mapSize.set(2048, 2048);
scene.add(dirLight);
// Floor
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(20, 20),
new THREE.MeshStandardMaterial({ color: 0x999999 })
);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
// Controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// Load model
let mixer;
const clock = new THREE.Clock();
const loader = new GLTFLoader();
loader.load('character.glb', (gltf) => {
const model = gltf.scene;
model.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
scene.add(model);
// Play animations if present
if (gltf.animations.length > 0) {
mixer = new THREE.AnimationMixer(model);
gltf.animations.forEach((clip) => {
mixer.clipAction(clip).play();
});
}
});
// Render loop
function animate() {
const delta = clock.getDelta();
if (mixer) mixer.update(delta);
controls.update();
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
// Resize handling
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
5.3 Shadows
Complete shadow setup with multiple light types:
// Renderer
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Directional light shadows
const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
dirLight.position.set(10, 20, 10);
dirLight.castShadow = true;
dirLight.shadow.mapSize.set(2048, 2048);
dirLight.shadow.camera.near = 1;
dirLight.shadow.camera.far = 50;
dirLight.shadow.camera.left = -15;
dirLight.shadow.camera.right = 15;
dirLight.shadow.camera.top = 15;
dirLight.shadow.camera.bottom = -15;
dirLight.shadow.bias = -0.0001;
scene.add(dirLight);
// Spot light shadows
const spotLight = new THREE.SpotLight(0xffa500, 5);
spotLight.position.set(-5, 10, 0);
spotLight.angle = Math.PI / 6;
spotLight.penumbra = 0.3;
spotLight.castShadow = true;
spotLight.shadow.mapSize.set(1024, 1024);
scene.add(spotLight);
scene.add(spotLight.target);
// Objects: cast and receive
mesh.castShadow = true;
ground.receiveShadow = true;
5.4 Environment Maps and IBL
Use HDR environment maps for realistic lighting and reflections:
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
const pmremGenerator = new THREE.PMREMGenerator(renderer);
new RGBELoader().load('environment.hdr', (hdrTexture) => {
const envMap = pmremGenerator.fromEquirectangular(hdrTexture).texture;
scene.background = envMap; // visible background
scene.environment = envMap; // IBL for all PBR materials
hdrTexture.dispose();
pmremGenerator.dispose();
});
5.5 Instanced Rendering
Render many copies of the same geometry efficiently with InstancedMesh:
const count = 10000;
const geometry = new THREE.SphereGeometry(0.1, 16, 8);
const material = new THREE.MeshStandardMaterial({ color: 0xff8800 });
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);
const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
dummy.position.set(
(Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 20
);
dummy.rotation.set(
Math.random() * Math.PI,
Math.random() * Math.PI,
0
);
dummy.scale.setScalar(0.5 + Math.random());
dummy.updateMatrix();
instancedMesh.setMatrixAt(i, dummy.matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true;
scene.add(instancedMesh);
For per-instance colors:
const color = new THREE.Color();
for (let i = 0; i < count; i++) {
color.setHSL(Math.random(), 0.8, 0.5);
instancedMesh.setColorAt(i, color);
}
instancedMesh.instanceColor.needsUpdate = true;
5.6 BatchedMesh (Dynamic Batching)
BatchedMesh allows combining different geometries into a single draw call with dynamic updates:
const batchedMesh = new THREE.BatchedMesh(
100, // max geometry count
5000, // max vertex count
10000, // max index count
material
);
const boxGeoId = batchedMesh.addGeometry(new THREE.BoxGeometry(1, 1, 1));
const sphereGeoId = batchedMesh.addGeometry(new THREE.SphereGeometry(0.5, 16, 8));
// Add instances of those geometries
const inst1 = batchedMesh.addInstance(boxGeoId);
const inst2 = batchedMesh.addInstance(sphereGeoId);
const matrix = new THREE.Matrix4();
matrix.makeTranslation(2, 0, 0);
batchedMesh.setMatrixAt(inst1, matrix);
matrix.makeTranslation(-2, 0, 0);
batchedMesh.setMatrixAt(inst2, matrix);
scene.add(batchedMesh);
5.7 Post-Processing
Apply screen-space effects after rendering:
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5, // strength
0.4, // radius
0.85 // threshold
);
composer.addPass(bloomPass);
// Replace renderer.render() with:
function animate() {
composer.render();
}
renderer.setAnimationLoop(animate);
The PostProcessing class (available with WebGPURenderer) provides a node-based alternative:
import * as THREE from 'three/webgpu';
const postProcessing = new THREE.PostProcessing(renderer);
// Configure passes via the node system
5.8 WebXR (VR/AR)
import { VRButton } from 'three/addons/webxr/VRButton.js';
renderer.xr.enabled = true;
document.body.appendChild(VRButton.createButton(renderer));
// Use setAnimationLoop — required for XR
renderer.setAnimationLoop((timestamp, frame) => {
renderer.render(scene, camera);
});
Caution
WebXR is currently supported only with
WebGLRenderer. UsingWebGPURendererwith XR triggers:"THREE.XRManager: XR is currently not supported with a WebGPU backend. Use WebGL."
For AR:
import { ARButton } from 'three/addons/webxr/ARButton.js';
renderer.xr.enabled = true;
document.body.appendChild(ARButton.createButton(renderer));
5.9 Exporting Scenes
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
const exporter = new GLTFExporter();
// Export as JSON (.gltf)
exporter.parse(
scene,
(result) => {
const json = JSON.stringify(result, null, 2);
downloadFile(json, 'scene.gltf', 'application/json');
},
(error) => { console.error(error); },
{ binary: false }
);
// Export as binary (.glb)
exporter.parse(
scene,
(result) => {
downloadFile(result, 'scene.glb', 'application/octet-stream');
},
(error) => { console.error(error); },
{ binary: true }
);
Other exporters: OBJExporter, STLExporter, PLYExporter, USDZExporter.
5.10 Particles
Create particle systems with Points:
const particleCount = 5000;
const positions = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount; i++) {
positions[i * 3] = (Math.random() - 0.5) * 20;
positions[i * 3 + 1] = (Math.random() - 0.5) * 20;
positions[i * 3 + 2] = (Math.random() - 0.5) * 20;
colors[i * 3] = Math.random();
colors[i * 3 + 1] = Math.random();
colors[i * 3 + 2] = Math.random();
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({
size: 0.1,
sizeAttenuation: true,
vertexColors: true, // use per-vertex colors
transparent: true,
alphaMap: particleTexture,
depthWrite: false, // prevent particles occluding each other
});
const particles = new THREE.Points(geometry, material);
scene.add(particles);
5.11 Text Rendering
three.js does not include a built-in text geometry in core, but provides solutions via addons:
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
const fontLoader = new FontLoader();
fontLoader.load('fonts/helvetiker_regular.typeface.json', (font) => {
const geometry = new TextGeometry('Hello three.js!', {
font: font,
size: 1,
depth: 0.2, // formerly called "height" in older versions
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 0.03,
bevelSize: 0.02,
bevelSegments: 5,
});
geometry.computeBoundingBox();
geometry.center();
const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: 0x2244aa }));
scene.add(mesh);
});
5.12 Helpers for Debugging
// Axes: red = X, green = Y, blue = Z
scene.add(new THREE.AxesHelper(5)); // length 5
// Grid on the XZ plane
scene.add(new THREE.GridHelper(10, 10)); // size, divisions
// Polar grid
scene.add(new THREE.PolarGridHelper(5, 8, 4, 64));
// Arrow
const arrow = new THREE.ArrowHelper(
new THREE.Vector3(0, 1, 0), // direction
new THREE.Vector3(0, 0, 0), // origin
2, // length
0xff0000 // color
);
scene.add(arrow);
// Bounding box visualization
const boxHelper = new THREE.BoxHelper(mesh, 0xffff00);
scene.add(boxHelper);
// Light helpers
scene.add(new THREE.DirectionalLightHelper(dirLight, 1));
scene.add(new THREE.PointLightHelper(pointLight, 0.5));
scene.add(new THREE.SpotLightHelper(spotLight));
scene.add(new THREE.HemisphereLightHelper(hemiLight, 1));
// Camera helper (shows frustum)
scene.add(new THREE.CameraHelper(otherCamera));
// Skeleton helper (for skinned meshes)
scene.add(new THREE.SkeletonHelper(skinnedMesh));
5.13 LOD (Level of Detail)
Swap meshes based on distance from camera for performance:
const lod = new THREE.LOD();
lod.addLevel(highDetailMesh, 0); // show when distance < 10
lod.addLevel(medDetailMesh, 10); // show when distance 10-50
lod.addLevel(lowDetailMesh, 50); // show when distance > 50
scene.add(lod);
// Must call update each frame
function animate() {
lod.update(camera);
renderer.render(scene, camera);
}
5.14 Skinned Meshes and Skeletons
For animated characters loaded from GLTF/FBX, the skeletal structure is set up automatically. For manual creation:
const bones = [];
const rootBone = new THREE.Bone();
rootBone.position.y = 0;
bones.push(rootBone);
const childBone = new THREE.Bone();
childBone.position.y = 1;
rootBone.add(childBone);
bones.push(childBone);
const skeleton = new THREE.Skeleton(bones);
const skinnedMesh = new THREE.SkinnedMesh(geometry, material);
skinnedMesh.add(rootBone);
skinnedMesh.bind(skeleton);
scene.add(skinnedMesh);
5.15 Clipping Planes
Cut away parts of objects to reveal interiors:
const clippingPlane = new THREE.Plane(new THREE.Vector3(0, -1, 0), 1);
const material = new THREE.MeshStandardMaterial({
color: 0xff0000,
clippingPlanes: [clippingPlane],
clipShadows: true,
side: THREE.DoubleSide,
});
renderer.localClippingEnabled = true;
5.16 Curves and Paths
three.js provides a complete curve system for paths, extrusions, and camera tracks:
// Catmull-Rom spline through points
const curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(-5, 0, 0),
new THREE.Vector3(-2, 3, 2),
new THREE.Vector3(2, -1, -3),
new THREE.Vector3(5, 0, 0),
]);
curve.closed = false;
// Sample points along the curve
const points = curve.getPoints(50);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const line = new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 0xff0000 }));
scene.add(line);
// Use as camera path
let t = 0;
function animate() {
t += 0.001;
if (t > 1) t = 0;
const point = curve.getPointAt(t);
const tangent = curve.getTangentAt(t);
camera.position.copy(point);
camera.lookAt(point.clone().add(tangent));
renderer.render(scene, camera);
}
Other curve types: EllipseCurve / ArcCurve, CubicBezierCurve3, QuadraticBezierCurve3, LineCurve3, SplineCurve.
5.17 Render Targets (Render to Texture)
Render a scene to a texture for effects like portals, monitors, or mirrors:
const renderTarget = new THREE.WebGLRenderTarget(512, 512);
const portalCamera = new THREE.PerspectiveCamera(75, 1, 0.1, 100);
function animate() {
// Render the portal scene to the render target
renderer.setRenderTarget(renderTarget);
renderer.render(portalScene, portalCamera);
// Apply as texture on a mesh
screenMaterial.map = renderTarget.texture;
// Render the main scene normally
renderer.setRenderTarget(null);
renderer.render(scene, camera);
}
5.18 Disposing Resources
three.js does not garbage-collect GPU resources automatically. You must dispose of objects when removing them:
// Dispose geometry
geometry.dispose();
// Dispose material (and its textures)
material.dispose();
if (material.map) material.map.dispose();
if (material.normalMap) material.normalMap.dispose();
if (material.roughnessMap) material.roughnessMap.dispose();
// Dispose render targets
renderTarget.dispose();
// Remove from scene
scene.remove(mesh);
// Recursive disposal helper
function disposeHierarchy(object) {
object.traverse((child) => {
if (child.isMesh) {
child.geometry.dispose();
if (Array.isArray(child.material)) {
child.material.forEach((m) => m.dispose());
} else {
child.material.dispose();
}
}
});
}
// Dispose renderer on app shutdown
renderer.dispose();
Caution
Failing to dispose of removed objects causes GPU memory leaks. This is especially critical in single-page applications where scenes are created and destroyed repeatedly without page reloads.
6. Troubleshooting
Error: "Error creating WebGL context."
Cause: The browser cannot create a WebGL 2 context. This happens when:
- The GPU or driver does not support WebGL 2
- Hardware acceleration is disabled in browser settings
- Too many active WebGL contexts (browsers limit to ~8-16 concurrent contexts)
- The GPU process has crashed
Solution:
- Check
chrome://gpu(or equivalent) for WebGL status - Enable hardware acceleration in browser settings
- Update GPU drivers
- Dispose unused renderers to free context slots
- Test with a known working WebGL site (e.g.,
get.webgl.org)
Prevention: Call renderer.dispose() when destroying three.js instances, especially in SPAs.
Error: "Error creating WebGL context with your selected attributes."
Cause: The requested WebGL context attributes (antialias, stencil, depth, alpha, etc.) are not supported together on this GPU.
Solution:
- Remove
antialias: trueas a first test - Try disabling
stenciloralpha - Test with default attributes:
new THREE.WebGLRenderer()
Prevention: Only request attributes you actually need.
Error: "THREE.WebGLRenderer: WebGL 1 is not supported since r163."
Cause: The browser is falling back to WebGL 1, which three.js no longer supports as of r163.
Solution:
- Update the browser to a version that supports WebGL 2
- On Safari, ensure macOS 12+ / iOS 15+ is used
- For legacy hardware, use three.js r162 or earlier
Prevention: Check WebGL 2 support before initializing:
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl2');
if (!gl) {
alert('WebGL 2 is not available.');
}
Error: "Renderer: .render() called before the backend is initialized. Use .init() first."
Cause: WebGPURenderer.render() was called before the async init() completed.
Solution:
const renderer = new THREE.WebGPURenderer();
await renderer.init(); // must await before first render
renderer.setAnimationLoop(animate);
Prevention: Always await renderer.init() immediately after constructing WebGPURenderer.
Error: "THREE.XRManager: XR is currently not supported with a WebGPU backend. Use WebGL."
Cause: Attempting to use WebXR features with WebGPURenderer.
Solution: Use WebGLRenderer for all WebXR (VR/AR) applications:
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.xr.enabled = true;
Prevention: Do not use three/webgpu import for XR applications.
Warning
WebXR and WebGPU interop is not yet available. This is a known limitation. See GitHub issue #33404 for related WebXR tracking.
Error: "THREE.BufferAttribute: array should be a Typed Array."
Cause: A regular JavaScript array was passed to BufferAttribute instead of a typed array.
Solution:
// Wrong
const positions = [0, 0, 0, 1, 1, 1];
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
// Correct
const positions = new Float32Array([0, 0, 0, 1, 1, 1]);
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
Prevention: Always use Float32Array for positions/normals/UVs, Uint16Array or Uint32Array for indices.
Error: "THREE.KeyframeTrack: track name is undefined"
Cause: A KeyframeTrack was created with an undefined or empty name parameter.
Solution: Ensure every keyframe track has a valid name in the format objectName.property:
const track = new THREE.NumberKeyframeTrack(
'cube.position[x]', // must be a valid, non-empty string
[0, 1, 2], // times
[0, 5, 0] // values
);
Prevention: Validate track names when constructing AnimationClip objects manually.
Error: "PropertyBinding: Cannot parse trackName:"
Cause: An animation track name in a loaded model or manually created clip uses a format the PropertyBinding parser cannot interpret.
Solution:
- Check the track name format — it should follow
objectName.propertyorobjectName.property[index] - If loading from a file, re-export the model with correct animation target names
- Use
AnimationUtils.makeClipAdditive()or manually remap track names
Prevention: When creating clips programmatically, follow the naming convention: meshName.position, meshName.quaternion, meshName.scale, meshName.morphTargetInfluences[0].
Error: "WARNING: Multiple instances of Three.js being imported."
Cause: Two or more copies of the three.js library are loaded on the page. This typically happens with:
- Mixed CDN and npm imports
- Multiple bundled copies from different packages
- Both
build/three.module.jsandsrc/Three.jsimported
Solution:
- Check your bundle for duplicate three.js copies:
npm ls three - Use your bundler's alias/dedupe feature to force a single copy
- Ensure addons import from the same three.js instance
Prevention: Import three.js from exactly one source. Pin the version in package.json and use npm dedupe.
Important
Duplicate three.js instances cause subtle, hard-to-debug failures. Materials from one instance silently fail when used with a renderer from another. Type checks like
object.isMeshreturnfalseacross instances.
Error: "DepthTexture format must be either THREE.DepthFormat or THREE.DepthStencilFormat"
Cause: A DepthTexture was created with an unsupported format (e.g., THREE.RGBAFormat).
Solution:
const depthTexture = new THREE.DepthTexture(width, height);
depthTexture.format = THREE.DepthFormat; // or THREE.DepthStencilFormat
Prevention: Only use DepthFormat or DepthStencilFormat when constructing DepthTexture.
Error: "WebGLRenderTarget: Attached DepthTexture is initialized to the incorrect size."
Cause: The DepthTexture attached to a WebGLRenderTarget has dimensions that do not match the render target's size.
Solution:
const width = 1024;
const height = 1024;
const depthTexture = new THREE.DepthTexture(width, height);
const renderTarget = new THREE.WebGLRenderTarget(width, height, {
depthTexture: depthTexture,
});
Prevention: Always create the DepthTexture with the same width and height as the WebGLRenderTarget it is attached to. Update both when resizing.
Problem: Black Screen (Nothing Renders)
Cause: Multiple possible reasons — no lights, camera inside object, wrong camera direction, objects too far away, or material not visible.
Solution (checklist):
- Add an
AmbientLightto rule out lighting:scene.add(new THREE.AmbientLight(0xffffff, 1)) - Verify camera position is outside objects and
near/farrange covers the scene - Use
MeshBasicMaterialtemporarily (does not require lights) - Add
AxesHelperto verify orientation:scene.add(new THREE.AxesHelper(5)) - Check
renderer.domElementis in the DOM and has non-zero dimensions - Confirm objects are added to the scene (not just created)
- Check
material.visibleandobject.visibleare bothtrue
Prevention: Start with the minimal Quick Start template and add complexity incrementally.
Problem: Z-Fighting (Flickering Surfaces)
Cause: Two surfaces at the same depth produce alternating rendering between frames.
Solution:
- Offset one surface slightly:
mesh.position.z += 0.001 - Use
material.polygonOffset,material.polygonOffsetFactor,material.polygonOffsetUnits - Enable logarithmic depth buffer:
new THREE.WebGLRenderer({ logarithmicDepthBuffer: true }) - Narrow the camera's near/far range to improve depth precision
Prevention: Avoid placing coplanar surfaces at exactly the same position. Keep camera.near as large as practical and camera.far as small as practical.
Problem: Textures Appear Black or Wrong Color
Cause: Color space mismatch. By default, loaded textures are in LinearSRGBColorSpace, but color/albedo maps should be in SRGBColorSpace.
Solution:
// For color maps (albedo, emissive):
texture.colorSpace = THREE.SRGBColorSpace;
// For data maps (normal, roughness, metalness, ao, displacement):
// Leave as default (LinearSRGBColorSpace) — do NOT set SRGBColorSpace
// Set renderer output color space
renderer.outputColorSpace = THREE.SRGBColorSpace;
Prevention: Set colorSpace = THREE.SRGBColorSpace on every color/albedo/emissive texture immediately after loading.
Problem: GLTFLoader Fails with Non-ASCII Filenames
GitHub issue #29963.
Cause: GLTF files referencing textures or buffers with UTF-8 encoded filenames may fail to resolve paths correctly.
Solution:
- Rename referenced files to use ASCII-only characters
- Re-export the GLTF file with ASCII filenames
- Use
.glbformat instead (all resources embedded in a single binary)
Prevention: Use .glb for distribution. If using .gltf, ensure all referenced filenames use ASCII characters only.
Problem: Lighting Incorrect on Negatively Scaled Objects
GitHub issue #31994.
Cause: When a GLTF model node has negative scaling (e.g., mirroring via scale.x = -1), lighting calculations can produce incorrect results because the normal matrix is not properly adjusted.
Solution:
- Avoid negative scaling on the mesh; instead, mirror the geometry data
- If negative scaling is required, manually flip normals on the affected geometry
Prevention: Mirror geometry at export time rather than using negative scale values at runtime.
7. FAQ
Q: What is the difference between WebGLRenderer and WebGPURenderer?
WebGLRenderer is the mature, battle-tested renderer that works in all modern browsers. WebGPURenderer is the newer renderer built on the WebGPU API, offering better performance for complex scenes and access to compute shaders via the node/TSL system. Use WebGLRenderer for production applications that need broad browser support. Use WebGPURenderer for experimental work, compute-heavy applications, or when targeting Chrome 113+. Note that WebXR currently only works with WebGLRenderer.
Q: What is TSL (Three Shading Language)?
TSL is three.js's node-based shader authoring system. Instead of writing raw GLSL, you compose shader logic from JavaScript function calls (float(), vec3(), color(), texture(), etc.) that compile to the appropriate shader code for the active backend (WebGL GLSL or WebGPU WGSL). TSL is available via import { ... } from 'three/tsl' or included automatically when importing from three/webgpu.
Q: Do I need a build tool to use three.js?
No. You can use native ES modules with an import map directly in a <script type="module"> tag (see Section 2.3). For production, a bundler like Vite, Webpack, or Rollup is recommended for tree-shaking, minification, and asset handling.
Q: How do I handle transparency properly?
Set material.transparent = true and configure material.opacity (0-1). For textured transparency, use material.alphaMap or material.alphaTest. For order-independent transparency on particles, set material.depthWrite = false. For complex transparent scenes, you may need to manually set renderOrder on objects or use separate render passes.
Q: What 3D model format should I use?
glTF/GLB (.gltf / .glb) is the recommended format. It is compact, well-specified, supports PBR materials, animations, and morph targets, and is loaded via GLTFLoader. For Draco-compressed meshes, add DRACOLoader. For KTX2 compressed textures, add KTX2Loader.
Q: How do I get the world position of a child object?
const worldPosition = new THREE.Vector3();
childObject.getWorldPosition(worldPosition);
This accounts for all parent transforms in the hierarchy. Similarly, use getWorldQuaternion() for rotation and getWorldScale() for scale.
Q: Why is my model too big / too small / not visible?
Models from different 3D applications use different unit scales. After loading, check the model's bounding box and rescale:
const box = new THREE.Box3().setFromObject(model);
const size = box.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
const scale = 2 / maxDim; // fit into a 2-unit box
model.scale.setScalar(scale);
Q: How do I pick objects with the mouse?
Use Raycaster with normalized device coordinates. See Section 4.10 for the complete pattern. For performance with many objects, use Layers to limit which objects the raycaster tests against.
Q: Can I use three.js with React / Vue / Angular?
Yes. three.js is framework-agnostic. Create the renderer, scene, and camera in a component's lifecycle methods and mount renderer.domElement to a container ref. Libraries like react-three-fiber (R3F) provide React-specific bindings. Ensure you dispose() all resources when the component unmounts.
Q: How do I reduce draw calls?
- Merge static geometries into a single
BufferGeometryusingBufferGeometryUtils.mergeGeometries() - Use
InstancedMeshfor many copies of the same geometry (see Section 5.5) - Use
BatchedMeshfor multiple different geometries in one draw call (see Section 5.6) - Share materials — each unique material typically produces an additional draw call
- Use texture atlases to combine multiple textures into one
Q: How do I render to a specific HTML element instead of the full page?
Pass your container element or an existing <canvas> to the renderer:
// Option A: provide a canvas
const canvas = document.getElementById('myCanvas');
const renderer = new THREE.WebGLRenderer({ canvas });
// Option B: append the auto-created canvas to a container
const container = document.getElementById('container');
renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(renderer.domElement);
Q: How do I take a screenshot of the scene?
Enable preserveDrawingBuffer on the renderer, then use toDataURL():
const renderer = new THREE.WebGLRenderer({ preserveDrawingBuffer: true });
renderer.render(scene, camera);
const dataUrl = renderer.domElement.toDataURL('image/png');
Q: What is the difference between Mesh, InstancedMesh, and BatchedMesh?
Mesh renders one object with one draw call. InstancedMesh renders many copies of the same geometry with unique transforms/colors in a single draw call. BatchedMesh renders instances of different geometries in a single draw call with dynamic add/remove support. Use Mesh for unique objects, InstancedMesh for many identical objects (trees, particles, crowds), and BatchedMesh when you need mixed geometries with dynamic updates.
Q: How do I animate morph targets?
Morph targets (blend shapes) are stored in the geometry and controlled via mesh.morphTargetInfluences:
// If loaded from GLTF, morph targets are auto-attached
// Manual control:
mesh.morphTargetInfluences[0] = 0.5; // 0 = base shape, 1 = fully morphed
// Or animate via AnimationMixer — GLTF files often include morph target animations
Q: How do I use three.js with TypeScript?
three.js ships its own TypeScript type definitions. Install three and import normally — types are included:
import * as THREE from 'three';
const scene: THREE.Scene = new THREE.Scene();
const camera: THREE.PerspectiveCamera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
Q: How do I debug performance issues?
Use renderer.info to check draw calls, triangles, textures, and geometries in memory:
console.log(renderer.info.render.calls); // draw calls per frame
console.log(renderer.info.render.triangles); // triangles per frame
console.log(renderer.info.memory.geometries); // geometries in GPU memory
console.log(renderer.info.memory.textures); // textures in GPU memory
Also use the browser's DevTools Performance tab and the three.js Devtools browser extension for scene inspection.
For architecture details, internal subsystem design, deployment, and advanced configuration, see the three.js Admin Guide.