CHAPTER 16

Object interaction—picking and collision detection

16.1 Introduction to picking

16.2 PickShapes

16.3 PickTool

16.4 PickCanvas

16.5 PickIntersection

16.6 PickResult

16.7 VRML picking example

16.8 Using picking for collision detection

16.9 Conclusions

Two example applications of Java 3D’s support for picking are presented here. The first example, in section 16.7, loads a VRML scene and reports the name of the object from the VRML scene that was picked when a mouse click occurs. The second example uses Java 3D’s Bounds-based picking to implement simple collision detection. The example creates four Sphere objects within a cube, which ricochet off each other and the walls of the cube.

16.1 Introduction to picking

Picking is the act of identifying objects in the 3D scene, usually with a pointing device, such as the mouse. Java 3D’s support for behaviors and picking can also be used to implement simple collision detection and response within a 3D scene. Picking is central to many of the direct-manipulation UI paradigms. Using direct manipulation, for example, you would translate an object by clicking it and moving the mouse, as opposed to typing the object ID and its new position into an edit field. For precision work, the edit field will work best; however, it abstracts the user from the 3D scene as compared to direct manipulation.

Java 3D 1.2 includes some new classes that make picking relatively easy. These classes are a big improvement over the 1.1 picking classes, which have been widely deprecated.

Before going into the details of the examples, I will describe the Java 3D 1.2 picking classes. The Java documentation for the picking classes is excellent, probably the best in Java 3D, so it is useful to familiarize yourself with it.

16.2 PickShapes

java.lang.Object
  |
  +--javax.media.j3d.PickShape

PickShapes are used by the PickTool to provide information about the volume of space, segment, or infinite ray that the objects in the scene should be tested against. Java 3D supports the PickShapes listed in table 16.1:

Table 16.1 PickShapes supported by Java 3D
PickShapes Description
PickBounds PickBounds is a finite pick shape defined with a Bounds object.
PickConeRay PickConeRay is an infinite cone ray pick shape.
PickConeSegment PickConeSegment is a finite cone segment pick shape.
PickCylinderRay PickCylinderRay is an infinite cylindrical ray pick shape.
PickCylinderSegment PickCylinderSegment is a finite cylindrical segment pick shape.
PickPoint PickPoint is a pick shape defined as a single point.
PickRay PickRay is an infinite ray pick shape.
PickSegment PickSegment is a line segment pick shape.

By using the appropriate PickShape you can find the objects in your scene that

16.3 PickTool

java.lang.Object
  |
  +--com.sun.j3d.utils.picking.PickTool

PickTool is the base class for picking operations. The picking methods will return a PickResult object for each object picked, which can then be queried to obtain more detailed information about the specific objects that were picked. The pick mode specifies the detail level of picking before the PickResult is returned (see table 16.2).

Table 16.2 PickTools
PickTool Mode Description
BOUNDS Pick using the bounds of the pickable nodes. The PickResult returned will contain the SceneGraphPath to the picked Node.
GEOMETRY Pick using the geometry of the pickable nodes. The PickResult returned will contain the SceneGraphPath to the picked Node. Geometry nodes in the scene must have the ALLOW_INTERSECT capability set for this mode.
GEOMETRY_INTERSECT_INFO Same as GEOMETRY, but the PickResult will include information on each intersection of the pick shape with the geometry. The intersection information includes the subprimitive picked (i.e., the point, line, triangle, or quad), the closest vertex to the center of the pick shape, and the intersection's coordinate, normal, color, and texture coordinates. To allow this to be generated, Shape3D and Morph nodes must have the ALLOW_GEOMETRY_READ capability set, and GeometryArrays must have the ALLOW_FORMAT_READ, ALLOW_COUNT_READ, and ALLOW_COORDINATE_READ capabilities set, plus the ALLOW_COORDINATE_INDEX_READ capability for indexed geometry. To query the intersection color, normal, or texture coordinates, the corresponding READ capability bits must be set on the GeometryArray.

The utility method PickTool.setCapabilities(Node, int) can be used before the scenegraph is made live to set the capabilities of Shape3D, Morph, or Geometry nodes to allow picking.

A PickResult from a lower level of detail pick can be used to inquire about more detailed information if the capability bits are set. This can be used to filter the PickResults before the more computationally intensive intersection processing. For example, the application can do a BOUNDS pick and then selectively query intersections on some of the PickResults. This will save the effort of doing intersection computation on the other PickResults. However, querying the intersections from a GEOMETRY pick will make the intersection computation happen twice, so use GEOMETRY_INTERSECT_INFO if you want to inquire the intersection information on all the PickResults.

When using pickAllSorted or pickClosest methods, the picks will be sorted by the distance from the start point of the pick shape to the intersection point.

Morph nodes cannot be picked using the displayed geometry in GEOMETRY_INTERSECT_INFO mode due to limitations in the current Java3D core API (the current geometry of the Morph cannot be queryed). Instead, they are picked by using the geometry at index 0 in the Morph. This limitation may be eliminated in a future release of Java3D.

If the pick shape is a PickBounds, the pick result will contain only the scenegraph path, even if the mode is GEOMETRY_INTERSECT_INFO.

16.4 PickCanvas

java.lang.Object
  |
  +--com.sun.j3d.utils.picking.PickTool
        |
        +--com.sun.j3d.utils.picking.PickCanvas

