Continous Rendering
The renderers we've written so far have been inert. The models are stuck in time and place. That's because we've used only a single draw call. Only once do the models pass through the graphics pipeline, and only once are they written to the framebuffer. To animate them, we need to draw them repeatedly, each time with a slightly modified transformation. This constant redrawing is continuous rendering.
We usually use loops to make code repeat. That's not safe in a browser. Loops take over, blocking the viewer from interacting with other page elements—like a stop button. JavaScript provides a better way to repeatedly call a function: requestAnimationFrame. Here's how we might use it:
function animate() {
updateTransformation();
render();
requestAnimationFrame(animate);
}
async function initialize() {
// ...
requestAnimationFrame(animate);
}
function animate() { updateTransformation(); render(); requestAnimationFrame(animate); } async function initialize() { // ... requestAnimationFrame(animate); }
When we call requestAnimationFrame
, we pass it an uncalled function. A task is placed in an event queue. When the browser has an idle moment, it pulls the task from the queue and calls the function. Other events in the queue do not get starved out; they are processed between frames.
Suppose we want to spin a model around the y-axis. In a naive first attempt, we add some global state for the transformation and update it on each call to animate
:
let radians = 0;
let transform: Matrix4 = Matrix4.identity();
function animate() {
radians += 0.1;
transform = Matrix4.rotateY(radians);
render();
requestAnimationFrame(animate);
}
let radians = 0; let transform: Matrix4 = Matrix4.identity(); function animate() { radians += 0.1; transform = Matrix4.rotateY(radians); render(); requestAnimationFrame(animate); }
When we test this on our own computer, we find that our model spins along nicely. Then we visit our grandparents and try to show them our renderer on their computer, which takes five minutes to boot. Sadly, the model rotates very slowly. Here's the problem: our continuous rendering depends on the frame rate. On a slow computer, animate
won't be called very often.
Imagine if a networked game had behavior dependent on the frame rate. Some players would have a significant advantage over others. Being faster may be just as perilous as being slower.
To fix this, the change in transformation must be proportional to how much actual time has elapsed since the previous frame. We add a then
timestamp to hold the time of the previous frame and receive a now
timestamp when animate
is called:
let radians = 0;
let transform: Matrix4 = Matrix4.identity();
let then: DOMHighResTimeStamp | null = null;
function animate(now: DOMHighResTimeStamp) {
let elapsed = then ? now - then : 0;
radians += elapsed * 0.03;
transform = Matrix4.rotateY(radians);
render();
requestAnimationFrame(animate);
then = now;
}
let radians = 0; let transform: Matrix4 = Matrix4.identity(); let then: DOMHighResTimeStamp | null = null; function animate(now: DOMHighResTimeStamp) { let elapsed = then ? now - then : 0; radians += elapsed * 0.03; transform = Matrix4.rotateY(radians); render(); requestAnimationFrame(animate); then = now; }
The timestamps are in milliseconds. On the first call, the elapsed time will be 0. The radians
value grows by 3% the elapsed time.
This cube should spin at the same rate on everyone's computer:
That doesn't mean the animation won't be choppy on a slow computer.
Continuous rendering necessarily consumes more energy than on-demand rendering. We tend not to care when our computer is plugged into a wall. But viewers on laptops or mobile devices will notice their battery drain faster. One consolation is that requestAnimationFrame
is ignored if the browser tab is not visible. Energy is only consumed when the viewer is actively engaged with the renderer.