C.1 Box objects and GeomBuffer
C.2 Primitives and the geometry cache
Arguably there is a bug in the Box
class because the faces of the cube are defined using two triangles. TriangleArrays
are quicker to render than QuadArrays
, but when the Box
is rendered as a wireframe (i.e., only the edges of the Box
are drawn), an extra diagonal line is drawn that separates the two triangles that define a face. This bug was not present in Java 3D 1.1 and was introduced in Java 3D 1.1.1. With luck, the bug will be rectified in subsequent releases.
If you require that your Box
objects be rendered as wireframes, the following class can be used instead of Box
to ensure the faces are rendered correctly. The Box
class must be simply modified to create an OldGeomBuffer
object instead of a GeomBuffer
.
From CuboidTest\Cuboid.java |
/*
* Based on Sun's Box.java 1.13 98/11/23 10:23:02
* Work around for the Box bug when rendered in Wireframe mode.
* override this method
*/
public Cuboid( float xdim, float ydim, float zdim,
int primflags, Appearance ap)
{
int i;
double sign;
xDim = xdim;
yDim = ydim;
zDim = zdim;
flags = primflags;
//Depends on whether normal inward bit is set.
if ((flags & GENERATE_NORMALS_INWARD) != 0)
sign = -1.0;
else
sign = 1.0;
TransformGroup objTrans = new TransformGroup();
objTrans.setCapability(ALLOW_CHILDREN_READ);
this.addChild(objTrans);
Shape3D shape[] = new Shape3D[6];
for (i = FRONT; i <= BOTTOM; i++)
{
OldGeomBuffer gbuf = new OldGeomBuffer(4);
gbuf.begin(OldGeomBuffer.QUAD_STRIP);
for (int j = 0; j < 2; j++)
{
gbuf.normal3d( (double) normals[i].x*sign,
(double) normals[i].y*sign,
(double) normals[i].z*sign);
gbuf.texCoord2d(tcoords[i*8 + j*2], tcoords[i*8 + j*2 + 1]);
gbuf.vertex3d( (double) verts[i*12 + j*3]*xdim,
(double) verts[i*12+ j*3 + 1]*ydim,
(double) verts[i*12+ j*3 + 2]*zdim );
}
for (int j = 3; j > 1; j--)
{
gbuf.normal3d( (double) normals[i].x*sign,
(double) normals[i].y*sign,
(double) normals[i].z*sign);
gbuf.texCoord2d(tcoords[i*8 + j*2], tcoords[i*8 + j*2 + 1]);
gbuf.vertex3d( (double) verts[i*12 + j*3]*xdim,
(double) verts[i*12+ j*3 + 1]*ydim,
(double) verts[i*12+ j*3 + 2]*zdim );
}
gbuf.end();
shape[i] = new Shape3D(gbuf.getGeom(flags));
numVerts = gbuf.getNumVerts();
numTris = gbuf.getNumTris();
if ((flags & ENABLE_APPEARANCE_MODIFY) != 0)
{
(shape[i]).setCapability(Shape3D.ALLOW_APPEARANCE_READ);
(shape[i]).setCapability(Shape3D.ALLOW_APPEARANCE_WRITE);
}
objTrans.addChild(shape[i]);
}
if (ap == null)
{
setAppearance();
}
else
setAppearance(ap);
}
GeometryBuffer
must also be simply modified (in fact, the original 1.1 version can be used), to create a QuadArray
inside processQuadStrips
—newer versions create a TriangleStripArray
. Copy the GeomBuffer
file (defined in the com.sun.j3d.utils.geometry
package, for which there is source code). Save the file as OldGeomBuffer
and replace the processQuadStrips
method from GeomBuffer
with the method which follows.
From CuboidTest\OldGeomBuffer.java |
/*
* OldGeometryBuffer.java - based on Sun's GeomBuffer.java.
* Work around for the Box bug when rendered in Wireframe mode.
* This version actually returns Quadstrips for a Quadstrip array,
* unlike the newer version that returns TriangleStrips....
* override this method
*/
private GeometryArray processQuadStrips()
{
GeometryArray obj = null;
int i;
int totalVerts = 0;
for (i = 0; i < currPrimCnt; i++)
{
int numQuadStripVerts;
numQuadStripVerts = currPrimEndVertex[i] - currPrimStartVertex[i];
totalVerts += (numQuadStripVerts/2 - 1) * 4;
}
if (debug >= 1) System.out.println("totalVerts " + totalVerts);
if (((flags & GENERATE_NORMALS) != 0) &&
((flags & GENERATE_TEXTURE_COORDS) != 0))
{
obj = new QuadArray(totalVerts,
QuadArray.COORDINATES |
QuadArray.NORMALS |
QuadArray.TEXTURE_COORDINATE_2);
}
else if (((flags & GENERATE_NORMALS) == 0) &&
((flags & GENERATE_TEXTURE_COORDS) != 0))
{
obj = new QuadArray(totalVerts,
QuadArray.COORDINATES |
QuadArray.TEXTURE_COORDINATE_2);
}
else if (((flags & GENERATE_NORMALS) != 0) &&
((flags & GENERATE_TEXTURE_COORDS) == 0))
{
obj = new QuadArray(totalVerts,
QuadArray.COORDINATES |
QuadArray.NORMALS);
}
else
{
obj = new QuadArray(totalVerts,
QuadArray.COORDINATES);
}
Point3f[] newpts = new Point3f[totalVerts];
Vector3f[] newnormals = new Vector3f[totalVerts];
Point2f[] newtcoords = new Point2f[totalVerts];
int currVert = 0;
for (i = 0; i < currPrimCnt; i++)
{
for (int j = currPrimStartVertex[i] + 2;
j < currPrimEndVertex[i];j+=2)
{
outVertex(newpts, newnormals, newtcoords, currVert++,
pts, normals, tcoords, j - 2);
outVertex(newpts, newnormals, newtcoords, currVert++,
pts, normals, tcoords, j - 1);
outVertex(newpts, newnormals, newtcoords, currVert++,
pts, normals, tcoords, j + 1);
outVertex(newpts, newnormals, newtcoords, currVert++,
pts, normals, tcoords, j);
numTris += 2;
}
}
numVerts = currVert;
obj.setCoordinates(0, newpts);
if ((flags & GENERATE_NORMALS) != 0)
obj.setNormals(0, newnormals);
if ((flags & GENERATE_TEXTURE_COORDS) != 0)
obj.setTextureCoordinates(0, newtcoords);
geometry = obj;
return obj;
}
A feature of the Primitive
-derived classes is that they support the geometry cache (or some of them do). The geometry cache is intended to save CPU time when building Primitive
-derived objects by caching GeomBuffer
objects and returning them as appropriate. For example, if your application requires 100 Spheres
with radius 50, the geometry cache will create the geometry for the first sphere and return this geometry for the remaining 99. Mysteriously, only the Cone
, Cylinder
, and Sphere Primitives
use the geometry cache.
The source code to implement the geometry cache is useful because it presents an object lesson in how not to design such a facility. The geometry cache is implemented using a static hashtable of String
keys that are used to retrieve an Object
instance (in this case, GeomBuffer
). The Strings
that are used as keys are built from four int
and three float
parameters. Problems with this crude, inefficient, and simplistic design are:
ints
and three floats
were arbitrarily chosen to uniquely designate a geometric Primitive
. If a Primitive
-derived object cannot be uniquely described using these parameters, the architecture will fail. A better architecture would have been to store each Primitive
type in its own Hashtable and use the relevant object’s hashCode
function to generate an int
key to reference the geometry. In this way, responsibility for generating hash codes is delegated to the derived class (as is customary in Java), and there can be no interaction between derived classes since they are stored in separate Hashtables.Strings
to look up the objects in the geometry cache wastes memory as well as CPU time. String
manipulations are relatively costly and are wholly unnecessary in this context.Geometry
objects are never dereferenced and garbage collected.From Primitive.java |
//The data structure used to cache GeomBuffer objects
static Hashtable geomCache = new Hashtable();
String strfloat(float x)
{
}
// Add a GeomBuffer to the cache
protected void cacheGeometry( int kind, float a, float b, float c,
int d, int e, int flags,
GeomBuffer geo)
{
String key = new String(kind+strfloat(a)+strfloat(b)+
strfloat(c)+d+e+flags);
geomCache.put(key, geo);
}
// Retrieve a GeomBuffer object
protected GeomBuffer getCachedGeometry( int kind, float a, float b,
float c, int d, int e,
int flags)
{
String key = new String(kind+strfloat(a)+strfloat(b)+ strfloat(c)
+d+e+flags);
Object cache = geomCache.get(key);
return((GeomBuffer) cache);
}
From Cylinder.java |
//The Geometry Cache in use
GeomBuffer cache =
getCachedGeometry( Primitive.CYLINDER, radius, radius height,
xdivision, ydivision, primflags);
if (cache != null)
{
shape[BODY] = new Shape3D(cache.getComputedGeometry());
numVerts += cache.getNumVerts();
numTris += cache.getNumTris();
}
Java 3D programmers coming from an OpenGL background will recognize much of the code used to define the vertices and normal vectors of the Box
primitive, defined in com.sun.j3d.utils.geometry.Box
.
GeomBuffer gbuf = new GeomBuffer(4);
//extract of code to generate the geometry of a Box
gbuf.begin(GeomBuffer.QUAD_STRIP);
gbuf.normal3d( (double) normals[i].x*sign, (double)
normals[i].y*sign, (double) normals[i].z*sign);
gbuf.texCoord2d(tcoords[i*8 + j*2], tcoords[i*8 + j*2 + 1]);
gbuf.vertex3d( (double) verts[i*12 + j*3]*xdim, (double)
verts[i*12+ j*3 + 1]*ydim,
(double) verts[i*12+ j*3 + 2]*zdim );
gbuf.end();
//create a Shape3D object to wrap the GeomBuffer
Shape3D shape =
new Shape3D( gbuf.getGeom( GeomBuffer.GENERATE_NORMALS ) );
The GeomBuffer
class has been designed to allow OpenGL programmers to quickly and easily generate Java 3D geometry in a manner similar to defining an OpenGL display list (for example). In the preceding example a GeomBuffer
is created to hold four vertices defined as a quad strip which draws a connected group of quadrilaterals. One quadrilateral is defined for each pair of vertices presented after the first pair. Vertices 2n - 1, 2n, 2n + 2, and 2n + 1 define quadrilateral n, and n quadrilaterals are drawn.
The GeomBuffer
class is used in many of the classes derived from Primitive
since, I suspect, this code has been ported from an OpenGL-based implementation and the GeomBuffer
was created to simplify porting.
int QUAD_STRIP = 0x01;
int TRIANGLES = 0x02;
int QUADS = 0x04;
At present, an instance of a GeomBuffer
can contain only a single primitive type; that is, one cannot mix quad strips and Triangles
(for example) in a single GeomBuffer
.
Except for a bug that causes the GeomBuffer
to generate a TriangleStripArray
for a QUAD_STRIP
instead of a QuadStripArray
, the class is easy to use and allows OpenGL code to be quickly inserted into a Java 3D application.