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