Introduction
Overview
History
Advice
Books
Tutorials

Theory
Concepts
Math
Modeling
Rendering
API

3D on the Web
Pure Java
Java3D
Flash
VRML
Other

Source Code
VB
Java Applet I
Java Applet II
JavaScript
Java3D
VML

Resources
Web Sites
Mailing Lists
USENET
Vendors
News

Source Code - Java Applet I
Any of today's programming languages can be used to generate 3D images of varying degrees of performance and quality. The standard Java language has various features which allow the creation of 3D applications. Even though a new generation of Java API called Java3D promises improved performance and reduced coding effort most PCs only have a copy of Java installed. Few PCs have the Java3D software users are often unwilling to download the required megabytes of files to get the capability. This page describes an applet called gbCube which displays a rotating cube. I've also written a more generic 3D object viewer call gbViewer which can display 3D objects read in from standard 3D file formats.

Overview HTML Source Code Discussion Download gbViewer


Return to top of document

gbCube Overview

If you've read my page on generating a 3D rotating cube with VB, then you'll already have a good idea of what to expect on this page. I've written a Java Applet which will display a 3D rotating cube. I list the code below, along with discussion to explain how the code works and special issues or concerns that a programmer should be aware of.

Regardless of the language used to create a 3D graphic scene, the same basic set of steps must be accomplished. The entire sequence is called a 3D graphics pipeline and consists of the following major steps:

  • Modelling
  • Rotation
  • Depth Sorting (Painter's Algorithm)
  • Backface culling
  • Projection
  • Shading
There are other steps as well, such as lighting, but this page will be limited to just those topics listed above. Future updates to the Java Applet are likely which will expand the capabilities that are included within the applet. Here is the current version of the applet:


Return to top of document

HTML

Whether using pure Java or Java3D, the mechanics of embedding an applet into an HTML page are the same. This section provides a discussion of the HTML code needed to embed the applet.

<html>
<head>
<title></title>
</head>
<body>
<h1>Java Applet Demo</h1>
<applet code=3dcubejava.class width=300 height=400>
<param name=Speed value=10>
<param name=POV value=10>
<param name=Theta value=.05>
</applet>
</body>
</html>

In this simple example a Java applet named "3dcube.class" is embedded in the HTML page, with a window of size 300x400 pixels reserved for the output of the applet.

Three parameters (Speed, POV, and Theta) are passed from the HTML code to the Java applet, with values of 10, 10, and 0.05 respectively.


Return to top of document

gbCube - Pure Java Rotating 3D Cube

It is entirely possible to create 3D images using pure Java code - without using the recent Java3D advanced features. This section examines the source code of gbCube, a Java applet which creates a rotating 3D cube using only built-in Java features.

Program Operational Overview
The paint() method of the applet is used to call out the various elements of the 3D graphics pipeline. The paint() method is set for a continuous loop through the use of the repaint() method. A 25ms time delay is used to control the speed of rotation.

The paint() method is as follows:

    public void paint(Graphics screen) {
      if (Continue) {
        SortByDepth(screen);
        BackFaceCulling(screen);
        ApplyProjection(screen);
        DrawCube(screen);
        try { Thread.sleep(Delay); } catch _
                (InterruptedException e) {}   //delay
        RotatePoints(screen);
        repaint();
       }
    }

The screen object is shared between the various pipeline method. It shows as an argument for each of the methods.

The shading used by gbCube is called flat shading - all the pixels of a triangle are colored exactly the same, as provided by the Java fillPolygon method. This approach is simple to use and very fast but is not very realistic. It does not provide color gradients nor does it take into account shadows resulting from the 3D scene's light source. Future enhancements to gbCube will include a light source and more advanced shading algorithms, such as Phong shading.

As was noted in the 3D math page at this site, standard trigonometric calculations or matrix operations can be used to perform the calculations needed to animate a 3D scene. In the example that follows matrices are not used. Code examples of matrix math are, however, provided elsewhere at this site.

Modelling
To be consistent with discussions elsewhere on this site, the cube is modelled using triangles. Twelve triangles are needed, 2 for each of the 6 cube faces. The cube model requires only 8 points, with points shared by multiple triangles.

This model uses arrays to store point and triangle information. The following initialization of variables is used:

    double Theta = 0.005;     //angle of rotation in radians
    double L = 50;            //temp variable used to love Px,Py,Pz,PPx,PPy
    double POV = 500;         //distance from eye to display screen
    double Offset = 100;      //used to center the cube in the applet window
    Polygon T = new Polygon(); //Polygon object used for drawing triangles
    int Delay = 25;            //delay between rotations in milliseconds

    double[] Px  = {-L,-L, L, L,-L,-L,L, L};  //real point x-coord, 8 pts
    double[] Py  = {-L, L, L,-L,-L, L,L,-L};  //real point y-coord, 8 pts
    double[] Pz  = {-L,-L,-L,-L, L, L,L, L};  //real point z-coord, 8 pts
    double[] PPx = {-L,-L, L, L,-L,-L,L, L};  //projected point x-coord, 8 pts
    double[] PPy = {-L, L, L,-L,-L, L,L,-L};  //projected point y-coord, 8 pts

    int    V1temp, V2temp, V3temp;       //temp variables used in sorting
    double V4temp, V5temp;               //temp variables used in sorting
    double oldX, oldY, oldZ;             //temp variables used in rotating

    int[] V1 = {0, 0, 4, 4, 7, 7, 3, 3, 2, 2, 3, 3,};   //vertex1
    int[] V2 = {3, 2, 0, 1, 4, 5, 7, 6, 6, 5, 0, 4,};   //vertex2
    int[] V3 = {2, 1, 1, 5, 5, 6, 6, 2, 5, 1, 4, 7,};   //vertex3
    double[] V4 = new double[12];          //Average Z of all 3 vertices 
    double[] V5 = new double[12];          //DotProduct of Normal and POV

    double CPX1,CPX2,CPX3,CPY1,CPY2,CPY3;  //temp var used in Cross Product
    double CPZ1,CPZ2,CPZ3,DPX,DPY,DPZ;  //temp var used in Cross/Dot Product

    boolean Continue = true;    //controls whether painting continues        

The Px, Py, and Pz arrays contain the coordinates of the 8 points. The PPx and PPx arrays contain the projections of those points onto the computer screen.

The x, y, and z coordinates of the vertices of the 12 triangles are kept in arrays V1, V2, and V3. The order of the initial point assignment is made to ensure a counter-clockwise listing. The coordinates assume the cube is centered about 0,0,0.

The average Z value of the vertices in each triangle are kept in array V4. This value is used for sorting triangles by depth - part of the Painter's algorithm for drawing 3D scenes.

The Dot Product of the normal to each triangle and the scene's point of view is kept in array V5. This value is used to determine whether a triangle is facing the viewer and should then be drawn, or facing away and does need to be drawn.

The Continue variable is used to escape the repeat of the paint() method. It is set to false whenever the browser tells the applet to stop.

Each triangle, consisting of 3 vertices, is loaded into the Polygon object T before drawing. The variable is loaded (with vertex coordinates) and cleared each time a triangle needs to be drawn.

Initialization of the global variables and arrays is performed outside the init() method only because it was simpler to write the code that way. Normally, initialization of variables would take place within the init() method.

An alternate approach to defining 8 common points from which all triangles were made, could have to define each triangle with 9 coordinates (no storage of point information). That approach would have used 9 arrays, one for each coordinate value making up each triangle (3 points x 3 axes). Either approach would work, but the common point approach results in fewer calculations and faster speed.

Java provides the fillPolygon method for filling screen areas bounded by points. In this case, the x,y coordinates of each triangle vertex are incorporated into a polygon using the addPoint method, followed by use of the fillPolygon method to render the triangles. An additional call is made to the Java method drawPolygon to draw the edges of the triangles in a different color - improving the visibility of the cube.

Depth Sorting
At the end of the 3D graphics pipeline, the DrawCube subroutine draws the triangles one at a time in the order the vertices are stored in arrays V1, V2, and V3.

To improve the realism of the drawing, gbCube calculates the average z coordinate of each triangle and then sorts the triangles (arrays V1, V2, and V3) the objects farthest away are drawn first. The z-depth of each triangle is stored in the array V4.

This approach is called the Painter's Algorithm and ensures that the nearest objects will be in front of the farthest objects. It works well but has limitations, such as not working well for intersecting triangles. There are variations of the Painter's Algorithm which address these shortcomings but gbCube uses only the sort routine. For a simple cube this approaches works just fine.

    public void SortByDepth(Graphics screen) {
       for (int w = 0; w < 12 ; w++) {
          V4[w] = (Pz[V1[w]]+Pz[V2[w]]+Pz[V3[w]]) / 3;
       }
       for (int g = 0; g < 11 ; g++) {
          for (int h = 0; h < 12; h++) {
             if (V4[g] < V4[h]) {
                V1temp = V1[g]; V2temp = V2[g]; V3temp = V3[g]; _
                                   V4temp = V4[g]; V5temp = V5[g];
                V1[g]=V1[h];    V2[g]=V2[h];    V3[g]=V3[h];    _
                                   V4[g]=V4[h];    V5[g]=V5[h];
                V1[h]=V1temp;   V2[h]=V2temp;   V3[h]=V3temp;   _
                                   V4[h]=V4temp;   V5[h]=V5temp;
             }
          }
       }
    }

The sort algorithm used here is called a bubble sort. It works well enough for a few hundred triangles to be sorted, but is not suited for more complex 3D scenes. Other sort routines can be written which sort up to a hundred times faster. These will be included in future gbCube updates.

BackFaceCulling
As has been discussed, any triangle pointing away from the point of view cannot be seen - it's on the back side of the 3D scene. Identifying such triangles, and not displaying them, is called backface culling and typically results in eliminating the need to display about half of the triangles in the 3D scene.

    public void BackFaceCulling(Graphics screen) {
       for (int w = 0; w < 12 ; w++) {        
          // Cross Product
          CPX1 = Px[V2[w]] - Px[V1[w]];
          CPY1 = Py[V2[w]] - Py[V1[w]];
          CPZ1 = Pz[V2[w]] - Pz[V1[w]];
          CPX2 = Px[V3[w]] - Px[V1[w]];
          CPY2 = Py[V3[w]] - Py[V1[w]];
          CPZ2 = Pz[V3[w]] - Pz[V1[w]];
          DPX = CPY1 * CPZ2 - CPY2 * CPZ1;
          DPY = CPX2 * CPZ1 - CPX1 * CPZ2;
          DPZ = CPX1 * CPY2 - CPX2 * CPY1;
          // DotProduct uses POV vector 0,0,POV  as x1,y1,z1
          V5[w] = 0 * DPX + 0 * DPY + POV * DPZ;
       }
    }

This code first calculates the cross product of the first two line segments of each triangle. Then a dot product is calculated between the resulting normal vector and the POV vector. The result for each triangle is stored in array V5.

When the code is executed to draw the triangles, only those facing the viewer (positive dot product) will be drawn.

Another very key point to notice in the source code is that the cross product equations you've seen so far assume that the vector components represent position vectors - with starting points at the origin (0,0,0). To calculate the cross product between two triangle edges you must use the displacement vectors which are calculated by the difference of the starting and ending points of the triangle line segments.

CalculatePointProjections
Until now all point coordinates have been world coordinates - the position of the 3D cube in space. These point coordinates must now be projected onto the computer screen - onto a 2D surface. The subroutine is as follows:

     // calculate projection coordinates
     for (int s = 0; s < 8 ; s++) {
         PPx[s] = Px[s]+Offset;
         PPy[s] = Py[s]+Offset;
     }

Each point of the 3D scene must be displayed on the 2D computer screen. The mapping of the points from the 3D scene to the computer screen is called point projection. There are two general forms of projection, parallel and perspective.

With parallel projection, the x-y coordinates of a 3D point simply map one-to-one to the computer screen. The z coordinates are simply dropped. While very simple to perform, the resulting images do not display realistic images in that objects far away will appear to be the same size as objects close in.

gbCube uses parallel projection only because I haven't gotten around to adding the projection equations. This should be added soon.

Perspective projection, which uses the z dimensions to adjust the 2D images to create more realistic images. With perspective projection, objects farther away will appear smaller in the resulting This simulates real life views of scenes with depth.

Shading
The final step in the 3D graphics pipeline used by gbCube is to draw the cube on the computer screen. The x-y coordinates of the three vertices in each triangle are used in the Java fillPolygon and drawPolygon methods to display the cube. Flat shading is used (same color for all pixels within a triangle). Later versions of this applet will include improved rendering, such as Phong shading.

    public void DrawCube(Graphics screen) {
       screen.clearRect(0,0,getSize().width, getSize().height);
       for (int w = 0; w < 12 ; w++) {
          screen.setColor(Color.red);   
          if (V5[w] > 0) {              
             T.addPoint ((int)(PPx[V1[w]]+Offset), (int)(PPy[V1[w]]+Offset));
             T.addPoint ((int)(PPx[V2[w]]+Offset), (int)(PPy[V2[w]]+Offset));
             T.addPoint ((int)(PPx[V3[w]]+Offset), (int)(PPy[V3[w]]+Offset));
             screen.fillPolygon(T);
             screen.setColor(Color.blue);   
             screen.drawPolygon(T);        
             T.reset();
          }
       }
    }

Note that both the drawPolygon and fillPolygon methods are used. This is done using two colors so that the edges of the cube are clear. The drawing is made using the projection x-y coordinates, not the true coordinates of the 3D cube points. If the fillPolygon statement were commented out gbCube would display only a wire-frame image.

Following the display and shading of all triangles a 25ms time delay is introduced, with code as follows:

   try { Thread.sleep(Delay); } catch (InterruptedException e) {} 

Conventional Java syntax would have resulted in spreading the timer code over multiple lines, but it was visually convenient to have the code listed on a single line.

Rotation
The last step in the gbCube 3D graphics pipeline is to rotate each of the eight points through an angle of rotation, in preparation for the next cycle. While separate angles of rotation for each axis are possible, gbCube uses the same angle of rotation for each axis.

Rotation about each axis is calculated separately and successively applied to get the accumulative effect of rotation about all 3 axes.

    public void RotatePoints(Graphics screen) {
       for (int w=0; w < 8; w++) {
          oldY = Py[w]; oldZ = Pz[w];
          Py[w] = oldY * Math.cos(Theta) - oldZ * Math.sin(Theta);
          //rotate about X
          Pz[w] = oldY * Math.sin(Theta) + oldZ * Math.cos(Theta);  
          //rotate about X

          oldX = Px[w]; oldZ = Pz[w];
          Px[w] = oldZ * Math.sin(Theta) + oldX * Math.cos(Theta);  
          //rotate about Y
          Pz[w] = oldZ * Math.cos(Theta) - oldX * Math.sin(Theta);  
          //rotate about Y

          oldX = Px[w]; oldY = Py[w];
          Px[w] = oldX * Math.cos(Theta) - oldY * Math.sin(Theta);   
          //rotate about Z
          Py[w] = oldX * Math.sin(Theta) + oldY * Math.cos(Theta);  
          //rotate about Z
       }
    }