Three.js Basics — Scene, Camera, Renderer
Three.js abstracts WebGL into a JS-friendly scene graph. This tutorial walks through every core concept from scratch: renderer setup, camera types, geometry, materials, lighting and the animation loop. By the end you'll have a 3D spinning mesh you understand completely.
-
Basic HTML & JavaScript (variables, functions,
const/let) - Familiarity with Cartesian coordinates (x, y, z)
- No prior Three.js or WebGL knowledge needed
Create the WebGLRenderer
The renderer owns the <canvas> element and the
WebGL context. Create it, size it, and append it to the DOM:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style> body { margin: 0; overflow: hidden; background: #000; } </style>
</head>
<body>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.168/build/three.module.js';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);
</script>
</body>
</html>
Why setPixelRatio(Math.min(..., 2))?
Retina displays have devicePixelRatio of 2–3. Rendering at 3× costs
9× the pixels for barely visible quality gain. Capping at 2 is a
good default.
Scene and PerspectiveCamera
Every Three.js project needs exactly one Scene (the root
container) and at least one camera.
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0f); // dark background
// PerspectiveCamera(fov, aspect, near, far)
const camera = new THREE.PerspectiveCamera(
60, // vertical field of view in degrees
innerWidth / innerHeight, // aspect ratio
0.01, // near clipping plane
1000 // far clipping plane
);
camera.position.set(0, 1, 5); // move camera back so we can see the origin
The near/far planes define the visible depth range.
Objects closer than near or further than
far are clipped (invisible). Avoid near = 0 — it causes
z-fighting artefacts.
Add Geometry and Material
Three.js separates geometry (the shape: vertices, faces) from material (the appearance: colour, shininess, texture). Combine them in a Mesh:
// Geometry: built-in primitives
const geo = new THREE.BoxGeometry(1, 1, 1); // width, height, depth
// Material: how it looks
const mat = new THREE.MeshStandardMaterial({
color: 0x4488ff,
metalness: 0.3,
roughness: 0.6,
});
// Mesh = geometry + material
const cube = new THREE.Mesh(geo, mat);
scene.add(cube);
Other useful geometry primitives:
SphereGeometry(radius, widthSeg, heightSeg),
CylinderGeometry, PlaneGeometry(w, h),
TorusGeometry.
Material types to know:
-
MeshBasicMaterial— no lighting. Always the same colour. Good for debug/wireframes. -
MeshStandardMaterial— PBR (metalness/roughness). Needs lights. -
MeshPhongMaterial— older Blinn-Phong shading. Fast, less accurate. Needs lights. -
ShaderMaterial— fully custom GLSL vertex + fragment shader.
Add Lighting
MeshStandardMaterial requires lights — without them the
mesh is completely black:
// Ambient light: uniform low-level illumination everywhere
const ambient = new THREE.AmbientLight(0xffffff, 0.4); // colour, intensity
scene.add(ambient);
// Directional light: parallel rays (simulates the sun)
const dirLight = new THREE.DirectionalLight(0xffffff, 1.2);
dirLight.position.set(5, 8, 5);
scene.add(dirLight);
// Point light: omnidirectional, falls off with distance
const point = new THREE.PointLight(0x4488ff, 2, 20); // colour, intensity, distance
point.position.set(-3, 2, 3);
scene.add(point);
A common setup: one AmbientLight (low intensity) + one
DirectionalLight (strong) is enough for most scenes.
Animate with requestAnimationFrame
Three.js doesn't have a built-in loop — call
renderer.render(scene, camera) yourself each frame using
requestAnimationFrame:
let prevTime = performance.now();
function animate(time) {
requestAnimationFrame(animate);
const dt = Math.min((time - prevTime) / 1000, 0.05); // seconds
prevTime = time;
// Rotate cube over time
cube.rotation.x += dt * 0.8;
cube.rotation.y += dt * 1.2;
renderer.render(scene, camera);
}
requestAnimationFrame(animate);
Always multiply movement by dt (delta time). This makes
the animation frame-rate independent — it runs identically at 30fps,
60fps and 120fps.
Handle Window Resize
Without this, resizing the browser window stretches/squishes the canvas:
window.addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix(); // required after changing aspect!
renderer.setSize(innerWidth, innerHeight);
});
Complete Example
All steps combined into one copy-pasteable file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style> * { margin:0; } body { overflow: hidden; background: #000; } </style>
</head>
<body>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.168/build/three.module.js';
// Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);
// Scene + Camera
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0f);
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.01, 1000);
camera.position.set(0, 1, 5);
// Mesh
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshStandardMaterial({ color: 0x4488ff, metalness: 0.3, roughness: 0.6 })
);
scene.add(cube);
// Lighting
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
const dirLight = new THREE.DirectionalLight(0xffffff, 1.2);
dirLight.position.set(5, 8, 5);
scene.add(dirLight);
// Resize
window.addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
// Animate
let prevTime = performance.now();
(function animate(time) {
requestAnimationFrame(animate);
const dt = Math.min((time - prevTime) / 1000, 0.05);
prevTime = time;
cube.rotation.x += dt * 0.8;
cube.rotation.y += dt * 1.2;
renderer.render(scene, camera);
})(performance.now());
</script>
</body>
</html>
✅ What you'll see
A blue metallic cube spinning slowly on a dark background. The rotation speed is frame-rate independent. Resize the window — the aspect ratio stays correct.