BuildBot

Scene to Screen

The animation loop

Lesson 4 of 5

What you'll learn

  • Drive frames with renderer.setAnimationLoop instead of a hand-rolled requestAnimationFrame
  • Use Clock's delta time so motion runs at the same real-world speed on any display
  • Pick objects under the pointer with Raycaster.setFromCamera

Lesson 1's renderer.render() draws one frame. Animation is that call repeated in step with the display — 60, 120, or 144 times a second, whatever the device refreshes at. You could recurse with requestAnimationFrame, but the current idiom is to hand your frame function to the renderer:

const clock = new THREE.Clock();

renderer.setAnimationLoop(() => {
  const delta = clock.getDelta();            // seconds since the previous frame
  cube.rotation.y += Math.PI * 0.5 * delta;  // 90°/second on ANY display
  renderer.render(scene, camera);
});
// renderer.setAnimationLoop(null) stops it.

setAnimationLoop uses requestAnimationFrame under the hood but also keeps working in WebXR sessions, where the headset — not the browser window — drives the frame timing. The crucial habit is the second line: multiply motion by delta time. Frames are not evenly spaced (a 120 Hz display fires twice as often as a 60 Hz one, and any frame can hitch), so "rotate 0.01 per frame" runs at different speeds on different machines. "Rotate speed * delta" runs at the same speed everywhere, because the deltas always sum to real elapsed seconds.

Picking with a raycast

The loop draws pixels, but users click on objects. A Raycaster bridges the two: convert the pointer into normalized device coordinates (−1..1), fire a ray from the camera through that point, and ask what it hits — nearest first.

const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();

window.addEventListener("pointerdown", (e) => {
  pointer.x = (e.clientX / window.innerWidth) * 2 - 1;
  pointer.y = -(e.clientY / window.innerHeight) * 2 + 1;
  raycaster.setFromCamera(pointer, camera);
  const hits = raycaster.intersectObjects(scene.children, true);
  if (hits.length) hits[0].object.material.color.set(0xff0055);
});

Delta spikes on tab switch

Background tabs pause the loop, so the first delta after returning can be huge — and speed × delta teleports things. Clamp it: const dt = Math.min(clock.getDelta(), 0.1).

The challenge runs a real frame loop with deliberately jittery frame times and measures actual elapsed time each tick — exactly what Clock.getDelta does. Watch the deltas vary while the angle stays locked to the wall clock.

A frame loop with delta time

Run it. Frames are scheduled unevenly on purpose (16ms and 50ms), like a real device hitching. Because rotation is speed × delta, the final angle matches speed × real elapsed time anyway. Change SPEED and re-run.

Loading editor…
Knowledge check

Why multiply per-frame movement by delta time?

Next: cubes and spheres got us this far — real apps load real models, and glTF is how they arrive.

Saved on this device. Sign in to sync your progress everywhere.