When you have finished the assignment below, post the working applet and the source code onto your class web site. You don't need to send email.
This assignment is all about kinematics. That means you're going to make things move! As we described in class on Monday, this is a leap from making individual objects move around, where each object is moved by an independent 4×4 transformation matrix, to making entire coordinated systems of objects move, through a hierarchy (or tree) of transformation matrices.
Your assignment is make an interesting and original animated scene that has such a hierarchy of moving objects in it. Your scene can consist of one or more human figures, or cows or dragons, automobiles, martian creatures, clocks, butterflies, or anything else that strikes your fancy. Make sure to have fun with it.
It is very important that you read the part at the bottom of this assignment about matrix multiplication order. Otherwise you'll have trouble getting things right.
To review: the way you were doing things before, at each animation frame you were able to string together a set of transformations to make a transformation matrix. Using that technique, at each frame you could construct a matrix, and then transform all your points by that matrix. Below is a typical example:
double matrix[][] = new double[4][4]; // TRANSFORMATION MATRIX; CHANGES EACH FRAME double time = 0; // TIME COUNTER TO ADVANCE THE ANIMATION double cube[][] = {{-1,-1,-1},{1,-1,-1},{-1,1,-1},{1,1,-1}, // THE VERTICES OF AN *UNTRANSFORMED* CUBE {-1,-1, 1},{1,-1, 1},{-1,1, 1},{1,1, 1}}; double temp[][] = new double[cube.length][3]; // STORE THE TRANSFORMED VERTICES EACH FRAME int cubeEdges[][] = {{0,1},{1,3},{3,2},{2,0}, ... }; // EDGES OF THE CUBE (PAIRS OF VERTEX INDICES) ... public void render(Graphics g) { ... double x = Math.cos(2 * Math.PI * time); // COMPUTE TIME-VARYING PARAMETERS double y = Math.sin(2 * Math.PI * time); time += 0.01; Matrix.identity(matrix); // CLEAR THE MATRIX Matrix.translate(matrix, x,y,0); // MOVE IN A CIRCLE Matrix.scale(matrix, .1,.1,.1); // SCALE SMALLER for (int i = 0 ; i < cube.length ; i++) // TRANSFORM THE CUBE BY THE MATRIX Matrix.transform(cube[i], temp[i], matrix); for (int i = 0 ; i < cubeEdges.length ; i++) { // DRAW EDGES BETWEEN THE TRANSFORMED POINTS int i0 = cubeEdges[i][0]; int i1 = cubeEdges[i][0]; g.drawLine(ViewPortX(temp[i0]), ViewPortY(temp[i0], ViewPortX(temp[i1]), ViewPortY(temp[i1]); } }
Notice that the above example built a single transformation matrix in each frame, in steps, and then used that single matrix to transform geometry before rendering it.
The key advance we'll be working on is to describe a scene as an entire tree of transformations. At every node of this tree, some part of your scene can be transformed and then rendered. As we discussed, the data structure that lets you traverse a tree is a stack, so you'll need a matrix stack, such as:
double mStack[][][] = new double[STACKSIZE][4][4];and a top-of-stack pointer:
int mTop = 0;At each frame of your animation the stack needs to start off in its "initial" state:
mTop = 0; Matrix.identity(mStack[mTop]);Then to animate the scene for that frame of animation, you would do a sequence of operations that use this matrix stack. Some of these operations modify the matrix stack itself; others use the matrix at the top of the stack to transform geometry in your scene.
There are several sorts of operations you can perform:
You already know how to modify a single matrix,
by using the methods
identity
,
translate
,
rotateX
,
rotateY
,
rotateZ
and
scale
.
For example, you can modify the matrix on
top of the stack with such operations as:
Matrix.identity(mStack[mTop]);or
Matrix.rotateX(mStack[mTop], theta);Also you'll want to be able to push and pop the matrix, to reflect entering and then leaving local branches of the tree that describes the scene (such as the arms and legs of a human figure). A
push
operation
can be effected by:
Matrix.copy(mStack[mTop], mStack[mTop+1]); mTop++;and a
pop
operation
can be effected by:
mTop--;I encourage you to create
push()
and
pop()
methods for this.
Make sure that you add error handling
to these methods to check for stack
overflow or underflow!
Finally, while you're traversing the tree you'll want to render various parts of your scene which are embedded in various "nodes" of the tree. you'll need to transform the actual objects in your scene (arms, legs, trees, automobile parts, etc). You can do this by transforming them by whatever matrix is on top of the stack when that node is reached. For example, if you have a sphere mesh, you might have the following lines of code somewhere:
Matrix.copy(mStack[mTop], sphMesh.matrix); sphMesh.draw(epsilon);
Matrix multiplication order:
There is something that might seem counter-intuitive about how you need to multiply the matrices together when traversing the heirarchy tree of your scene. In general, there are two distinct ways you could do matrix multiplication upon a matrix A, by another matrix B:
B × ABecause matrix multiplication is not commutative, these two operations will usually produce different results. Up to now you might have been doing the first method, which is called pre-multiplication of matrix A by matrix B. The second method, in contrast, is called the post-multiplication of matrix A by matrix B.
or
A × B
It turns out that when you want to make hierarchies, you need to post-multiply. This is because as you traverse the tree, going from global parts (eg: pelvis) to local parts (eg: elbow, wrist, finger), you don't want your new transformations to take place in "global coordinates", but rather in the (already transformed) coordinate system of the parent part. For example: a person's right knee should bend about the transformed x access at the end of the person's right thigh, not about the global x axis.
To reiterate:
Go back and look at the Matrix.multiply
method
that you have been using within
your
translate
,
rotateX
,
rotateY
,
rotateZ
and
scale
methods.
You are either doing a pre-multiplication
or a post-multiplication.
If you want to create a hierarchy
of objects, you need to be
post-multiplying.
If you're not sure whether you're doing it right, the good news is that there are only two possibilities: if one ordering is wrong, then the other one will work!
Let's take a simple test case, which you can use to test out your matrix multiplication, to see whether you are getting the order right for doing hierarchies. Consider a swinging pendulum with a shaft 10 units long, which swings from a height of 10 units up in y. We can build the pendulum out of two cubes: an elongated one for the pendulum shaft, and a slightly flattened one for the weight at the end.
Let's assume that we have some
cube
object which,
when not transformed,
extends
from -1.0...+1.0 in x,
from -1.0...+1.0 in y and
from -1.0...+1.0 in z,
and that this cube is drawn by
cube.draw()
.
We can model the pendulum by transforming and then drawing the cube twice: once for the shaft, and then differently for the weight.
Matrix.identity(m); Matrix.translate(m, 0,5,0); // SLIDE THE SHAFT UPWARDS Matrix.scale(m, .1,5,.1); // SCALE IT TO ELONGATE IN Y Matrix.copy(m, cube.matrix); // TRANSFORM THE SHAFT cube.draw(); // DRAW THE SHAFT Matrix.identity(m); Matrix.scale(m, .5,.5,.1); // FLATTEN IN Z Matrix.copy(m, cube.matrix); // TRANSFORM THE WEIGHT cube.draw(); // DRAW THE WEIGHTNotice that the code in red only works properly because we have implemented the
scale
method by using post-multiplication
on the results of scaleMatrix
.
This ensures that the scaling will
take place around the middle of the moved shaft,
rather than around the global origin.
But how would we swing this cube?
If you have implemented
push()
and
pop()
methods,
as well as methods
translate
,
rotateX
,
rotateY
,
rotateZ
and
scale
that always modify
mStack[mTop]
,
then this is very straightforward:
identity(); translate(0,10,0); // SLIDE UP TO TOP OF SHAFT rotateZ(theta); // ANIMATE SWING OF PENDULUM ABOUT TOP OF SHAFT push(); // TRANSFORM AND RENDER THE SHAFT: translate(0,-5,0); // SLIDE DOWN TO MIDDLE OF SHAFT scale(.1,5,.1); // SCALE THE SHAFT Matrix.copy(mStack[mTop], cube.matrix); // TRANSFORM AND DRAW cube.draw(); pop(); push(); // TRANSFORM AND RENDER THE WEIGHT translate(0,-10,0); // SLIDE DOWN TO BOTTOM OF SHAFT scale(.5,.5,.1); // SCALE THE WEIGHT Matrix.copy(mStack[mTop], cube.matrix); // TRANSFORM AND DRAW cube.draw(); pop();