Tutorial
⏱️ ~40 minutes 🎓 Beginner 🛠️ HTML · JavaScript · Three.js

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.

Prerequisites

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:

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.

Continue Learning

🛠

Experiment in Playground

Write, run and tweak Three.js code directly in your browser — no setup required.

Open Playground → View Simulation ↗