Java 3D Programming by Daniel Selman - HTML preview

PLEASE NOTE: This is an HTML preview only and some elements such as links or page numbers may be incorrect.
Download the book in PDF, ePub, Kindle for a complete version.

Appendix C

Example code

C.1 Box objects and GeomBuffer

C.2 Primitives and the geometry cache

C.3 GeomBuffer

C.1 Box objects and GeomBuffer

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;

 }

_________________________________________________________________

C.2 Primitives and the geometry cache

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:

  • The design is not extensible. Three 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.
  • Using 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.
  • The geometry cache can help with saving only a few CPU cycles involved with creating the geometry—it does not save any runtime memory or help consolidate objects.
  • Since the static Hashtable is never emptied, memory consumption is increased because cached 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)

{

return (new Float(x)).toString();

}

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

}

_________________________________________________________________

C.3 GeomBuffer

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.