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

GBIC >> 3D Graphics >> Pure Java Viewer
Source Code - Java Applet II
In an earlier page at this site the code for creating a simple rotating cube applet was discussed. This page describes a more useful applet, called gbViewer, that is a complete 3D object file viewer capable of reading industry standard 3D file formats and provides a graphical user interface.

Overview HTML Source Code Discussion Download


Return to top of document

gbViewer Overview

The gbViewer applet allows the user to select and display a 3D object file. The file list is provided via the HTML code and the files must reside in the directory where the HTML file is located. Here's the gbViewer applet in action:

Like any 3D application, gbViewer includes code to implements a 3D graphics pipeline, consisting of the following elements:

  • Modelling
  • Rotation
  • Depth Sorting (Painter's Algorithm)
  • Backface culling
  • Projection
  • Shading
There 3D pipeline can include other steps as well, such as lighting. gbViewer does not currently support light sources within the 3D scene.

In addition to the 3D graphics pipeline content the gbViewer applet also provides the additional following capabilties and features:

  • File loading (STL and TOV formats for now, more to be added later)
  • Graphical User Interface (display controls)
  • Display flicker control (double buffering and method overrides)
  • Applet parameters and initialization (more extensive than the simple rotating cube applet)
  • Animation (automatic looping of the 3D graphics pipeline code)


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 gbViewer applet.

    <html>
    <head>
    <title></title>
    </head>
    <body>
    <h1>Java Applet Demo</h1>
    <applet code=3dviewer.class width=500 height=400>
    <param name=Theta value=0.02>
    <param name=Delay value=25>
    <param name=Fudge value=1.0>
    <param name=FileName value\=
    cube.txt,
    helicopter.txt,
    sphere.txt,
    face.txt,
    block.stl,
    joint.stl,
    \=>
    </applet>
    </body>
    </html>

All of the gbViewer code is contained in a single Java class, "3dviewer.class". There are aspects of the code, such as the file loaders, which could just as easily have been written as separate classes and then imported into gbViewer. As more file formats are added in the future, I'm likely to use that option to avoid having to work directly with the main class file.

gbViewer supports inclusion of four parameters that may be fed to the applet from the HTML code. These may be placed in any order within the HTML file.

  • Theta
    gbView provides rotation of a 3D object about the X, Y, and Z axes - as opposed to rotation of the object about its center of gravity or about an arbitrary point/line in space. Rotation is performed incrementally, rotating each point by a small, fixed angle Theta. The new point coordinates resulting from rotation are calculated by sequentially rotating the points about the X, Y, and Z coordinates - three separate calculations.

    Theta may be loaded into gbView as a parameter contained in the HTML code. It may also be changed from the gbView user interface. Theta is entered as radians.

  • Delay
    gbViewer calculates the rotated coordinates of the 3D object, renders (displays) the object, and then goes to "sleep" for the interval defined by Delay. Following the Delay, the cycle repeats itself: rotate, display, sleep. Delay is entered as milliseconds.

    Note that Delay is not the same thing as the time between rotations. This is because the time needed to calculate rotated coordinates and to display the object can vary with complexity of the 3D object being displayed.

    With additional code, the gbViewer applet could have been written to sustain a fixed time per rotation. However, such code would also have to watch for situations where the calculations took more time the the desired delay interval allows.

  • Fudge
    gbView attempts to automatically adjust its display scale so that the 3D object fills the applet viewing window. Depending on the geometries involved in the 3D object the automatic scale factor may not provide the desired results. The Fudge parameter is used as a multiplier to adjust the automatically generated scale factor.

    The user inteface of gbViewer can also be used to adjust the scale of the display, but the Fudge parameter allows for a hands-off adjustment.

  • FileName
    The FileName parameter can consist of one or more file names, separated by commas. The first filename on the list is automatically displayed by gbViewer. The complete list of filenames are displayed in a dropdown list and may be selected for display after the gbViewer applet has started.

    The files must be resident in the same directory where the calling HTML file and applet are contained.

    gbViewer currently support two file formats and are discussed in more detail below.


Return to top of document

gbViewer - Source Code Discussion

It is entirely possible to display 3D objects using pure Java code - without using the recent Java3D advanced features. In this section an applet based on the pure Java approach is presented. The completed 3D object viewer is called gbViewer and is available for download. The applet is fully functional but I am continuing to improve it so check regularly for updates.

Program Operational Overview
gbViewer supports the rendering of objects which are defined by the point and triangles that comprise the surface of the 3D object.

Animation
gbViewer uses a separate thread to contain run() method for looping through the 3D graphics. Starting the animation of the applet is done by creating the thread, in which run() is automatically called. The stop() method destroys the thread, stopping the animation. The start() method is used to re-create the thread and to continue the animation.

The body of the run() method contains the complete 3D graphics pipeline, including the code for creating a delay between animation scenes. The ManualPipeLine() method supports animation curing a mouse drag. The Nudge is used to complete a single cycle of animation (for better control of the object position where a screen capture might be necessary).

To avoid flicker on the applet window, the update() method is overridden and is used to place a call to paint().

You might also note that paint() is only used to draw the image from the buffer that is being used as part of the double-bufferring technique for eliminating screen flicker.

    public void start() { 
        animator = new Thread(this);
        animator.start();           
    }

    public void stop() { 
        animator = null; 
    }
    public void run() {
        while(Thread.currentThread()== animator) {
            RotatePointsX(); 
            RotatePointsY(); 
            RotatePointsZ(); 
            SortByDepth();
            BackFaceCulling();
            ApplyProjection();
            DrawObject();
            try { Thread.sleep(Delay); } catch (InterruptedException e) {}
            repaint();
        }
    }
    public void ManualPipeLine() {
        SortByDepth();
        BackFaceCulling();
        ApplyProjection();
        DrawObject();
        repaint();
    }
    public void Nudge() {
        RotatePointsX(); 
        RotatePointsY(); 
        RotatePointsZ(); 
        ManualPipeLine();
    }
    public void update(Graphics screen) {
        paint(screen);
    }
    public void paint(Graphics screen) {
        screen.drawImage(BackPage,0,0,null);
    }

Application Initialization
...

import java.awt.*;
import java.io.*;
import java.net.*;
import java.awt.event.*;

public class gbviewer extends java.applet.Applet implements ActionListener, 
ItemListener, MouseListener, MouseMotionListener, Runnable {

    Thread animator;

    Image BackPage;
    Graphics offScreen;

    double ThetaX = 0.025, ThetaY = 0.025, ThetaZ = 0.025;
    double L = 50, POV = 500, XOffset = 100, YOffset = 100;
    double Scale = 1.0, XMax = 0.0, XMin = 0.0, Fudge = 0.0;
    double YMax = 0.0, YMin = 0.0, ZMax = 0.0, ZMin = 0.0;
    Polygon T = new Polygon();
    int iLoop = 0, Delay = 15, mx = 0, my = 0, mrSpeed = 4;
    int iLine = 0, width = 0, height = 0, lesser = 0, colorIndex = 11;
    int NumberColors = 0, NumberPoints = 0;
    int NumberLines = 0, NumberTriangles = 0;
    Color[] ColorList = {Color.black, Color.blue, Color.cyan, _
                        Color.darkGray, Color.gray, Color.green, _
                        Color.lightGray, Color.magenta, Color.orange, _
                        Color.pink, Color.red, Color.white, Color.yellow};
    double[] Px = new double[3000];          
                 // read xyz coordinates, + projected xy coordinates
    double[] Py = new double[3000];
    double[] Pz = new double[3000];
    double[] PPx = new double[3000];
    double[] PPy = new double[3000];

    int    V1temp, V2temp, V3temp, V6temp, V7temp, V8temp;   
    //temp variables used in sorting

    double V4temp, V5temp, oldX, oldY, oldZ;                 
    //temp variables used in rotating and sorting

    String temp, filename, filelist, line, host, title = "gb3DViewer";
    String[] fields;

    Color[] C = new Color[1000];
    int[] V1 = new int[5000];        // vertices 123 of triangles
    int[] V2 = new int[5000];
    int[] V3 = new int[5000];
    
    double[] V4 = new double[5000];  
    // Average Z and Dot Product (Normal and POV)

    double[] V5 = new double[5000];
    int[] V6 = new int[5000];       // Color of triangle
    int[] V7 = new int[5000];       // Color of triangle (random)
    int[] V8 = new int[5000];       // Color of triangle (gradient)

    double CPX1, CPX2, CPX3, CPY1, CPY2, CPY3;    
    //temp variables used in Cross Product

    double CPZ1, CPZ2, CPZ3, DPX, DPY, DPZ;       
    //temp variables used in Cross/Dot Product

    Button bStart, bStop, bSpeedP, bSpeedM, bNudge, bBack;
    Button bMoveXP, bMoveXM, bMoveYP, bMoveYM, bMoveZP, bMoveZM;
    Checkbox cShade, cEdges, cXRotate, cYRotate, cZRotate;
    Checkbox cInfo, cBack, cSort, cProj;
    Checkbox cFile, cRandom, cGradient, cGlitter;
    CheckboxGroup radioGroup = new CheckboxGroup();

    Choice cFiles, cData;
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.

As was noted in the 3D math page at this site, standard trigonometric calculations or matrix operations can be used to perform the calculations described 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.

Variables/Arrays/Objects
The folowing init() method is used to initialize the variables, arrays and objects used by gbViewer. Comments are provided within the code, as well as at the end of the code listing.

    public void init() { 
        setBackground(Color.white); 
        temp = getParameter("Fudge");
        Fudge = Float.valueOf(temp).floatValue();
        temp = getParameter("Delay");
        Delay = Integer.parseInt(temp);
        temp = getParameter("Theta");
        ThetaX = Float.valueOf(temp).floatValue();
        ThetaY = ThetaX;
        ThetaZ = ThetaX;
        width = getSize().width;
        height = getSize().height;
        XOffset = width / 2 + 80;    //80 allows for button width
        YOffset = height / 2;
        if (width > height) 
            lesser = height;
        else
            lesser = width;
        BackPage = createImage(width,height);
        offScreen = BackPage.getGraphics();
        setLayout(null);
        bStart = new Button("Start");
        bStop = new Button("Stop");
        bNudge = new Button("Nudge");
        bSpeedP = new Button("Speed+");
        bSpeedM = new Button("Speed-");
        bMoveXP = new Button("Move X+");
        bMoveXM = new Button("Move X-");
        bMoveYP = new Button("Move Y+");
        bMoveYM = new Button("Move Y-");
        bMoveZP = new Button("Zoom+");
        bMoveZM = new Button("Zoom-");
        bBack = new Button("Back Color");
        cFiles = new Choice();
        cData = new Choice();
        cShade = new Checkbox("Shade");
        cEdges = new Checkbox("Edges");
        cXRotate = new Checkbox("X-Rotation");
        cYRotate = new Checkbox("Y-Rotation");
        cZRotate = new Checkbox("Z-Rotation");
        cInfo = new Checkbox("Info");
        cBack = new Checkbox("Backface");
        cSort = new Checkbox("Painter");
        cProj = new Checkbox("Projection");

        cFile = new Checkbox("File",radioGroup,true);
        cRandom = new Checkbox("Random",radioGroup,false);
        cGradient = new Checkbox("Gradient",radioGroup,false);
        cGlitter = new Checkbox("Glitter",radioGroup,false);
        
        bStart.addActionListener(this);
        bStop.addActionListener(this);
        bNudge.addActionListener(this);
        bSpeedP.addActionListener(this);
        bSpeedM.addActionListener(this);
        bMoveXP.addActionListener(this);
        bMoveXM.addActionListener(this);
        bMoveYP.addActionListener(this);
        bMoveYM.addActionListener(this);
        bMoveZP.addActionListener(this);
        bMoveZM.addActionListener(this);
        bBack.addActionListener(this);

        cFile.addItemListener(this);
        cRandom.addItemListener(this);
        cGradient.addItemListener(this);
        cGlitter.addItemListener(this);
        cShade.addItemListener(this);
        cEdges.addItemListener(this);
        cBack.addItemListener(this);
        cSort.addItemListener(this);
        cProj.addItemListener(this);
        cFiles.addItemListener(this); 

        filelist = getParameter("FileName");
        fields = filelist.split("[,\\s]+");
        filename = fields[0];
        for (int w=0; w < fields.length; w++) {
            cFiles.addItem(fields[w]);
        }  

        cData.addItemListener(this);
        cData.addItem("");

        add(bStart); add(bStop); add(cShade); add(cEdges); 
        add(cFiles); add(bNudge);
        add(cXRotate); add(cYRotate); add(cZRotate); add(cInfo); 
        add(cSort); add(cBack); add(cProj);
        add(bSpeedP); add(bSpeedM); add(bBack);  add(cData);
        add(bMoveXP); add(bMoveXM); add(bMoveYP); add(bMoveYM); 
        add(bMoveZP); add(bMoveZM);
        add(cRandom); add(cGradient); add(cFile); add(cGlitter);

        bStart.setBounds(0,0,80,15);        //  L,T,W,H
        bNudge.setBounds(0,15,80,15);
        bStop.setBounds(0,30,80,15);
        bSpeedP.setBounds(0,50,80,15);
        bSpeedM.setBounds(0,65,80,15);
        bMoveXP.setBounds(0,85,80,15);
        bMoveXM.setBounds(0,100,80,15);
        bMoveYP.setBounds(0,115,80,15);
        bMoveYM.setBounds(0,130,80,15);
        bMoveZP.setBounds(0,150,80,15);
        bMoveZM.setBounds(0,165,80,15);
        cFile.setBounds(0,185,80,15);
        cRandom.setBounds(0,200,80,15);
        cGradient.setBounds(0,215,80,15);
        cGlitter.setBounds(0,230,80,15);
        bBack.setBounds(0,250,80,15);
        cShade.setBounds(0,270,80,15);
        cEdges.setBounds(0,285,80,15);
        cXRotate.setBounds(0,310,80,15);
        cYRotate.setBounds(0,325,80,15);
        cZRotate.setBounds(0,340,80,15);
        cBack.setBounds(0,360,80,15);
        cSort.setBounds(0,375,80,15);
        cProj.setBounds(0,390,80,15);
        cInfo.setBounds(0,410,80,15);
        cFiles.setBounds(0,430,80,15);
        cData.setBounds(0,450,80,15);

        cShade.setState(true); cEdges.setState(true); 
        cFile.setState(true);
        cXRotate.setState(true);  cYRotate.setState(true);  
        cZRotate.setState(true);  
        cSort.setState(true);  cBack.setState(true);  
        cProj.setState(true);  
        addMouseMotionListener(this);

        LoadFileName();
    }    

3D Graphics Pipeline

  • RotatePoints 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. Rotation about each axis is calculated separately and successively applied to get the accumulative effect of rotation about all 3 axes.

    The code for this is written as three separate methods rather than in a single method, to enable using the mouse to rotate the 3D object about a single axis.

    The Theta parameter entered via the HTML applet code is used as the starting value for all three incremental rotation angles.

        public void RotatePointsX() {
           if (cXRotate.getState() == true) {
               for (int w=0; w < NumberPoints; w++) {
                  oldY = Py[w]; oldZ = Pz[w];
                  Py[w] = oldY * Math.cos(ThetaX) - oldZ * _
                         Math.sin(ThetaX);  //rotate about X
                  Pz[w] = oldY * Math.sin(ThetaX) + oldZ * _
                         Math.cos(ThetaX);  //rotate about X
               }
           }
        }
        public void RotatePointsY() {
           if (cYRotate.getState() == true) {
               for (int w=0; w < NumberPoints; w++) {
                  oldX = Px[w]; oldZ = Pz[w];
                  Px[w] = oldZ * Math.sin(ThetaY) + oldX * _
                          Math.cos(ThetaY);  //rotate about Y
                  Pz[w] = oldZ * Math.cos(ThetaY) - oldX * _
                          Math.sin(ThetaY);  //rotate about Y
               }
           }
        }
        public void RotatePointsZ() {
           if (cZRotate.getState() == true) {
               for (int w=0; w < NumberPoints; w++) {
                  oldX = Px[w]; oldY = Py[w];
                  Px[w] = oldX * Math.cos(ThetaZ) - oldY * _
                          Math.sin(ThetaZ);  //rotate about Z
                  Py[w] = oldX * Math.sin(ThetaZ) + oldY * _
                          Math.cos(ThetaZ);  //rotate about Z
               }
           }
        }
    

  • SortByDepth Depth Sorting
    At the end of the 3D graphics pipeline, the DrawObject 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, gbViewer 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 average 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 gbViewer uses only the sort routine to implement the Painter's algorithm.

        public void SortByDepth() {
           if (cSort.getState() == true) {
               for (int w = 0; w < NumberTriangles ; w++) {
                  V4[w] = (Pz[V1[w]]+Pz[V2[w]]+Pz[V3[w]]) / 3;
               }
               for (int g = 0; g < NumberTriangles -1 ; g++) {
                  for (int h = 0; h < NumberTriangles; h++) {
                     if (V4[g] < V4[h]) {
                        V1temp = V1[g]; V2temp = V2[g]; 
                        V3temp = V3[g]; V4temp = V4[g]; 
                        V5temp = V5[g]; V6temp = V6[g]; 
                        V7temp = V7[g]; V8temp = V8[g]; 
                        V1[g]=V1[h];    V2[g]=V2[h];    
                        V3[g]=V3[h];    V4[g]=V4[h];    
                        V5[g]=V5[h];    V6[g]=V6[h];    
                        V7[g]=V7[h];    V8[g]=V8[h];     
                        V1[h]=V1temp;   V2[h]=V2temp;   
                        V3[h]=V3temp;   V4[h]=V4temp;   
                        V5[h]=V5temp;  V6[h]=V6temp;    
                        V7[h]=V7temp;   V8[h]=V8temp;                  
                     }
                  }
               }
           }
        }
    

    The sort algorithm used here is called a bubble sort. It works well enough for a limited number of 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 gbViewer 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() {
           if (cBack.getState() == true) {
               for (int w = 0; w < NumberTriangles ; 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.

  • ApplyProjection Up to this point in the 3D graphics pipeline, all point coordinates have been world coordinates - the position of the 3D object in space. These point coordinates must now be projected onto the computer screen - onto a 2D surface. The subroutine is as follows:

        public void ApplyProjection() {
           if (cProj.getState() == true) {
               for (int w = 0; w < NumberPoints ; w++) {
                  PPx[w] = Px[w]*Scale*Fudge;
                  PPy[w] = Py[w]*Scale*Fudge;
               }
           }
           else {
               for (int w = 0; w < NumberPoints ; w++) {
                  PPx[w] = Px[w]*Scale*Fudge;
                  PPy[w] = Py[w]*Scale*Fudge;
               }
           }
        }
    

    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.

    gbViewer uses parallel projection only because I haven't gotten around to adding the perspective 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.

  • DrawObject The final step in the 3D graphics pipeline used by gbViewer is to draw the cube on the computer screen. This is called rendering. 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 DrawObject() {
            offScreen.clearRect(0,0,width,height);
            offScreen.setColor(ColorList[colorIndex]);
            offScreen.fillRect(90,0,width-90,height);
            for (int w = 0; w < NumberTriangles ; w++) {
                if ((cShade.getState() == false) & _
                             (cEdges.getState() == false)) {
                    offScreen.setColor(Color.blue);   
                    offScreen.drawLine ((int)(PPx[V1[w]]+XOffset), 
                                        (int)(PPy[V1[w]]+YOffset), _
                                        (int)(PPx[V2[w]]+XOffset), _
                                        (int)(PPy[V2[w]]+YOffset));
                    offScreen.drawLine ((int)(PPx[V1[w]]+XOffset), _
                                        (int)(PPy[V1[w]]+YOffset),_
                                        (int)(PPx[V3[w]]+XOffset), _
                                        (int)(PPy[V3[w]]+YOffset));
                    offScreen.drawLine ((int)(PPx[V3[w]]+XOffset), 
                                        (int)(PPy[V3[w]]+YOffset),_
                                        (int)(PPx[V2[w]]+XOffset), _
                                        (int)(PPy[V2[w]]+YOffset));
                    }
                if (V5[w] > 0) {              
                    T.addPoint ((int)(PPx[V1[w]]+XOffset), _
                                             (int)(PPy[V1[w]]+YOffset));
                    T.addPoint ((int)(PPx[V2[w]]+XOffset), _
                                             (int)(PPy[V2[w]]+YOffset));
                    T.addPoint ((int)(PPx[V3[w]]+XOffset), _
                                             (int)(PPy[V3[w]]+YOffset));
                    if (cShade.getState() == true) {
                        if (cFile.getState() == true)          
                                     offScreen.setColor(C[V6[w]]);   
                        else if (cRandom.getState() == true)   
                                     offScreen.setColor(C[V7[w]]);
                        else if (cGlitter.getState() == true)  
                                     offScreen.setColor(C[(int) _
                                     (Math.random()*NumberColors)]);
                        else if (cGradient.getState() == true) 
                                     offScreen.setColor(C[V8[w]]);
                                     offScreen.fillPolygon(T);
                    }
                    if (cEdges.getState() == true) {
                        offScreen.setColor(Color.blue);   
                        offScreen.drawPolygon(T);    
                    }    
                    T.reset();
                }
            }
            if (cInfo.getState() == true) {
                offScreen.drawString(title + ": " + filename,100,10);
                offScreen.drawString(Integer.toString(iLoop++))
                       + "   " + ) Integer.toString(NumberColors) _
                       + "  " + Integer.toString(NumberPoints) + "," 
                   + Integer.toString(NumberTriangles) + "," + 
                       + Double.toString((int)Scale), 100,25);
            }  
      }
    

    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.

    The shading used by gbViewer 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 gbViewer will include a light source and more advanced shading algorithms, such as Phong shading.

File Loaders
  • LoadFileName()
  • LoadSTLName()
  • LoadTXTName()
Graphical User Interface
gbViewer has all of its user interface controls placed down the left edge of the applet. Settings made will not be persitent (leaving the page loses the settings). Their functions are as follows
  • Start/Nudge/Stop
    gbViewer starts up in automatic rotation mode. These buttons can be used to stop and re-start the animation. The Nudge button moves the 3D object through a single rotation cycle. It can be used to more precisely position the display on the screen.
  • Speed+/Speed-
    These buttons increase/decrease the Delay variable by 1 millisecond at a time.
  • MoveX+/MoveX-/MoveY+/MoveY-
    These buttons translate (move) the 3D object within the screen by 10 units at a time.
  • Zoom+/Zoom-
    These buttons increase/decrease the size of the display 10% at a click.
  • File/Random/Gradient/Glitter
  • BackColor
    Pressing this button cycles through 12 different background colors.
  • Shade/Edges
    This button turns on the use of shading or outlining the edges of the triangles for easier viewing. When both are turned off, gbViewer provides a wireframe display of the 3D object.
  • X-Rotation/Y-Rotation/Z-Rotation
    Selection of an axis checkbox turns on rotation about that axis.
  • Backface/Painter/Projection
    For evaluation purposes only, these checkboxes allows the user to enable/disable various apsects of the 3D graphics pipeline.
  • Info
    This checkbox enables display of basic 3D object information - number of colors, points, lines, and triangles. Scale factor is also displayed.
  • FileList
    This dropdown control lists the files supplied from the calling HTML file. Selecting a filename will load that file from the server.

The interaction between the user interface components is handled by the following four methods.

    public void actionPerformed(ActionEvent evt) {
        if (evt.getSource() == bStart) start();
        if (evt.getSource() == bStop) stop();
        if (evt.getSource() == bSpeedP) Delay = Delay - 1; 
        if (Delay <= 0) Delay = 0;
        if (evt.getSource() == bSpeedM) Delay = Delay + 1;
        if (evt.getSource() == bMoveXP) 
            { XOffset = XOffset + 10 ; ManualPipeLine(); }
        if (evt.getSource() == bMoveXM) 
            { XOffset = XOffset - 10 ; ManualPipeLine(); }
        if (evt.getSource() == bMoveYP) 
            { YOffset = YOffset - 10 ; ManualPipeLine(); }
        if (evt.getSource() == bMoveYM) 
            { YOffset = YOffset + 10 ; ManualPipeLine(); }
        if (evt.getSource() == bMoveZP) 
            { Scale = Scale * 1.1 ; ManualPipeLine(); }
        if (evt.getSource() == bMoveZM) 
            { Scale = Scale / 1.1 ; ManualPipeLine(); }
        if (evt.getSource() == bNudge) Nudge() ;
        if (evt.getSource() == bBack) {
             colorIndex++;
             if (colorIndex > 12) colorIndex = 0;
             ManualPipeLine();
        }
        repaint();
    }

    public void mousePressed(MouseEvent e){
        mx = e.getX();
        my = e.getY();
        e.consume();
    }
    public void mouseDragged(MouseEvent e){
        int new_mx = e.getX();
        int new_my = e.getY();
        if ( (new_mx - mx) > 0 ) {
            ThetaY = ThetaY * mrSpeed;
            RotatePointsY(); 
            ManualPipeLine();
            ThetaY = ThetaY / mrSpeed;
        }
        if ( (new_mx - mx) < 0 ) {
            ThetaY = -ThetaY * mrSpeed;
            RotatePointsY(); 
            ManualPipeLine();
            ThetaY = -ThetaY / mrSpeed;
        }
        if ( (new_my - my) > 0 ) {
            ThetaX = -ThetaX * mrSpeed;
            RotatePointsX(); 
            ManualPipeLine();
            ThetaX = -ThetaX / mrSpeed;
        }
        if ( (new_my - my) < 0 ) {
            ThetaX = ThetaX * mrSpeed;
            RotatePointsX(); 
            ManualPipeLine();
            ThetaX = ThetaX / mrSpeed;
        }
        mx = new_mx;
        my = new_my;
        e.consume();
    }

    public void itemStateChanged(ItemEvent e) {
        if (e.getItemSelectable() == cBack) ManualPipeLine();
        if (e.getItemSelectable() == cSort) ManualPipeLine();
        if (e.getItemSelectable() == cProj) ManualPipeLine();
        if (e.getItemSelectable() == cShade) ManualPipeLine();
        if (e.getItemSelectable() == cEdges) ManualPipeLine();
        if (e.getItemSelectable() == cFiles) {
            stop();
            filename = cFiles.getSelectedItem();
            LoadFileName();
            start();
            repaint();
        }
    }


Return to top of document

File Formats

gbView currently supports two file formats, STL and TOV. Both are text files which describe an object in terms of points and triangles.

The LoadFileName() method is called when a filename is selected from the dropdown file list. This method determines the file extension of the selected file and calls the appropriate method for load a file of that type.

    public void LoadFileName() {
        if (filename.substring(filename.length() - 3).equals("txt")) 
                     LoadTXTName();
        if (filename.substring(filename.length() - 3).equals("stl")) 
                     LoadSTLName();
    }

STL
STL is a format used by the rapid prototyping industry. Computer aided design (CAD) systems typically output these files. The files are used to control special equipment which can then manufacture the 3D object in a matter of hours. The resulting product is typically made of a limited range of materials and are not as precised as lathed products, but are adequate for evaluation of the product prior to final approval of the design for production.

STL files can be created in text or binary formats. Binary file sizes are much smaller than their equivalent text file. At this time, gbView supports only the text format.

The STL file format basically defines each of the triangles which make up the surface of the 3D object. The triangles may be listed in any order. The vertices of each triangle are listed in the right-hand format (curl right hand in direction of the point listing and the thumb points in the direction of the normal of the triangle). The text file format is as follows:

    solid sample.stl
      facet normal -1.000000 0.000000 0.000000  
        outer loop  
          vertex 140.502634 233.993075 -38.310362  
          vertex 140.502634 229.424780 -38.359042  
          vertex 140.502634 242.525774 -27.097848  
        endloop  
      endfacet  
      facet normal  0.903689 0.004563 0.428166  
        outerloop  
          vertex 134.521310 273.427873 30.342009  
          vertex 134.521310 308.505852 30.715799  
          vertex 140.502634 334.576026 18.369396  
        endloop  
      endfacet  
     ...
    endsolid sample.stl 

The gbViewer method for reading the STL file format is as follows:

    public void LoadSTLName() {
        try { 
           String aLine = "";
           iLine = 0;  Scale = 100.0;
           XMax = 0.0; XMin = 0.0; YMax = 0.0; YMin = 0.0; 
           ZMax = 0.0; ZMin = 0.0;
           NumberColors = 1; NumberPoints = 0; NumberLines = 0; 
           NumberTriangles = 0;
           URL source = new URL(getCodeBase() + filename); 
           DataInputStream in = new DataInputStream(source.openStream());
           C[0] = new Color(240,240,240);
           aLine = in.readLine();
           
           while (aLine != null) {           
                             //while ((aLine = in.readLine()) != null)
               aLine = in.readLine();
               aLine = in.readLine();
               for (int w=0; w < 3; w++) {
                   aLine = in.readLine();
                   aLine = aLine.trim();
                   fields = aLine.split("\\s+");
                   Px[NumberPoints]= Double.valueOf(fields[1]).doubleValue();
                   Py[NumberPoints]= Double.valueOf(fields[2]).doubleValue();
                   Pz[NumberPoints]= Double.valueOf(fields[3]).doubleValue();
                   cData.addItem(Double.toString(Px[NumberPoints]) + _
                                        Double.toString(Py[NumberPoints]) + _
                                        Double.toString(Pz[NumberPoints]));
                   XMax = Math.max(Px[NumberPoints],XMax);
                   YMax = Math.max(Py[NumberPoints],YMax);
                   ZMax = Math.max(Pz[NumberPoints],ZMax);
                   XMin = Math.min(Px[NumberPoints],XMin);
                   YMin = Math.min(Py[NumberPoints],YMin);
                   ZMin = Math.min(Pz[NumberPoints],ZMin);
                   NumberPoints++;
               }
               V1[NumberTriangles] = NumberPoints - 3;
               V2[NumberTriangles] = NumberPoints - 2;
               V3[NumberTriangles] = NumberPoints - 1;
               V6[NumberTriangles] = 0;
               V7[NumberTriangles] = 0;
               V8[NumberTriangles] = 0;
               NumberTriangles++;
               aLine = in.readLine();
               aLine = in.readLine();
               if ((Math.abs(XMax) + Math.abs(XMin)) <  lesser)
                    Scale = 0.4 * (float)lesser / _
                            (Math.abs(XMax) + Math.abs(XMin));
               else
                    Scale = 0.4 * (Math.abs(XMax) + Math.abs(XMin)) / _
                              (float)lesser;
               }
               in.close();
           }
        catch (Exception e) { 
           offScreen.drawString("failure loading " + filename ,200,10);
           e.printStackTrace();  
           }
    }

TOV
TOV is a 3D object display applet created by The JMaker. I use the applet on my MultiChip Module site. The format is given below. Comments may be included in the file, preceded by the # symbol.

    Title             # title (required, but not used by gbView)
    -120 150 10       # staring angles in degrees along X, Y, and Z 
                      #(required, but not used by gbViewer)
    100               # opacity in percent  (required, but not 
                      # used by gbViewer)
    c 128 128 256     # color - with integer r, g, and b 
                      # components, integer
    c  64 128 128     # color - with integer r, g, and b 
                      # components, integer
    p 1.0 1.0 2.0     # point - x, y, and z coordinates, floating point
    p 1 2 4           # point - x, y, and z coordinates, floating point
    p 5 4 1           # point - x, y, and z coordinates, floating point
    l 1 2             # line  - connecting points 1 and 2  - gbView does 
                      # not use, not required, integer
    l 2 3             # line  - connecting points 2 and 3  - gbView does 
                      # not use, not required, integer
    t 1 2 3   2       # triangle - defined by points 1, 2, and 3.  
                      # color #2 used to fill the triangle, integer

The gbViewer method for reading the TOV file format is as follows:

    public void LoadTXTName() {
        try { 
           String aLine = "";
           iLine = 0;  Scale = 1.0;
           XMax = 0.0; XMin = 0.0; YMax = 0.0; 
           YMin = 0.0; ZMax = 0.0; ZMin = 0.0;
           URL source = new URL(getCodeBase() + filename); 
           DataInputStream in = new DataInputStream(source.openStream());
           title = in.readLine();
           aLine = in.readLine();    //starting angle (not used)
           aLine = in.readLine();    //opacity (not used)
           aLine = in.readLine();
           fields = aLine.split("\\s+");
           NumberColors = Integer.parseInt(fields[0]); 
           NumberPoints = Integer.parseInt(fields[1]);
           NumberLines = Integer.parseInt(fields[2]);
           NumberTriangles = Integer.parseInt(fields[3]);
           showStatus (Integer.toString(NumberColors));
           while ((aLine = in.readLine()) != null) { 
               iLine++;
               fields = aLine.split("\\s+");    
                          //fields = aLine.split("[,\\s]+");
               if (iLine <= NumberColors) 
                   C[iLine -1] = new Color(Integer.parseInt(fields[1]), _
                         Integer.parseInt(fields[2]), _
                         Integer.parseInt(fields[3]));
               else if (iLine <= NumberPoints + NumberColors) {
                   Px[iLine - NumberColors - 1]= +
                         Double.valueOf(fields[1]).doubleValue();
                   Py[iLine - NumberColors - 1]= _
                         Double.valueOf(fields[2]).doubleValue();
                   Pz[iLine - NumberColors - 1]= _
                         Double.valueOf(fields[3]).doubleValue();
                   XMax = Math.max(Px[iLine - NumberColors - 1],XMax);
                   YMax = Math.max(Py[iLine - NumberColors - 1],YMax);
                   ZMax = Math.max(Pz[iLine - NumberColors - 1],ZMax);
                   XMin = Math.min(Px[iLine - NumberColors - 1],XMin);
                   YMin = Math.min(Py[iLine - NumberColors - 1],YMin);
                   ZMin = Math.min(Pz[iLine - NumberColors - 1],ZMin);
                   }
               else {
                   V1[iLine - NumberColors - NumberPoints - 1]= _
                                    Integer.parseInt(fields[1]);
                   V2[iLine - NumberColors - NumberPoints - 1]= _
                                    Integer.parseInt(fields[2]);
                   V3[iLine - NumberColors - NumberPoints - 1]= _
                                    Integer.parseInt(fields[3]);
                   V6[iLine - NumberColors - NumberPoints - 1]= _
                                    Integer.parseInt(fields[4]);
                    }
               }
               if ((Math.abs(XMax) + Math.abs(XMin)) <  lesser)
                    Scale = 0.4 * (float)lesser / _
                           (Math.abs(XMax) + Math.abs(XMin));
               else
                    Scale = 0.4 * (Math.abs(XMax) + Math.abs(XMin)) / _
                            (float)lesser;
               for (int w=0; w < NumberTriangles; w++) {
                   V7[w]=V6[(int)(Math.random()*NumberColors)];
                   V8[w]= V6[w];
               }
               in.close();
           } 
        catch (Exception e) { 
           offScreen.drawString("failure loading " + filename ,200,10);
           e.printStackTrace();  
           }
    }