Three.js has a reputation for being a big library with an intimidating API surface, but the core of it is three objects and one loop. Once that clicks, everything else — loaders, post-processing, physics integrations — is additive. This is the version of the mental model we wish someone had handed us before our first scene.
The Scene, Camera, and Renderer Trio
A Scene is just a container — a graph of objects, lights, and cameras with no opinion on how anything looks. A PerspectiveCamera defines the viewpoint: field of view in degrees, aspect ratio (almost always window.innerWidth / window.innerHeight), and near/far clipping planes that bound what actually gets drawn. The WebGLRenderer is the thing that actually rasterizes the scene from the camera's point of view into a canvas element, which you attach to the DOM once and then leave alone.
The Render Loop
Nothing in three.js redraws itself automatically. You own a function that calls renderer.render(scene, camera), and you schedule it with requestAnimationFrame rather than setInterval so it syncs to the browser's refresh rate and pauses politely when the tab isn't visible. Any animation — a rotation, a moving light, a camera fly-through — is just a value you mutate once per frame before that render call runs.
Adding Geometry and Light
A Mesh is a geometry (the shape's vertex data — BoxGeometry, SphereGeometry, or something loaded from a file) paired with a material (how the surface responds to light — MeshBasicMaterial ignores light entirely, MeshStandardMaterial reacts to it physically). Add lights only if your material needs them; a scene full of MeshStandardMaterial objects with no light source renders solid black, which is the single most common "why is my cube invisible" moment for anyone starting out.
import * as THREE from 'three';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 3;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x4c8bf5 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(2, 3, 4);
scene.add(directionalLight);
function animate() {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
- Scene — the graph holding every object, light, and camera
- PerspectiveCamera / OrthographicCamera — the viewpoint the renderer draws from
- WebGLRenderer — rasterizes the scene into a canvas element
- BufferGeometry subclasses — the vertex data describing a shape
- Material subclasses — how a surface reacts to light and color
- AmbientLight / DirectionalLight / PointLight — what MeshStandardMaterial needs to be visible at all
Call renderer.setSize and update camera.aspect (then camera.updateProjectionMatrix()) on window resize. Skip this and your scene will stay stretched to whatever size it was on first load, which is an easy thing to miss until someone resizes their browser.
Wrapping Up
Scene, camera, renderer, loop — that is the whole foundation. Everything more advanced in three.js is a variation on adding objects to the scene or changing how the renderer draws them, so getting comfortable with these four pieces first makes the rest of the library far less intimidating.