CroftSoft / Portfolio / Tag3D

Java 3D with RMI

David Wallace Croft
croft@alumni.caltech.edu

1999-02-07


Index

Introduction

This tutorial reviews the source code used to combine Java 3D with RMI to effect a prototype multi-user online virtual reality.

Code

The source and compiled code for this presentation may be downloaded from
http://www.orbs.com/projects/tag3d/. The source code is available under an Open Source license.

State

I use the State interface class to indicate that I am communicating a unique object state such as its current position in a virtual space. These states are queued for broadcast. If the object's location in space changes again before the previous change could be broadcast, I remove the outdated State objects from the transmission queue when I insert the latest.

     package com.orbs.open.a.util.state;



     /*********************************************************************

     * The state of an object may be communicated by a State object.

     * Each State object has a unique key, usually the object or its unique

     * identifier whose state or a portion of its state is reflected by

     * this State object.

     *

     * State objects are considered equal if their classes and keys are

     * equal.  This makes a State object useful in Set collections where

     * the state or the latest subset of the state of the key object should

     * should only be contained once.  One application is the queued

     * transmission of object state information wherein only the latest

     * state data should be retained.

     *

     * @see

     *   StateLib

     * @author

     *   David W. Croft

     * @version

     *   1999-02-06

     *********************************************************************/



     public interface State

     //////////////////////////////////////////////////////////////////////

     //////////////////////////////////////////////////////////////////////

     {



     /*********************************************************************

     * Returns the State key, usually the object or its unique identifier

     * whose state or a portion of its state is reflected by this State

     * object.

     *********************************************************************/

     public Object  getKey ( );



     /*********************************************************************

     * Returns true if the classes and State keys are equal.

     *********************************************************************/

     public boolean  equals ( Object  other );



     /*********************************************************************

     * Returns the hash code of the State key.

     *********************************************************************/

     public int  hashCode ( );



     //////////////////////////////////////////////////////////////////////

     //////////////////////////////////////////////////////////////////////

     }

Serializing Transform3D

Here is the concrete implementation of the State that I use. Note that the Java 3D class Transform3D cannot be transmitted directly via Remote Method Invocation (RMI) since it is not serializable. Transform3DState, however, is. The Transform3D data, an array of 16 doubles representing the 4 by 4 transform matrix, is conserved as an instance variable within Transform3DState.

     package com.orbs.open.a.media.j3d;



     import java.io.Serializable;



     import javax.media.j3d.Transform3D;



     import com.orbs.open.a.util.state.State;

     import com.orbs.open.a.util.state.StateLib;



     /*********************************************************************

     *

     * A State object that carries Transform3D data.

     *

     * @author

     *   David W. Croft

     * @version

     *   1999-02-06

     *********************************************************************/



     public class  Transform3DState implements State, Serializable

     //////////////////////////////////////////////////////////////////////

     //////////////////////////////////////////////////////////////////////

     {



     private static final long  serialVersionUID = 1L;



     private Object      key;

     private double [ ]  transform;



     //////////////////////////////////////////////////////////////////////

     //////////////////////////////////////////////////////////////////////



     public  Transform3DState (

       Object       key,

       Transform3D  transform3D )

     //////////////////////////////////////////////////////////////////////

     {

       this.key = key;

       transform = new double [ 16 ];

       transform3D.get ( transform );

     }



     //////////////////////////////////////////////////////////////////////

     //////////////////////////////////////////////////////////////////////



     public Object  getKey ( ) { return key; }



     public void  getTransform3D ( Transform3D  transform3D )

     //////////////////////////////////////////////////////////////////////

     {

       transform3D.set ( transform );

     }



     //////////////////////////////////////////////////////////////////////

     //////////////////////////////////////////////////////////////////////



     /*********************************************************************

     * Returns true if the classes and State keys are equal.

     *********************************************************************/

     public boolean  equals ( Object  other )

     //////////////////////////////////////////////////////////////////////

     {

       return StateLib.equals ( this, other );

     }



     /*********************************************************************

     * Returns the hash code of the State key.

     *********************************************************************/

     public int  hashCode ( )

     //////////////////////////////////////////////////////////////////////

     {

       return StateLib.hashCode ( this );

     }



     //////////////////////////////////////////////////////////////////////

     //////////////////////////////////////////////////////////////////////

     }

