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.

CHAPTER 8

Geometry reference

8.1 Shape3D

8.2 Primitive

8.3 GeomBuffer

8.4 Rasters

8.5 Text2D

8.6 Text3D

8.7 Morph

8.8 Summary

Just as life offers many choices, Java 3D provides many geometry generation options. By reading the relevant section for the geometry option you are interested in using, you can quickly come up to speed on typical usage and learn useful programming tips.

The sections in this chapter describe the mechanics and implementation issues for the geometry generation options available in Java 3D. These are:

  • The Shape3D classes (part of the core javax.media.j3d package)
  • Primitive derived classes (from com.sun.java.j3d.utils including Box, Cone, Sphere, and so on
  • Text (2D and 3D) generation

8.1 Shape3D

java.lang.Object

 |

 +--javax.media.j3d.SceneGraphObject

  |

  +--javax.media.j3d.Node

   |

   +--javax.media.j3d.Leaf

    |

    +--javax.media.j3d.Shape3D

The Shape3D class is essential to defining viewable geometry in Java 3D. The Shape3D class packages the geometry information for a visual object along with the appearance information that governs how the geometry is rendered. The Appearance class is covered in chapter 9 and includes a variety of rendering attributes (material, line, surface attributes, etc.).

In addition, each Shape3D maintains a Bounds object for use in collision detection and intersection testing between PickRays (lines) and other Shape3D objects in the scene.

As is customary, Shape3D ensures access to internal variables and attributes is subject to the capability bits that have been set for the Shape3D object.

An example class derived from Shape3D is the ColorCube class. The source code for the ColorCube class is available in the com.sun.java.j3d.utils.geometry package where ColorCube is defined. The basic principle is to define geometry using one of the Geometry-derived classes such as QuadArray and then assign the geometry to the Shape3D using setGeometry(...).

GeometryArray-derived classes can also store the normal vectors, colors, and texture coordinates for each vertex defined.

8.1.1 The user data field

A useful feature, defined in Shape3D’s SceneGraphObject base class, is that each Shape3D has a user data object associated with it. This allows an arbitrary Object-derived class to be attached to the Shape3D object using:

public void setUserData(java.lang.Object userData);

public java.lang.Object getUserData();

The user data object can then be queried in response to scenegraph operations, for example selecting with the mouse. A selection utility will typically return the Shape3D object that was selected and an application-specific data structure will need to be retrieved to apply the results of the selection operation—this can be stored in the user data field of the Shape3D object.

The user data field can also be used to remove a scenegraph object once it has been added—a useful function. This technique is described in chapter 5.

8.2 Primitives

java.lang.Object

 |

 +--javax.media.j3d.SceneGraphObject

  |

  +--javax.media.j3d.Node

   |

   +--javax.media.j3d.Group

    |

    +--com.sun.j3d.utils.geometry.Primitive

img63.png

Figure 8.1

Geometric objects derived from Primitive: sphere, box, cylinder, and cone. Note the different resolution options and how the Primitives are triangulated when drawn as wire frames

Primitive is not part of the Java 3D package (javax.media.j3d) but has been defined in the Java 3D utilities package (com.sun.java.j3d.utils). The Primitive class serves as the base class for several simple geometric shapes that can act as building blocks for Java 3D developers. The Java 3D Primitive-derived classes have been illustrated in figure 8.1.

Primitive is derived from Group—that is, it is not a Shape3D object. This is important—a Primitive is not a shape or geometric object, but is rather a collection of Shape3D objects. Primitive provides methods to access the subcomponents of the Group. There is therefore an important distinction between modifying the characteristics of the Primitive (Group settings) and modifying the properties of the child Shape3D objects (Shape3D settings such as Material, Appearance, etc.)

Because of serious class design issues regarding the Primitive base class, I cannot recommend that any of the Primitive-derived classes be used in a Java 3D application of any complexity. The fundamental issue is that the Primitive class defines a “has-a” relationship for its geometry. That is, a single Primitive is defined such that it is a container for its geometric parts. For example, Box has six geometric parts:

  • FRONT,
  • BACK,
  • LEFT,
  • RIGHT
  • TOP,

and

  • BOTTOM

To facilitate this relationship the Primitive class is derived from Group.

The Java 3D scenegraph, however, can only have Leaf-derived geometric objects as leaf nodes, and many operations, such as picking for mouse-based selection, are performed on Shape3D-derived objects. This lack of compatibility between Shape3D objects and Primitive objects is something that Sun will address with future releases of Java 3D.

Additionally, it is not generally possible to derive a class from Primitive because the class is not designed to be extensible. For example Primitive.java contains the following code:

static final int SPHERE = 0x01;

static final int CYLINDER = 0x02;

static final int CONE = 0x04;

static final int BOX = 0x08;

This extremely non-object-oriented approach to differentiating between instances of objects derived from Primitive essentially makes it impossible for application developers to safely create new Primitive-derived classes. In general, classes at the top of a class hierarchy should not have knowledge of derived classes.

Example code for all of the Primitive-derived classes is defined in the Sun ConicWorld Java 3D demo directory. The ConicWorld demo creates a variety of Primitive objects with various lighting, material, and texture attributes.

The following subsections describe the simple geometric Primitives defined in

com.sun.j3d.utils.geometry.

8.2.1 Box

java.lang.Object

 |

 +--javax.media.j3d.SceneGraphObject

  |

  +--javax.media.j3d.Node

   |

   +--javax.media.j3d.Group

    |

    +--com.sun.j3d.utils.geometry.Primitive

     |

     +--com.sun.j3d.utils.geometry.Box

Box defines a simple six-sided cube, illustrated in figure 8.2. Unfortunately for Swing developers this name conflicts with the Swing class javax.swing.Box so if your application uses Swing you should reference the Java 3D class explicitly, as in the following:

com.sun.j3d.utils.geometry.Box box =

new com.sun.j3d.utils.geometry.Box( 0.5f, 0.5f, 0.5f, null);

img64.png

Figure 8.2

The Box Primitive

Box contains the following Shape3D objects:

  • FRONT = 0;
  • BACK = 1;
  • RIGHT = 2;
  • LEFT = 3;
  • TOP = 4;
  • BOTTOM = 5;

The identifiers are used as indices into the Primitive Group Node. In other words, to retrieve the FRONT face of the cube, you must call:

Shape3D frontFace = box.getShape(Box.FRONT );

This is defined in Box.java as:

public Shape3D getShape(int partId)

{

 if (partId < FRONT || partId > BOTTOM)

return null;

return (Shape3D)((Group)getChild(0)).getChild(partId);

}

The faces of the Box are Shape3D objects and are children of the first Node added to the Group. The first Node of the Group is a TransformGroup object that allows the Box to be moved as a whole. Thus, modifying the scale, rotation, and translation of the TransformGroup’s Transform3D applies the scale, rotation, and translation to all of its child Nodes also.

The Sun example ConicWorld/BoxExample.java provides an example of creating Boxes and loading and applying texture images to Boxes.

//Set Appearance attributes

//first, create an appearance

Appearance ap = new Appearance();

//create a colored material

Color3f aColor = new Color3f(0.1f, 0.1f, 0.1f);

Color3f eColor = new Color3f(0.0f, 0.0f, 0.0f);

Color3f dColor = new Color3f(0.8f, 0.8f, 0.8f);

Color3f sColor = new Color3f(1.0f, 1.0f, 1.0f);

Material m = new Material(aColor, eColor, dColor, sColor, 80.0f);

//enable lighting and assign material

m.setLightingEnable(true);

ap.setMaterial(m);

//render the Box as a wire frame

PolygonAttributes polyAttrbutes = new PolygonAttributes(); polyAttrbutes.setPolygonMode( PolygonAttributes.POLYGON_LINE ); polyAttrbutes.setCullFace(PolygonAttributes.CULL_NONE); ap.setPolygonAttributes(polyAttrbutes);

//create the box and assign the appearance

Box BoxObj = new Box(1.5f, 1.5f, 0.8f, Box.GENERATE_NORMALS | Box.GENERATE_TEXTURE_COORDS, ap);

//load and assign a texture image and set texture parameters TextureLoader tex = new TextureLoader("texture.jpg", "RGB", this);

if (tex != null)

ap.setTexture(tex.getTexture());

TextureAttributes texAttr = new TextureAttributes();

texAttr.setTextureMode(TextureAttributes.MODULATE);

ap.setTextureAttributes(texAttr);

8.2.2 Cone

java.lang.Object

 |

 +--javax.media.j3d.SceneGraphObject

  |

  +--javax.media.j3d.Node

   |

   +--javax.media.j3d.Group

    |

    +--com.sun.j3d.utils.geometry.Primitive

     |

     +--com.sun.j3d.utils.geometry.Cone

Cone defines a simple Cone Primitive with a radius and a height, illustrated in figures 8.3 and 8.4. It is a capped cone centered at the origin with its central axis aligned along the Y-axis. The center of the Cone is defined to be the center of its bounding box (rather than its centroid). If the GENERATE_TEXTURE_COORDS flag is set, the texture coordinates are generated so that the texture gets mapped onto the Cone similarly to a Cylinder, except without a top cap.

Cone consists of the following Shape3D objects:

  • ALIGN="JUSTIFY">int BODY = 0;
  • "JUSTIFY">int CAP = 1;

img65.png

Figure 8.3 The Cone Primitive (low resolution)

img66.png

Figure 8.4 The Cone Primitive (high resolution)

By default, 15 surfaces are generated for the sides of the Cone. This can be increased or decreased by using the most customizable constructor:

public Cone(float radius, float height, int primflags, int xdivision, int ydivision, Appearance ap)

The geometry for the Cone consists of a Cylinder tapering from the supplied radius at one end to zero radius at the other end. A disk is created using the same number of divisions as the Cylinder and aligned to close the open end of the Cylinder.

8.2.3 Cylinder

java.lang.Object

 |

 +--javax.media.j3d.SceneGraphObject

  |

  +--javax.media.j3d.Node

   |

   +--javax.media.j3d.Group

    |

    +--com.sun.j3d.utils.geometry.Primitive

     |

     +--com.sun.j3d.utils.geometry.Cylinder

The Cylinder Primitive defines a capped cylinder, illustrated in figure 8.5. Cylinder is composed from three Shape3D components: Body, Top disk, and Bottom disk.

img67.png

Figure 8.5 The Cylinder Primitive

  • int BODY = 0;
  • int TOP = 1;
  • int BOTTOM = 2;

The default number of surfaces created for the body is 15 along the X-axis and 1 along the Y-axis, the disks are created as 15-sided polygons. Again, use the most complex form of the constructor to vary the number of surfaces generated for the cylinder:

public Cylinder(float radius, float height, int primflags, int xdivision, int ydivision, Appearance ap) {

8.2.4 Sphere

java.lang.Object

 |

 +--javax.media.j3d.SceneGraphObject

  |

  +--javax.media.j3d.Node

   |

   +--javax.media.j3d.Group

    |

    +--com.sun.j3d.utils.geometry.Primitive

     |

     +--com.sun.j3d.utils.geometry.Sphere

The Sphere Primitive defines a sphere with 15 divisions in both the X- and Y-axes, illustrated in figures 8.6 through 8.8. Use the most customizable form of the constructor to vary the number of surfaces created for the Sphere:

public Sphere(float radius, int primflags, int divisions, Appearance ap)

img68.png

Figure 8.6 The Sphere Primitive (low resolution)

img69.png

Figure 8.7 The Sphere Primitive (high resolution)

img70.png

Figure 8.8 Sphere Primitive with an applied texture image of the Earth

8.2.5 Primitive flags

All of the Primitives have a primitive flags (primflags) argument in one of their constructors. Primitive flags influence the attributes applied to the Shape3D geometry when it is generated internally for the Primitive. The available primitive flags are shown in table 8.1.

Table 8.1 Primitive flags for Primitive derived classes

img71.png

After a Primitive has been generated, the capabilities for the Shape3D subparts can also be accessed by calling

getShape(partid).setCapability(ALLOW_INTERSECT);

Note that the setPrimitiveFlags method should not be used, as it does not have any effect once the Primitive has been created.

Unless primitive flags are explicitly supplied, the default GENERATE_NORMALS primitive flag is used. In other words, both vertex coordinates and normal vectors are generated (to allow surfaces to be lighted).

8.2.6 Primitives and the geometry cache

The Primitive-derived classes use a very simplistic cache to minimize the CPU time used to create the geometry for each class. An analysis of this capability is included in appendix C.

8.3 GeomBuffer

The Primitive-derived classes internally make use of the GeomBuffer helper class. This class allows geometry to be defined using an API similar to OpenGL’s stateful display list geometry definition API. This capability is discussed in more detail in appendix C and may be useful for porting OpenGL programs.

8.4 Rasters

java.lang.Object

 |

 +--javax.media.j3d.SceneGraphObject

  |

  +--javax.media.j3d.NodeComponent

   |

   +--javax.media.j3d.Geometry

    |

    +--javax.media.j3d.Raster

The Raster class serves double duty in Java 3D. It can be used to either render an image into the 3D scene at a given location or to read the depth components of the 3D scene. The first application is much more common and easier to describe because it truly represents geometry definition.

8.4.1 Rendering an image using a Raster

A Raster object can be used to simply paste a 2D image into the 3D view. The Raster has a 3D location associated with it, and this serves as the upper-left corner of the rendered image. Note however that the image for the Raster is rendered as-is and will not have any scaling, translation, or rotation applied to it—regardless of the Raster’s position within the scenegraph. A Raster might be appropriate for graphical coordinate axis labels, for example. Since Raster is derived from Geometry it must be encapsulated by a Shape3D Node before it can be added to the scenegraph.

There are six basic steps to using a Raster:

1. Create the BufferedImage.

2. Read in or generate the image data.

3. Create the ImageComponent2D to wrap the BufferedImage.

4. Create the Raster to wrap the ImageComponent2D.

5. Create the Shape3D to contain the Raster.

6. Add the Shape3D to the scenegraph.

For example:

//create the image to be rendered using a Raster

BufferedImage bufferedImage =

new BufferedImage( 128, 128, BufferedImage.TYPE_INT_RGB);

//load or do something to the image here…

//wrap the BufferedImage in an ImageComponent2D

ImageComponent2D imageComponent2D =

new ImageComponent2D( ImageComponent2D.FORMAT_RGB, bufferedImage);

imageComponent2D.setCapability( ImageComponent.ALLOW_IMAGE_READ );

imageComponent2D.setCapability( ImageComponent.ALLOW_SIZE_READ );

//create the Raster for the image

m_RenderRaster = new Raster(  new Point3f( 0.0f, 0.0f, 0.0f ),

Raster.RASTER_COLOR,

0, 0,

bufferedImage.getWidth(),

bufferedImage.getHeight(),

imageComponent2D,

null );

m_RenderRaster.setCapability( Raster.ALLOW_IMAGE_WRITE );

m_RenderRaster.setCapability( Raster.ALLOW_SIZE_READ );

//wrap the Raster in a Shape3D

Shape3D shape = new Shape3D( m_RenderRaster );

8.4.2 Retrieving scene depth components using a Raster

The other, more unusual, application for a Raster is to use it to retrieve the depth components of the 3D scene. These are stored in the Z-buffer, a multibyte array that stores the depth into the scene for each rendered pixel. The depth to the first occurrence of geometry is stored in the Z-buffer, and floating point values are scaled such that the closest value to the user is zero while the farthest value is one.

Querying the Z-buffer directly is quite uncommon, but may be useful for such application-specific functionality as hit testing or rendering.

The following example illustrates overriding the Canvas3D postSwap method to retrieve the contents of the Z-buffer for the scene using a Raster object. The Z-buffer Raster is then used to dynamically update an image Raster so that the depth components can be rendered graphically. The output from the RasterTest illustrating this technique is shown in figure 8.9.

img72.png

Figure 8.9

Two frames from RasterTest. Each frame contains a rotating cube and a Raster displaying the depth components of the entire frame. The Raster is visible because it also has a depth component

 From RasterTest.java

//size of the window, and hence size of the depth component array

private static int     m_kWidth = 300;

private static int     m_kHeight = 300;

//the Raster used to store depth components

private Raster     m_DepthRaster = null;

//the Raster used to render an image into the 3D view

private Raster     m_RenderRaster = null;

//an array of integer values for the depth components

private int[]     m_DepthData = null;

//create the image to be rendered using a Raster

BufferedImage bufferedImage = new BufferedImage( 128, 128, BufferedImage.TYPE_INT_RGB ); ImageComponent2D imageComponent2D = new ImageComponent2D( ImageComponent2D.FORMAT_RGB, bufferedImage );

imageComponent2D.setCapability( ImageComponent.ALLOW_IMAGE_READ );

imageComponent2D.setCapability( ImageComponent.ALLOW_SIZE_READ );

//create the depth component to store the 3D depth values

DepthComponentInt depthComponent = new DepthComponentInt( m_kWidth, m_kHeight );

depthComponent.setCapability( DepthComponent.ALLOW_DATA_READ );

//create the Raster for the image

m_RenderRaster = new Raster( new Point3f( 0.0f, 0.0f, 0.0f ),