The selections of source code here are intended to show you the key elements of the code explained in the second and third articles in this series. In the interest of reaching these bits of code expediently, I have eliminated most of the applet's code. One thing I decided to omit was field definitions. The relevant fields will be explained in the annotations, but the important code is in the definitions of the methods, not the fields. Even some methods, though, such as the drawing methods for the Facet and GraphModelFacets classes, include so much detail irrelevant to this discussion that I decided it would be better to omit them here.
My focus is on providing the key code behind the conversion from three dimensions to two dimensions and the associated mouse-based rotation action, as explained in the second article in this series, and the key code for the center point projection, as explained in the main body of the third article. However, since it may help you understand where the bits of code fit into the broader structure of the applet, here is an outline of the applet main class, called Facets, and the inner classes within it:
public class Facets extends javax.swing.JApplet implements java.awt.event.MouseMotionListener, java.awt.event.MouseListener { public class DoubleTriad {...} public class CoordsConverter3d {...} public class GraphCanvas3d extends javax.swing.JComponent { public class GraphModelFacets { private class Facet extends java.util.ArrayList<DoubleTriad> {...} ... } ... } ... }Note that this applet uses Java2 Swing components for graphing. More complete source code for this applet is available directly from my Web site.
public class DoubleTriad { public double x, y, z; public DoubleTriad(double xnew, double ynew, double znew) { x = xnew; y = ynew; z = znew; } }Points in two dimensions are represented using the java.awt.geom.Point2D.Double class.
private void setScale() { tr.setToIdentity(); tr.scale(((double) wd)/20.0,((double) ht)/(-20.0)); tr.translate(10.0,-10.0); scales3d.x = 20.0/(xmax-xmin); scales3d.y = 20.0/(ymax-ymin); scales3d.z = 20.0/(zmax-zmin); shifts3d.x = -10.0-scales3d.x*xmin; shifts3d.y = -10.0-scales3d.y*ymin; shifts3d.z = -10.0-scales3d.z*zmin; // ... }The method setViewAngles() in CoordsConverter3d resets the view angles that specify the view vector. (See the second article in this series for an explanation of view vectors.) Definitions for several relevant fields are not shown below: the view angles are stored in the fields va and vb; the sine and cosine of va are stored in sa and ca, respectively; similarly, sine and cosine of vb are stored in sb and cb, respectively; and since va and vb specify the view angle in degrees, their values must be converted to radians for the Math.sin() and Math.cos() methods, so the final field DEG2RAD provides the conversion constant, Math.PI/180. All of these fields are of type double. The sine and cosine of the view angles are computed and stored as soon as the angles are set, then used later for computing projections -- computing and storing these values now saves repeated calls to Math.sin() and Math.cos(), both of which are computationally expensive.
public void setViewAngles(double newa, double newb) { va=newa; vb=newb; sa=Math.sin(DEG2RAD*va); ca=Math.cos(DEG2RAD*va); sb=Math.sin(DEG2RAD*vb); cb=Math.cos(DEG2RAD*vb); // ... }To retrieve the view angles, the class CoordsConverter3d provides this getViewAngles() method:
public java.awt.geom.Point2D.Double getViewAngles() { return new java.awt.geom.Point2D.Double(va, vb); }Finally, the methods project() in CoordsConverter3d carry out the projection from three dimensions to two dimensions; the java.awt.geom.AffineTransform object that was defined earlier must then be used to convert to pixel coordinates. Note in particular here the uses of the sines and cosines of the view angles, stored as noted above. Again, there are two versions of this method, depending on whether the point is specified as a DoubleTriad or three separate double's.
public Point2D.Double project(DoubleTriad pt) { DoubleTriad tmp = new DoubleTriad(0.0,0.0,0.0); tmp.x = scales3d.x*pt.x+shifts3d.x; tmp.y = scales3d.y*pt.y+shifts3d.y; tmp.z = scales3d.z*pt.z+shifts3d.z; return new Point2D.Double(-tmp.x*sa+tmp.y*ca, -tmp.x*ca*cb-tmp.y*sa*cb+tmp.z*sb); }
public Point2D.Double project(double x, double y, double z) { return project(new DoubleTriad(x,y,z)); }The last piece of the rotation puzzle is the mouse action, which is handled in the main applet class Facets as part of implementing the java.awt.event.MouseMotionListener and java.awt.event.MouseListener interfaces, in particular in the mouseDragged() and mousePressed() methods. Here, the fields prevx and prevy store the pixel coordinates associated with the previous relevant MouseEvent, c is an instance of CoordsConverter3d as above, and canvas is the instance of GraphCanvas3d. First, the mousePressed() method simply stores the position of the mouse event when the mouse button is first pressed within the displayed graph. Note that once the data is stored, the event is "consumed" so that it doesn't affect anything else.
public void mousePressed(java.awt.event.MouseEvent e) { prevx = e.getX(); prevy = e.getY(); e.consume(); }In the mouseDragged() method, note in particular the call to c.setViewAngles(), followed by canvas.repaint(). In both directions, one pixel of mouse motion translates into one degree of rotation. Again, the method ends by consuming the mouse event.
public void mouseDragged(java.awt.event.MouseEvent e) { int x = e.getX(); int y = e.getY(); java.awt.geom.Point2D.Double p=c.getViewAngles(); c.setViewAngles(p.x-(x-prevx),p.y-(y-prevy)); canvas.repaint(); prevx = x; prevy = y; e.consume(); }
public boolean add(DoubleTriad t) { boolean b = super.add(t); midpoint = new DoubleTriad(0,0,0); for (DoubleTriad q : this) { midpoint.x += q.x; midpoint.y += q.y; midpoint.z += q.z; } midpoint.x = midpoint.x/size(); midpoint.y = midpoint.y/size(); midpoint.z = midpoint.z/size(); return b; }
public void add(double x, double y, double z) { add(new DoubleTriad(x,y,z)); }The field c, actually defined in the main applet class but used extensively here, is an instance of CoordsConverter3d. The Facet class also stores a base color for use in painting the facet in the current graphics context. The paintFacet(), not shown here, takes the vertices as stored ArrayList, calls c.project(), and uses the resulting java.awt.geom.Point2D.Double's to construct a java.awt.geom.GeneralPath, making sure it is a closed polygonal path. This path is then converted to pixel coordinates using the AffineTransform stored in the CoordsConverter3d, then drawn, first as a filled polygon in a lightened color, then in the heavier color in outline. The paintFacet() method also uses the value of the offset field to lighten the color of the surface when it is drawn in the background.
The method recalcOffset() in class Facet recalculates the value of the offset field, by computing the dot product of the center p (taken as a vector) with the unit view vector. The CoordsConverter3d class includes the method dotview() to compute this dot product:
public double dotview(DoubleTriad t) { return t.x*viewvec.x+t.y*viewvec.y+t.z*viewvec.z; }Then the recalcOffset() method in class Facet is:
public void recalcOffset() { offset = c.dotview(midpoint); }As will be seen later, the offset is also used for sorting the facets, but when retrieved from outside the Facet class, it is normally accessed using this getOffset() method in the class Facet.
public double getOffset() { return offset; }In the class GraphModelFacets, the field model is of type java.util.ArrayList<Facet>. All facets, even from different surfaces, must be stored together in model in order to be sure that the sorting routine can be applied to all facets, so that when surfaces intersect, the parts in front will be drawn last, no matter from which surface they come. This is actually why a facet's base color is stored in the Facet class rather than storing it only once for the whole surface -- different facets in model come from surfaces of different colors. The sorting routine, sort(), is called here in the paintModel() method. The field f is an instance of a class that implements the java.util.Comparator interface and compares Facet's by comparing their offset's.
public void paintModel(Graphics2D g) { for (Facet q : model) q.recalcOffset(); sort(model, f); for (Facet q : model) q.paintFacet(g); }The sort() method is a local method that implements Hoare's QuickSort algorithm, but the signature of the method is identical to the java.util.Collections.sort() method, so other sorting routines can be substituted easily.
The last of the inner classes, GraphCanvas3d, is an
extension of the javax.swing.JComponent
class, and it handles most of the actual drawing, including
calling the GraphModelFacets.paintModel()
and drawing the coordinate axes.