PickCanvas, a subclass of PickTool, simplifies picking using mouse events from an AWT Canvas. This class allows picking using Canvas x,y locations by generating the appropriate pick shape.

The pick tolerance specifies the distance from the pick center to include in the pick shape. A tolerance of 0.0 may slightly speed up picking, but also makes it very difficult to pick points and lines. The pick canvas can be used to make a series of picks; for example, to initialize the pick canvas, do the following:

PickCanvas pickCanvas = new PickCanvas(canvas, scene);
pickCanvas.setMode(PickTool.GEOMETRY_INTERSECT_INFO);
pickCanvas.setTolerance(4.0f);

Then for each mouse event:

pickCanvas.setShapeLocation(mouseEvent);
PickResult[] results = pickCanvas.pickAll();

For the pickAllSorted or pickClosest methods, the picks will be sorted by the distance from the ViewPlatform to the intersection point.

16.5 PickIntersection

java.lang.Object
  |
  +--com.sun.j3d.utils.picking.PickIntersection

PickIntersection holds information about an intersection of a PickShape with a Node as part of a PickResult. Information about the intersected geometry, intersected primitive, intersection point, and closest vertex can be queryed.

The intersected geometry is indicated by an index into the list of geometry arrays on the PickResult. It can also be queryed from this object.

The intersected primitive indicates which primitive out of the GeometryArray was intersected (where the primitive is a point, line, triangle, or quad, not a com.sun.j3d.utils.geometry.Primitive). For example, the intersection would indicate which triangle out of a triangle strip was intersected. The methods which return primitive data will have one value if the primitive is a point, two values if the primitive is a line, three values if the primitive is a triangle, and four values if the primitive is quad.

The primitive’s VWorld coordinates are saved when the intersection is calculated. The local coordinates, normal color, and texture coordinates for the primitive can also be queryed if they are present and readable.

The intersection point is the location on the primitive which intersects the pick shape closest to the center of the pick shape. The intersection point’s location in VWorld coordinates is saved when the intersection is calculated. The local coordinates, and the normal, color, and texture coordinates of the intersection can be interpolated if they are present and readable.

The closest vertex is the vertex of the primitive closest to the intersection point. The vertex index, VWorld coordinates, and local coordinates of the closest vertex can be queryed. The normal, color, and texture coordinate of the closest vertex can be queryed from the geometry array:

Vector3f getNormal(PickIntersection pi, int vertexIndex)
{
 int index;
 Vector3d normal = new Vector3f();
 GeometryArray ga = pickIntersection.getGeometryArray();
 if (pickIntersection.geometryIsIndexed())
 {
  index = ga.getNormalIndex(vertexIndex);
 }
 else
 {
  index = vertexIndex;
 }
 ga.getNormal(index, normal);
 return normal;
}

The color, normal, and texture coordinate information for the intersected primitive and the intersection point can be queryed. The geometry includes them, and the corresponding READ capability bits are set. PickTool.setCapabilities(Node, int) can be used to set the capability bits to allow this data to be queryed.

16.6 PickResult

java.lang.Object
  |
  +--com.sun.j3d.utils.picking.PickResult

PickResult stores information about a pick hit. Detailed information about the pick and each intersection of the PickShape with the picked Node can be inquired. The PickResult is constructed with basic information, and more detailed information is generated as needed. The additional information is only available if capability bits on the scenegraph Nodes are set properly; PickTool.setCapabilities(Node, int) can be used to ensure correct capabilities are set. Inquiring data that is not available due to capabilities not being set will generate a CapabilityNotSetException.

A PickResult can be used to calculate intersections on Node which is not part of a live scenegraph using the constructor which takes a local to VWorld transformation for the Node.

Pick hits on TriangleStrip primitives will store the triangle points in the PickIntersection with the vertices in counterclockwise order. For triangles that start with an odd numbered vertex, this will be the opposite of the order of the points in the TriangleStrip. This way the triangle in the PickIntersection will be displayed the same way as the triangle in the strip.

If the Shape3D being picked has multiple geometry arrays, the arrays are stored in the PickResult and referred to by a geometry index. If the Shape3D refers to a CompressedGeometry, the geometry is decompressed into an array of Shape3D nodes that can be queryed. The geometry NodeComponents for the Shape3D nodes are stored and used as if the Shape3D had multiple geometries. If there are multiple CompressedGeometries on the Shape3D, the decompressed Shape3Ds and GeometryArrays will be stored sequentially.

The intersection point for Morph nodes cannot be calculated using the displayed geometry due to limitations in the current Java3D core API (the current geometry of the Morph cannot be inquired). Instead, the geometry at index 0 in the Morph is used. This limitation may be eliminated in a future release of Java3D.

16.7 VRML picking example

The VrmlPickingTest example illustrates how the PickCanvas and PickResult classes can be used. The example loads a VRML format data file and allows the user to rotate, translate, and scale the loaded model. When the mouse is clicked, a list is generated of the intersections of the model with a PickCylinderRay that passes perpendicularly through the clicked screen location into the 3D scene. All the intersections with the model are reported, as well as the closest intersection.

