16.8 Using picking for collision detection
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.
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.
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:
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
PickBounds
)
PickConeRay
)
PickConeSegment
)
PickCylinderRay
)
PickCylinderSegment
)
PickPoint
)
PickRay
)
PickSegment
)
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).
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
.
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.
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.
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.
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 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):
*** MouseClick ***
Sorted PickResult 0: PickResult:
sgp:javax.media.j3d.Locale@124fb8e :
javax.media.j3d.BranchGroup :
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.
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 Two frames from thePickCollisionTest
. 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 );
}
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.