Transforming the View

In order for the user to "fly" through the virtual space with 6 degrees of freedom (x, y, z, pitch, yaw, roll), we need methods to manipulate the user's current view transform. Note that the rotation and translation operations below act upon the axes relative to the user's orientation, as opposed to the universal axes. That way, if the user is hanging "upside-down" relative to the rest of the universe and he presses the up arrow, he sees himself as going up even though he is going down in universal coordinates.

     package com.orbs.open.a.media.j3d;



     import java.awt.event.KeyEvent;



     import javax.media.j3d.*;

     import javax.vecmath.*;



     /*********************************************************************

     *

     * Static method library to manipulate Transform3D data.

     * Primarily focuses on transforming a view through the 6 degrees of

     * freedom.

     *

     * Reference:

     * 

     * Foley, et al., Computer Graphics:  Principles and Practice.

     * Provides the mathematics for the 3D rotation matrices.

     *

     * @author

     *   David W. Croft

     * @version

     *   1998-12-06

     *********************************************************************/



     public class  Transform3DLib

     //////////////////////////////////////////////////////////////////////

     //////////////////////////////////////////////////////////////////////

     {



     /** Relative X axis */

     public static final int  X = 0;



     /** Relative Y axis */

     public static final int  Y = 1;



     /** Relative Z axis */

     public static final int  Z = 2;



     private  Transform3DLib ( ) { }



     //////////////////////////////////////////////////////////////////////

     //////////////////////////////////////////////////////////////////////



     /*********************************************************************

     * Rotates the view by the given rotation matrix.

     *

     * @param  transform3D

     *   The transform containing the current rotation matrix.

     * @param  rotation

     *   The rotation matrix to be muliplied.

     * @return

     *    The transform3D argument has its current rotation matrix replaced

     *    by its old value as multiplied by the rotation argument.

     *********************************************************************/

     public static void  viewRotate (

       Transform3D  transform3D, Matrix3d  rotation )

     //////////////////////////////////////////////////////////////////////

     {

       Matrix3d  oldRotation = new Matrix3d ( );

       transform3D.get ( oldRotation );

       oldRotation.mul ( rotation );

       transform3D.setRotation ( oldRotation );

     }



     /*********************************************************************

     * Pitches the view by a given number of radians.

     *

     * @param  transform3D

     *   The transform containing the current rotation matrix.

     * @param  radians

     *   The angle to rotate the transform about its relative X axis.

     * @return

     *    The rotation matrix is updated in the transform3D argument.

     *********************************************************************/

     public static void  viewPitch (

       Transform3D  transform3D, double  radians )

     //////////////////////////////////////////////////////////////////////

     {

       double  sin = Math.sin ( radians );

       double  cos = Math.cos ( radians );



       viewRotate ( transform3D, new Matrix3d (

         1.0, 0.0,  0.0,

         0.0, cos, -sin,

         0.0, sin,  cos ) );

     }



     /*********************************************************************

     * Yaws the view by a given number of radians.

     *

     * @param  transform3D

     *   The transform containing the current rotation matrix.

     * @param  radians

     *   The angle to rotate the transform about its relative Y axis.

     * @return

     *    The rotation matrix is updated in the transform3D argument.

     *********************************************************************/

     public static void  viewYaw (

       Transform3D  transform3D, double  radians )

     //////////////////////////////////////////////////////////////////////

     {

       double  sin = Math.sin ( radians );

       double  cos = Math.cos ( radians );



       viewRotate ( transform3D, new Matrix3d (

          cos, 0.0, sin,

          0.0, 1.0, 0.0,

         -sin, 0.0, cos ) );

     }



     /*********************************************************************

     * Rolls the view by a given number of radians.

     *

     * @param  transform3D

     *   The transform containing the current rotation matrix.

     * @param  radians

     *   The angle to rotate the transform about its relative Z axis.

     * @return

     *    The rotation matrix is updated in the transform3D argument.

     *********************************************************************/

     public static void  viewRoll  (

       Transform3D  transform3D, double  radians )

     //////////////////////////////////////////////////////////////////////

     {

       double  sin = Math.sin ( radians );

       double  cos = Math.cos ( radians );



       viewRotate ( transform3D, new Matrix3d (

         cos, -sin, 0.0,

         sin,  cos, 0.0,

         0.0,  0.0, 1.0 ) );

     }



     /*********************************************************************

     * Translates the view by a given distance along a relative axis.

     *

     * @param  transform3D

     *   The transform containing the current rotation and translation

     *   matrix.

     * @param  dimension

     *   The relative axis to translate along.

     *   Use the public constants X, Y, and Z provided by this class.

     * @return

     *    The translation vector is updated in the transform3D argument.

     *********************************************************************/

     public static void  viewTranslate (

       Transform3D  transform3D,

       int          dimension,

       double       distance )

     //////////////////////////////////////////////////////////////////////

     {

       Matrix3d  rotation    = new Matrix3d ( );

       Vector3d  translation = new Vector3d ( );

       transform3D.get ( rotation, translation );



       // Get the alignment of the rotated relative axes.

       Vector3d  r = new Vector3d ( );

       rotation.getColumn ( dimension, r );



       r.scale ( distance );



       translation.add ( r );



       transform3D.setTranslation ( translation );

     }



     /*********************************************************************

     * Transforms the view based upon a user keyboard input.

     *

     * @param  transform3D

     *   The transform containing the current rotation and translation

     *   matrices, usually the user's view transform.

     * @param  keyEvent

     *    Arrow keys in combination with no other key, the Shift key, or

     *    the Alt key will rotate or translate the transform about the

     *    relative X, Y, and Z axes respectively for 6 degrees of freedom.

     * @param  deltaRotation

     *    The number of radians to rotate the view upon a keyboard input.

     * @param  deltaTranslation

     *    The distance to translate the view upon a keyboard input.

     * @return

     *    Returns true if any changes were made to transform3D argument.

     *********************************************************************/

     public static boolean  viewKeyPressed (

       Transform3D  transform3D,

       KeyEvent     keyEvent,

       double       deltaRotation,

       double       deltaTranslation )

     //////////////////////////////////////////////////////////////////////

     {

       boolean  moved = false;



       int  keyCode = keyEvent.getKeyCode ( );



       if ( keyEvent.isAltDown ( ) )

       {

         switch ( keyCode )

         {

           case KeyEvent.VK_UP   :

             viewTranslate ( transform3D, Z, -deltaTranslation );

             moved = true;

             break;

           case KeyEvent.VK_DOWN :

             viewTranslate ( transform3D, Z,  deltaTranslation );

             moved = true;

             break;

           case KeyEvent.VK_LEFT :

             viewRoll ( transform3D,  deltaRotation );

             moved = true;

             break;

           case KeyEvent.VK_RIGHT:

             viewRoll ( transform3D, -deltaRotation );

             moved = true;

             break;

         }

       }

       else if ( keyEvent.isShiftDown ( ) )

       {

         switch ( keyCode )

         {

           case KeyEvent.VK_UP   :

             viewTranslate (

               transform3D, Y,  deltaTranslation );

             moved = true;

             break;

           case KeyEvent.VK_DOWN :

             viewTranslate (

               transform3D, Y, -deltaTranslation );

             moved = true;

             break;

           case KeyEvent.VK_LEFT :

             viewYaw ( transform3D,  deltaRotation );

             moved = true;

             break;

           case KeyEvent.VK_RIGHT:

             viewYaw ( transform3D, -deltaRotation );

             moved = true;

             break;

         }

       }

       else

       {

         switch ( keyCode )

         {

           case KeyEvent.VK_UP   :

             viewPitch  ( transform3D, -deltaRotation );

             moved = true;

             break;

           case KeyEvent.VK_DOWN :

             viewPitch  ( transform3D,  deltaRotation );

             moved = true;

             break;

           case KeyEvent.VK_LEFT :

             viewTranslate (

               transform3D, X, -deltaTranslation );

             moved = true;

             break;

           case KeyEvent.VK_RIGHT:

             viewTranslate (

               transform3D, X,  deltaTranslation );

             moved = true;

             break;

         }

       }



       return moved;

     }



     //////////////////////////////////////////////////////////////////////

     //////////////////////////////////////////////////////////////////////

     }

