Java 3D with RMI
David Wallace Croft
croft@alumni.caltech.edu
1999-02-07
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.
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.
© 1999
David Wallace Croft
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.
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.
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
1999; Brown, et al.
1997; Rushforth, et al.
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.