Notes on Animation
With animation, we change the scene dynamically, so instead of a still scene (even if the user moves the camera), the scene changes over time.
Animation Basics
Animation of simple motion is fairly straightforward. Instead of producing one frame, we produce several. The way this is usually done in a modern web browser is with
requestAnimationFrame(func)
(This feature is fairly new; older browsers may not support it.)
This function asks the browser when you have time, please invoke
this function
. This is a kind of idle callback. This is
similar to other callbacks: we register it and the browser calls
it. This callback, however, is called when the graphics system has
nothing better to do — it's idle (and the browser window or
tab is exposed, and so forth) — the browser will be smart about
not invoking your function when it's unnecessary.
If the idle callback does the following:
- adjusts some global variables or other parameters of the graphics program,
- re-renders the scene, and
- requests another animation frame
then the effect will be to continually adjust the variables and redisplay your scene, thereby producing an animation. If you think back to the camera homework, that program adjusted the camera parameters and redrew the scene; that could easily be turned into an animation.
Examples:
Dirksen's examples from Chapter 1:
Bouncing Ball:
But you didn't think it was that easy, did you? It's not. The hard part is always in modeling the physics of the situation. We'll look at some hard cases later.
Organizing your Code
Animations can sometimes be hard to debug, because the state of variables is changing so quickly, often 60 times per second. Consequently, I strongly suggest setting up your code to make it easy to
- reset the animation to the initial state
- advance the animation by one step
- start the animation going (continuously
looping
) - stop the animation (freezing it at the current state).
We can see all of these in the following demo:
But before we take on the bouncing ball, let's look at a demo that is a lot easier:
Here's the complete code. Read over it a bit, figuring out what you can, then we'll pick it apart one piece at a time.
We see several functions here. The first is resetting the state of
the animation to the initial state. We will keep the collection of
values that comprises the state of the animation in a JS
object. Here it doesn't have a lot in it, but for more complicated
animations, it would have a lot more. We'll have one property for
each value that changes with time, plus, of course,
time
itself.
// State variables of the animation var animationState; // sets the animationState to its initial setting function resetAnimationState() { animationState = { time: 0, // rotation angles rx: 0, ry: 0, rz: 0, lastParam: null }; }
When we want to reset the animation, we need to invoke this function, update objects that depend on it, and re-draw the scene. That's what this does:
function firstState() { resetAnimationState(); TW.render(); }
Next, we need to look at how to advance the simulation by one step. We'll advance time, set the cube's rotation around all the axes, and redraw. Note, below, that we've separated the updating from the rendering.
function updateState() { animationState.time += 1; // increase the total rotations by the user-specified velocity animationState.rx += guiParams.vx; animationState.ry += guiParams.vy; animationState.rz += guiParams.vz; // transfer the state info to the cube cube.rotation.x = animationState.rx; cube.rotation.y = animationState.ry; cube.rotation.z = animationState.rz; } function oneStep() { updateState(); TW.render(); }
Next, let's look at how to start the animation. Given the building blocks above, this code probably doesn't have too many surprises:
// Stored so that we can cancel the animation if we want var animationId = null; function animate(timestamp) { oneStep(); animationId = requestAnimationFrame(animate); console.log("Starting animation using " + animationId); }
Finally, we need to be able to stop the animation.
The requestAnimationFrame()
function (defined by the browser,
not by Three.js or even by WebGL; you can use it with 2D canvas drawing)
returns an integer that is meaningless by itself, but can be given as an
argument to cancelAnimationFrame()
to cancel the request:
function stopAnimation() { if( animationId != null ) { cancelAnimationFrame(animationId); } }
It's good to bind each of these to keys, so that we can control the animation from the keyboard, without cluttering the visual interface:
TW.setKeyboardCallback("0", firstState, "reset animation"); TW.setKeyboardCallback("1", oneStep, "advance by one step"); TW.setKeyboardCallback("g", animate, "go: start animation"); TW.setKeyboardCallback(" ", stopAnimation, "stop animation");
Animation Techniques
We can break animation techniques down into two broad categories, roughly:
- derivative: how does the scene change from frame to frame?
- positional: where are things supposed to be at a given time?
(Drawing on a concept from calculus, you can see that the first technique gets its name because it is the derivative of the position function.) The rest of this reading describes derivative techniques; the next reading will describe positional techniques.
Derivative Techniques
Often the simplest technique is just to adjust the scene in some straightforward way, ignoring time. That's what we did with the spinning cube.
Essentially, all our idle callback computations are based on something like:
function updateState() { // both of these are globals position += velocity; updateScene(); TW.render(); requestAnimationFrame(updateState); }
We named the variable on the right velocity
because by
definition, velocity is the change in position. If the velocity is large,
there will be a big change in position; if the velocity is small, there
will be a small change. Note also that velocity can be either positive or
negative, so position can increase or decrease. (In practice, these
variables would probably live in an object, as we did earlier, but we've
omitted those details here.)
You'll notice that time does not appear in the computation above. Essentially, each frame of our animation is one time step, which means we can think of the above computation as the following:
function updateState() { var deltaT = 1; position += velocity * deltaT; TW.render(); requestAnimationFrame(updateState); }
where deltaT
is our time step, and it has the value 1 by
the way we are building the animation. As you probably remember from
high-school physics, if an object is moving at 10 meters per second, and
deltaT is 5 seconds, the object moves 50 meters. That's exactly what we're
doing above.
If your model is based on real-world objects with real-world positions and speeds (say, meters and meters per second), you need to understand that each frame of the animation is one time unit (say, one second).
The time unit and speeds are also crucial for determining the
smoothness of your animation. If your object jumps by a whole
bunch from one frame to the next, the animation may look jerky. To fix
this, you'd need to reduce your deltaT
, say from one second
to half a second, or even a tenth of a second. Thus, your computations
become:
function updateState() { var deltaT = 0.1; position += velocity * deltaT; TW.render(); requestAnimationFrame(updateState); }
For a slightly more complex motion, still based on the derivative approach, consider the mass spring demo. This is a classic example from physics, where the mass moves due to a force exerted on it by the spring. The spring exerts a force that depends on the amount that the spring is stretched and hence on the position of the mass. The force yields an acceleration that depends on the mass (more mass, less acceleration). Acceleration, of course, results in a changing velocity, and velocity results in changing position. Thus, at each time step, the idle callback computes:
function updateState() { // all of these are globals massA = - springK / mass * massX; massV += massA * DeltaT; massX += massV * DeltaT; TW.render(); requestAnimationFrame(updateState); }
The nature of this particular model is such that the velocity is sometimes negative and sometimes positive, so the mass moves back and forth, oscillating endlessly.
There are variant mass-spring models with damping (friction) that slows the mass down based on its velocity. A google search for "mass-spring" yields many results, so feel free to investigate if you like.