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.
Frequently Asked Questions
What will I learn in this tutorial?
Learn the core Three.js concepts: WebGLRenderer, scene graph, cameras, meshes, lighting and the animation loop. Hands-on from scratch in 40 minutes.
What topics are covered in this tutorial?
This tutorial covers: Create the WebGLRenderer, Scene and PerspectiveCamera, Add Geometry and Material, Add Lighting, Animate with requestAnimationFrame, Handle window resize, Complete example.
What tools and technologies does this tutorial use?
This tutorial uses HTML, JavaScript, Three.js.
How long does this tutorial take?
This tutorial takes approximately 40 minutes to complete.
What prerequisites do I need before starting?
Before starting, you should have: Basic HTML & JavaScript (variables, functions, const / let ); Familiarity with Cartesian coordinates (x, y, z); No prior Three.js or WebGL knowledge needed.