The Animation Loop

Bringing your scene to life frame by frame

How Animation Works in Three.js

To create animation, you need to render your scene many times per second - typically 60 frames per second (60 FPS). Each frame, you update your objects slightly, then render. The browser provides requestAnimationFrame to make this smooth and efficient.

The Basic Animation Loop

Here's the simplest animation loop structure:

typescript
// The tick function runs every frame
function tick() {
  // Update objects
  mesh.rotation.y += 0.01;

  // Render the scene
  renderer.render(scene, camera);

  // Call tick again on the next frame
  requestAnimationFrame(tick);
}

// Start the loop
tick();

requestAnimationFrame is smart - it pauses when the browser tab is hidden (saving CPU/GPU), and syncs with your monitor's refresh rate for smooth animation.

Frame: 0

The Problem with Fixed Increments

Adding a fixed amount each frame (like rotation.y += 0.01) has a problem: animation speed depends on frame rate. On a 120Hz monitor, your cube spins twice as fast as on a 60Hz monitor!

Watch the two cubes below - the left uses a fixed increment, the right uses delta time. They look the same here, but would differ on monitors with different refresh rates:

Using Delta Time

The solution is to base animation on elapsed time, not frame count. requestAnimationFrame passes a timestamp you can use:

typescript
let previousTime = 0;

function tick(currentTime) {
  // Convert to seconds
  currentTime *= 0.001;

  // Calculate time since last frame
  const deltaTime = currentTime - previousTime;
  previousTime = currentTime;

  // Now use deltaTime for consistent speed
  // This rotates ~1 radian per second regardless of frame rate
  mesh.rotation.y += 1 * deltaTime;

  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}

requestAnimationFrame(tick);

THREE.Clock

Three.js provides a Clock class that handles timing for you:

typescript
const clock = new THREE.Clock();

function tick() {
  // Get time since clock started
  const elapsedTime = clock.getElapsedTime();

  // Get time since last tick
  const deltaTime = clock.getDelta();

  // Use elapsed time for smooth oscillation
  mesh.position.y = Math.sin(elapsedTime);

  // Use delta time for consistent rotation speed
  mesh.rotation.y += 2 * deltaTime; // 2 radians per second

  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}

tick();
Elapsed: 0.00s

Pausing and Resuming

You might want to pause animation when the user switches tabs or clicks a pause button:

typescript
let animationId = null;
let isPaused = false;

function tick() {
  if (isPaused) return;

  mesh.rotation.y += 0.01;
  renderer.render(scene, camera);
  animationId = requestAnimationFrame(tick);
}

function pause() {
  isPaused = true;
  if (animationId) {
    cancelAnimationFrame(animationId);
    animationId = null;
  }
}

function resume() {
  if (!isPaused) return;
  isPaused = false;
  tick();
}

// Start
tick();

Using setAnimationLoop

The renderer has a built-in setAnimationLoop method. This is especially useful for WebXR (VR/AR) since it uses the correct timing for headsets:

typescript
// Set up the animation loop
renderer.setAnimationLoop((time) => {
  // time is in milliseconds
  mesh.rotation.y = time * 0.001;
  renderer.render(scene, camera);
});

// To stop the loop
renderer.setAnimationLoop(null);

Performance Tips

Your animation loop runs 60+ times per second. Keep it lean:

typescript
// BAD - Creating objects every frame
function tick() {
  const newVector = new THREE.Vector3(1, 2, 3); // Garbage collection!
  mesh.position.copy(newVector);
}

// GOOD - Reuse objects
const tempVector = new THREE.Vector3();
function tick() {
  tempVector.set(1, 2, 3);
  mesh.position.copy(tempVector);
}

// BAD - Heavy computation every frame
function tick() {
  const result = expensiveCalculation(); // Slow!
}

// GOOD - Only compute when needed
let cachedResult = expensiveCalculation();
function tick() {
  // Use cachedResult
}

// Recalculate only when necessary
function onDataChange() {
  cachedResult = expensiveCalculation();
}

Cleanup

Always clean up when your component unmounts to prevent memory leaks:

typescript
// Store the animation ID
let animationId = null;

function tick() {
  // ... animation code
  animationId = requestAnimationFrame(tick);
}

// When component unmounts
function cleanup() {
  if (animationId) {
    cancelAnimationFrame(animationId);
  }
  renderer.dispose();
  // Also dispose geometries, materials, textures...
}
←   Position, Rotation, ScaleGeometries   →