9.7 A Tumbling Cube in 3-D Space


Java Number Cruncher: The Java Programmer's Guide to Numerical Computing
By Ronald  Mak

Table of Contents
Chapter  9.   Basic Matrix Operations

9.7 A Tumbling Cube in 3-D Space

Our interactive program will use the graphic image of a wire-frame cube. The program will animate this cube image by making it tumble in an enclosed three-dimensional space it will move and rotate, bounce off the walls of the enclosed space, and grow larger and smaller to create the illusion that it is moving toward or away from the viewer. Screen 9-1 shows a couple of screen shots of this program in action. Besides the cube, the program also displays the current values of the concatenated transformation matrix.

Screen 9-1. The tumbling cube and the concatenated transformation matrix of Program 9 C1.



We won't look at the entire program in this chapter, just the source files that use the matrix classes.

Listing 9-1a shows the Vertex class, which, as described previously, we'll make a subclass of RowVector . The constructor puts the x, y, and z coordinates into the first three elements and a 1 into the fourth element.

The class overrides the multiply() method to transform the vertex by multiplying it by a transformation matrix. This multiplication will give new values for the x, y, and z coordinates.

Listing 9-1a The Vertex class.
 package numbercruncher.program9_1; import numbercruncher.matrix.RowVector; import numbercruncher.matrix.SquareMatrix; import numbercruncher.matrix.MatrixException; /**  * Represent a vertex of the wire-frame cube in three dimensions.  */ class Vertex extends RowVector {     /**      * Constructor.      * @param x the x value      * @param y the y value      * @param z the z value      */     Vertex(float x, float y, float z)     {         super(4);         values[0][0] = x;         values[0][1] = y;         values[0][2] = z;         values[0][3] = 1;     }     /**      * Return this vertex's x value.      * @return the x value      */     float x() { return values[0][0]; }     /**      * Return this vertex's y value.      * @return the y value      */     float y() { return values[0][1]; }     /**      * Return this vertex's z value.      * @return the z value      */     float z() { return values[0][2]; }     /**      * Transform this vector by multiplying it      * by a transformation matrix.      * @param t the transformation matrix      */     void multiply(SquareMatrix t) throws MatrixException     {         RowVector rv = t.multiply(this);         this.values = rv.values();     } } 

Listing 9-1b shows class WireFrameCube , which represents our graphic cube. It consists of eight vertices and six faces. Each face is a square, which is represented by the indices of its four vertices. Initially, the cube is centered about the origin, and each edge has length 1.

Listing 9-1b The WireFrameCube class.
 package numbercruncher.program9_1; import java.awt.*; /**  * A wire-frame cube to transform and display.  */ class WireFrameCube {     /**      * Represent each face of the cube.      */     private class Face     {         /** indices of the face's vertices */   int indices[];         /**          * Constructor.          * @param v1 the first vertex          * @param v2 the second vertex          * @param v3 the third vertex          * @param v4 the fourth vertex          */         Face(int v1, int v2, int v3, int v4)         {             indices = new int[] {v1, v2, v3, v4};         }     }     /** The cube's vertices. */     private Vertex vertices[] = {         new Vertex(-0.5f, -0.5f, -0.5f),         new Vertex(+0.5f, -0.5f, -0.5f),         new Vertex(-0.5f, +0.5f, -0.5f),         new Vertex(+0.5f, +0.5f, -0.5f),         new Vertex(-0.5f, -0.5f, +0.5f),         new Vertex(+0.5f, -0.5f, +0.5f),         new Vertex(-0.5f, +0.5f, +0.5f),         new Vertex(+0.5f, +0.5f, +0.5f),     };     /** The cube's faces. */     private Face faces[] = {         new Face(0, 1, 3, 2),         new Face(0, 1, 5, 4),         new Face(2, 3, 7, 6),         new Face(0, 4, 6, 2),         new Face(1, 5, 7, 3),         new Face(4, 5, 7, 6),     };     /**      * Draw the transformed cube.      * @param g the graphics context      * @param transformation the transformation to apply      */     void draw(Graphics g, Transformation transformation)     {         // Transform the vertices.         transformation.transform(vertices);         // Loop for each face.         for (int i = 0; i < faces.length; ++i) {             int indices[] = faces[i].indices;             // Draw the edges of the face.             for (int j = 0; j < indices.length; ++j) {                 int k  = (j + 1)%indices.length;                 int c1 = Math.round(vertices[indices[j]].x());                 int r1 = Math.round(vertices[indices[j]].y());                 int c2 = Math.round(vertices[indices[k]].x());                 int r2 = Math.round(vertices[indices[k]].y());                 // Set the color based on the edge's position.                 Color color =                     transformation.behindCenter(vertices[indices[j]],                                                 vertices[indices[k]])                         ? Color.lightGray : Color.black;                 // Draw the edge.                 g.setColor(color);                 g.drawLine(c1, r1, c2, r2);             }         }     } } 

The constructor receives a graphics context, so that the cube can draw itself, and a transformation object, which we'll examine next .

The cube's draw() method first asks the transformation object to transform all of its vertices. Then it proceeds to draw the edges between the transformed vertices. The transformation object has a behindCenter() method, which indicates whether or not an edge is behind the cube's center. To further create the illusion of a three-dimensional cube, the edges in front of the center are drawn in black, and those in back are drawn in light gray, as seen in Screen 9-1.

Listing 9-1c shows the Transformation class. This class contains all the transformation matrices needed to animate the cube image: a translation matrix ( translate ), a scaling matrix ( scale ), and three rotation matrices, one for each axis ( rotateX , rotateY , and rotateZ ). It also has a matrix that concatenates the three rotations ( rotate ) and a matrix that concatenates all the transformations ( transform ). It uses a separate Vertex object ( center ) to keep track of the cube's center.

Listing 9-1c The Transformation class.
 package numbercruncher.program9_1; import numbercruncher.matrix.SquareMatrix; import numbercruncher.matrix.IdentityMatrix; import numbercruncher.matrix.MatrixException; /**  * Transformations of a graphic image.  */ class Transformation {     /** translation matrix */     private SquareMatrix translate = new IdentityMatrix(4);     /** scaling matrix */     private SquareMatrix scale = new IdentityMatrix(4);     /** matrix to rotate about the x axis */     private SquareMatrix rotateX = new IdentityMatrix(4);     /** matrix to rotate about the y axis */     private SquareMatrix rotateY = new IdentityMatrix(4);     /** matrix to rotate about the z axis */     private SquareMatrix rotateZ = new IdentityMatrix(4);     /** concatenated rotation matrix */     private SquareMatrix rotate = new IdentityMatrix(4);     /** concatenated transformation matrix */     private SquareMatrix transform = new IdentityMatrix(4);     /** center of rotation */     private Vertex center = new Vertex(0, 0, 0);     /**      * Initialize for a new set of transformations.      */     void init()     {         IdentityMatrix.convert(transform);     }     /**      * Reset to the initial conditions.      */     void reset()     {         center = new Vertex(0, 0, 0);         setTranslation(0, 0, 0);         setScaling(1, 1, 1);         setRotation(0, 0, 0);     }     /**      * Set the translation matrix.      * @param tx the change in the x direction      * @param ty the change in the y direction      * @param tz the change in the z direction      */     void setTranslation(float tx, float ty, float tz)     {         try {             translate.set(3, 0, tx);             translate.set(3, 1, ty);             translate.set(3, 2, tz);         }         catch(MatrixException ex) {}     }     /**      * Set the scaling matrix.      * @param sx the scaling factor in the x direction      * @param sy the scaling factor in the y direction      * @param sz the scaling factor in the z direction      */     void setScaling(float sx, float sy, float sz)     {         try {             scale.set(0, 0, sx);             scale.set(1, 1, sy);             scale.set(2, 2, sz);         }         catch(MatrixException ex) {}     }     /**      * Set the rotation matrix.      * @param thetaX amount (in radians) to rotate around the x axis      * @param thetaY amount (in radians) to rotate around the y axis      * @param thetaZ amount (in radians) to rotate around the z axis      */     void setRotation(float thetaX, float thetaY, float thetaZ)     {         try {             float sin = (float) Math.sin(thetaX);             float cos = (float) Math.cos(thetaX);             // Rotate about the x axis.             rotateX.set(1, 1,  cos);             rotateX.set(1, 2, -sin);             rotateX.set(2, 1,  sin);             rotateX.set(2, 2,  cos);             sin = (float) Math.sin(thetaY);             cos = (float) Math.cos(thetaY);             // Rotate about the y axis.             rotateY.set(0, 0,  cos);             rotateY.set(0, 2,  sin);             rotateY.set(2, 0, -sin);             rotateY.set(2, 2,  cos);             sin = (float) Math.sin(thetaZ);             cos = (float) Math.cos(thetaZ);             // Rotate about the z axis.             rotateZ.set(0, 0,  cos);             rotateZ.set(0, 1, -sin);             rotateZ.set(1, 0,  sin);             rotateZ.set(1, 1,  cos);             // Concatenate rotations.             rotate = rotateX.multiply(rotateY.multiply(rotateZ));         }         catch(MatrixException ex) {}     }     /**      * Transform a set of vertices based on previously-set      * translation, scaling, and rotation.  Concatenate the      * transformations in the order:  scale, rotate, translate.      * @param vertices the vertices to transform      */     void transform(Vertex vertices[])     {         // Scale and rotate about the origin.         toOrigin();         scale();         rotate();         reposition();         translate();         // Apply the concatenated transformations.         try {             // Do the vertices.             for (int i = 0; i < vertices.length; ++i) {                 Vertex v = vertices[i];                 v.multiply(transform);             }             // Do the center of rotation.             center.multiply(transform);         }         catch(MatrixException ex) {}     }     /**      * Check for a bounce against any wall of the space.      * Return true if bounced.      * @param width the width of the space      * @param height the height of the space      * @param depth the depth of the space      * @return true if bounced, else false      */     boolean bounced(float width, float height, float depth)     {         boolean b = false;         try {             // Bounced off the sides?             if ((center.x() < 0)  (center.x() > width)) {                 translate.set(3, 0, -translate.at(3, 0));                 b = true;             }             // Bounced off the top or bottom?             if ((center.y() < 0)  (center.y() > height)) {                 translate.set(3, 1, -translate.at(3, 1));                 b = true;             }             // Bounced off the front or back?             if ((center.z() < 0)  (center.z() > depth)) {                 translate.set(3, 2, -translate.at(3, 2));                 // Invert the scale factor.                 float scaleFactor = 1/scale.at(0, 0);                 scale.set(0, 0, scaleFactor);                 scale.set(1, 1, scaleFactor);                 scale.set(2, 2, scaleFactor);                 b = true;             }         }         catch(MatrixException ex) {}         return b;     }     /**      * Check if a line is behind the center of rotation.      * @param v1 the vertex of one end of the line      * @param v2 the vertex of the other end of the line      * @return true if behind, else false      */     boolean behindCenter(Vertex v1, Vertex v2)     {         return (v1.z() < center.z()) && (v2.z() < center.z());     }     /**      * Return a value from the concatenated transformation matrix.      * @param r the value's row      * @param c the value's column      * @return the value      */     float at(int r, int c)     {         try {             return transform.at(r, c);         }         catch(MatrixException ex) {             return Float.NaN;         }     }     /**      * Concatenate a translation.      * @param translate the translation matrix to use      */     private void translate(SquareMatrix translate)     {         try {             transform = transform.multiply(translate);         }         catch(MatrixException ex) {}     }     /**      * Concatenate the preset translation.      */     private void translate()     {         translate(translate);     }     /**      * Concatenate the preset scaling.      */     private void scale()     {         try {             transform = transform.multiply(scale);         }         catch(MatrixException ex) {}     }     /**      * Concatenate the preset rotation.      */     private void rotate()     {         try {             transform = transform.multiply(rotate);         }         catch(MatrixException ex) {}     }     /**      * Translate back to the origin.      */     private void toOrigin()     {         try {             SquareMatrix tempTranslate = new IdentityMatrix(4);             tempTranslate.set(3, 0, -center.x());             tempTranslate.set(3, 1, -center.y());             tempTranslate.set(3, 2, -center.z());             translate(tempTranslate);         }         catch(MatrixException ex) {}     }     /**      * Translate back into position.      */     private void reposition()     {         try {             SquareMatrix tempTranslate = new IdentityMatrix(4);             tempTranslate.set(3, 0, center.x());             tempTranslate.set(3, 1, center.y());             tempTranslate.set(3, 2, center.z());             translate(tempTranslate);         }         catch(MatrixException ex) {}     } } 

Method init() initializes transform to the identity matrix to prepare it for a sequence of transformations. Method reset() puts the cube image back to its starting point at the origin and "clears" the translation, scaling, and rotation matrices.

Method setTranslation() sets the translation value into matrix translate. Similarly, method setScaling() sets the scale factors into matrix scale.

Method setRotatation() sets the rotation values into the three rotation matrices rotateX , rotateY , and rotateZ . Then it multiplies the matrices together to set the concatenated rotation matrix rotate .

Method transform() , which we saw in Listing 9-1b being called by the method WireFrameCube.draw() , transforms each vertex of the graphic image.

First, the method must concatenate all the transformations. It calls method toOrigin() to move the object back to the origin, and then it calls methods scale() and rotate() to scale and rotate the image about the origin. Next, it calls method reposition() to put the scaled and rotated image back to its location. Finally, a call to translate() moves the image to its new location. As we'll soon see, these calls concatenate all the transformations into matrix transform.

Now method transform() simply multiplies each vertex of the graphic image by the matrix transform. It also multiplies the center vertex by transform to move it along with the object.

Methods toOrigin() and reposition() use temporary translation matrices and the center vertex to move the graphic image to and from the origin. The methods translate() , scale() , and rotate() simply multiply the concatenated transform matrix by the individual transformation matrices transform , scale , and rotate, respectively.

Method bounce() checks whether or not the graphic image has bounced off any of the six walls of the enclosed three-dimensional space. Bouncing off a wall is achieved simply by negating the appropriate t x , t y , or t z value in the translate matrix. If the object bounces off either the back or front wall, the method also replaces the scaling factors in the scale matrix by their reciprocals we want the image to grow as it approaches the viewer and to shrink as it recedes from the viewer. The method returns true if a bounce occurred. Otherwise, it returns false .

Method behindCenter() , which we also saw in Listing 9-1b being called by method WireFrameCube.draw() , checks the two vertices of an edge. It deems the edge to be behind the graphic image's center if both vertices are behind the center, and then it returns true . Otherwise, it returns false .

Method at() simply returns the current values of the concatenated transform matrix. This allows our program to display the values.

Class CubePanel is a subclass of java.awt.Panel, and it represents the enclosed three-dimensional space for the tumbling wire-frame cube image. Its constructor receives a Transformation object, and it creates a WireFrameCube object. See Listing 9-1d.

Listing 9-1d Class CubePanel .
 package numbercruncher.program9_1; import java.awt.*; /**  * The panel that represents the enclosed 3-D space  * for the tumbling wire-frame cube.  */ public class CubePanel extends Panel {     private static final float MAX_TRANSLATE = 5;     private static final float MAX_SCALING   = 3;     /** width of space */   private int width;     /** height of space */  private int height;     /** depth of space */   private int depth;     /** image buffer */             private Image    buffer;     /** buffer graphics context */  private Graphics bg;     /** true for first draw */  private boolean first = true;     /** wire frame cube */  private WireFrameCube  cube;     /** transformation */   private Transformation transformation;     /** parent panel */     private TransformationPanel parent;     /**      * Constructor.      * @param transformation the graphics transformation      * @param parent the parent panel      */     CubePanel(Transformation transformation,               TransformationPanel parent)     {         this.transformation = transformation;         this.parent         = parent;         this.cube           = new WireFrameCube();         setBackground(Color.white);     }     /**      * Reset the cube to its starting position.      */     public void reset()     {         cube  = new WireFrameCube();         first = true;         bg    = null;         repaint();     }     /**      * Draw the contents of the panel.      */     public void draw()     {         if (bg == null) return;         bg.clearRect(0, 0, width, height);         transformation.init();         if (first) {             firstDraw();         }         else {             subsequentDraw();         }         repaint();         parent.updateMatrixDisplay();     }     /**      * Paint without first clearing.      * @param g the graphics context      */     public void update(Graphics g) { paint(g); }     /**      * Paint the contents of the image buffer.      * @param g the graphics context      */     public void paint(Graphics g)     {         // Has the buffer been created?         if (bg == null) {             Rectangle r = getBounds();             width  = r.width;             height = r.height;             depth  = width;             // Create the image buffer and get its graphics context.             buffer = createImage(width, height);             bg     = buffer.getGraphics();             draw();         }         // Paint the buffer contents.         g.drawImage(buffer, 0, 0, null);     }     /**      * First time drawing.      */     private void firstDraw()     {         // Scale and move to the center.         transformation.setScaling(50, 50, 50);         transformation.setTranslation(width/2, height/2, depth/2);         cube.draw(bg, transformation);         // Random subsequent translations.         float xDelta = (float) (2*MAX_TRANSLATE*Math.random()                                     - MAX_TRANSLATE);         float yDelta = (float) (2*MAX_TRANSLATE*Math.random()                                     - MAX_TRANSLATE);         float zDelta = (float) (2*MAX_TRANSLATE*Math.random()                                     - MAX_TRANSLATE);         transformation.setTranslation(xDelta, yDelta, zDelta);         // Set the scale factor based on the space's depth and         // whether the cube is moving towards or away from the viewer.         // At maximum z, the cube should be twice its original size,         // and at minimum z, it should be half its original size.         float steps       = (depth/2)/Math.abs(zDelta);         float scaleFactor = (float) Math.pow(MAX_SCALING, 1/steps);         if (zDelta < 0) scaleFactor = 1/scaleFactor;         transformation.setScaling(                             scaleFactor, scaleFactor, scaleFactor);         setRandomRotation();         first = false;     }     /**      * Subsequent drawing.      */     private void subsequentDraw()     {         // Draw the transformed cube.         cube.draw(bg, transformation);         // If there was a bounce, set new random rotation angles.         if (transformation.bounced(width, height, depth)) {             setRandomRotation();         }     }     /**      * Set random rotation angles about the axes.      */     private void setRandomRotation()     {         transformation.setRotation(                         (float) (0.1*Math.random()),     // x axis                         (float) (0.1*Math.random()),     // y axis                         (float) (0.1*Math.random()));    // z axis     } } 

The class uses an off-screen image buffer to draw the image. Method draw() first initializes the Transformation object. Then, if this is the very first time it will draw the cube, or the first time after the cube has been reset, it calls method firstDraw() . Otherwise, it calls method subsequentDraw() . In either case, it finishes by causing a repaint and an update of the displayed values of the concatenated transformation matrix.

Method firstDraw() scales the cube (which, when created, is centered at the origin and has edges of length 1) and moves it to the center of the panel. Then, the method sets random translation values. Next, it computes and sets the proper scaling factors, so that the cube will appear to be half its size at the back of the space and twice its size at the front of the space. Finally, the method calls method setRandomRotation() to set random rotation values.

Method subsequentDraw() invokes the cube's draw() method, passing it the graphics context of the off-screen image buffer and the Transformation object. The cube will then transform and draw itself, as we saw in Listing 9-1b. Method subsequentDraw() then checks for a bounce, and if one occurred, it makes things a bit more interesting by setting new random rotation values.


Java Number Cruncher. The Java Programmer's Guide to Numerical Computing
Java Number Cruncher: The Java Programmers Guide to Numerical Computing
ISBN: 0130460419
EAN: 2147483647
Year: 2001
Pages: 141
Authors: Ronald Mak

Similar book on Amazon

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net