Navigate through virtual worlds using Java 3D

Use level-of-detail and fly-through behaviors

Page 4 of 5
public FlyingPlatform(Canvas3D aCanvas, ElevationModelInterface aModel)
{
  super(aCanvas,MOUSE_MOTION_LISTENER|MOUSE_LISTENER|KEY_LISTENER);
  aCanvas.requestFocus();  // Get the focus to the Canvas, allows keyboard inputs
  model = aModel;
  canvas = aCanvas;
  HOME_Y = model.getElevationAt(0,0)+INITIAL_TERRAIN_FOLLOW_ALTITUDE;
  moveAmt = Math.round(model.getModelLength()/100);
  platformVect = new Vector3f(HOME_X,HOME_Y,HOME_Z);
  Container c = canvas.getParent();
  while(c.getParent() != null)
      c= c.getParent();
  settingsDialog = new SettingsDialog((Frame)c);
  popupMenu.add(settingsMenu);
  popupMenu.add(levelOffMenu);
  popupMenu.add(terrainFollowMenu);
  popupMenu.addSeparator();
  popupMenu.add(aerialViewMenu);
  popupMenu.add(homeBaseMenu);
  canvas.add(popupMenu);
  terrainFollowMenu.addItemListener(this);
  settingsMenu.addActionListener(this);
  homeBaseMenu.addActionListener(this);
  levelOffMenu.addActionListener(this);
  aerialViewMenu.addActionListener(this);
}

The heart of FlyingPlatform's functionality lies in the class's ability to maintain the transformation that controls the ViewPlatform. The FlyingPlatform has four class variables that hold a vector for the location and three rotation angles, one for each axis (x, y, z). Initially, these values are set to the home base values. Home base is defined as the center of the x, z plane, 100 meters above the ground looking north. Each time the user changes the ViewPlatform's location or rotation, one or more of the class variables change and the integrateTransforms() method is called to reset the ViewPlatform's transformation. integrateTransforms() creates a separate Transform3D object for the location and the rotation angles, then the Transform3D objects are multiplied together to create one transform representing their total effect on the ViewPlatform. The visual effect that the transformations have on the view is the reverse order of their multiplication. In this case, we rotate on the z axis first, then the x axis, then the y axis, and lastly move to a location in space.

Special note: For this application, the rotation transformation order is important. The y rotation is performed last so it does not affect the z and x rotations. Once these operations are complete, ViewPlatform.setTransform is called to apply the new transform. Thus, any mouse or keyboard event-processing routine need only change one of these class variables and then call integrateTransforms() to take effect. The following code segment is from FlyingPlatform:

 /** Holds view platform location*/
 private Vector3f platformVect;
/** Holds current X axis attitude */
 private float xAngle = HOME_XANGLE; // Degrees
/** Holds current Y axis attitude */
 private float yAngle = HOME_YANGLE; // Degrees
/** Holds current Z axis attitude */
 private float zAngle = HOME_ZANGLE; // Degrees
...
/**
 *  Reset the view platform transformation based on
 * the x, y, z rotation and location information
 *
 */
protected void integrateTransforms()
{
   Transform3D tVect = new Transform3D();
   Transform3D tXRot = new Transform3D();
   Transform3D tYRot = new Transform3D();
   Transform3D tZRot = new Transform3D();
   tVect.set(platformVect);
   tXRot.set(new AxisAngle4d(1.0,0.0,0.0,Math.toRadians(xAngle)));
   tYRot.set(new AxisAngle4d(0.0,1.0,0.0,Math.toRadians(yAngle)));
   tZRot.set(new AxisAngle4d(0.0,0.0,1.0,Math.toRadians(zAngle)));
   tVect.mul(tYRot);
   tVect.mul(tXRot);
   tVect.mul(tZRot);
   targetTransform = tVect;
   vp.getViewPlatformTransform().setTransform(tVect);
}

You'll find creating methods to listen for keyboard and mouse input, change a variable, and then call a method pretty straightforward. The more difficult part comes when you want to move forward/backward through a scene. What values are added to or subtracted from x, y, z when you want to move 20 meters forward and you are in a 33-degree nose-up position, with a 25-degree bank and headed northeast? Don't go hunting for your ninth grade trigonometry book and try to generate the proper equation. Instead, use the transformation functions built into Java 3D. Here's how:

The ViewPlatform is initially set at the origin pointing down the negative z axis. Therefore, moving forward 10 units requires decreasing z by 10. This movement needs to translate into movement in all three directions. moveForward(float amt) completes this task by creating a Transform3D object based on the x, y, z axis rotations applied in the correct order (same order used by integrateTransforms()), then applying this transformation to a Vector3f stored in the tv object based on the movement. moveForward() creates a set of transformations for rotations, multiplies them together, creates a Vector3f representing movement in the z direction, then calls the transform function to translate tv. In my example, the vector (0, 0, -10) translates into (5.930, 5.44, -5.93). This vector can now be added to the platformVect that maintains the ViewPlatform's location. If terrain-following is enabled, then platformVect's y coordinate is updated based on the ground elevation below the new x, z location and the terrain-following altitude. The code segment below performs the moveForward function of FlyingPlatform:

/**
 * Move the ViewPlatform forward by desired number of meters.
 * Forward implies in the direction that it is currently pointed.
 * If terrain-following is enabled, then keep the altitude a steady
 * amount above the ground.
 * @param amt number of meters to move forward
 */
  public void moveForward(float amt)
  {
  //
  //  Calculate x, y, z movement.
  //
  // Set up Transforms
     Transform3D tTemp = new Transform3D();
     Transform3D tXRot = new Transform3D();
     Transform3D tYRot = new Transform3D();
     Transform3D tZRot = new Transform3D();
     tXRot.set(new AxisAngle4d(1.0,0.0,0.0,Math.toRadians(xAngle)));
     tYRot.set(new AxisAngle4d(0.0,1.0,0.0,Math.toRadians(yAngle)));
     tZRot.set(new AxisAngle4d(0.0,0.0,1.0,Math.toRadians(zAngle)));
     tTemp.mul(tYRot);
     tTemp.mul(tXRot);
     tTemp.mul(tZRot);
  //
  // Move forward in z direction.
  // Implies decreasing z since we are looking at the origin from the pos z.
     Vector3f tv = new Vector3f(0,0,-amt);
     tTemp.transform(tv);  // Translates z movement into x, y, z movement.
  //
  //  Set new values for the platform location vector.
  // If terrain-following is on, then find the terrain elevation at the new x, z
  // coordinate and base the new altitude on that. Else, use the computed altitude.
  //
    if(followTerrain)
    {
     platformVect.y = model.getElevationAt(platformVect.x+tv.x,platformVect.z+tv.z)
                      +terrainFollowAltitude;
    }
    else
     platformVect.y += tv.y;
    platformVect.x += tv.x;
    platformVect.z += tv.z;
    integrateTransforms();  // Apply transformations.
  }

One final trick I use when implementing FlyingPlatform is adding a level of sensitivity to mouse moves. In the routines for processing mouse motions shown below, I use the variables oldx, oldy to store the last mouse drag's location. If the mouse moves without a button pressed down (mouseMoved()) then these variables set to invalid values (-1). The first time the mouse moves with a button pressed down (mouseDragged()), the location is saved in oldx, oldy. Subsequent mouseDragged() calls compare the values in oldx, oldy with the current x, y values to determine the direction of motion (up, down, left, right). Changes to the ViewPlatform location and orientation are made based on the movement direction and which mouse button is pressed. A sensitivity (3 pixels) value causes the program to ignore small, perhaps unintentional movements. It is important to note that the x, y mouse coordinates have no relation to the x, y, z terrain coordinates. Mouse coordinates are in screen pixel units (with 0, 0 being at the top of the screen). The code segment shown below contains the MouseEvent processing functions of FlyingPlatform:

public void mouseMoved(MouseEvent e)
 {
  oldx = -1;
  oldy = -1;
 }
public void mouseDragged(MouseEvent e)
{
  int mods = e.getModifiersEx();
  int x = e.getX();
  int y = e.getY();
  if(oldx < 0 || oldy < 0)
  {
   oldx = x;
   oldy = y;
   return;
  }
//
// Skip the event if it moved just a little
//
  if(Math.abs(y-oldy) < sensitivity &&
     Math.abs(x-oldx) < sensitivity)
     return;
//
// First, check to see if both buttons are down
//
   if((mods & MouseEvent.BUTTON1_DOWN_MASK) != 0
      && (mods & MouseEvent.BUTTON3_DOWN_MASK) != 0)
   {
     if(y > oldy+sensitivity)
       increaseXRotate(turnAmt);
     if(y < oldy-sensitivity)
       increaseXRotate(-turnAmt);
     return;
   }
//
// Process left only down
//
   if((mods & MouseEvent.BUTTON1_DOWN_MASK) != 0)
   {
     if(y > oldy+sensitivity) //Mouse moves down screen
       moveForward(-moveAmt);
     if(y < oldy-sensitivity) // Mouse moves up screen
       moveForward(moveAmt);
     if(x > oldx+sensitivity)
       increaseYRotate(-turnAmt);
     if(x < oldx-sensitivity)
       increaseYRotate(turnAmt);
   }
//
// Process right button down
//
   if((mods & MouseEvent.BUTTON3_DOWN_MASK) != 0)
    {
      if(y > oldy+sensitivity)// Mouse moves down screen
        increaseY(-moveAmt);
      if(y < oldy-sensitivity)// Mouse moves up screen
        increaseY(moveAmt);
      if(x > oldx+sensitivity)
        increaseZRotate(turnAmt);
      if(x < oldx-sensitivity)
        increaseZRotate(-turnAmt);
    }
   oldx = x;  // Save for comparison on next mouse move
   oldy = y;
 }