Relaying the Updates

When a user enters a keyboard command to transform his view, a new Transform3DState object is created and sent to the StateMulticaster for broadcast to the listeners, whether they be local or remote.

Note that although this uses the current view transform to determine the update, it does not directly modify the view. Instead, the view is updated elsewhere and only later after it has been received via transmission from the StateMulticaster. This means that the client generating the keyboard events to control the view will not see the updates reflected until they have been relayed or queued for relay to all of the other clients in the same universe.

This could cause a problem if there is, for example, a keyboard input to go left immediately followed by a keyboard input to go right before the left view transform update could be propagated; the view will appear to shift left once then teleport two shifts to the right. It may be necessary to discard keyboard inputs to resolve this delay problem although the code currently does not do so.


     package com.orbs.open.a.app.tag3d;



     import java.awt.event.KeyEvent;

     import java.awt.event.KeyListener;



     import javax.media.j3d.Transform3D;

     import javax.media.j3d.TransformGroup;



     import com.orbs.open.a.media.j3d.Transform3DLib;

     import com.orbs.open.a.media.j3d.Transform3DState;

     import com.orbs.open.a.util.state.StateMulticaster;



     /*********************************************************************

     * Handles keyboard events from the Canvas3D.

     *

     * @author

     *   David W. Croft

     * @version

     *   1999-02-07

     *********************************************************************/



     public class  Tag3DKeyListener implements KeyListener

     //////////////////////////////////////////////////////////////////////

     //////////////////////////////////////////////////////////////////////

     {



     protected double            deltaRotation;

     protected double            deltaTranslation;

     protected String            id;

     protected StateMulticaster  stateMulticaster;

     protected TransformGroup    viewTransformGroup;



     //////////////////////////////////////////////////////////////////////

     //////////////////////////////////////////////////////////////////////



     /*********************************************************************

     * How keyboard input view effects and where the updates are broadcast.

     *

     * @param  deltaRotation

     *   The number of radians to rotate the view upon a keyboard input.

     * @param  deltaTranslation

     *   The distance to translate the view upon a keyboard input.

     * @param  id

     *   The unique identifier for the object controlled by the keyboard.

     * @param  stateMulticaster

     *   Destination for Transform3DState updates for this view.

     * @param  viewTransformGroup

     *   Contains the current view transform for the object controlled.

     *********************************************************************/

     public  Tag3DKeyListener (

       double            deltaRotation,

       double            deltaTranslation,

       String            id,

       StateMulticaster  stateMulticaster,

       TransformGroup    viewTransformGroup )

     //////////////////////////////////////////////////////////////////////

     {

       this.deltaRotation      = deltaRotation;

       this.deltaTranslation   = deltaTranslation;

       this.id                 = id;

       this.stateMulticaster   = stateMulticaster;

       this.viewTransformGroup = viewTransformGroup;

     }



     //////////////////////////////////////////////////////////////////////

     // KeyListener interface methods

     //////////////////////////////////////////////////////////////////////



     /*********************************************************************

     * Interprets view transform inputs and broadcasts the updates.

     *

     * @param  keyEvent

     *   Arrow keys in combination with no other key, the Shift key, or

     *   the Alt key will rotate or translate the transform about the

     *   relative X, Y, and Z axes respectively for 6 degrees of freedom.

     * @return

     *   Sends a Transform3DState update to the StateMulticaster if needed.

     *********************************************************************/

     public synchronized void  keyPressed ( KeyEvent  keyEvent )

     //////////////////////////////////////////////////////////////////////

     {

       Transform3D  transform3D = new Transform3D ( );

       viewTransformGroup.getTransform ( transform3D );



       if ( Transform3DLib.viewKeyPressed (

         transform3D, keyEvent, deltaRotation, deltaTranslation ) )

       {

         stateMulticaster.update (

           new Transform3DState ( id, transform3D ) );

       }

     }



     public void  keyReleased ( KeyEvent  keyEvent ) { }

     public void  keyTyped    ( KeyEvent  keyEvent ) { }



     //////////////////////////////////////////////////////////////////////

     //////////////////////////////////////////////////////////////////////

     }