When a mouse click occurs, VrmlPickingTest produces the following output (which corresponds to clicking the mouse in the position shown in figure 16.1. The output is a list of the PickResult objects (sorted from nearest to farthest):

*** MouseClick ***

First, the path through the scenegraph to the intersected node is displayed. In this case, the path is the VirtualUniverse’s Locale, a BranchGroup, the Sphere primitive (user data is “Sphere”), then finally a Shape3D containing a TriangleStripArray.

Figure 16.1

Figure 16.1 A VRML scene loaded into the VrmlPickingTest example. Note the position of the cursor (the hand icon), which corresponds to the scene intersections in the code which follows

Sorted PickResult 0: PickResult:
 sgp:javax.media.j3d.Locale@124fb8e :
 javax.media.j3d.BranchGroup :
 com.sun.j3d.utils.geometry.Sphere, Sphere :
 javax.media.j3d.Shape3D,
 Spherejavax.media.j3d.TriangleStripArray@12b486c

The transformation matrix required to convert the terminal node of the scenegraph path (Shape3D) to Virtual World coordinates is displayed. This is the transformation matrix that was in effect when the pick took place.

LocalToVworld Transform:
-0.5243562077058301, -0.8502316137753383, 0.04644104194946784, 0.35220520974733893
0.3928339572694004, -0.19315917400790997, 0.8990945531548112, 5.215472765774056
-0.7554680995624017, 0.4896894285499475, 0.4352840614012915, 0.5764203070064573
0.0, 0.0, 0.0, 1.0

Next, the intersection information for the ray is displayed. In this case, the ray intersected six Nodes in the model. For each intersection, the distance of the intersection from the ViewPlatform is calculated along with the point’s coordinates in Virtual World coordinates. As you can see from the Z-coordinates and distances, five of the intersections were with Nodes at the front of the Sphere while one was with a node at the rear of the Sphere.

 node:javax.media.j3d.Shape3D@12b485a
PickIntersection:  geomIndex = 0
 dist:13.190385327169809
 ptVW:(0.3082159939737674, 5.101595194311164, -0.40544525181089597)
PickIntersection: geomIndex = 0 dist:11.285273011880047 ptVW:(0.2697997524391042, 4.782074528439611, 1.4723671948932975)
PickIntersection: geomIndex = 0 dist:11.28272787795884 ptVW:(0.2766647006417829, 4.784127302928557, 1.4754390646997604)
PickIntersection: geomIndex = 0 dist:11.282690605316592 ptVW:(0.26386760841671225, 4.797646503054273, 1.4773578620510737)
PickIntersection: geomIndex = 0 dist:11.279971427880689 ptVW:(0.27735265885195876, 4.796380438058344, 1.4802262351804227)
PickIntersection: geomIndex = 0 dist:11.28272787795884 ptVW:(0.2766647006417829, 4.784127302928557, 1.4754390646997604)
Sorted Object 0: Sphere
Closest Object: Sphere

The second illustrated pick intersection is more complex (figure 16.2). As you can see, the sphere is still the closest intersection; however, the pick ray passes through the entire model. In this example (output following), the VRML part intersections are (sorted from nearest to farthest):

  1. Sphere: 2 intersections
  2. Cone: 2 intersections
  3. Cone: 2 intersections
  4. Box: 2 intersections
  5. Cone: 7 intersections
*** MouseClick ***
Sorted PickResult 0: PickResult:
sgp:javax.media.j3d.Locale@124fb8e :
javax.media.j3d.BranchGroup :

Figure 16.2

Figure 16.2 A VRML scene loaded into the VrmlPickingTest example. Note the position of the cursor (the hand icon) which corresponds to the scene intersections in the code that follows

com.sun.j3d.utils.geometry.Sphere, Sphere :
javax.media.j3d.Shape3D, Sphere
javax.media.j3d.TriangleStripArray@12b486c
LocalToVworld Transform:
0.974680683424301, -0.19810018807842686, -0.10370092016385302,
-0.5185046008192652
0.2217376557236817, 0.9160774460188752, 0.3341175316108114,
2.8105876580540574
0.028809328241108344, -0.34865230298798766, 0.9368092624581957, 3.084046312290978
0.0, 0.0, 0.0, 1.0
node:javax.media.j3d.Shape3D@12b485a PickIntersection: geomIndex = 0 dist:10.31109258625374 ptVW:(-0.503754844446497, 2.138046095717119, 2.3502490354035483)
PickIntersection: geomIndex = 0 dist:10.315735064224192 ptVW:(-0.48806121433886257, 2.1446076441445165, 2.3442903032651294)
PickIntersection: geomIndex = 0 dist:10.311507103034156 ptVW:(-0.46680214250863505, 2.1403178766932185, 2.3478813387527073)
PickIntersection: geomIndex = 0 dist:8.737141773923474 ptVW:(-0.41919205110931124, 2.265854783380931, 3.916754614302066)
PickIntersection: geomIndex = 0 dist:8.771580342395431 ptVW:(-0.41919205110931124, 2.265854783380931, 3.916754614302066)
PickIntersection: geomIndex = 0 dist:8.732273133281984 ptVW:(-0.41290180559136586, 2.275910225691348, 3.9205080490411017)
PickIntersection: geomIndex = 0 dist:8.73669779993455 ptVW:(-0.4106277895151771, 2.2691852339960756, 3.916514821486335)
Sorted Object 0: Sphere
Sorted PickResult 1: PickResult:
sgp:javax.media.j3d.Locale@124fb8e : javax.media.j3d.BranchGroup : javax.media.j3d.Shape3D, Cone javax.media.j3d.TriangleFanArray@1262519 LocalToVworld Transform: 0.974680683424301, -0.19810018807842686, -0.10370092016385302, -0.3111027604915591 0.2217376557236817, 0.9160774460188752, 0.3341175316108114, 2.1423525948324347 0.028809328241108344, -0.34865230298798766, 0.9368092624581957, 1.2104277873745866 0.0, 0.0, 0.0, 1.0
node:javax.media.j3d.Shape3D@1261cac PickIntersection: geomIndex = 0 dist:10.943688351941072 ptVW:(-0.510896717862459, 2.1149716954978928, 1.7318035261186269)
PickIntersection: geomIndex = 0 dist:10.92767850911496 ptVW:(-0.5066210416916537, 2.112735307161519, 1.7330744444918968)
Sorted Object 1: Cone
Sorted PickResult 2: PickResult:
sgp:javax.media.j3d.Locale@124fb8e : javax.media.j3d.BranchGroup : javax.media.j3d.Shape3D, Cone javax.media.j3d.TriangleFanArray@1262519 LocalToVworld Transform: 0.974680683424301, -0.19810018807842686, -0.10370092016385302, -0.3111027604915591 0.2217376557236817, 0.9160774460188752, 0.3341175316108114, 2.1423525948324347 0.028809328241108344, -0.34865230298798766, 0.9368092624581957, 1.2104277873745866 0.0, 0.0, 0.0, 1.0
node:javax.media.j3d.Shape3D@1261cac PickIntersection: geomIndex = 0 dist:10.943688351941072 ptVW:(-0.510896717862459, 2.1149716954978928, 1.7318035261186269)
PickIntersection: geomIndex = 0 dist:10.92767850911496 ptVW:(-0.5066210416916537, 2.112735307161519, 1.7330744444918968)
Sorted Object 2: Cone
Sorted PickResult 3: PickResult:
sgp:javax.media.j3d.Locale@124fb8e : javax.media.j3d.BranchGroup : javax.media.j3d.Shape3D, Box javax.media.j3d.QuadArray@1264877
LocalToVworld Transform: 0.974680683424301, -0.10370092016385303, 0.19810018807842686, -0.10370092016385303 0.2217376557236817, 0.3341175316108115, -0.9160774460188752, 1.4741175316108115 0.028809328241108344, 0.9368092624581957, 0.3486523029879877, -0.6631907375418048 0.0, 0.0, 0.0, 1.0
node:javax.media.j3d.Shape3D@1264cfe PickIntersection: geomIndex = 0 dist:12.494425040536017 ptVW:(-0.5914732681836042, 1.9639480320061125, 0.17556762285086336)
PickIntersection: geomIndex = 0 dist:14.587993543333791 ptVW:(-0.6908450104199546, 1.7903467955691152, -1.9084230065569017)
Sorted Object 3: Box
Sorted PickResult 4: PickResult:
sgp:javax.media.j3d.Locale@124fb8e : javax.media.j3d.BranchGroup : javax.media.j3d.Shape3D, Cone javax.media.j3d.TriangleFanArray@124fa1a LocalToVworld Transform: 0.974680683424301, -0.19810018807842686, -0.10370092016385302, -0.2074018403277061 0.2217376557236817, 0.9160774460188752, 0.3341175316108114, 1.8082350632216233 0.028809328241108344, -0.34865230298798766, 0.9368092624581957, 0.2736185249163908 0.0, 0.0, 0.0, 1.0
node:javax.media.j3d.Shape3D@124fa08 PickIntersection: geomIndex = 0 dist:12.494425040536019 ptVW:(-0.5914732681836044, 1.9639480320061125, 0.17556762285086158)
PickIntersection: geomIndex = 0 dist:12.500884811804253 ptVW:(-0.5720301373107639, 1.9989535603646984, 0.16523500707364264)
Sorted Object 4: Cone
Closest PickResult: PickResult:
sgp:javax.media.j3d.Locale@124fb8e : javax.media.j3d.BranchGroup : com.sun.j3d.utils.geometry.Sphere, Sphere : javax.media.j3d.Shape3D, Sphere javax.media.j3d.TriangleStripArray@12b486c
LocalToVworld Transform: 0.974680683424301, -0.19810018807842686, -0.10370092016385302, -0.5185046008192652 0.2217376557236817, 0.9160774460188752, 0.3341175316108114, 2.8105876580540574 0.028809328241108344, -0.34865230298798766, 0.9368092624581957, 3.084046312290978 0.0, 0.0, 0.0, 1.0
node:javax.media.j3d.Shape3D@12b485a PickIntersection: geomIndex = 0 dist:10.31109258625374 ptVW:(-0.503754844446497, 2.138046095717119, 2.3502490354035483)
PickIntersection: geomIndex = 0 dist:10.315735064224192 ptVW:(-0.48806121433886257, 2.1446076441445165, 2.3442903032651294)
PickIntersection: geomIndex = 0 dist:10.311507103034156 ptVW:(-0.46680214250863505, 2.1403178766932185, 2.3478813387527073)
PickIntersection: geomIndex = 0 dist:8.737141773923474 ptVW:(-0.41919205110931124, 2.265854783380931, 3.916754614302066)
PickIntersection: geomIndex = 0 dist:8.771580342395431 ptVW:(-0.41919205110931124, 2.265854783380931, 3.916754614302066)
PickIntersection: geomIndex = 0 dist:8.732273133281984 ptVW:(-0.41290180559136586, 2.275910225691348, 3.9205080490411017)
PickIntersection: geomIndex = 0 dist:8.73669779993455 ptVW:(-0.4106277895151771, 2.2691852339960756, 3.916514821486335)
Closest Object: Sphere

Note that multiple intersections can be reported because the pick ray used for intersection testing actually has a width (tolerance). The tolerance makes it easier for users to pick small objects or lines at the expense of absolute accuracy. You should also note that generating all the picking information in the preceding code is computationally quite expensive, so you should use simple BOUNDS picking whenever possible, unless you need to know the exact location within a shape that was picked.

A method to generate the picking output is shown in the following example:

From VrmlPickingTest.java

//This example loads a VRML file, automatically computes
//the view point to view the objects in the file,
//and then mouse picks. For each pick, all the selected components
//of the scene are reported (by their VRML name).
//The VRML scene can be rotated, scaled, and
//translated using the mouse.
public class VrmlPickingTest extends Java3dApplet implements
  MouseListener
{
 PickCanvas      pickCanvas = null;
public VrmlPickingTest() { }
public VrmlPickingTest( String[] args ) { saveCommandLineArguments( args ); initJava3d(); }
protected void addCanvas3D( Canvas3D c3d ) { setLayout( new BorderLayout() ); add( "Center", c3d ); doLayout();
if ( m_SceneBranchGroup != null ) { c3d.addMouseListener( this ); pickCanvas = new PickCanvas( c3d, m_SceneBranchGroup ); pickCanvas.setMode( PickTool.GEOMETRY_INTERSECT_INFO ); pickCanvas.setTolerance( 4.0f ); } c3d.setCursor( new Cursor( Cursor.HAND_CURSOR ) ); }
public TransformGroup[] getViewTransformGroupArray() { TransformGroup[] tgArray = new TransformGroup[1]; tgArray[0] = new TransformGroup();
Transform3D viewTrans = new Transform3D(); Transform3D eyeTrans = new Transform3D();
BoundingSphere sceneBounds = (BoundingSphere) m_SceneBranchGroup.getBounds();
//point the view at the center of the object Point3d center = new Point3d(); sceneBounds.getCenter( center ); double radius = sceneBounds.getRadius(); Vector3d temp = new Vector3d( center ); viewTrans.set( temp );
//pull the eye back far enough to see the whole object double eyeDist = 1.4 * radius / Math.tan( Math.toRadians( 40 ) / 2.0); temp.x = 0.0; temp.y = 0.0; temp.z = eyeDist; eyeTrans.set( temp ); viewTrans.mul( eyeTrans );
//set the view transform tgArray[0].setTransform( viewTrans );
return tgArray; }
protected BranchGroup createSceneBranchGroup() { BranchGroup objRoot = super.createSceneBranchGroup();
Bounds lightBounds = getApplicationBounds();
AmbientLight ambLight = new AmbientLight( true, new Color3f( 1.0f, 1.0f, 1.0f) ); ambLight.setInfluencingBounds( lightBounds ); objRoot.addChild( ambLight );
DirectionalLight headLight = new DirectionalLight(); headLight.setInfluencingBounds( lightBounds );
objRoot.addChild( headLight );
TransformGroup mouseGroup = createMouseBehaviorsGroup();
String vrmlFile = null;
try { URL codebase = getWorkingDirectory(); vrmlFile = codebase.toExternalForm() + "/VRML/BoxConeSphere.wrl"; } catch( MalformedURLException mue ) { }
if ( m_szCommandLineArray != null ) { switch ( m_szCommandLineArray.length ) { case 0: break;
case 1: vrmlFile = m_szCommandLineArray[0]; break;
default: System.err.println("Usage: VrmlPickingTest [pathname|URL]"); System.exit( -1 ); } }
BranchGroup sceneRoot = loadVrmlFile( vrmlFile );
if ( sceneRoot != null ) mouseGroup.addChild( sceneRoot );
objRoot.addChild( mouseGroup );
return objRoot; }
private TransformGroup createMouseBehaviorsGroup() { TransformGroup examineGroup = new TransformGroup(); examineGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); examineGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
Bounds behaviorBounds = getApplicationBounds();
MouseRotate mr = new MouseRotate( examineGroup ); mr.setSchedulingBounds( behaviorBounds ); examineGroup.addChild( mr );
MouseTranslate mt = new MouseTranslate( examineGroup ); mt.setSchedulingBounds( behaviorBounds ); examineGroup.addChild( mt );
MouseZoom mz = new MouseZoom( examineGroup ); mz.setSchedulingBounds( behaviorBounds ); examineGroup.addChild( mz );
return examineGroup; }
private BranchGroup loadVrmlFile( String location ) { BranchGroup sceneGroup = null; Scene scene = null;
VrmlLoader loader = new VrmlLoader();
try { URL loadUrl = new URL(location); try { //load the scene scene = loader.load(new URL(location)); } catch (Exception e) { System.out.println("Exception loading URL:" + e); } } catch (MalformedURLException badUrl) { //location may be a path name try { //load the scene scene = loader.load(location); } catch (Exception e) { System.out.println("Exception loading file from path:" + e); } }
if (scene != null) { //get the scene group sceneGroup = scene.getSceneGroup();
sceneGroup.setCapability( BranchGroup.ALLOW_BOUNDS_READ ); sceneGroup.setCapability( BranchGroup.ALLOW_CHILDREN_READ );
Hashtable namedObjects = scene.getNamedObjects(); System.out.println("*** Named Objects in VRML file: \n" + namedObjects);
//recursively set the user data here so we can find our objects //when they are picked java.util.Enumeration enumValues = namedObjects.elements(); java.util.Enumeration enumKeys = namedObjects.keys();
if( enumValues != null ) { while( enumValues.hasMoreElements() != false ) { Object value = enumValues.nextElement(); Object key = enumKeys.nextElement();
recursiveSetUserData( value, key ); } } }
return sceneGroup; }
//Method to recursively set the user data for objects //in the scenegraph tree we also set the capabilities //on Shape3D and Morph objects required by the PickTool void recursiveSetUserData( Object value, Object key ) { if( value instanceof SceneGraphObject != false ) { //set the user data for the item SceneGraphObject sg = (SceneGraphObject) value; sg.setUserData( key );
//recursively process group if( sg instanceof Group ) { Group g = (Group) sg;
//recurse on child nodes java.util.Enumeration enumKids = g.getAllChildren();
while( enumKids.hasMoreElements() != false ) recursiveSetUserData( enumKids.nextElement(), key ); } else if ( sg instanceof Shape3D || sg instanceof Morph ) { PickTool.setCapabilities( (Node) sg, PickTool.INTERSECT_FULL ); } } }
public void mouseClicked(MouseEvent e) { System.out.println("*** MouseClick ***");
pickCanvas.setShapeLocation( e ); PickResult[] results = pickCanvas.pickAllSorted();
if( results != null ) { for (int n = 0; n < results.length; n++ ) { PickResult pickResult = results[n];
System.out.println( "Sorted PickResult " + n + ": " + pickResult );
Node actualNode = pickResult.getObject();
if( actualNode.getUserData() != null ) { System.out.println( "Sorted Object " + n + ": " + actualNode.getUserData() ); } } }
PickResult pickResult = pickCanvas.pickClosest();
if( pickResult != null ) { System.out.println( "Closest PickResult: " + pickResult );
Node actualNode = pickResult.getObject();
if( actualNode.getUserData() != null ) { System.out.println( "Closest Object: " + actualNode.getUserData() ); } }
}
public void mouseEntered(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}
public void mousePressed(MouseEvent e) {}
public void mouseReleased(MouseEvent e) {}
public static void main( String[] args ) { VrmlPickingTest pickingTest = new VrmlPickingTest( args );
new MainFrame( pickingTest, 400, 400 ); } }

