WebGL is a JavaScript API that lets you draw 3D graphics in the browser using your computer's GPU. Three.js is the most popular library that sits on top of WebGL, handling the boilerplate so you can focus on what you are actually drawing. If you have ever wanted to put a spinning cube, a particle system, or a 3D product viewer on a website, you need WebGL and Three.js.

This article walks you through building a real spinning cube in about fifteen minutes. By the end you will understand the structure of every Three.js scene you ever write.

Why WebGL matters

Before WebGL, the only way to draw 3D on the web was Java applets, Flash, or Microsoft Silverlight — all of which are now dead. WebGL is the open standard that replaced them, supported natively in every modern browser without a plugin. It is also the foundation of WebGPU, the next-generation API that will eventually replace it.

Three.js, started in 2010 by Ricardo Cabello (Mr.doob), is the de facto wrapper. Most 3D on the web is built with Three.js, from product configurators to data visualisations to browser games. The API is well-designed, the documentation is excellent, and the community is huge.

The mental model: scene, camera, renderer

Every Three.js scene has three core objects:

  • Scene — a container that holds everything you want to draw: meshes, lights, cameras.
  • Camera — the viewpoint. By default a PerspectiveCamera that simulates human vision.
  • Renderer — the thing that actually draws the scene onto a canvas element.

You add objects (meshes) to the scene, position the camera, and call renderer.render(scene, camera) every frame. The renderer talks to WebGL, which talks to your GPU, which draws the pixels.

Your first scene: a spinning cube

Save this as cube.html:

<!doctype html>
<html>
<head><title>Cube</title><style>body{margin:0}canvas{display:block}</style></head>
<body>
<script type="importmap">
{
  "imports": {
    "three": "https://unpkg.com/three@0.160.0/build/three.module.js"
  }
}
</script>
<script type="module">
import * as THREE from "three";

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth/innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);

const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshNormalMaterial();
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

camera.position.z = 3;