State Update Management and Remote Setup

The StateListener object listens for Transform3DState update events as broadcast from the central StateMulticaster. When an update is received, the State key is accessed to identify which object in the virtual space has moved. The TransformGroup associated with that key is then updated. The update is soon reflected visually by either a change in the field of view, in the case of an update of the user's local client, or movement of one of the objects in space within the user's field of view, in the case of an update of a remote client.

If the address of a remote object, in the form of an RMI URL, is given to serve as a remote central StateMulticaster, all clients within the same universe attach themselves as listeners and send their updates to that StateMulticaster. Note that I am using remote interfaces for both the server and the client so the rmiregistry utility must be running on all machines. If the the rmiregistry were not running on the client, the client might have to periodically poll the server for updates instead of being event driven.

Another alternative, not implemented in this source code, is to have the server, upon receipt of an State update, send a UDP broadcast to the clients to "awaken" them so that they might connect to the server only when needed. This removes the need for a client-side rmiregistry process.




     package com.orbs.open.a.app.tag3d;



     import java.rmi.*;

     import java.util.HashMap;

     import java.util.Map;



     import javax.media.j3d.*;



     import com.orbs.open.a.media.j3d.Transform3DState;

     import com.orbs.open.a.util.state.*;



     /*********************************************************************

     * Bidirectional routing of Transform3DState updates.

     *

     * @author

     *   David W. Croft

     * @version

     *   1999-02-07

     *********************************************************************/



     public class  Tag3DStateManager

       implements StateListener, StateMulticaster

     //////////////////////////////////////////////////////////////////////

     //////////////////////////////////////////////////////////////////////

     {



     protected Map               stateMap = new HashMap ( );



     protected StateMulticaster  stateMulticaster;

     protected Tag3DWorld        tag3DWorld;

     protected String            id;



     //////////////////////////////////////////////////////////////////////

     //////////////////////////////////////////////////////////////////////



     /*********************************************************************

     * Establishes the objects for bidirectional communication.

     *

     * @param  stateMulticaster

     *   All of Tag3DStateManager StateMulticaster interface method calls

     *   are relayed to the StateMulticaster implementation provided by

     *   this argument.

     * @param  tag3DWorld

     *   Updated when the State events are received via the StateListener

     *   method.

     * @param  id

     *   The unique identifier for this particular client.

     *********************************************************************/

     public  Tag3DStateManager (

       StateMulticaster  stateMulticaster,

       Tag3DWorld        tag3DWorld,

       String            id )

     //////////////////////////////////////////////////////////////////////

     {

       this.stateMulticaster = stateMulticaster;

       this.tag3DWorld       = tag3DWorld;

       this.id               = id;



       stateMap.put ( id, tag3DWorld.viewTransformGroup );



       addStateListener ( this );

     }



     /*********************************************************************

     * this ( new QueuedStateMulticaster ( ), tag3DWorld, id );

     *********************************************************************/

     public  Tag3DStateManager (

       Tag3DWorld        tag3DWorld,

       String            id )

     //////////////////////////////////////////////////////////////////////

     {

       this ( new QueuedStateMulticaster ( ), tag3DWorld, id );

     }



     //////////////////////////////////////////////////////////////////////

     // StateListener interface method

     //////////////////////////////////////////////////////////////////////



     /*********************************************************************

     * Handles Transform3DState update events from the StateMulticaster.

     * Updates the appropriate transformGroup in the stateMap as keyed by

     * the State key.  If the key does not exist in the stateMap, updates

     * the tag3DWorld with a newly created transformGroup and adds it to

     * the stateMap.

     *********************************************************************/

     public void  stateListen ( State  state )

     //////////////////////////////////////////////////////////////////////

     {

       if ( !( state instanceof Transform3DState ) ) return;



       String  key = ( String ) state.getKey ( );

       Transform3D  transform3D = new Transform3D ( );

       ( ( Transform3DState ) state ).getTransform3D ( transform3D );



       TransformGroup  transformGroup = null;



       synchronized ( stateMap )

       {

         transformGroup = ( TransformGroup ) stateMap.get ( key );



         // If no transformGroup currently exists for this I.D. key,

         // create one; otherwise, update it.



         if ( transformGroup == null )

         {

           transformGroup = tag3DWorld.addHead ( key, transform3D );

           stateMap.put ( key, transformGroup );

         }

         else

         {

           transformGroup.setTransform ( transform3D );

         }

       }

     }



     //////////////////////////////////////////////////////////////////////

     // StateMulticaster interface methods

     //////////////////////////////////////////////////////////////////////



     public void  update ( State  state )

     //////////////////////////////////////////////////////////////////////

     {

       stateMulticaster.update ( state );

     }



     public boolean  addStateListener ( StateListener  stateListener )

     //////////////////////////////////////////////////////////////////////

     {

       return stateMulticaster.addStateListener ( stateListener );

     }



     public boolean  removeStateListener ( StateListener  stateListener )

     //////////////////////////////////////////////////////////////////////

     {

       return stateMulticaster.removeStateListener ( stateListener );

     }



     //////////////////////////////////////////////////////////////////////

     //////////////////////////////////////////////////////////////////////



     /*********************************************************************

     * Sets up this Tag3DStateManager object to use a remote

     * StateMulticaster and subscribes itself as a remote StateListener.

     *

     * If the remote StateMulticaster cannot be contacted, an attempt will

     * be made to establish this Tag3DStateManager object as the remote

     * StateMulticaster.

     *

     * @param  remoteName

     *   The RMI URL of the remote StateMulticaster.

     *********************************************************************/

     public void  setupRemote ( String  remoteName )

     //////////////////////////////////////////////////////////////////////

     {

       try

       {

         StateMulticasterRemote  stateMulticasterRemote = null;



         try

         {

           stateMulticasterRemote

             = ( StateMulticasterRemote ) Naming.lookup ( remoteName );

           stateMulticaster

             = new StateMulticasterAmbassador ( stateMulticasterRemote );

           StateListenerRemote  stateListenerRemote

             = new StateListenerProxy ( this );

           Naming.rebind ( id, stateListenerRemote );

           addStateListener ( new StateListenerAmbassador (

             stateListenerRemote ) );

         }

         catch ( Exception  ex )

         {

         }



         if ( stateMulticasterRemote == null )

         {

           stateMulticasterRemote = new StateMulticasterProxy ( this );

           Naming.rebind ( remoteName, stateMulticasterRemote );

         }

       }

       catch ( Exception  ex )

       {

         ex.printStackTrace ( );

       }

     }



     //////////////////////////////////////////////////////////////////////

     //////////////////////////////////////////////////////////////////////

     }

Further Information


Open Content

© 1999 David Wallace Croft
Distributed under the terms of the OpenContent License (OPL).
http://www.orbs.com/projects/tag3d/j3drmi/
David Wallace Croft, croft@alumni.caltech.edu
First posted 1999-02-07.