Lessons learned

Java 3D depends on the correct installation of graphics cards and drivers, which it relies on. Before working with Java 3D, I suggest getting the latest version of drivers and firmware for your system. In my case, I use a Pentium 3 with an ATI Radeon AGP graphics adapter running Windows 2000 Professional. For Java 3D to work correctly I had to turn off some of the card's hardware acceleration (the symptom in my case was that the Canvas3D would not refresh when the window resized).

Also, Java 3D can require a lot of memory. Depending on your system's configuration and the Java engine you run, you might (probably will) have to run applications using the -Xmx switch to increase the maximum amount of memory Java is allowed to allocate. I've set my system to always use -Xmx256m so that I don't have to worry about it. The default is only 64 megabytes.

In addition, the demonstration program can be fine-tuned by changing the exaggeration constant to show greater differences in elevations and the SECONDS_PER_SEGMENT constant to have larger or smaller segments in ElevationModel. Also, the resolutions array initial values in LODSegment can be modified to give sharper pictures.

Some specific recommendations for developers:

  • Use the "strip" object forms (TriangleStripArray, LineStripArray, and so on) whenever possible. Interleaved and by-reference options provide the best use of memory and processor resources.
  • Since Java 3D internally converts geometry data to floats, the use of doubles is not warranted.
  • Call the garbage collector once the model has been created. Incurring this overhead predictably during initialization is better than having it start up during use. Opening and closing buffered files and creating geometry can leave a lot of trash.
  • Unless you need a particular method provided by Color3f, Point3f, or Vector3f, store coordinate and normal information as arrays of floats or other native data formats. Object-oriented purists could make a case for the use of standard objects over arrays of primitive data types on ideological grounds, however, doing so might compromise performance.
  • The use of indexed geometry is questionable. While it appears to save memory by not having to store vertex information more than once, the complexity of creating the index arrays (a separate one is required for coordinates, colors, and normals) and the fact that Java 3D converts the data in the indexed arrays to a nonindexed format anyway erases any perceived advantage. Future graphics hardware might support indexed geometry on a cross-platform basis.

Start navigating in Java 3D

In this article, I demonstrated how to efficiently create and navigate through 3D worlds using Java 3D and DEM data files available from USGS. The objects developed for the application demonstrate how to parse the DEM data and create geometry from it (DemFile, ElevationFile); how to create efficient Java 3D data structures using interleaved arrays and level-of-detail representation (ElevationModel, ElevationSegment, LODSegment); and how to create a user interface that allows the user to navigate through the virtual world (View3DPanel, FlyingPlatform). These objects are documented using javadoc and available for download in Resources.

The work presented here is part of a larger ongoing project; its goal is to create a large virtual world based on USGS mapping data that allows real-time display and fly-through capabilities. This virtual world can then be used as a basis for GIS applications in areas such as meteorology, flood and erosion control, wilderness fire-fighting, environmental and growth management, among others. My current work includes creating extensions to the LODSegment and DistanceLOD objects to allow segments to be swapped in and out of memory. This will allow mapping beyond the 1-degree-by-1-degree area to be created and interactively navigated.

Dr. Pendergast is an associate professor at Florida Gulf Coast University. Dr. Pendergast received an MS and PhD from the University of Arizona and a BSE in electrical computer engineering from the University of Michigan. He has worked as an engineer for Control Data Corporation, Harris Corporation, Ventana Corporation, and taught at the University of Florida and Washington State University. His works have appeared in books and journals, and he has presented his work at numerous conferences. He is a member of the ACM (Association for Computer Machinery) and IEEE (Institute of Electrical and Electronics Engineers) computer societies.
Related:
| 1 2 3 4 5 Page 4