Library · scene graph
Three.js
A scene, not a pipeline. The default answer for most 3D on the web.
The reference scene
Not running on this page. Three.js is not a dependency of this site, and rendering a screenshot while implying it was live would be exactly the kind of thing this site exists not to do. The code below is real and is what you would write — the WebGL and WebGPU pages have the same scene actually running, for comparison.
What it is
Three.js gives you the vocabulary you actually think in: scenes, meshes, materials, lights, cameras. It handles the buffer juggling, the matrix chain, the render loop and a great deal of cross-device sanity, and it has by far the largest ecosystem of loaders, controls and examples of anything here.
It is the right default for most 3D on the web, and it is worth learning after the fundamentals rather than instead of them — otherwise the day something looks wrong you have no model of what it is doing on your behalf.
Reach for it when
- You want a scene with models, materials, lights and orbit controls, this week.
- You need glTF loading, post-processing, shadows or text — all solved and maintained.
- You are working with others: it is the thing most people already know.
Look elsewhere when
- You are trying to learn what the GPU does. Three.js is very good at hiding exactly that.
- Bundle size is critical and you draw one simple thing. The full library dwarfs a hand-written effect.
The code
The same plasma, written the way Three.js wants it written.
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ canvas });
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const material = new THREE.ShaderMaterial({
uniforms: { uTime: { value: 0 } },
vertexShader, // the same trivial pass-through
fragmentShader, // the same cosine palette
});
scene.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material));
renderer.setAnimationLoop((now) => {
material.uniforms.uTime.value = now / 1000;
renderer.render(scene, camera);
});The same GLSL as the WebGL page, with the plumbing replaced by three lines of object construction.
npm install threeimport { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const gltf = await new GLTFLoader().loadAsync('/robot.glb');
scene.add(gltf.scene);
// Everything that arrives with it — materials, textures, skeletons,
// animation clips — is already wired up and ready to play.
const mixer = new THREE.AnimationMixer(gltf.scene);
mixer.clipAction(gltf.animations[0]).play();This is the single strongest argument for the library. glTF is a container format with its own material model, texture packing, skinning and animation semantics; parsing it correctly by hand is weeks of work you would be redoing rather than doing.
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// Resizing correctly means three things, and forgetting the third is
// why so many canvases look soft on a retina screen.
function resize() {
const { clientWidth: w, clientHeight: h } = renderer.domElement;
renderer.setSize(w, h, false);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
new ResizeObserver(resize).observe(renderer.domElement);Orbit controls and a correct resize are the two pieces of plumbing every scene needs and nobody enjoys writing. Note updateProjectionMatrix: changing the aspect field alone does nothing, because the matrix was built once.
A lit, spinning cube
Twelve triangles, three vertex attributes, an index buffer, a model and a view-projection matrix, one directional light, backface culling and a depth buffer. The picture is identical in all four; the amount you have to write to get it is not.
24 lines here, against 18 for the plasma
Not running on this page, for the same reason as above — Three.js is not a dependency of this site. The code is real; the picture it makes is the one on the WebGL page.
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 100);
camera.position.set(0, 1.4, 4.2);
camera.lookAt(0, 0, 0);
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshLambertMaterial({ vertexColors: true }),
);
scene.add(cube);
const key = new THREE.DirectionalLight(0xffffff, 2);
key.position.set(0.4, 0.8, 0.6);
scene.add(key, new THREE.AmbientLight(0xffffff, 0.12));
renderer.setAnimationLoop((t) => {
cube.rotation.y = t * 0.0006;
cube.rotation.x = t * 0.00035;
renderer.render(scene, camera);
});The depth buffer, the culling, the normal matrix, the vertex layout and the light rig are all still there — they are simply not yours to write. This is where the gap is widest: a quarter of the lines for a scene that takes real effort to get right by hand.
Things that will catch you
- Dispose what you create
- Geometries, materials and textures hold GPU memory that garbage collection will not reclaim. Long-lived apps that build scenes dynamically leak steadily until they crash the tab.
- React Three Fiber is the React binding, not a fork
- If you are in React, r3f expresses the same objects as components and is generally the nicer way in. Everything you learn about Three.js still applies underneath it.
- setAnimationLoop, not requestAnimationFrame
- It looks like a stylistic choice and is not: only setAnimationLoop works in WebXR sessions, and switching later is a nuisance.
Where to learn it
These and 35 more on the reading path.