Animating with nested matrices (Oct 2 lecture) - course notes and homework due Oct 9 before class

This week we reviewed Denis Zorin's lecture on nested matrices, and we also started talking about how to animate things over time.

I went over the point that the relative movements within a complex nested geometry (such as a human body or an automobile) can be structured as a tree. As computer scientists we know that in order to traverse a tree you use a stack, and that in order to traverse a stack, you need push() and pop() operations.

There are two distinct ways to do this - either by using an explicit stack and explicit push() and pop() methods, or else to create parent→child relationships between objects, whereby the child object is always rendered relative to its parent. In the latter case we use recursive method calls to evaluate children, children of children, etc., so the matrix stack is implicit, because it is implemented by the data stack of recursive method calls.

In this lecture we focused only on the explicit approach, although you are free to use the implicit approach in your homework if you are feeling ambitious.

I had suggested implementing the stack via:

   int ns = 0;
   Matrix S[] = new Matrix[100];
As noted in class, it is sufficient to give a fixed length for the matrix stack because transformation nesting tends not to have unbounded depth.

In this approach, your primitive transforations operations:

   rotateX(theta)
   rotateY(theta)
   rotateZ(theta)
   translate(x,y,z)
   scale(x,y,z)
should modify the matrix on the top of the stack S[sp].

You will also want to push the current matrix on the stack (ie: to store the current value to we can modify a temporary copy) and to pop the stack (ie: to restore the stored value that we had pushed). To do this, you need two more simple methods:

   void push() {
      copy(S[sp], S[sp+1]);
      sp++;
   }
   void pop() {
      --sp;
   }
where the method copy(Matrix src,Matric dst) above copies from its first argument to its second argument.

You should add code to your implementation of the push() and pop() methods to check for stack overflow and underflow.

In order to apply the matrix on the top of the stack to an object's matrix, you will also want to provide a transform(shape) method that copies the matrix on top of the stack into that shape's transformation matrix for this frame:

   void transform(Shape shape) {
      copy(S[sp], shape.matrix);
   }

Your assignment is to implement some sort of cool and interesting nested mechanized figure, such as a person, or a robot, or a vehicle, or a bobbing bird or anything else that might be fun, and that you think the rest of the class would enjoy seeing.

In order to make things move over time, you'll need some of the arguments in your calls to your primitive matrix tranformation methods to vary over time. As we discussed in class, one way to do this is to use a key frame approach, in which you specify values at particular key animation frames, and then interpolate values at other times that lie between these key frames.

One particular way to do this, which we went over in class, is to provide an array of {key time, key value} pairs, and to implement a method eval(double keyData[][], double time) that extracts the time-varying animation value at any arbitrary time.

For now, we will define the interpolation between key frames to be linear, so that the behavior of the function as a whole is piecewise linear. For example, suppose we want a value to rise up from 0.0 at time 0.0 to 10.0 at time 5.0, hold steady at 10.0 until time 15.0, and then fall back down to 0.0 at time 20.0. We would then define the doubly nested array:

  
   double keyData[][] = {
	{  0.0,  0.0 },
	{  5.0, 10.0 },
	{ 15.0, 10.0 },
	{ 20.0,  0.0 },
   };
Then at any given time we can get the value by invoking eval(keyData, time) .

You will need to implement eval. If the argument time is less than the first key time keyData[0][0] then your method should just return the first key value keyData[0][1]. Similarly, if the argument time is greater than the last key time keyData[n-1][0] (where n = keyData.length), then your method should just return the last key value keyData[n-1][1].

You can compute the linearly interpolated value between two successive key times by using ratios. Using the shorhand notation:

   Ti = keyData[i][0]
and
   Vi = keyData[i][1]
then the value of the interpolated function at a particular time (where Ti ≤ time < Ti+1), is computed by:
   Vtime = Vi + (Vi+1 - Vi) * (time - Ti) / (Ti+1 - Ti)

As a simple example of putting the above concepts into practice, here is code excerpted from a scene that contains a ball bouncing on top of a box that is sliding left to right:

   ...
   Shape box, ball;
   ...
   void initialize() {
      ...
      cube = new Shape(CUBE);
      box  = new Shape(SPHERE);
      ...
   }
   ...
   double slidingData[][] = {
      {  0.0, -2.0 },
      { 10.0,  2.0 },
      { 20.0, -2.0 },
   };
   ...
   double bouncingData[][] = {
      {  0.0, 1.0 },
      {  5.0, 2.0 },
      { 10.0, 1.0 },
   };
   ...
   void animate(double time) {
      ...
      initializeMatrixStack();
      ...
      push();
	 translate(eval(slidingData, time % 20.0), 0, 0);
	 transform(box);
         push();
	    translate(0, eval(bouncingData, time % 10.0), 0);
	    transform(ball);
         pop();
      pop();
      ...
   }
   ...