Every Three.js application follows the same fundamental pattern. Once you understand these three objects, you can build anything:
The render loop calls renderer.render(scene, camera) every frame using requestAnimationFrame. Between renders, you update object positions/rotations to create animation.
The renderer is configured for quality: antialiasing smooths jagged edges, devicePixelRatio ensures sharpness on retina screens, and we set the size to fill the entire browser window.
// Create the 3D world
var scene = new THREE.Scene();
// Camera: fieldOfView, aspectRatio, nearClip, farClip
var camera = new THREE.PerspectiveCamera(75, innerWidth/innerHeight, 0.1, 1000);
camera.position.z = 3; // Move camera back so we can see the cube
// Renderer: draws the scene to a canvas
var renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(devicePixelRatio);
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
Why position.z = 3? By default, both the camera and objects are at position (0,0,0). If the camera is inside the cube, you can't see it. Moving the camera back on the Z axis (toward the viewer) frames the cube in view.
In Three.js, visible objects are Meshes — a combination of two things:
BoxGeometry(1, 1, 1) creates a 1×1×1 unit cube. Three.js has built-in geometries for spheres, cylinders, planes, torus, and more.MeshPhongMaterial responds to light with shiny specular highlights (like plastic or metal). MeshBasicMaterial ignores light entirely (flat color).// Geometry: the shape (1x1x1 cube)
var geometry = new THREE.BoxGeometry(1, 1, 1);
// Material: the surface (shiny green, responds to light)
var material = new THREE.MeshPhongMaterial({
color: 0x2ad1d6, // Teal color (hex)
shininess: 100, // How shiny (0=matte, 100=mirror-like)
specular: 0x444444 // Color of the shiny highlight
});
// Mesh: combine geometry + material into a visible object
var cube = new THREE.Mesh(geometry, material);
scene.add(cube); // Add to the scene so it gets rendered
Color format: Three.js uses hex colors like CSS but with 0x prefix instead of #. So CSS #2ad1d6 becomes 0x2ad1d6 in Three.js.
The #1 beginner mistake: creating a MeshPhongMaterial cube and seeing nothing but black. Phong materials require light to be visible. Without lights in the scene, light-responsive materials render as pure black.
We add two lights:
// Directional light: like sunlight from upper-right
var light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(2, 3, 4); // Direction the light comes FROM
scene.add(light);
// Ambient light: soft fill so unlit sides aren't pure black
scene.add(new THREE.AmbientLight(0xffffff, 0.3));
Tip: If you want objects visible without adding lights, use MeshBasicMaterial instead — it ignores lighting entirely and shows the flat color. Good for prototyping, but looks less realistic.
requestAnimationFrame is the browser's way of saying "call this function before the next screen refresh" (typically 60 times per second). Each frame, we rotate the cube slightly and re-render the scene.
function animate() {
requestAnimationFrame(animate); // Schedule next frame
// Rotate the cube a tiny bit each frame
cube.rotation.x += 0.01; // Tilt forward/backward
cube.rotation.y += 0.01; // Spin left/right
renderer.render(scene, camera); // Draw the frame
}
animate(); // Start the loop!
Rotation units: Three.js uses radians, not degrees. 0.01 radians per frame × 60 frames/second = ~0.6 radians/second = ~34 degrees/second. A full rotation (2π radians) takes about 10 seconds.
Why not setInterval? requestAnimationFrame automatically pauses when the tab is hidden (saves battery), syncs with the display refresh rate (no tearing), and is optimized by the browser for smooth animation.
Without a resize handler, the scene looks stretched or squished when the browser window changes size. We need to update two things: the camera's aspect ratio and the renderer's canvas size.
window.addEventListener('resize', function() {
camera.aspect = innerWidth / innerHeight; // Update aspect ratio
camera.updateProjectionMatrix(); // Apply the change
renderer.setSize(innerWidth, innerHeight); // Resize canvas
});
updateProjectionMatrix() is required after changing any camera property (aspect, fov, near, far). Without it, the change won't take effect until the next camera creation.
OrbitControls lets the user drag to rotate the camera around the cube, scroll to zoom, and right-click to pan. It's the standard way to let users inspect 3D objects from any angle.
// Add OrbitControls for interactive camera rotation
var controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // Smooth inertia after releasing drag
controls.dampingFactor = 0.05; // How quickly rotation slows down
controls.autoRotate = true; // Slowly spin when not interacting
// IMPORTANT: call controls.update() in the animation loop
function animate() {
requestAnimationFrame(animate);
controls.update(); // Required for damping and auto-rotate
renderer.render(scene, camera);
}
Damping vs no damping: Without damping, the camera stops instantly when you release the mouse — feels abrupt. With damping, it decelerates smoothly like a real object with inertia. Always enable it for polished UX.
TextureLoaderrenderer.shadowMap.enabled = true and configure light/object shadow propertiesEach of these topics has its own tutorial in the Tutorials section.
Copy this code into an HTML file and open it in your browser to see it in action: