Course outline
The Render Loop
In one line
A loop draws the scene again and again. You learn setAnimationLoop, why motion must use delta time (so 60 Hz and 120 Hz screens agree), what THREE.Timer does instead of the deprecated Clock, how to draw only when something changed, and what a background tab does.
Think of it like this
A flip book. Each page is a frame. At 30 pages a second the hand moves a lot per page; at 120 you must draw a smaller move per page, or it races. What stays fixed is the speed in the story: "3 metres a second", not "0.05 metres a page".
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- frame
- One picture drawn by the loop. At 60 Hz you get about 60 per second, each in about 16.7 ms.
- delta time
- The seconds that passed since the previous frame. Multiply speeds by it so motion does not depend on the screen's refresh rate.
- refresh rate (Hz)
- How many times per second the screen can show a new picture: 60 on most, 120 or more on many phones and gaming screens.
- requestAnimationFrame
- The browser function that runs your code before the next screen refresh. It pauses in hidden tabs.
Step by step
01Hand the loop a function
renderer.setAnimationLoop((time) => { ... }). time is the same millisecond timestamp that requestAnimationFrame gives: milliseconds since the page started. Inside, move things and call render.
02Update the timer once per frame
timer.update(time) first, then timer.getDelta() in seconds. Reading it again later in the same frame returns the same number. That is the whole difference from Clock.
03Multiply speed by delta
Write speeds as units per second: const TURNS_PER_SECOND = 0.5. Each frame, wheel.rotation.z -= delta * TURNS_PER_SECOND * Math.PI * 2. Radians per second times seconds gives radians.
04Connect the timer to the page
timer.connect(document) makes hidden tabs report a delta of 0. You can test it without hiding a tab: the second example fakes the document's hidden flag and the timer reports 0.0000 where the plain one reports 5 seconds.
Try it yourself
- 1
Change the speed, not the code
In the first example set
TURNS_PER_SECONDto2. The console prints2 turns per second = 12.5664 radians per secondand the wheel visibly spins four times as fast, whatever your screen's refresh rate. - 2
Slow motion with the timer
Add
timer.setTimescale(0.25);aftertimer.connect(document);. The wheel now turns at a quarter of the speed:getDelta()multiplies every delta by the timescale. This is how games do slow motion and pause (0) without touching their movement code.
Code & diagrams
The status bar stays at 2 draw calls (tyre and spoke). The speed does not change with the frame rate.
Console
Run the scene to see its console.Expected console output
speed: 0.5 turns per second = 3.1416 radians per second
first frame drawn, delta is a number: trueNo canvas here: the example feeds made-up times to the same Timer maths. The hidden tab is faked with the document's `hidden` property.
Console
Run the scene to see its console.Expected console output
30 Hz 30 frames | per frame: 1.50 m | per second: 3.00 m
60 Hz 60 frames | per frame: 3.00 m | per second: 3.00 m
120 Hz 120 frames | per frame: 6.00 m | per second: 3.00 m
a normal 60 Hz frame: 0.0167
a frame that took 100 ms: 0.1000
plain Timer, back after 5 s: 5.0000 seconds in one step
connected Timer, tab hidden: 0.0000 seconds in one step
connected Timer, tab visible again, small step: trueClick the canvas: the box turns a quarter turn and the status bar's frame counter moves by one. While nothing changes it stays still.
Console
Run the scene to see its console.Expected console output
renders after 10 ticks with no changes: 1
renders after one request: 2Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Move by a bit per frame
In the first example replace the rotation line with wheel.rotation.z -= 0.05;.
Break #2
Use the old Clock
Replace the three timer lines with const clock = new THREE.Clock(); and const delta = clock.getDelta(); in the loop.
Myth vs fact
Myth
Hand-written requestAnimationFrame is as good as setAnimationLoop.
Fact
For plain WebGL it works. But WebXR (Phase 18) uses a different frame callback and the WebGPU renderer expects three.js to own the loop.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Each browser callback in
WebGLAnimation.jsre-requests the next frame first and then runs your function, so an exception in it does not stop the loop: it fails again every frame and the console fills up. - ▸
Timer.update(timestamp)computes delta from the timestamp you pass, which is the frame's start time.performance.now()inside the loop would be a little later and uneven, so pass the loop's owntime.
Remember this
- 1
The problem this solves: screens refresh at 30, 60, 90, 120 or 144 times a second, and a slow frame can take 100 ms. Code that moves an object "a bit per frame" runs twice as fast on a 120 Hz screen and slower on a busy phone. You need motion defined per second, and a loop that does no work when nothing changes.
- 2
**
renderer.setAnimationLoop(fn)** runsfn(time, frame)before every screen refresh;setAnimationLoop(null)stops it. In r186 it wrapsrequestAnimationFrame(WebGLAnimation.js). Use it rather than your ownrequestAnimationFrameloop: WebXR (Phase 18) and the WebGPU renderer (Phase 13) need three.js to own the loop. - 3
Delta time is the seconds since the previous frame (about 0.0167 at 60 Hz, 0.0083 at 120 Hz). Multiply every speed by it:
angle += delta * turnsPerSecond * 2π. The second example shows why: 0.05 m per frame gives 3, 6 or 1.5 m in one second depending on the screen; 3 m/s × delta gives 3 m on all three. - 4
**
THREE.TimerreplacesTHREE.Clock.** r186 still ships both (core/Clock.js,core/Timer.js), andnew THREE.Clock()printsTHREE.Clock: This module has been deprecated. Please use THREE.Timer instead.(deprecated since r183). WithTimeryou calltimer.update(time)once per frame, andgetDelta()andgetElapsed()then return the same values however often you read them.Clock.getDelta()changed the clock on every call, so two reads in a frame double-counted. - 5
Background tabs. A hidden tab stops calling
requestAnimationFrame; on return a plain timer sees one huge step (5 seconds in the example) and everything jumps.timer.connect(document)uses the Page Visibility API: while hidden,update()reports 0, and when visible again the timer resets. Always connect it, and calltimer.dispose()when done. - 6
A slow frame (a delta of 0.1 s) is not hidden by the timer: the object moves a tenth of a second's worth at once, a visible jump, though the average speed stays right. Games clamp it (
Math.min(delta, 0.05)) so a hitch cannot fling an object through a wall. - 7
Render on demand. A showroom is still most of the time. Run the loop every frame but call
render()only when adirtyflag is set (third example): a click, a slider or a finished download sets it. The GPU then idles while the picture is still. A constantly animating page (the turntable) makes every frame dirty, so use this for mostly-still pages.
Explain it without notes
Why must movement be multiplied by delta time? Give a number for 60 Hz and 120 Hz.
What does timer.connect(document) fix, and what would go wrong without it?
Practice
Make the render-on-demand example also redraw when the window is resized. Which lines do you add?
Make a loop that never lets one frame advance the animation by more than 50 ms.
Done when you can
Write a loop with
setAnimationLoopand stop it withsetAnimationLoop(null)Use
THREE.Timerand delta time so speed is per secondExplain what a background tab and a slow frame do to delta, and how to protect against both
Draw on demand with a dirty flag