Wednesday, 8 April 2009

Adventures in 3D: Part VII - Matrix Revolutions

(What dastardly cunning, a piece about matrices on the 10th anniversary of The Matrix)

I already warned you that there was matrix maths coming up, so hold on to your hats. Once you've got to know them, you'll see that the principle of matrices is actually pretty simple, in that they just encode relatively complex equations in a simple form. Teaching matrix maths is outside the scope of this series, so I'll trust you'll do your own reading and just dive on in. I've also borrowed a Matrix class, as building our own is sure to be more of an education in bugfixing than 3D graphics.

Our ultimate aim at the moment is to change our viewpoint - the code at the moment has us fixed in one position and able to spin the world. We want to fix the world and be able to move around it, you know, like this. But first we'll slide in gently with using matrices, as they're a handy way to handle rotation.

Naturally you can, at the drop of a hat, quote the formula for rotation of points around an axis in 3D. Around the X axis, that is

y[i] = (y[i] * Math.cos(r)) - (z[i] * Math.sin(r));
z[i] = (z[i] * Math.cos(r)) + (y[i] * Math.sin(r));

Assuming we represent our 3D points as a column matrix [x,y,z]T, these two equations can be neatly made into a matrix:

| 1    0      0   |
| 0 cos(r) -sin(r)|
| 0 sin(r) cos(r)|

and we can also do the same for rotation around Y and Z axes:

|cos(r)  0  sin(r)|
| 0 1 0 |
|-sin(r) 0 cos(r)|

|cos(r) -sin(r) 0 |
|sin(r) cos(r) 0 |
| 0 0 1 |

So with our new Matrix class we can, for any given angle r, construct a matrix that encodes the rotation around the appropriate axis. When rotate() is called on the BasicSceneObject, we can build that matrix, and I'll add an overloaded form of rotate() on the abstract SceneObject so we can pass in that matrix to do the rotation. We push our points into a 1x3 matrix, multiply that by the 3x3 rotation matrix, then get the values from the result matrix and put those back into our points.

public void rotate(RotationMatrix rotationMatrix) {
Matrix[] points = new Matrix[3];

for(int i=0;i<3;i++) {
points[i] = new Matrix(new double[][] {{x[i]}, {y[i]}, {z[i]}});
Matrix result = rotationMatrix.times(points[i]);
x[i] = result.get(0,0);
y[i] = result.get(1,0);
z[i] = result.get(2,0);

normal = getNormal().normalise();

To build the rotation matrix, I created a RotationMatrix class, which is really just a utility class for building the matrices specified above, given an angle and an axis of rotation.

public static RotationMatrix getInstance(double theta, RotationAxis axis) {
switch(axis) {
case X:
return new RotationMatrix(new double[][] {
{1, 0, 0},
{0,sin(theta),cos(theta)} });
case Y:
return new RotationMatrix(new double[][] {
{0, 1, 0},
{-sin(theta),0,cos(theta)} });
case Z:
return new RotationMatrix(new double[][] {
{0, 0, 1} });
return null;

The final piece is to create the matrix and pass it to rotate() when the mouse is moved.

RotationMatrix yRot = RotationMatrix.getInstance(xangle, RotationAxis.Y);
RotationMatrix xRot = RotationMatrix.getInstance(yangle, RotationAxis.X);
for (SceneObject d : scene) {

Give that a bash, and watch in amazement as your scene does exactly the same thing that it's always done. Except in a bit of a neater way. Which is no bad thing, right? But we can make it even better than that. One lovely property of matrices is that if you have two matrices to do two rotations, you can just multiply the two matrices and get a single matrix that does both rotations in one step:

RotationMatrix yRot = RotationMatrix.getInstance(xangle, RotationAxis.Y);
RotationMatrix xRot = RotationMatrix.getInstance(yangle, RotationAxis.X);
RotationMatrix totalRot = xRot.times(yRot);
for (SceneObject d : scene) {

Great stuff. I love you matrices. If you love matrices too, download the source.

No comments: