Enter the third dimension

Use a Swing component to explore three-dimensional computer graphics

1 2 3 4 Page 2
Page 2 of 4
 

private JPanel buildGUI () { // Although I'm ignoring the computer screen's aspect ratio (I don't care // if individual pixels aren't square), I want the applet's dimensions to // be square.

int width = getWidth (); if (width != getHeight ()) return null;

// Create a 3D panel where each side is 60 percent of the applet's width. Set // the default viewpoint from which an observer views the scene. Also, // set the default perspective to halfway between extreme telephoto and // extreme wide-angle.

final Panel3D p3d = new Panel3D (width*3/5); p3d.lookFrom (posX, posY, posZ); p3d.perspective (ds);

// GUI components are stored in the following panel.

JPanel panel = new JPanel (); panel.setLayout (new BorderLayout ());

// The top panel contains the X and Y slider controls; it also contains // the 3D panel.

JPanel panelTop = new JPanel ();

// Build the top panel.

JPanel panelTemp = new JPanel (); panelTemp.add (new JLabel ("X"));

final JSlider sliderX = new JSlider (JSlider.VERTICAL, MINX, MAXX, DEFX); sliderX.setMinorTickSpacing (5); sliderX.setMajorTickSpacing (10); sliderX.setPaintTicks (true); sliderX.setPaintLabels (true); sliderX.setLabelTable (sliderX.createStandardLabels (10)); ChangeListener cl; cl = new ChangeListener () { public void stateChanged (ChangeEvent e) { posX = sliderX.getValue (); p3d.lookFrom (posX, posY, posZ); p3d.perspective (ds); } }; sliderX.addChangeListener (cl); panelTemp.add (sliderX);

panelTop.add (panelTemp);

panelTemp = new JPanel (); panelTemp.add (p3d);

panelTop.add (panelTemp);

panelTemp = new JPanel (); panelTemp.add (new JLabel ("Y"));

final JSlider sliderY = new JSlider (JSlider.VERTICAL, MINY, MAXY, DEFY); sliderY.setMinorTickSpacing (5); sliderY.setMajorTickSpacing (10); sliderY.setPaintTicks (true); sliderY.setPaintLabels (true); sliderY.setLabelTable (sliderY.createStandardLabels (10)); cl = new ChangeListener () { public void stateChanged (ChangeEvent e) { posY = sliderY.getValue (); p3d.lookFrom (posX, posY, posZ); p3d.perspective (ds); } }; sliderY.addChangeListener (cl); panelTemp.add (sliderY);

panelTop.add (panelTemp);

// Place the top panel in the top area of the overall panel.

panel.add (panelTop, BorderLayout.NORTH);

// The bottom panel contains the Z slider control; it also contains the // field of view slider and a panel of buttons.

JPanel panelBottom = new JPanel (); panelBottom.setLayout (new BorderLayout ());

// Build the bottom panel.

panelTemp = new JPanel (); panelTemp.add (new JLabel ("Z"));

final JSlider sliderZ = new JSlider (JSlider.HORIZONTAL, MINZ, MAXZ, DEFZ); sliderZ.setMinorTickSpacing (5); sliderZ.setMajorTickSpacing (10); sliderZ.setPaintTicks (true); sliderZ.setPaintLabels (true); sliderZ.setLabelTable (sliderZ.createStandardLabels (10)); cl = new ChangeListener () { public void stateChanged (ChangeEvent e) { posZ = sliderZ.getValue (); p3d.lookFrom (posX, posY, posZ); p3d.perspective (ds); } }; sliderZ.addChangeListener (cl); panelTemp.add (sliderZ);

panelBottom.add (panelTemp, BorderLayout.NORTH);

JPanel panelFOV = new JPanel (); panelFOV.setLayout (new BorderLayout ());

panelFOV.add (new JLabel ("Field Of View", JLabel.CENTER), BorderLayout.CENTER);

final JSlider sliderFOV = new JSlider (JSlider.HORIZONTAL, 0, 180, 90); sliderFOV.setMinorTickSpacing (5); sliderFOV.setMajorTickSpacing (10); sliderFOV.setPaintTicks (true); sliderFOV.setPaintLabels (true); sliderFOV.setLabelTable (sliderFOV.createStandardLabels (10)); cl = new ChangeListener () { public void stateChanged (ChangeEvent e) { int angle = sliderFOV.getValue ();

if (angle == 0) ds = Double.MAX_VALUE; else if (angle == 180) ds = 0.0; else { double rads; rads = Math.toRadians (angle / 2.0); ds = 1.0 / Math.tan (rads); }

p3d.lookFrom (posX, posY, posZ); p3d.perspective (ds); } }; sliderFOV.addChangeListener (cl); panelFOV.add (sliderFOV, BorderLayout.SOUTH);

panelBottom.add (panelFOV, BorderLayout.CENTER);

JButton btnCube = new JButton ("Cube"); ActionListener al; al = new ActionListener () { public void actionPerformed (ActionEvent e) { try { p3d.load ("cube.3d"); } catch (Exception e2) { JOptionPane.showMessageDialog (Demo3D.this, e2.getMessage ()); } } }; btnCube.addActionListener (al);

JButton btnPyramid = new JButton ("Pyramid"); al = new ActionListener () { public void actionPerformed (ActionEvent e) { try { p3d.load ("pyramid.3d"); } catch (Exception e2) { JOptionPane.showMessageDialog (Demo3D.this, e2.getMessage ()); } } }; btnPyramid.addActionListener (al);

btnCube.setPreferredSize (btnPyramid.getPreferredSize ());

JButton btnTower = new JButton ("Tower"); al = new ActionListener () { public void actionPerformed (ActionEvent e) { try { p3d.load ("tower.3d"); } catch (Exception e2) { JOptionPane.showMessageDialog (Demo3D.this, e2.getMessage ()); } } }; btnTower.addActionListener (al);

btnTower.setPreferredSize (btnPyramid.getPreferredSize ());

panelTemp = new JPanel (); panelTemp.setLayout (new FlowLayout (FlowLayout.CENTER, 15, 15));

panelTemp.add (btnCube); panelTemp.add (btnPyramid); panelTemp.add (btnTower);

panelBottom.add (panelTemp, BorderLayout.SOUTH);

// Place the bottom panel in the center area of the overall panel to // occupy empty space below the top area of the overall panel.

panel.add (panelBottom, BorderLayout.CENTER);

return panel; }

In addition to Listing 1's cube.3d, this article's source code contains source files Demo3D.java and Panel3D.java, model files pyramid.3d and tower.3d, and Listing 2's Demo3D.html. After compiling both source files via javac Demo3D.java, run this applet by invoking appletviewer Demo3D.html.

Listing 2. Demo3D.html

 <applet code=Demo3D.class width=550 height=550>
</applet>
Note
Because Panel3D's load() method accesses the file system, and because the JVM's security manager forbids Web-browser-run applets from accessing files, you must create a signed jar file to use Demo3D with a Web browser. Check out my previous article "Let the Games Begin" (JavaWorld, July 2005) for an example.

The fundamentals of 3D graphics

Many people find 3D graphics difficult to understand. In this section, I attempt to overcome this difficulty by presenting 3D graphics fundamentals in the context of the 3D panel component. I discuss coordinate systems and transformations, basic transformations, world modeling, world viewing, perspective, clipping, and projection onto a 2D screen. Panel3D.java excerpts illustrate these fundamentals.

Coordinate systems and transformations

Transforming a 3D model into the wire-frame image that appears on the screen involves coordinate systems and transformations. A coordinate system provides positional information for a model's objects (model components: a vehicle model's body and tire components, for example). Transformations position objects, map a coordinate system to another coordinate system, and more. Coordinate systems include:

  • Object coordinate system: Although not used by Panel3D, the object coordinate system is a convenience for defining objects, via object coordinates, independently of where they are located in a 3D model's world. Multiple instances of an object can then appear (via suitable transformations) in the world, which saves space. A transformation maps this coordinate system to the world coordinate system.
  • World coordinate system: A 3D model describes a world of objects. The world coordinate system locates objects within this world via world coordinates. Unlike object coordinates, these coordinates connect all of the world's objects together. A transformation maps this coordinate system to the eye coordinate system.
  • Eye coordinate system: A world is viewed similarly to how a person sees the world through a camera: the eye coordinate system's viewpoint origin represents the person's eye; the positive z axis represents the camera's direction of view. The world's objects are located relative to the viewpoint via eye coordinates. A transformation maps this coordinate system to the clipping coordinate system.
  • Clipping coordinate system: Because perspective can increase the sizes of a world's objects, invisible lines could wrap around the screen. To prevent this from happening, each line must be clipped to exclude the invisible portion of the line (or the entire line). If the line is visible, as determined by the clipping coordinates of its endpoints, a transformation maps it to the screen coordinate system.
  • Screen coordinate system: Because the screen coordinate system is a 2D coordinate system, converting the endpoints of a line's clipping coordinates to this coordinate system's (x, y) screen coordinates requires that each endpoint's x and y coordinates be divided by its z coordinate. Following a multiplication and addition to each transformed endpoint's x and y values, the line appears on the screen.

Although it's helpful to think of a transformation in terms of simple math equations (x' = x + tx, which adds a translation offset to old x, to achieve new x', for example), equations aren't practical from a performance perspective. Evaluating the same series of equations for thousands of, or more, points is time-consuming. It's cheaper performance-wise to use a matrix.

A matrix contains numbers in a grid of rows and columns; a transformation can be represented as a 4-row by 4-column matrix. Thanks to matrix multiplication, transformation matrices can be multiplied together before evaluating points. This results in fewer calculations per point—a significant time savings. Because matrices are so useful, I've nested the following Matrix class in the Panel3D class:

 

private class Matrix { private double [][] matrix;

Matrix (int nrows, int ncols) { matrix = new double [nrows][]; for (int row = 0; row < ncols; row++) matrix [row] = new double [ncols]; }

int getCols () { return matrix [0].length; }

int getRows () { return matrix.length; }

double getValue (int row, int col) { return matrix [row] [col]; }

void setValue (int row, int col, double value) { matrix [row] [col] = value; }

Matrix multiply (Matrix m) { Matrix result = new Matrix (matrix.length, matrix [0].length);

for (int i = 0; i < getRows (); i++) for (int j = 0; j < m.getCols (); j++) for (int k = 0; k < m.getRows (); k++) result.setValue (i, j, result.getValue (i, j) + matrix [i][k] * m.getValue (k, j));

return result; } }

Basic transformations

The translation, rotation, and scaling transformations are used in modeling and viewing. Translation changes an object's or coordinate system's position. To change an object's position, add positive offsets to each point's x, y, and z values via equations:

  • x' = x + Tx
  • y' = y + Ty
  • z' = z + Tz

The old position is (x, y, z), the new position is (x', y', z'), and Tx, Ty, and Tz are offsets. The equivalent matrix operation:

 [x' y' z' 1] = [x y z 1] [1    0    0    0]
                         [0    1    0    0]
                         [0    0    1    0]
                         [Tx   Ty   Tz   1]

To move the coordinate system, add negative offsets to each point's x, y, and z values. This transformation is represented by equations:

  • x' = x - Tx
  • y' = y - Ty
  • z' = z - Tz

The old coordinate system's (0, 0, 0) origin becomes (-Tx, -Ty, -Tz) in the new coordinate system; (Tx, Ty, Tz) in the old coordinate system becomes origin (0, 0, 0) in the new coordinate system—or the following matrix operation:

 [x y z 1] = [x' y' z' 1] [1    0    0    0]
                         [0    1    0    0]
                         [0    0    1    0]
                         [-Tx  -Ty  -Tz  1]

The Panel3D class provides a private void translate(double tx, double ty, double tz) method that handles translation. After creating a 4-row by 4-column translation Matrix object, and populating this object's rows and columns with the values in tx, ty, and tz, this method multiplies the current transformation matrix by the translation matrix:

 

private void translate (double tx, double ty, double tz) { Matrix t = new Matrix (4, 4); t.setValue (0, 0, 1.0); t.setValue (3, 0, tx); t.setValue (1, 1, 1.0); t.setValue (3, 1, ty); t.setValue (2, 2, 1.0); t.setValue (3, 2, tz); t.setValue (3, 3, 1.0);

transform = transform.multiply (t); }

Rotation transformations change an object's orientation (via a positive angle) with respect to the x, y, and z axes. Each transformation has an inverse that changes the orientation of the coordinate system (via a negative angle) with respect to an axis. When an object is rotated, the angle of rotation around a given axis is measured in a clockwise direction when you look down the rotation axis to the origin—see Figure 2.

Figure 2. Rotations around the positive z, y, and x axes. Click on thumbnail to view full-sized image.

Rotation around the z coordinate axis is represented by equations

  • x' = x * cos a + y * sin a
  • y' = x * -sin a + y * cos a
  • z' = 1

Angle a is measured in radians. Alternatively, this rotation can be represented by the following matrix operation:

 [x' y' z' 1] = [x y z 1] [cos a   -sin a  0       0]
                         [sin a   cos a   0       0]
                         [0       0       1       0]
                         [0       0       0       1]

Rotation around the y coordinate axis is represented by equations:

  • x' = x * cos a + z * -sin a
  • y' = 1
  • z' = x * sin a + z * cos a

This rotation is also represented by the matrix operation specified below:

 [x' y' z' 1] = [x y z 1] [cos a   0       sin a   0]
                         [0       1       0       0]
                         [-sin a  0       cos a   0]
                         [0       0       0       1]

Rotation around the x coordinate axis is represented by equations:

  • x' = 1
  • y' = y * cos a + z * sin a
  • z' = y * -sin a + z * cos a

In matrix form, this operation becomes:

 [x' y' z' 1] = [x y z 1] [1       0       0       0]
                         [0       cos a   -sin a  0]
                         [0       sin a   cos a   0]
                         [0       0       0       1]

The Panel3D class provides methods private void rotateX(double angle), private void rotateY(double angle), and private void rotateZ(double angle) to rotate angle degrees around x, y, and z, respectively. Each method converts from degrees to radians, creates/sets up a rotation Matrix object, and multiplies the current transformation matrix by the rotation matrix:

 

private void rotateX (double angle) { angle = Math.toRadians (angle);

Matrix r = new Matrix (4, 4); r.setValue (0, 0, 1.0); r.setValue (1, 1, Math.cos (angle)); r.setValue (2, 1, Math.sin (angle)); r.setValue (1, 2, -Math.sin (angle)); r.setValue (2, 2, Math.cos (angle)); r.setValue (3, 3, 1.0);

transform = transform.multiply (r); }

private void rotateY (double angle) { angle = Math.toRadians (angle);

Matrix r = new Matrix (4, 4); r.setValue (0, 0, Math.cos (angle)); r.setValue (2, 0, -Math.sin (angle)); r.setValue (1, 1, 1.0); r.setValue (0, 2, Math.sin (angle)); r.setValue (2, 2, Math.cos (angle)); r.setValue (3, 3, 1.0);

transform = transform.multiply (r); }

private void rotateZ (double angle) { angle = Math.toRadians (angle);

Matrix r = new Matrix (4, 4); r.setValue (0, 0, Math.cos (angle)); r.setValue (1, 0, Math.sin (angle)); r.setValue (0, 1, -Math.sin (angle)); r.setValue (1, 1, Math.cos (angle)); r.setValue (2, 2, 1.0); r.setValue (3, 3, 1.0);

transform = transform.multiply (r); }

Scaling is a transformation that changes an object's size, converts from the right-handed representation of the world coordinate system to the left-handed representation of the eye coordinate system, and more. When working with objects, this transformation is represented by equations:

  • x' = x * Sx
  • y' = y * Sy
  • z' = z * Sz

The Sx, Sy, Sz are scaling multipliers. The matrix operation appears below:

 [x' y' z' 1] = [x y z 1] [Sx   0    0    0]
                         [0    Sy   0    0]
                         [0    0    Sz   0]
                         [0    0    0    1]

The world coordinate system's right-handed representation appears in Figure 2. To convert to the eye coordinate system's left-handed representation (the right-handed representation with the positive z axis pointed in the opposite direction), multiply each z coordinate by -1. This transformation is represented by equations:

  • x' = x * Sx
  • y' = y * Sy
  • z' = z * -Sz

Or by the following matrix operation:

 [x' y' z' 1] = [x y z 1] [Sx   0    0    0]
                         [0    Sy   0    0]
                         [0    0    -Sz  0]
                         [0    0    0    1]
Tip
To remember the right-handed world coordinate system's axis orientations, hold your right hand flat with the palm up and the thumb extending out so that the thumb and first finger align with the x and y axes, respectively. The z direction points upward from the palm and is indicated by the second finger. To remember the left-handed eye coordinate system's axis orientations, hold your left hand flat with the thumb extending out so that the thumb and first finger align with the x and y axes, respectively. The z direction points outward from the palm and is indicated by the second finger.

The Panel3D class provides a private void scale(double sx, double sy, double sz) method that handles scaling. After creating a 4-row by 4-column scaling Matrix object, and populating this object's rows and columns with the values in sx, sy, and sz, this method multiplies the current transformation matrix by the scaling matrix:

 

private void scale (double sx, double sy, double sz) { Matrix s = new Matrix (4, 4); s.setValue (0, 0, sx); s.setValue (1, 1, sy); s.setValue (2, 2, sz); s.setValue (3, 3, 1.0);

transform = transform.multiply (s); }

World modeling

A world begins as a model. Models typically present geometric information, such as point locations or object dimensions; topological information, such as how points form polygons, how polygons form objects, and how objects form a world; and shading information. As Listing 1 illustrates, the 3D panel's simple model consists of a vertices (points) list for its geometry and a list of edges connecting vertices for its topology. Shading isn't present.

The Panel3D class's load() method stores a model as follows: Each vertex's x, y, and z values are placed into a Panel3D.Vertex object, which is mapped to the vertex name via an entry in a java.util.Hashmap; each edge's start and end vertex names are placed into a Panel3D.Edge object, which is stored in a java.util.Vector.

1 2 3 4 Page 2
Page 2 of 4