The VrmlPickingTest example sets up a simple AWT application or applet, loads a VRML file into it and when an AWT MouseEvent is generated calculates PickResults for each intersection between the ray perpendicular to the Canvas and the loaded VRML model.

The initJava3D method initializes Java 3D—it creates a VirtualUniverse from scratch, and does not use the SimpleUniverse utility class. By overriding Java3dApplet methods, users of the class can customize the functionality and configuration of the VirtualUniverse created by the base class. In this example, the addCanvas3D method is overridden to assign a Canvas3D to a AWT Container, the getViewTransformGroupArray method creates an array of TransformGroups to be used on the view side of the scenegraph, while the createSceneBranchGroup method returns a BranchGroup containing all the scenegraph elements for the scene side of the scenegraph. The remaining methods are utility methods defined by the VrmlPickingTest class to implement the example functionality.

The addCanvas3D method sets up the AWT UI elements; specifically it assigns a BorderLayout algorithm and adds a Canvas3D to the center area of the AWT Component. Additionally the addCanvas3D method adds a MouseListener instance of this to the Canvas3D so that the VrmlPickingTest class will receive a callback from AWT when mouse events occur. A PickCanvas is created for the Canvas3D using the PickTool.GEOMETRY_INTERSECT_INFO intersection mode to calculate PickResults. The tolerance for picking is set to 4.0. Finally, the cursor for the Canvas3D is set to the standard AWT hand-cursor icon.

