Time for Action: Bouncing Ball

Let's look at an example covering how we'd animate many objects in our scene:

  1. Open ch05_03_bouncing-balls.html in your browser:

  1. The orbiting camera is activated by default. Move the camera and you will see how all the objects adjust to the global transform (camera) and continue bouncing according to their local transforms.
  2. Let's explain how we keep track of each ball in more detail.
  3. Define the appropriate global variables and constants:
let
gl, scene, program, camera, transforms,
elapsedTime, initialTime,
fixedLight = false,
balls = [],
sceneTime = 0,
animationRate = 15,
gravity = 9.8,
ballsCount = 50;
  1. Initialize the balls array. We use a for loop in the load function to achieve this:
function load() {
scene.add(new Floor(80, 2));
for (let i = 0; i < ballsCount; i++) {
balls.push(new BouncingBall());
scene.load('/common/models/geometries/ball.json', `ball${i}`);
}
}
  1. The BouncingBall class initializes the simulation variables for each ball in the balls array. One of these attributes is the position, which we select randomly. You can see how we do this by using the generatePosition function:
function generatePosition() {
return [
Math.floor(Math.random() * 50) - Math.floor(Math.random() *
50),
Math.floor(Math.random() * 30) + 50,
Math.floor(Math.random() * 50)
];
}
  1. After adding a new ball to the balls array, we add a new ball object (geometry) to the scene instance. Please note that the alias we create includes the current index of the ball object in the balls array. For example, if we add the 32nd ball to the array, the alias that the corresponding geometry will have in scene will be ball32.
  2. The only other object that we add to the scene here is Floor. We have used this object in previous exercises. You can find the code for the Floor class in common/js/Floor.js.
  3. Let's talk about the draw function. Here, we go through the elements of scene and retrieve each object's alias. If the alias contains the word ball, we know that the alias corresponds to its index in the ball array. We could have probably used an associative array here to make it look nicer, but doing so does not really change our goal. The main point here is to make sure that we can associate the simulation variables for each ball with the corresponding object (geometry) in scene.
  1. Notice that for each object (ball geometry) in scene, we extract the current position and the color from the respective BouncingBall instance in the balls array.
  2. Alter the current Model-View matrix for each ball by using a matrix stack to handle local transformations, as described previously. In our case, we want the animation for each ball to be independent from the transformations of the camera and one another.
  3. So far, we’ve described how the bouncing balls are created (load) and how they are rendered (draw). None of these functions modifies the current position of the balls. We do that by using BouncingBall.update. This code uses the animation time (the sceneTime global variable) to calculate the position for the bouncing ball. Since each BouncingBall has its own simulation parameters, we can calculate the position for each given position when sceneTime is provided . In short, the ball position is a function of time and, as such, it falls into the category of motion described by parametric curves.
  4. The BouncingBall.update method is called inside the animate function. As we saw previously, this function is invoked by the animation timer each time the timer is up. Inside this function, you can see how the simulation variables are updated to reflect the current state of that ball in the simulation.

What just happened?

We’ve learned how to handle several object-local transformations while preserving global transformations by using a matrix stack strategy. In the bouncing ball example, we used an animation timer for the animation that is independent from the rendering timer. Finally, we saw how the bouncing ball update method shows how parametric curves work.