function loop() {
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;
  renderer.render(scene, camera);
  requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>

Open it in a browser. You should see a colourful cube spinning in the middle of the screen. That is a complete Three.js application. From here you can swap the geometry, the material, add lights, animate the camera, or load a 3D model.

Geometries, materials, and meshes

A geometry is the shape: vertices, faces, edges. Three.js ships with built-in primitives like BoxGeometry, SphereGeometry, TorusGeometry, and PlaneGeometry. For anything complex, you load a model from a file (glTF is the standard format).

A material is the surface: colour, texture, how it reacts to light. The simplest is MeshBasicMaterial (no lighting, just colour). MeshNormalMaterial colours each face by its normal, which is great for debugging. For realistic surfaces, use MeshStandardMaterial with appropriate roughness and metalness values.

A mesh combines a geometry and a material. It is the thing you add to the scene.

Lights and shadows

Without lights, materials that depend on lighting render as black silhouettes. Three.js supports several light types:

  • AmbientLight — soft, omnipresent light. No shadows.
  • DirectionalLight — light from a single direction, like the sun.
  • PointLight — a light at a specific point, like a bulb.
  • SpotLight — a cone of light, like a torch.

For shadows to work, you need a light that casts them (castShadow = true) and meshes that receive them. The renderer must also be configured with renderer.shadowMap.enabled = true. Shadows are expensive — turn them off unless you need them.

Animation loops

Three.js animation is driven by requestAnimationFrame. The pattern is:

function loop() {
  // update scene state
  cube.rotation.y += 0.01;
  // re-render
  renderer.render(scene, camera);
  // queue next frame
  requestAnimationFrame(loop);
}
loop();

That gives you roughly 60 frames per second on a typical monitor. The browser automatically throttles to match the display's refresh rate. For physics simulations, use a fixed time step instead of frame-based updates — it makes motion stable regardless of frame rate.

Interactivity: orbit controls

Three.js ships with OrbitControls in the examples folder. It lets the user rotate the scene by dragging, zoom with the scroll wheel, and pan with the right mouse button:

import { OrbitControls } from "three/addons/controls/OrbitControls.js";

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;  // smooth inertia

Call controls.update() inside the animation loop. Almost every Three.js demo uses OrbitControls — it is the single biggest quality-of-life improvement you can add.

Loading 3D models

For anything beyond primitives, you load a model file. The glTF format is the modern standard, designed for the web:

import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";

const loader = new GLTFLoader();
loader.load("model.glb", (gltf) => {
  scene.add(gltf.scene);
});

You can get glTF models from Sketchfab, Khronos sample models, or export them from Blender. The Three.js docs cover the various loaders and supported formats.

Performance considerations

WebGL is fast but it is not magic. A few rules of thumb:

  • Reduce the number of draw calls. Merge geometries where possible.
  • Use simpler materials when you can. MeshBasicMaterial is much cheaper than MeshStandardMaterial.
  • Be careful with shadows and anti-aliasing. Both are expensive.
  • Profile with the browser's Performance tab to find bottlenecks.
  • Use renderer.setPixelRatio to cap the rendering resolution on high-DPI displays.

For most small scenes (under 1000 objects), performance is fine on any device made in the last five years. Once you push past that, profiling becomes essential.

Common pitfalls

  • Camera too close or too far. If everything is black or everything is one colour, check the camera's near and far planes and its position.
  • Lighting missing. Standard materials need lights. Add at least an ambient and a directional light.
  • Not resizing on window resize. Listen to resize and update renderer.setSize and camera.aspect.
  • Mixing units. Three.js has no units, but pick a convention. 1 unit = 1 metre is the usual choice. Mixing units (1 unit = 1cm here, 1 unit = 1m there) leads to invisible objects or gigantic ones.

Shaders and custom materials

Three.js materials are written in a language called GLSL, which runs on the GPU. When you need an effect that no built-in material provides (water, fire, custom lighting, post-processing), you write a custom shader. Three.js makes this approachable with ShaderMaterial:

const material = new THREE.ShaderMaterial({
  vertexShader: `
    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    void main() {
      gl_FragColor = vec4(1.0, 0.5, 0.3, 1.0);
    }
  `,
});

The vertex shader runs once per vertex. The fragment shader runs once per pixel. The example above paints every surface orange. Real shaders add uniforms (values you can change from JavaScript), varyings (values passed from vertex to fragment shader), and texture lookups. The Book of Shaders is the classic place to learn GLSL from scratch.

Post-processing

Three.js has a post-processing pipeline that runs after the main render. Bloom (glowing highlights), depth of field (blur for far/near objects), tone mapping (making colours look cinematic) — all of these are post-processing effects. Use the EffectComposer from the addons:

import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";

const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
composer.addPass(new UnrealBloomPass());

Then call composer.render() instead of renderer.render() in your animation loop. Post-processing makes scenes look dramatically better, but each pass is a full-screen draw call — use them sparingly on lower-end devices.

That last paragraph alone — the homework — is where most of the learning happens. Do not skip it.

Further reading

WebGL is raw. Three.js makes it bearable. These are the sources we trust.

FAQ

Do I need to know WebGL to use Three.js?

No. Three.js abstracts away the shaders, buffers, and matrices. You can build sophisticated scenes without ever writing a single WebGL call. Knowing WebGL helps for advanced effects but is not required to start.

Three.js or Babylon.js?

Three.js is smaller, more popular, and has more third-party tools. Babylon.js has stronger built-in physics and a slightly more game-engine feel. For most web projects, Three.js is the right choice.

How do I add a sky?

Use scene.background = new THREE.Color(0x87ceeb) for a solid colour, or load a cube texture with CubeTextureLoader for a real sky. Three.js also has a sky shader in the examples for realistic atmospheric scattering.

How do I export from Blender?

File > Export > glTF 2.0. Choose ".glb" for a single binary file, or ".gltf + .bin + textures" for separate files. The default settings are usually fine.

What about WebGPU?

WebGPU is the next-generation graphics API. It is starting to land in browsers as of 2026. Three.js has a WebGPURenderer in development. For most projects, WebGL via Three.js is the safe and supported choice for now.

Can I use Three.js with React or Vue?

Yes. @react-three/fiber wraps Three.js as React components. tresjs does the same for Vue. Both are mature and widely used.

How do I draw text in 3D?

Use TextGeometry from the addons with a loaded font (JSON or TTF). For labels floating in space, you can also use HTML overlays positioned by projecting 3D coordinates to 2D screen space.

How do I make the scene full-screen?

Set the canvas's CSS to fill the viewport (position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;) and pass the window size to the renderer on every resize event.

Homework

Extend the spinning cube into a real interactive scene. Save it as scene.html and add:

  • OrbitControls so you can rotate the camera with the mouse.
  • A second, smaller cube next to the first, with a different material.
  • At least one light source that the cubes react to.
  • Resize handling so it works on window resize.
  • A simple animation: make one cube orbit the other.
  • Optional: load a glTF model and place it in the scene.

Once you have a scene you can spin and zoom around in, you have all the basic skills you need to start building real 3D web experiences, whether for product configurators, data visualisations, or browser games. From here, the natural next steps are custom shaders (for water, fire, glow effects) and physics libraries like Cannon or Rapier.