Animation Loop
This note is based on DISCOVER three.js, mostly excerpts with some personal understanding
In each frame, we render once. If the object's properties change, it creates animation.
Setting up this loop is simple because three.js does all the hard work for us through the renderer.setAnimationLoop method.
Creating Loop.js
Create a systems/Loop.js:
import { Clock } from 'three';
const clock = new Clock();
class Loop {
constructor(camera, scene, renderer) {
this.camera = camera;
this.scene = scene;
this.renderer = renderer;
this.updatables = [];
}
start() {
this.renderer.setAnimationLoop(() => {
// tell every animated object to tick forward one frame
this.tick();
// render a frame
this.renderer.render(this.scene, this.camera);
});
}
stop() {
this.renderer.setAnimationLoop(null);
}
tick() {
// only call the getDelta function once per frame!
const delta = clock.getDelta();
// console.log(
// `The last frame rendered in ${delta * 1000} milliseconds`,
// );
for (const object of this.updatables) {
object.tick(delta);
}
}
}
export { Loop };- Use
.setAnimationLoop(callback)to create the loop, and pass.setAnimationLoop(null)to end the loop - The loop is internally implemented using
.requestAnimationFrame. tick()is the function that updates all animations, and this function should run once at the start of each frame. However, the word update is already heavily used throughout three.js, so we'll choose the word tick.- This implements decoupled logic, automatically calling the
tick()method on objects in the updatables array
Using it in World.js
import { createCamera } from './components/camera.js';
import { createCube } from './components/cube.js';
import { createLights } from './components/lights.js';
import { createScene } from './components/scene.js';
import { createRenderer } from './systems/renderer.js';
import { Resizer } from './systems/Resizer.js';
import { Loop } from './systems/Loop.js';
let camera;
let renderer;
let scene;
let loop;
class World {
constructor(container) {
camera = createCamera();
renderer = createRenderer();
scene = createScene();
loop = new Loop(camera, scene, renderer);
container.append(renderer.domElement);
const cube = createCube();
const light = createLights();
loop.updatables.push(cube);
scene.add(cube, light);
const resizer = new Resizer(container, camera, renderer);
resizer.onResize = () => {
this.render();
};
}
render() {
// draw a single frame
renderer.render(scene, camera);
}
start() {
loop.start();
}
stop() {
loop.stop();
}
}
export { World };- The cube is added as an animated object to
updatables. Note that you need to implement thecube.tick()method yourself, which the Loop will automatically call - Now that the loop is running, whenever we resize the window, a new frame will be generated in the next iteration of the loop. This is fast enough that you won't notice any delay, so we no longer need to manually redraw the scene on resize.
Calling in main.js
import { World } from './World/World.js';
function main() {
// Get a reference to the container element
const container = document.querySelector('#scene-container');
// create a new world
const world = new World(container);
// draw the scene
world.render();
// start the animation loop
world.start();
}
main();Adding tick to cube
import {
BoxBufferGeometry,
MathUtils,
Mesh,
MeshStandardMaterial,
} from 'three';
function createCube() {
const geometry = new BoxBufferGeometry(2, 2, 2);
const material = new MeshStandardMaterial({ color: 'purple' });
const cube = new Mesh(geometry, material);
cube.rotation.set(-0.5, -0.1, 0.8);
const radiansPerSecond = MathUtils.degToRad(30);
// this method will be called once per frame
cube.tick = (delta) => {
// increase the cube's rotation each frame
cube.rotation.z += radiansPerSecond * delta;
cube.rotation.x += radiansPerSecond * delta;
cube.rotation.y += radiansPerSecond * delta;
};
return cube;
}
export { createCube };Note: Adding properties to existing classes at runtime like this is called monkey patching (here, we're adding
.tickto aMeshinstance). This is common practice and won't cause any issues in our simple application. However, we shouldn't make this careless habit, as in some cases it can cause performance issues. We only allow ourselves to do this here because the alternative is more complex
Why multiply by delta? Explanation follows:
Frame Rate is Not Completely Stable
- We may not be able to generate frames successfully and quickly. If the device running your application isn't powerful enough to reach the target frame rate, the animation loop will run slower.
- Even on fast hardware, your application must share computing resources with other applications, and there may not always be enough.
- Even with a powerful GPU and a simple scene like this single cube, we won't achieve exactly 60 frames per second precision. Some frames render a bit faster, some a bit slower. This is normal. Part of the reason is that, for security reasons, browsers add about 1 millisecond of jitter to the
.getDeltaresult.
The delta here is provided by .getDelta in Loop.js, telling us how much time has passed since the last .getDelta call.
This way, if a frame is slow, delta is large, and the animation change is larger. The more time spent, the longer the movement distance, making the rate relatively stable.
Other Notes
Sometimes we need on-demand rendering, which requires manually starting and timely stopping