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();
}
}
|