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
PerspectiveCamerathat 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.
MeshBasicMaterialis much cheaper thanMeshStandardMaterial. - Be careful with shadows and anti-aliasing. Both are expensive.
- Profile with the browser's Performance tab to find bottlenecks.
- Use
renderer.setPixelRatioto 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
resizeand updaterenderer.setSizeandcamera.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.
- Three.js: Official Documentation — Every class, every method, with examples.
- Three.js: Examples Gallery — Runnable demos for almost any pattern you want to copy.
- MDN: WebGL API — The underlying browser API Three.js wraps.
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.