Java 3D Programming by Daniel Selman - HTML preview

PLEASE NOTE: This is an HTML preview only and some elements such as links or page numbers may be incorrect.
Download the book in PDF, ePub, Kindle for a complete version.

CHAPTER 13

Writing custom behaviors

13.1 The BehaviorTest example

13.2 ObjectSizeBehavior

13.3 ExplodeBehavior

13.4 StretchBehavior

13.5 Using behaviors for debugging

13.6 Summary

Some behaviors automatically execute complex code to modify objects within the scenegraph, so care must be taken to ensure that behavior processing does not bog down application performance. With careful design and knowledge of some of the limitations, behaviors can be a powerful asset in quickly developing or prototyping application logic.

By the end of this chapter you should have a good sense of how to develop your own behaviors. By mixing and matching your own and the built-in behaviors, you should be able to design your application logic within Java 3D’s behavior model.

13.1 The BehaviorTest example

There are occasions when the built-in behaviors do not provide enough functionality to capture the logic of your application. By creating your own classes derived from Behavior, you can easily integrate your application logic into Java 3D’s behavior processing framework.

The BehaviorTest example application uses four behaviors: the built-in RotationInterpolator and three custom behaviors of varying complexity: ObjectSizeBehavior, ExplodeBehavior, and StretchBehavior. See figure 13.1.

img163.png

Figure 13.1 The BehaviorTest example application. StretchBehavior is used to modify the geometry of the Sphere after every frame, while ObjectSizeBehavior reports the Bounds for the object after every 20 frames

ObjectSizeBehavior is the simplest, and it calculates the smallest BoundingBox that encloses a Shape3D’s geometry. The BoundingBox is recalculated every 20 frames, and the size of the BoundingBox is written to standard output. Note that the basic anatomy of a behavior described in section 11.3 is adhered to here.

ExplodeBehavior is more complex. Given a Shape3D object, it explodes the object after a specified number of milliseconds by rendering the Shape3D as points and modifying the coordinates within the Shape3D’s GeometryArray. The transparency of the object is gradually increased so that the object fades into the background.

StretchBehavior is the most complex of the custom behaviors. It operates upon a specified GeometryArray and animates the vertices within the array as if they were weights attached by springs to the origin. StretchBehavior listens for key presses and increases the acceleration of each vertex when a key is pressed. The increased acceleration causes the vertices to move away from the origin, which causes an increase in the restraining force from the spring. The vertices oscillate back and forth, finally coming to rest at their original position.

13.2 ObjectSizeBehavior

The ObjectSizeBehavior class implements a simple behavior that calculates and prints the size of an object based on the vertices in its GeometryArray.

 From ObjectSizeBehavior.java

class ObjectSizeBehavior extends Behavior

{

//the wake up condition for the behavior

protected WakeupCondition   m_WakeupCondition = null;

//the GeometryArray for the Shape3D that we are querying

protected GeometryArray   m_GeometryArray = null;

//cache some information on the model to save reallocation

protected float[]       m_CoordinateArray = null;

protected BoundingBox    m_BoundingBox = null;

protected Point3d       m_Point = null;;

public ObjectSizeBehavior( GeometryArray geomArray )

{

 //save the GeometryArray that we are modifying

 m_GeometryArray = geomArray;

 //set the capability bits that the behavior requires

 m_GeometryArray.setCapability(

  GeometryArray.ALLOW_COORDINATE_READ );

 m_GeometryArray.setCapability(

  GeometryArray.ALLOW_COUNT_READ );

 //allocate an array for the coordinates

 m_CoordinateArray =

  new float[ 3 * m_GeometryArray.getVertexCount() ];

 //create the BoundingBox used to calculate the size of the

 object m_BoundingBox = new BoundingBox();

 //create a temporary point

 m_Point = new Point3d();

 //create the WakeupCriterion for the behavior

 WakeupCriterion criterionArray[] = new WakeupCriterion[1];

  criterionArray[0] = new WakeupOnElapsedFrames( 20 );

 //save the WakeupCriterion for the behavior

 m_WakeupCondition = new WakeupOr( criterionArray );

}

public void initialize()

{

 //apply the initial WakeupCriterion

 wakeupOn( m_WakeupCondition );

}

public void processStimulus( java.util.Enumeration criteria )

{

 while( criteria.hasMoreElements() )

 {

  WakeupCriterion wakeUp =

   (WakeupCriterion) criteria.nextElement();

  //every N frames, recalculate the bounds for the points

  //in the GeometryArray

  if( wakeUp instanceof WakeupOnElapsedFrames )

  {

   //get all the coordinates

   m_GeometryArray.getCoordinates( 0, m_CoordinateArray );

   //clear the old BoundingBox

   m_BoundingBox.setLower( 0,0,0 );

   m_BoundingBox.setUpper( 0,0,0 );

   //loop over every vertex and combine with the BoundingBox

   for( int n = 0; n <m_CoordinateArray.length; n+=3 )

   {

    m_Point.x = m_CoordinateArray[n];

    m_Point.y = m_CoordinateArray[n+1];

    m_Point.z = m_CoordinateArray[n+2];

    m_BoundingBox.combine( m_Point );

   }

   System.out.println( "BoundingBox: " + m_BoundingBox );

  }

 }

 //assign the next WakeUpCondition, so we are notified again

 wakeupOn( m_WakeupCondition );

}

}