The createSceneBranchGroup method loads the VRML file using the VRML 97 VrmlLoader class. After having been loaded from a URL or File, the named objects in the VRML file are iterated and the User Data field is set to the VRML name of the object. Assigning the VRML name to the User Data will make it easy to identify which object in the scene has been picked with the mouse—we can just grab the picked Node and examine its user data field.

The PickResult calculation is performed within the AWT mouse callback method mouseClicked. When a mouse click occurs AWT will invoke the method passing in a MouseEvent object that describes the position of the mouse and button states when the click took place. We merely have to call pickCanvas.setShapeLocation to assign the MouseEvent to the PickCanvas and then call pickCanvas.pickAllSorted for the PickCanvas to return an array of PickResults with all the intersections sorted from furthest to nearest.

16.8 Using picking for collision detection

The Java 3D 1.2 picking utilities can also be used to implement simple collision detection with a scene. The basic idea is to create a custom behavior that checks for picking intersections at runtime. By triggering the behavior in every frame, it is possible to detect collisions between objects and add application logic to respond to them. Unfortunately this is not a 100 percent robust mechanism for detecting collisions—it is possible for an object to be moving so fast that in the time between frames it passes right through an object in the scene. By the time the behavior is invoked again, the object is no longer in collision, and the intersection will have been missed. One possible application-specific workaround is to do a single frame look ahead (or look behind) to check whether an intersection was missed. A commercial collision detection engine (such as VCollide) provides much more scaleable collision detection and can handle cases such as that just described. If your application relies heavily on collision detection (and there are no cheats that you can use), you should probably investigate a commercial library. Defining a scene with thousands of objects, which could all potentially collide with one another, requires specialized collision detection algorithms that fall outside of Java 3D.

