How It Works — The Three.js Pattern

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.

Scene & Renderer Setup

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.

Geometry + Material = Mesh

In Three.js, visible objects are Meshes — a combination of two things:

// 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.

Lighting — Why Objects Appear Black

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.

The Animation Loop

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.

Window Resize Handling

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 — Interactive Camera

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.

Next Steps — Where to Go From Here

Each of these topics has its own tutorial in the Tutorials section.

Key Takeaways

COMPLETE SOURCE CODE

Copy this code into an HTML file and open it in your browser to see it in action:

HTML / Three.js
<!DOCTYPE html>
<html>
<head>
    <title>3D Cube - Three.js</title>
    <style>
        body { margin: 0; overflow: hidden; background: #000; }
        canvas { display: block; }
    </style>
</head>
<body>
    <script src="https://cdn.jsdelivr.net/npm/three@latest/build/three.min.js"></script>

    <script>
    // ============================================================
    // SCENE - The 3D world container
    // ============================================================
    const scene = new THREE.Scene();

    // ============================================================
    // CAMERA - What the viewer sees
    // PerspectiveCamera(fov, aspect, near, far)
    // ============================================================
    const camera = new THREE.PerspectiveCamera(
        75,                                      // Field of view (degrees)
        window.innerWidth / window.innerHeight,  // Aspect ratio
        0.1,                                     // Near clip
        1000                                     // Far clip
    );
    camera.position.z = 5;  // Move camera back to see the cube

    // ============================================================
    // RENDERER - Draws the scene to a canvas
    // ============================================================
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    // ============================================================
    // CUBE - Geometry (shape) + Material (surface) = Mesh
    // ============================================================
    const geometry = new THREE.BoxGeometry(2, 2, 2);  // width, height, depth
    const material = new THREE.MeshPhongMaterial({
        color: 0x00ff88,   // Green color
        shininess: 100     // How shiny/reflective the surface is
    });
    const cube = new THREE.Mesh(geometry, material);
    scene.add(cube);

    // ============================================================
    // LIGHTING - Without light, MeshPhongMaterial is black
    // ============================================================
    scene.add(new THREE.AmbientLight(0xffffff, 0.6));  // Soft fill everywhere

    const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);  // Sun-like
    dirLight.position.set(10, 10, 5);
    scene.add(dirLight);

    // ============================================================
    // ANIMATION LOOP - Runs ~60 times per second
    // ============================================================
    function animate() {
        requestAnimationFrame(animate);
        cube.rotation.x += 0.01;  // Rotate around X
        cube.rotation.y += 0.01;  // Rotate around Y
        renderer.render(scene, camera);
    }
    animate();

    // ============================================================
    // RESIZE HANDLER - Keep aspect ratio correct
    // ============================================================
    window.addEventListener('resize', () => {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
    });
    </script>
</body>
</html>