Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

three.js User Guide

Generated May 2026 from mrdoob/three.js at 477da61

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

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 manual requestAnimationFrame calls. 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

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 its package.json). Use import syntax, not require(). 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

WebGPURenderer requires calling await 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's up property (default Vector3(0, 1, 0)) to determine which direction is "up." For cameras, this usually works as expected. For meshes, you may need to adjust up if 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

BufferAttribute requires 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 CameraHelper to 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

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. Using WebGPURenderer with 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:

  1. Check chrome://gpu (or equivalent) for WebGL status
  2. Enable hardware acceleration in browser settings
  3. Update GPU drivers
  4. Dispose unused renderers to free context slots
  5. 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:

  1. Remove antialias: true as a first test
  2. Try disabling stencil or alpha
  3. 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:

  1. Update the browser to a version that supports WebGL 2
  2. On Safari, ensure macOS 12+ / iOS 15+ is used
  3. 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:

  1. Check the track name format — it should follow objectName.property or objectName.property[index]
  2. If loading from a file, re-export the model with correct animation target names
  3. 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.js and src/Three.js imported

Solution:

  1. Check your bundle for duplicate three.js copies: npm ls three
  2. Use your bundler's alias/dedupe feature to force a single copy
  3. 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.isMesh return false across 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):

  1. Add an AmbientLight to rule out lighting: scene.add(new THREE.AmbientLight(0xffffff, 1))
  2. Verify camera position is outside objects and near/far range covers the scene
  3. Use MeshBasicMaterial temporarily (does not require lights)
  4. Add AxesHelper to verify orientation: scene.add(new THREE.AxesHelper(5))
  5. Check renderer.domElement is in the DOM and has non-zero dimensions
  6. Confirm objects are added to the scene (not just created)
  7. Check material.visible and object.visible are both true

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:

  1. Offset one surface slightly: mesh.position.z += 0.001
  2. Use material.polygonOffset, material.polygonOffsetFactor, material.polygonOffsetUnits
  3. Enable logarithmic depth buffer: new THREE.WebGLRenderer({ logarithmicDepthBuffer: true })
  4. 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:

  1. Rename referenced files to use ASCII-only characters
  2. Re-export the GLTF file with ASCII filenames
  3. Use .glb format 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:

  1. Avoid negative scaling on the mesh; instead, mirror the geometry data
  2. 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 BufferGeometry using BufferGeometryUtils.mergeGeometries()
  • Use InstancedMesh for many copies of the same geometry (see Section 5.5)
  • Use BatchedMesh for 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.