The remainder of this chapter will discuss an example that uses picking to implement simple collision detection. There are a limited number of collidable objects in the scene (10), and the speed of the moving objects has been defined such that it is not possible for the objects to pass through one another between frames (figure 16.3).

Figure 16.3

Figure 16.3 Two frames from the PickCollisionTest. The spheres bounce around within a large box, each side of which is defined by a scaled and translated ColorCube object. The spheres can bounce off one another as well as the sides of the box

From PickCollisionTest.java

/*
 * This example creates a large hollow box (out of ColorCubes,
 * one for each side of the box). Within the box, four Spheres are
 * created. Each Sphere has a behavior attached that detects
 * collisions with the sides of the box, and the other Spheres.
 * When a collision is detected, the trajectory of the Sphere is
 * reversed and the color of the Sphere changed. When a collision
 * is not detected, the Sphere is advanced along its
 * current trajectory.
 */
class PickCollisionTest extends Java3dApplet
  implements ActionListener
{
 private static int    m_kWidth = 400;
 private static int    m_kHeight = 400;
private static final int boxSize = 10;
public PickCollisionTest() { initJava3d(); }
public void actionPerformed( ActionEvent event ) { }
protected void addCanvas3D( Canvas3D c3d ) { add( c3d ); doLayout(); }
protected double getScale() { return 0.5; }
//recursively set the user data for objects in the scenegraph tree void recursiveSetUserData( SceneGraphObject root, Object value ) { root.setUserData( value );
//recursively process group if( root instanceof Group ) { Group g = (Group) root;
//recurse on child nodes java.util.Enumeration enumKids = g.getAllChildren();
while( enumKids.hasMoreElements() != false ) recursiveSetUserData( (SceneGraphObject) enumKids.nextElement(), value ); } }
protected void addCube( BranchGroup bg, double x, double y, double z, double sx, double sy, double sz, String name, boolean wireframe ) { //create four ColorCube objects TransformGroup cubeTg = new TransformGroup(); Transform3D t3d = new Transform3D(); t3d.setTranslation( new Vector3d( x, y, z ) ); t3d.setScale( new Vector3d( sx, sy, sz ) ); cubeTg.setTransform( t3d ); ColorCube cube = new ColorCube( 1.0 );
//we have to make the front face wireframe //we can't see inside the box! if ( wireframe ) { Appearance app = new Appearance(); app.setPolygonAttributes( new PolygonAttributes( PolygonAttributes.POLYGON_LINE, PolygonAttributes.CULL_NONE, 0 ) ); cube.setAppearance( app ); }
cubeTg.addChild( cube ); recursiveSetUserData( cubeTg, name );
bg.addChild( cubeTg ); }
protected void addSphere( BranchGroup bg, double x, double y, double z, Vector3d incVector, String name ) { Appearance app = new Appearance();
TransformGroup sphereTg = new TransformGroup(); Transform3D t3d = new Transform3D(); t3d.setTranslation( new Vector3d( x, y, z ) ); sphereTg.setTransform( t3d );
sphereTg.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); sphereTg.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
sphereTg.addChild( new Sphere( 1, app ) ); bg.addChild( sphereTg ); recursiveSetUserData( sphereTg, name );
//create the collision behavior CollisionBehavior collisionBehavior = new CollisionBehavior( bg, sphereTg, app, new Vector3d( x,y,z ), incVector ); collisionBehavior.setSchedulingBounds( getApplicationBounds() ); bg.addChild( collisionBehavior ); }
protected BranchGroup createSceneBranchGroup() { BranchGroup objRoot = super.createSceneBranchGroup();
Bounds lightBounds = getApplicationBounds();
AmbientLight ambLight = new AmbientLight( true, new Color3f(1.0f, 1.0f, 1.0f) ); ambLight.setInfluencingBounds( lightBounds ); objRoot.addChild( ambLight );
DirectionalLight headLight = new DirectionalLight(); headLight.setInfluencingBounds( lightBounds ); objRoot.addChild( headLight );
//create ColorCube objects, one for each side of a cube addCube( objRoot, 0,boxSize,0, boxSize,0.1,boxSize, "Top", false ); addCube( objRoot, 0,-boxSize,0, boxSize,0.1,boxSize, "Bottom", false ); addCube( objRoot, boxSize,0,0, 0.1,boxSize,boxSize, "Right", false ); addCube( objRoot, -boxSize,0,0, 0.1,boxSize,boxSize, "Left", false ); addCube( objRoot, 0,0,-boxSize, boxSize,boxSize,0.1, "Back", false ); addCube( objRoot, 0,0,boxSize, boxSize,boxSize,0.1, "Front", true );
//create the spheres addSphere( objRoot, 0,3,4, new Vector3d( 0.1,0.3,0.1), "Sphere 1" ); addSphere( objRoot, 3,0,-2, new Vector3d( 0.4,0.1,0.2), "Sphere 2" ); addSphere( objRoot, 0,-3,0, new Vector3d( 0.2,0.2,0.6), "Sphere 3" ); addSphere( objRoot, -3,0,-4, new Vector3d( 0.1,0.6,0.3), "Sphere 4" );
return objRoot; }
public static void main(String[] args) { PickCollisionTest pickCollisionTest = new PickCollisionTest(); pickCollisionTest.saveCommandLineArguments( args );
new MainFrame( pickCollisionTest, m_kWidth, m_kHeight ); } }
/* * This behavior detects collisions between the branch of a scene, * and a collision object. The Java 3D 1.2 picking utilities are used * to implement collision detection. The objects in the scene * that are collidable should have their user data set. The collision * object's user data is used to ignore collisions between the object * and itself. * * When a collision is detected the trajectory of the collision object * is reversed (plus a small random factor) and an Appearance object * is modified. * * When a collision is not detected the collision object is moved * along its current trajectory and the Appearance color is reset. * * Collision checking is run after every frame. */ class CollisionBehavior extends Behavior { //the wake up condition for the behavior protected WakeupCondition m_WakeupCondition = null;
//how often we check for a collision private static final int ELAPSED_FRAME_COUNT = 1;
//the branch that we check for collisions private BranchGroup pickRoot = null;
//the collision object that we are controlling private TransformGroup collisionObject = null;
//the appearance object that we are controlling private Appearance objectAppearance = null;
//cached PickBounds object used for collision detection private PickBounds pickBounds = null;
//cached Material objects that define the collided and //missed colors private Material collideMaterial = null; private Material missMaterial = null;
//the current trajectory of the object private Vector3d incrementVector = null;
//the current position of the object private Vector3d positionVector = null;
public CollisionBehavior( BranchGroup pickRoot, TransformGroup collisionObject, Appearance app, Vector3d posVector, Vector3d incVector ) { //save references to the objects this.pickRoot = pickRoot; this.collisionObject = collisionObject; this.objectAppearance = app;
incrementVector = incVector; positionVector = posVector;
//create the WakeupCriterion for the behavior WakeupCriterion criterionArray[] = new WakeupCriterion[1]; criterionArray[0] = new WakeupOnElapsedFrames( ELAPSED_FRAME_COUNT );
objectAppearance.setCapability( Appearance.ALLOW_MATERIAL_WRITE );
collisionObject.setCapability( TransformGroup.ALLOW_TRANSFORM_WRITE ); collisionObject.setCapability( Node.ALLOW_BOUNDS_READ );
//save the WakeupCriterion for the behavior m_WakeupCondition = new WakeupOr( criterionArray ); }
public void initialize() { //apply the initial WakeupCriterion wakeupOn( m_WakeupCondition );
Color3f objColor = new Color3f(1.0f, 0.1f, 0.2f); Color3f black = new Color3f(0.0f, 0.0f, 0.0f); collideMaterial = new Material(objColor, black, objColor, black, 80.0f);
objColor = new Color3f(0.0f, 0.1f, 0.8f); missMaterial = new Material(objColor, black, objColor, black, 80.0f);
objectAppearance.setMaterial( missMaterial ); }
protected void onCollide() { objectAppearance.setMaterial( collideMaterial );
incrementVector.negate();
//add a little randomness incrementVector.x += (Math.random() - 0.5) / 20.0; incrementVector.y += (Math.random() - 0.5) / 20.0; incrementVector.z += (Math.random() - 0.5) / 20.0; }

protected void onMiss() { objectAppearance.setMaterial( missMaterial ); }
protected void moveCollisionObject() { Transform3D t3d = new Transform3D();
positionVector.add (incrementVector ); t3d.setTranslation( positionVector );
collisionObject.setTransform( t3d ); }
public boolean isCollision( PickResult[] resultArray ) { if( resultArray == null || resultArray.length == 0 ) return false;
/* * We use the user data on the nodes to ignore the case * of the collisionObject having collided with itself! * The user data also gives us a good mechanism for reporting * the collisions. */ for( int n = 0; n < resultArray.length; n++ ) { Object userData = resultArray[n].getObject().getUserData();
if ( userData != null && userData instanceof String ) { //check that we are not colliding with ourselves... if ( ((String) userData).equals( (String) collisionObject.getUserData() ) == false ) { System.out.println( "Collision between: " + collisionObject.getUserData() + " and: " + userData ); return true; } } } return false; }
public void processStimulus( java.util.Enumeration criteria ) { while( criteria.hasMoreElements() ) { WakeupCriterion wakeUp = (WakeupCriterion) criteria.nextElement();
//every N frames, check for a collision if( wakeUp instanceof WakeupOnElapsedFrames ) { //create a PickBounds PickTool pickTool = new PickTool( pickRoot ); pickTool.setMode( PickTool.BOUNDS );
BoundingSphere bounds = (BoundingSphere) collisionObject.getBounds(); pickBounds = new PickBounds( new BoundingSphere( new Point3d( positionVector.x, positionVector.y, positionVector.z ), bounds.getRadius() ) ); pickTool.setShape( pickBounds, new Point3d( 0,0,0 ) ); PickResult[] resultArray = pickTool.pickAll();
if ( isCollision( resultArray ) ) onCollide(); else onMiss();
moveCollisionObject(); } }
//assign the next WakeUpCondition, so we are notified again wakeupOn( m_WakeupCondition ); }

16.9 Conclusions

Java 3D includes fairly good high-level support for user interaction through the extensive picking API. For fairly simple applications, the picking API also provides a basis for simple collision detection. For more complex applications, or applications that require the simulation-level accuracy, a dedicated collision detection library should be investigated. There are a number of commercial collision detection libraries that have been used with Java 3D. Please refer to the Java 3D interest email list for the latest references.

[previous]  |  [main]  |  [next]