_________________________________________________________________

To use the behavior one could write:

Sphere sphere = new Sphere( 3, Primitive.GENERATE_NORMALS | Primitive.GENERATE_TEXTURE_COORDS,

m_SizeBehavior = new ObjectSizeBehavior( (GeometryArray) sphere.getShape().getGeometry() );

m_SizeBehavior.setSchedulingBounds( getApplicationBounds() );

objRoot.addChild( m_SizeBehavior );

This code snippet creates the behavior and passes the geometry for a Sphere to the constructor, sets the scheduling bounds for the behavior and adds it to the scenegraph. Do not forget to add the behavior to the scenegraph, or it will not get scheduled.

Output from the behavior is simply:

Bounding box: Lower=-5.048 -5.044 -5.069 Upper=5.040 5.060 5.069

Bounding box: Lower=-5.048 -5.044 -5.069 Upper=5.040 5.060 5.069

Bounding box: Lower=-5.048 -5.044 -5.069 Upper=5.040 5.060 5.069

The behavior verifies the size of the geometry for the Shape3D every 20 frames. Note that the behavior follows the general anatomy of a behavior as was described in section 11.3.

When writing a behavior you should be very aware of the computational cost of the processing within the processStimulus method and how often the behavior is likely to be invoked. The ObjectSizeBehavior’s processStimulus method is called once every 20 frames, so any processing that is performed is going to have a fairly big impact on application performance. Whenever possible, avoid creating Objects (using the new operator) within the processStimulus method if it is going to be invoked frequently. Any Objects created by the behavior using the new operator and not assigned to a member variable will have to be garbage-collected. Not only is creating objects a relatively costly operation, but garbage collection can cause your application to noticeably pause during rendering.

For example, instead of creating a new BoundingBox, which would have had size 0, a single BoundingBox object was resized using:

m_BoundingBox.setLower( 0,0,0 );

m_BoundingBox.setUpper( 0,0,0 );

With Java 3D in general, you should avoid burning (allocate, followed by garbage-collect) Objects as much as possible, and minimize the work that the garbage collector has to perform.

13.3 ExplodeBehavior

The constructor for the ExplodeBehavior is as follows:

public ExplodeBehavior( Shape3D shape3D,

                        int nElapsedTime,int nNumFrames,

                        ExplosionListener listener )

The behavior attaches to the Shape3D specified and explodes the object after nElapsedTime milliseconds (figure 13.2). The explosion animation takes nNumFrames to complete, and, once complete, a notification is passed to the caller via an ExplosionListener interface method.

img164.png

Figure 13.2 The ExplodeBehavior: Frame 1, the original Shape3D; frames 2–4, some frames of the explosion animation

To model the simple explosion, the behavior switches the Shape3D’s appearance to rendering in points (by modifying the PolygonAttributes) and sets the point size (using pointattributes). the transparency of the shape3d is then set using transparencyattributes. the vertices of the shape3d’s geometry are then moved away from the origin with a slight random bias in the x+, y+, and z+ direction.

the explodebehavior moves through the following life cycle:

  1. the behavior is created.

  2. initialize is called by java 3d.

  3. wakeup condition is set to be wakeuponelapsedtime( n milliseconds ).

  4. processstimulus is called after n milliseconds.

  5. the appearance attributes are modified for the shape3d.

  6. the wakeup condition is set to wakeuponelapsedframes( 1 ).

  7. processstimulus is called after every frame.

  8. the geometryarray’s vertex coordinates is modified.

  9. coordinates are reassigned.

 10. If frame number < number of frames for animation

  • Set the WakeUp condition to WakeupOnElapsedFrames( 1 )

 11. Else

  • Restore the original Shape3D Appearance and coordinates.
  • Notify the ExplosionListener that the behavior is done.
  • Call setEnable( false ) to disabled the behavior.

The processStimulus method for the ExplodeBehavior is as follows.

 From ExplodeBehavior.java

public void processStimulus( java.util.Enumeration criteria )

{

 while( criteria.hasMoreElements() )

 {

  WakeupCriterion wakeUp =

    (WakeupCriterion) criteria.nextElement();

  if( wakeUp instanceof WakeupOnElapsedTime )

  {

   //we are starting the explosion,

   //apply the appearance changes we require

   PolygonAttributes polyAttribs =

    new PolygonAttributes( PolygonAttributes.POLYGON_POINT,

                           PolygonAttributes.CULL_NONE, 0 );

   m_Shape3D.getAppearance().setPolygonAttributes( polyAttribs

   );

   PointAttributes pointAttribs = new PointAttributes( 3, false

   );

   m_Shape3D.getAppearance().setPointAttributes( pointAttribs );

   m_Shape3D.getAppearance().setTexture( null );

   m_TransparencyAttributes =

    new TransparencyAttributes( TransparencyAttributes.NICEST, 0

    );

   m_TransparencyAttributes.setCapability(

    TransparencyAttributes.ALLOW_VALUE_WRITE );

   m_Shape3D.getAppearance().setTransparencyAttributes(

    m_TransparencyAttributes );

  }

  else

  {

   //we are mid explosion, modify the GeometryArray

   m_nFrameNumber++;

   m_GeometryArray.getCoordinates( 0, m_CoordinateArray );

   m_TransparencyAttributes.

    setTransparency( ((float) m_nFrameNumber) /

   ((float) m_nNumFrames) );

   m_Shape3D.getAppearance().

    setTransparencyAttributes( m_TransparencyAttributes );

   for( int n = 0; n <m_CoordinateArray.length; n+=3 )

   {

    m_Vector.x = m_CoordinateArray[n];

    m_Vector.y = m_CoordinateArray[n+1];

    m_Vector.z = m_CoordinateArray[n+2];

    m_Vector.normalize();

    m_CoordinateArray[n] += m_Vector.x * Math.random() +

     Math.random();

    m_CoordinateArray[n+1] += m_Vector.y * Math.random() +

     Math.random();

    m_CoordinateArray[n+2] += m_Vector.z * Math.random() +

     Math.random();

   }

   //assign the new coordinates

   m_GeometryArray.setCoordinates( 0, m_CoordinateArray );

  }

 }

 if( m_nFrameNumber  <m_nNumFrames )

 {

  //assign the next WakeUpCondition, so we are notified again

  wakeupOn( m_FrameWakeupCondition );

 }

 else

 {

  //we are at the end of the explosion

  //reapply the original appearance and GeometryArray

   coordinates setEnable( false );

  m_Shape3D.setAppearance( m_Appearance );

  m_GeometryArray.setCoordinates( 0, m_OriginalCoordinateArray

  );

  m_OriginalCoordinateArray = null;

  m_GeometryArray = null;

  m_CoordinateArray = null;

  m_TransparencyAttributes = null;

  //if we have a listener notify them that we are done

  if( m_Listener != null )

  wakeupOn( m_Listener.onExplosionFinished( this, m_Shape3D ) );

 }

________________________________________________________________

13.4 StretchBehavior

StretchBehavior implements a more complex behavior. The behavior modifies the coordinates within a GeometryArray based on simulated forces applied to the geometric model. Forces are modeled as springs from the origin to every vertex. Every vertex has a mass and an applied force, and hence an acceleration. Pressing a key will increase the acceleration at each vertex, upsetting the force equilibrium at vertices. The model will then start to oscillate in size under the influence of the springs. Because there are variations in mass between vertices, the model will distort slightly as it oscillates—the heavier vertices displacing less than the lighter ones. A damping effect is modeled by losing a portion of the vertex acceleration after each iteration. See figure 13.3.

img165.png

Figure 13.3 The StretchBehavior: Frame 1, the original Shape3D; frames 2–4, the vertices within the Shape3D’s geometry are oscillating as the vertices are affected by the springs from each vertex to the origin. The model is a Sphere primitive with an applied texture image. The Sphere was created with a resolution value of 32

NOTE

This is a computationally expensive behavior.

StretchBehavior responds to two WakeUp conditions: after every frame and after a key press. The WakeUp conditions for the behavior are specified as follows:

//create the WakeupCriterion for the behavior

WakeupCriterion criterionArray[] = new WakeupCriterion[2];

criterionArray[0] = new WakeupOnAWTEvent( KeyEvent.KEY_PRESSED);

criterionArray[1] = new WakeupOnElapsedFrames( 1 );

//save the WakeupCriterion for the behavior

m_WakeupCondition = new WakeupOr( criterionArray );

As usual, the WakeupCriterion is passed to the behavior inside the initialize method:

public void initialize()

{

 //apply the initial WakeupCriterion

 wakeupOn( m_WakeupCondition );

}

The processStimulus method of the behavior, which is called on every frame and in response to a key press, performs all the basic physics calculations and updates the positions of the coordinates within the GeometryArray.

 From StretchBehavior.java

public void processStimulus( java.util.Enumeration criteria )

{

 //update the positions of the vertices—regardless of criteria

 float elongation = 0;

 float force_spring = 0;

 float force_mass = 0;

 float force_sum = 0;

 float timeFactor = 0.1f;

 float accel_sum = 0;

 //loop over every vertex and calculate its new position

 //based on the sum of forces due to acceleration and the spring

 for( int n = 0; n <m_CoordinateArray.length; n+=3 )

 {

  m_Vector.x = m_CoordinateArray[n];

  m_Vector.y = m_CoordinateArray[n+1];

  m_Vector.z = m_CoordinateArray[n+2];

  //use squared lengths, as sqrt is costly

  elongation = m_LengthArray[n/3] - m_Vector.lengthSquared();

  //Fspring = k*Le

  force_spring = m_kSpringConstant * elongation;

  force_mass = m_AccelerationArray[n/3] * m_MassArray[n/3];

  //calculate resultant force

  force_sum = force_mass + force_spring;

  //a = F/m

  m_AccelerationArray[n/3] = (force_sum / m_MassArray[n/3]) *

  m_kAccelerationLossFactor;

  accel_sum += m_AccelerationArray[n/3];

  m_Vector.normalize();

  //apply a portion of the acceleration as change

  //in coordinate based on the normalized vector

  //from the origin to the vertex

  m_CoordinateArray[n] +=

   m_Vector.x * timeFactor * m_AccelerationArray[n/3];

  m_CoordinateArray[n+1] +=

   m_Vector.y * timeFactor * m_AccelerationArray[n/3];

  m_CoordinateArray[n+2] +=

   m_Vector.z * timeFactor * m_AccelerationArray[n/3];

 }

 //assign the new coordinates

 m_GeometryArray.setCoordinates( 0, m_CoordinateArray );

 while( criteria.hasMoreElements() )

 {

  WakeupCriterion wakeUp =

   (WakeupCriterion) criteria.nextElement();

  //if a key was pressed increase the acceleration at the

  vertices

  //a little to upset the equilibrium

  if( wakeUp instanceof WakeupOnAWTEvent )

  {

   for( int n = 0; n  <m_AccelerationArray.length; n++ )

    m_AccelerationArray[n] += 0.3f;

  }

  else

  {

   //otherwise, print the average acceleration

   System.out.print( "Average acceleration:\t"

   + accel_sum/m_AccelerationArray.length + "\n" );

  }

 }

 //assign the next WakeUpCondition, so we are notified again

 wakeupOn( m_WakeupCondition );

}

_________________________________________________________________

After pressing a key has disturbed the equilibrium of the model, it can take a considerable length of time to return to equilibrium. In figure 13.4 the model took over 500 frames to stabilize.

img166.png

Figure 13.4 The StretchBehavior causes the Sphere to oscillate in size. By plotting the average vertex acceleration, you can see that the model took in excess of 500 frames to stabilize. The parameters used were Spring Constant 0.8, Acceleration Loss Factor 0.98, and Vertex Mass 50 + 2.5 (average)

13.5 Using behaviors for debugging

A library of custom Behavior classes can be a very useful debugging aid, as they can be quickly added and removed from the scenegraph as needed. It is a simple step to conditionally add the debugging behaviors for development builds and remove them for production builds. For example, I have used the following two behaviors extensively:

1. BoundsBehavior is attached to a scenegraph Node and creates a wire frame ColorCube or Sphere to graphically represent the Bounds (BoundingBox or BoundingSphere) for the object at runtime.

2. FpsBehavior can be added anywhere in the scenegraph and writes the rendered FPS to the standard output window.

Both behaviors can be found in the org.selman.java3d.book package and are illustrated in the BehaviorTest example application.

13.5.1 Calculating the rendered FPS using a behavior

A useful method of displa