Displays a simple driving type game scene, using texture mapped cubes. It assigns an Avatar to the viewer and incorporates simple sounds and collision detection/notification.


{mosimage}

 import java.applet.Applet;
import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.Enumeration;
import java.util.Vector;

import javax.media.j3d.Alpha;
import javax.media.j3d.Appearance;
import javax.media.j3d.Background;
import javax.media.j3d.BackgroundSound;
import javax.media.j3d.Behavior;
import javax.media.j3d.BoundingSphere;
import javax.media.j3d.Bounds;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.GeometryArray;
import javax.media.j3d.Group;
import javax.media.j3d.MediaContainer;
import javax.media.j3d.Node;
import javax.media.j3d.PhysicalEnvironment;
import javax.media.j3d.PointSound;
import javax.media.j3d.PositionInterpolator;
import javax.media.j3d.QuadArray;
import javax.media.j3d.Shape3D;
import javax.media.j3d.Sound;
import javax.media.j3d.Texture;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.media.j3d.WakeupCondition;
import javax.media.j3d.WakeupCriterion;
import javax.media.j3d.WakeupOnAWTEvent;
import javax.media.j3d.WakeupOnCollisionEntry;
import javax.media.j3d.WakeupOnCollisionExit;
import javax.media.j3d.WakeupOr;
import javax.vecmath.Point2f;
import javax.vecmath.Point3d;
import javax.vecmath.Point3f;
import javax.vecmath.Quat4f;
import javax.vecmath.Vector3d;
import javax.vecmath.Vector3f;
import javax.vecmath.Vector4f;

import com.sun.j3d.audioengines.javasound.JavaSoundMixer;
import com.sun.j3d.loaders.Scene;
import com.sun.j3d.loaders.objectfile.ObjectFile;
import com.sun.j3d.utils.behaviors.interpolators.RotPosScaleTCBSplinePathInterpolator;
import com.sun.j3d.utils.behaviors.interpolators.TCBKeyFrame;
import com.sun.j3d.utils.geometry.Box;
import com.sun.j3d.utils.geometry.Primitive;
import com.sun.j3d.utils.geometry.Sphere;
import com.sun.j3d.utils.image.TextureLoader;
import com.sun.j3d.utils.universe.SimpleUniverse;
import com.sun.j3d.utils.universe.ViewerAvatar;

/**
 * Displays a simple driving type game scene, using texture mapped cubes. It
 * assigns an Avatar to the viewer and incorporates simple sounds and collision
 * detection/notification/
 * <p>
 * This example does not use the Java3dApplet base class but is based on a
 * SimpleUniverse construction instead. that way we can illustrate the
 * setPlatformGeometry call.
 */
public class AvatarTest extends Applet {

  public BranchGroup createSceneGraph() {
    BranchGroup bg = new BranchGroup();

    TransformGroup tgRoot = addBehaviors(bg);

    createBuildings(tgRoot);
    createRoad(tgRoot);
    createLand(tgRoot);
    createCars(tgRoot);
    createBackground(bg);

    return bg;
  }

  public void createBackground(Group bg) {
    // add the sky backdrop
    Background back = new Background();
    back.setApplicationBounds(getBoundingSphere());
    bg.addChild(back);

    BranchGroup bgGeometry = new BranchGroup();

    // create an appearance and assign the texture image
    Appearance app = new Appearance();
    Texture tex = new TextureLoader("back.jpg", this).getTexture();
    app.setTexture(tex);

    Sphere sphere = new Sphere(1.0f, Primitive.GENERATE_TEXTURE_COORDS
        | Primitive.GENERATE_NORMALS_INWARD, app);

    bgGeometry.addChild(sphere);
    back.setGeometry(bgGeometry);
  }

  public Group createLand(Group g) {
    Land land = new Land(this, g, ComplexObject.GEOMETRY
        | ComplexObject.TEXTURE);
    return land.createObject(new Appearance(), new Vector3d(0, 0, 0),
        new Vector3d(1, 1, 1), "land.jpg", null, null);
  }

  public Group createRoad(Group g) {
    Road road = new Road(this, g, ComplexObject.GEOMETRY
        | ComplexObject.TEXTURE);
    return road.createObject(new Appearance(), new Vector3d(0, 0, 0),
        new Vector3d(1, 1, 1), "road.jpg", null, null);
  }

  private float getRandomNumber(float basis, float random) {
    return basis + ((float) Math.random() * random * 2) - (random);
  }

  public Group createBuildings(Group g) {
    BranchGroup bg = new BranchGroup();

    for (int n = (int) Road.ROAD_LENGTH; n < 0; n = n + 10) {
      Building building = new Building(this, bg, ComplexObject.GEOMETRY
          | ComplexObject.TEXTURE | ComplexObject.COLLISION);

      building.createObject(new Appearance(), new Vector3d(
          getRandomNumber(-4.0f, 0.25f), getRandomNumber(1.0f, 0.5f),
          getRandomNumber(n, 0.5f)), new Vector3d(1, 1, 1),
          "house.jpg", null, null);

      building = new Building(this, bg, ComplexObject.GEOMETRY
          | ComplexObject.TEXTURE | ComplexObject.COLLISION);

      building.createObject(new Appearance(), new Vector3d(
          getRandomNumber(4.0f, 0.25f), getRandomNumber(1.0f, 0.5f),
          getRandomNumber(n, 0.5f)), new Vector3d(1, 1, 1),
          "house.jpg", null, null);

    }

    g.addChild(bg);

    return bg;
  }

  public Group createCars(Group g) {
    BranchGroup bg = new BranchGroup();

    for (int n = (int) Road.ROAD_LENGTH; n < 0; n = n + 10) {
      Car car = new Car(this, bg, ComplexObject.GEOMETRY
          | ComplexObject.TEXTURE | ComplexObject.SOUND);

      car.createObject(new Appearance(), new Vector3d(getRandomNumber(
          0.0f, 2.0f), Car.CAR_HEIGHT / 2.0f,
          getRandomNumber(n, 5.0f)), new Vector3d(1, 1, 1),
          "car0.jpg", "car.wav", "collide.wav");
    }

    g.addChild(bg);
    return bg;
  }

  public TransformGroup addBehaviors(Group bgRoot) {
    // Create the transform group node and initialize it to the
    // identity. Enable the TRANSFORM_WRITE capability so that
    // our behavior code can modify it at runtime. Add it to the
    // root of the subgraph.
    TransformGroup objTrans = new TransformGroup();
    objTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);

    Transform3D zAxis = new Transform3D();
    zAxis.rotY(Math.toRadians(90.0));

    Alpha zoomAlpha = new Alpha(-1, Alpha.INCREASING_ENABLE, 0, 0, 20000,
        0, 0, 0, 0, 0);

    PositionInterpolator posInt = new PositionInterpolator(zoomAlpha,
        objTrans, zAxis, 0, -160);

    posInt.setSchedulingBounds(getBoundingSphere());
    objTrans.addChild(posInt);

    bgRoot.addChild(objTrans);

    return objTrans;
  }

  BoundingSphere getBoundingSphere() {
    return new BoundingSphere(new Point3d(0.0, 0.0, 0.0), 400.0);
  }

  ViewerAvatar createAvatar() {
    ViewerAvatar va = new ViewerAvatar();
    TransformGroup tg = new TransformGroup();

    Car car = new Car(this, tg, ComplexObject.GEOMETRY
        | ComplexObject.TEXTURE | ComplexObject.COLLISION
        | ComplexObject.COLLISION_SOUND);

    car.createObject(new Appearance(), new Vector3d(0, -0.3, -0.3),
        new Vector3d(0.3, 0.3, 1), "platform.jpg", null, "collide.wav");

    tg.addChild(car);
    va.addChild(tg);

    return va;
  }

  public static void main(String[] args) {
    AvatarTest avatarTest = new AvatarTest();

    // Create a simple scene and attach it to the virtual universe
    SimpleUniverse u = new SimpleUniverse();

    PhysicalEnvironment physicalEnv = u.getViewer()
        .getPhysicalEnvironment();

    TransformGroup tg = u.getViewer().getViewingPlatform()
        .getViewPlatformTransform();

    Transform3D t3d = new Transform3D();
    t3d.set(new Vector3f(0, 0.5f, 0));
    tg.setTransform(t3d);

    CarSteering keys = new CarSteering(tg);
    keys.setSchedulingBounds(avatarTest.getBoundingSphere());

    u.getViewer().setAvatar(avatarTest.createAvatar());

    if (physicalEnv != null) {
      JavaSoundMixer javaSoundMixer = new JavaSoundMixer(physicalEnv);

      if (javaSoundMixer == null)
        System.out.println("Unable to create AudioDevice.");

      javaSoundMixer.initialize();
    }

    // Add everthing to the scene graph - it will now be displayed.
    BranchGroup scene = avatarTest.createSceneGraph();
    scene.addChild(keys);
//    Java3dTree j3dTree = new Java3dTree();

  //  j3dTree.recursiveApplyCapability(scene);

    u.addBranchGraph(scene);

    //j3dTree.updateNodes(u);

    u.getViewingPlatform().getViewPlatform().setActivationRadius(2);
  }
}

/**
 * This class is a simple behavior that invokes the KeyNavigator to modify the
 * view platform transform.
 */

class Building extends ComplexObject {
  private final float BUILDING_WIDTH = 1.0f;

  private final float BUILDING_LENGTH = 1.0f;

  public Building(Component comp, Group g, int nFlags) {
    super(comp, g, nFlags);
  }

  private float getRandomNumber(float basis, float random) {
    return basis + ((float) Math.random() * random * 2) - (random);
  }

  protected Group createGeometryGroup(Appearance app, Vector3d position,
      Vector3d scale, String szTextureFile, String szSoundFile) {
    int nPrimFlags = 0;

    if ((m_nFlags & ComplexObject.TEXTURE) == ComplexObject.TEXTURE) {
      nPrimFlags |= Primitive.GENERATE_TEXTURE_COORDS;
      setTexture(app, szTextureFile);
    }

    return new Box(getRandomNumber(BUILDING_WIDTH, 0.25f),
        (float) position.y, getRandomNumber(BUILDING_LENGTH, 0.15f),
        nPrimFlags, app);
  }
}

/**
 * This class is a simple behavior that invokes the KeyNavigator to modify the
 * view platform transform.
 */

class Land extends ComplexObject {
  private final float LAND_WIDTH = 100.0f;

  private final float LAND_HEIGHT = 0.0f;

  private final float LAND_LENGTH = -200.0f;

  public Land(Component comp, Group g, int nFlags) {
    super(comp, g, nFlags);
  }

  protected Group createGeometryGroup(Appearance app, Vector3d position,
      Vector3d scale, String szTextureFile, String szSoundFile) {
    QuadArray quadArray = new QuadArray(4, GeometryArray.COORDINATES
        | GeometryArray.TEXTURE_COORDINATE_2);

    float[] coordArray = { -LAND_WIDTH, LAND_HEIGHT, 0, LAND_WIDTH,
        LAND_HEIGHT, 0, LAND_WIDTH, LAND_HEIGHT, LAND_LENGTH,
        -LAND_WIDTH, LAND_HEIGHT, LAND_LENGTH };

    float[] texArray = { 0, 0, 1, 0, 1, 1, 0, 1 };

    quadArray.setCoordinates(0, coordArray, 0, 4);

    if ((m_nFlags & TEXTURE) == TEXTURE) {
      quadArray.setTextureCoordinates(0, 0, texArray, 0, 4);
      setTexture(app, szTextureFile);
    }

    Shape3D sh = new Shape3D(quadArray, app);

    BranchGroup bg = new BranchGroup();
    bg.addChild(sh);
    return bg;
  }
}

/**
 * This class is a simple behavior that invokes the KeyNavigator to modify the
 * view platform transform.
 */

class CollisionBehavior extends Behavior {
  private WakeupOnCollisionEntry wakeupOne = null;

  private WakeupOnCollisionExit wakeupTwo = null;

  private WakeupCriterion[] wakeupArray = new WakeupCriterion[2];

  private WakeupCondition wakeupCondition = null;

  private ComplexObject m_Owner = null;

  public CollisionBehavior(Node node, ComplexObject owner) {
    wakeupOne = new WakeupOnCollisionEntry(node,
        WakeupOnCollisionEntry.USE_BOUNDS);
    wakeupTwo = new WakeupOnCollisionExit(node,
        WakeupOnCollisionExit.USE_BOUNDS);

    wakeupArray[0] = wakeupOne;
    wakeupArray[1] = wakeupTwo;

    wakeupCondition = new WakeupOr(wakeupArray);

    m_Owner = owner;
  }

  /**
   * Override Behavior's initialize method to setup wakeup criteria.
   */
  public void initialize() {
    // Establish initial wakeup criteria
    wakeupOn(wakeupCondition);
  }

  /**
   * Override Behavior's stimulus method to handle the event.
   */
  public void processStimulus(Enumeration criteria) {
    WakeupCriterion genericEvt;

    while (criteria.hasMoreElements()) {
      genericEvt = (WakeupCriterion) criteria.nextElement();

      if (genericEvt instanceof WakeupOnCollisionEntry) {        m_Owner.onCollide(true);
      } else if (genericEvt instanceof WakeupOnCollisionExit) {
        m_Owner.onCollide(false);
      }
    }

    // Set wakeup criteria for next time
    wakeupOn(wakeupCondition);
  }
}

/**
 * This class is a simple behavior that invokes the KeyNavigator to modify the
 * view platform transform.
 */

class CarSteering extends Behavior {
  private WakeupOnAWTEvent wakeupOne = null;

  private WakeupCriterion[] wakeupArray = new WakeupCriterion[1];

  private WakeupCondition wakeupCondition = null;

  private final float TRANSLATE_LEFT = -0.05f;

  private final float TRANSLATE_RIGHT = 0.05f;

  TransformGroup m_TransformGroup = null;

  public CarSteering(TransformGroup tg) {
    m_TransformGroup = tg;

    try {
      m_TransformGroup
          .setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
      m_TransformGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
    } catch (Exception e) {
    }

    wakeupOne = new WakeupOnAWTEvent(KeyEvent.KEY_PRESSED);
    wakeupArray[0] = wakeupOne;
    wakeupCondition = new WakeupOr(wakeupArray);
  }

  /**
   * Override Behavior's initialize method to setup wakeup criteria.
   */
  public void initialize() {
    // Establish initial wakeup criteria
    wakeupOn(wakeupCondition);
  }

  /**
   * Override Behavior's stimulus method to handle the event.
   */
  public void processStimulus(Enumeration criteria) {
    WakeupOnAWTEvent ev;
    WakeupCriterion genericEvt;
    AWTEvent[] events;

    while (criteria.hasMoreElements()) {
      genericEvt = (WakeupCriterion) criteria.nextElement();

      if (genericEvt instanceof WakeupOnAWTEvent) {
        ev = (WakeupOnAWTEvent) genericEvt;
        events = ev.getAWTEvent();
        processAWTEvent(events);
      }
    }

    // Set wakeup criteria for next time
    wakeupOn(wakeupCondition);
  }

  /**
   * Process a keyboard event
   */
  private void processAWTEvent(AWTEvent[] events) {
    for (int n = 0; n < events.length; n++) {
      if (events[n] instanceof KeyEvent) {
        KeyEvent eventKey = (KeyEvent) events[n];

        if (eventKey.getID() == KeyEvent.KEY_PRESSED) {
          int keyCode = eventKey.getKeyCode();
          int keyChar = eventKey.getKeyChar();

          Vector3f translate = new Vector3f();

          Transform3D t3d = new Transform3D();
          m_TransformGroup.getTransform(t3d);
          t3d.get(translate);

          switch (keyCode) {
          case KeyEvent.VK_LEFT:
            translate.x += TRANSLATE_LEFT;
            break;

          case KeyEvent.VK_RIGHT:
            translate.x += TRANSLATE_RIGHT;
            break;
          }

          // System.out.println( "Steering: " + translate.x );
          translate.y = 0.5f;

          t3d.setTranslation(translate);
          m_TransformGroup.setTransform(t3d);
        }
      }
    }
  }
}

/**
 * This class is a simple behavior that invokes the KeyNavigator to modify the
 * view platform transform.
 */

class Car extends ComplexObject {
  public static final float CAR_WIDTH = 0.2f;

  public static final float CAR_HEIGHT = 0.2f;

  public static final float CAR_LENGTH = 0.6f;

  public Car(Component comp, Group g, int nFlags) {
    super(comp, g, nFlags);
  }

  private float getRandomNumber(float basis, float random) {
    return basis + ((float) Math.random() * random * 2) - (random);
  }

  public Bounds getGeometryBounds() {
    return new BoundingSphere(new Point3d(0, 0, 0), 0.2);
  }

  protected Group createGeometryGroup(Appearance app, Vector3d position,
      Vector3d scale, String szTextureFile, String szSoundFile) {
    int nPrimFlags = 0;

    if ((m_nFlags & ComplexObject.TEXTURE) == ComplexObject.TEXTURE) {
      nPrimFlags |= Primitive.GENERATE_TEXTURE_COORDS;
      setTexture(app, szTextureFile);
    }

    return new Box(CAR_WIDTH, (float) position.y, getRandomNumber(
        CAR_LENGTH, 0.01f), nPrimFlags, app);
  }
}

/**
 * This class is a simple behavior that invokes the KeyNavigator to modify the
 * view platform transform.
 */

class Road extends ComplexObject {
  public static final float ROAD_WIDTH = 3.0f;

  public static final float ROAD_HEIGHT = 0.01f;

  public static final float ROAD_LENGTH = -200.0f;

  public Road(Component comp, Group g, int nFlags) {
    super(comp, g, nFlags);
  }

  protected Group createGeometryGroup(Appearance app, Vector3d position,
      Vector3d scale, String szTextureFile, String szSoundFile) {
    // creates a segment of road 200 x 2

    QuadArray quadArray = new QuadArray(4, GeometryArray.COORDINATES
        | GeometryArray.TEXTURE_COORDINATE_2);

    float[] coordArray = { -ROAD_WIDTH, ROAD_HEIGHT, 0, ROAD_WIDTH,
        ROAD_HEIGHT, 0, ROAD_WIDTH, ROAD_HEIGHT, ROAD_LENGTH,
        -ROAD_WIDTH, ROAD_HEIGHT, ROAD_LENGTH };

    float[] texArray = { 0, 0, 1, 0, 1, 1, 0, 1 };

    quadArray.setCoordinates(0, coordArray, 0, 4);

    if ((m_nFlags & TEXTURE) == TEXTURE) {
      quadArray.setTextureCoordinates(0, 0, texArray, 0, 4);
      setTexture(app, szTextureFile);
    }

    Shape3D sh = new Shape3D(quadArray, app);

    BranchGroup bg = new BranchGroup();
    bg.addChild(sh);
    return bg;
  }
}

abstract class ComplexObject extends BranchGroup {
  protected Group m_ParentGroup = null;

  protected int m_nFlags = 0;

  protected BackgroundSound m_CollideSound = null;

  protected Component m_Component = null;

  protected TransformGroup m_TransformGroup = null;

  protected TransformGroup m_BehaviorTransformGroup = null;

  public static final int SOUND = 0x001;

  public static final int GEOMETRY = 0x002;

  public static final int TEXTURE = 0x004;

  public static final int COLLISION = 0x008;

  public static final int COLLISION_SOUND = 0x010;

  public ComplexObject(Component comp, Group group, int nFlags) {
    m_ParentGroup = group;
    m_nFlags = nFlags;
    m_Component = comp;
  }

  public Bounds getGeometryBounds() {
    return new BoundingSphere(new Point3d(0, 0, 0), 100);
  }

  private MediaContainer loadSoundFile(String szFile) {
    try {
      File file = new File(System.getProperty("user.dir"));
      URL url = file.toURL();

      URL soundUrl = new URL(url, szFile);
      return new MediaContainer(soundUrl);
    } catch (Exception e) {
      System.err.println("Error could not load sound file: " + e);
      System.exit(-1);
    }

    return null;
  }

  protected void setTexture(Appearance app, String szFile) {
    Texture tex = new TextureLoader(szFile, m_Component).getTexture();
    app.setTexture(tex);
  }

  abstract protected Group createGeometryGroup(Appearance app,
      Vector3d position, Vector3d scale, String szTextureFile,
      String szSoundFile);

  protected Group loadGeometryGroup(String szModel, Appearance app)
      throws java.io.FileNotFoundException {
    // load the object file
    Scene scene = null;
    Shape3D shape = null;

    // read in the geometry information from the data file
    ObjectFile objFileloader = new ObjectFile(ObjectFile.RESIZE);

    scene = objFileloader.load(szModel);

    // retrieve the Shape3D object from the scene
    BranchGroup branchGroup = scene.getSceneGroup();
    shape = (Shape3D) branchGroup.getChild(0);
    shape.setAppearance(app);

    return branchGroup;
  }

  protected int getSoundLoop(boolean bCollide) {
    return 1;
  }

  protected float getSoundPriority(boolean bCollide) {
    return 1.0f;
  }

  protected float getSoundInitialGain(boolean bCollide) {
    return 1.0f;
  }

  protected boolean getSoundInitialEnable(boolean bCollide) {
    return true;
  }

  protected boolean getSoundContinuousEnable(boolean bCollide) {
    return false;
  }

  protected Bounds getSoundSchedulingBounds(boolean bCollide) {
    return new BoundingSphere(new Point3d(0, 0, 0), 1.0);
  }

  protected boolean getSoundReleaseEnable(boolean bCollide) {
    return true;
  }

  protected Point2f[] getSoundDistanceGain(boolean bCollide) {
    return null;
  }

  protected void setSoundAttributes(Sound sound, boolean bCollide) {
    sound.setCapability(Sound.ALLOW_ENABLE_WRITE);
    sound.setCapability(Sound.ALLOW_ENABLE_READ);

    sound.setSchedulingBounds(getSoundSchedulingBounds(bCollide));
    sound.setEnable(getSoundInitialEnable(bCollide));
    sound.setLoop(getSoundLoop(bCollide));
    sound.setPriority(getSoundPriority(bCollide));
    sound.setInitialGain(getSoundInitialGain(bCollide));

    sound.setContinuousEnable(getSoundContinuousEnable(bCollide));
    sound.setReleaseEnable(bCollide);

    if (sound instanceof PointSound) {
      PointSound pointSound = (PointSound) sound;
      pointSound.setInitialGain(getSoundInitialGain(bCollide));

      Point2f[] gainArray = getSoundDistanceGain(bCollide);

      if (gainArray != null)
        pointSound.setDistanceGain(gainArray);
    }
  }

  public Group createObject(Appearance app, Vector3d position,
      Vector3d scale, String szTextureFile, String szSoundFile,
      String szCollisionSound) {
    m_TransformGroup = new TransformGroup();
    Transform3D t3d = new Transform3D();

    t3d.setScale(scale);
    t3d.setTranslation(position);

    m_TransformGroup.setTransform(t3d);

    m_BehaviorTransformGroup = new TransformGroup();

    if ((m_nFlags & GEOMETRY) == GEOMETRY)
      m_BehaviorTransformGroup.addChild(createGeometryGroup(app,
          position, scale, szTextureFile, szSoundFile));

    if ((m_nFlags & SOUND) == SOUND) {
      MediaContainer media = loadSoundFile(szSoundFile);
      PointSound pointSound = new PointSound(media,
          getSoundInitialGain(false), 0, 0, 0);
      setSoundAttributes(pointSound, false);
      m_BehaviorTransformGroup.addChild(pointSound);
    }

    if ((m_nFlags & COLLISION) == COLLISION) {
      m_BehaviorTransformGroup
          .setCapability(Node.ENABLE_COLLISION_REPORTING);
      m_BehaviorTransformGroup.setCollidable(true);
      m_BehaviorTransformGroup.setCollisionBounds(getGeometryBounds());

      if ((m_nFlags & COLLISION_SOUND) == COLLISION_SOUND) {
        MediaContainer collideMedia = loadSoundFile(szCollisionSound);

        m_CollideSound = new BackgroundSound(collideMedia, 1);
        setSoundAttributes(m_CollideSound, true);
        m_TransformGroup.addChild(m_CollideSound);
      }

      CollisionBehavior collision = new CollisionBehavior(
          m_BehaviorTransformGroup, this);
      collision.setSchedulingBounds(getGeometryBounds());

      m_BehaviorTransformGroup.addChild(collision);
    }

    m_TransformGroup.addChild(m_BehaviorTransformGroup);
    m_ParentGroup.addChild(m_TransformGroup);

    return m_BehaviorTransformGroup;
  }

  public void onCollide(boolean bCollide) {
    System.out.println("Collide: " + bCollide);

    if (m_CollideSound != null && bCollide == true)
      m_CollideSound.setEnable(true);
  }

  public void attachBehavior(Behavior beh) {
    m_BehaviorTransformGroup
        .setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
    beh.setSchedulingBounds(getGeometryBounds());
    m_BehaviorTransformGroup.addChild(beh);
  }

  public TransformGroup getBehaviorTransformGroup() {
    return m_BehaviorTransformGroup;
  }

  public void attachSplinePathInterpolator(Alpha alpha, Transform3D axis,
      URL urlKeyframes) {
    // read a spline path definition file and
    // add a Spline Path Interpolator to the TransformGroup for the object.

    m_BehaviorTransformGroup
        .setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);

    RotPosScaleTCBSplinePathInterpolator splineInterpolator = Utils
        .createSplinePathInterpolator(alpha, m_BehaviorTransformGroup,
            axis, urlKeyframes);

    if (splineInterpolator != null) {
      splineInterpolator.setSchedulingBounds(getGeometryBounds());
      m_BehaviorTransformGroup.addChild(splineInterpolator);
    } else {
      System.out.println("attachSplinePathInterpolator failed for: "
          + urlKeyframes);
    }
  }
}

//*****************************************************************************
/**
 * Utils
 * 
 * @author Daniel Selman
 * @version 1.0
 */
//*****************************************************************************

class Utils {
  // convert an angular rotation about an axis to a Quaternion
  static Quat4f createQuaternionFromAxisAndAngle(Vector3d axis, double angle) {
    double sin_a = Math.sin(angle / 2);
    double cos_a = Math.cos(angle / 2);

    // use a vector so we can call normalize
    Vector4f q = new Vector4f();

    q.x = (float) (axis.x * sin_a);
    q.y = (float) (axis.y * sin_a);
    q.z = (float) (axis.z * sin_a);
    q.w = (float) cos_a;

    // It is necessary to normalise the quaternion
    // in case any values are very close to zero.
    q.normalize();

    // convert to a Quat4f and return
    return new Quat4f(q);
  }

  // convert three rotations about the Euler axes to a Quaternion
  static Quat4f createQuaternionFromEuler(double angleX, double angleY,
      double angleZ) {
    // simply call createQuaternionFromAxisAndAngle
    // for each axis and multiply the results
    Quat4f qx = createQuaternionFromAxisAndAngle(new Vector3d(1, 0, 0),
        angleX);
    Quat4f qy = createQuaternionFromAxisAndAngle(new Vector3d(0, 1, 0),
        angleY);
    Quat4f qz = createQuaternionFromAxisAndAngle(new Vector3d(0, 0, 1),
        angleZ);

    // qx = qx * qy
    qx.mul(qy);

    // qx = qx * qz
    qx.mul(qz);

    return qx;
  }

  static public double getRandomNumber(double basis, double random) {
    return basis + ((float) Math.random() * random * 2f) - (random);
  }

  static public double getRandomNumber(double basis, double random,
      double scale) {
    double value = basis + ((float) Math.random() * random * 2f) - (random);
    return value * scale;
  }

  static public StringBuffer readFile(URL urlFile) {
    // allocate a temporary buffer to store the input file
    StringBuffer szBufferData = new StringBuffer();
    Vector keyFramesVector = new Vector();

    try {
      InputStream inputStream = urlFile.openStream();

      int nChar = 0;

      // read the entire file into the StringBuffer
      while (true) {
        nChar = inputStream.read();

        // if we have not hit the end of file
        // add the character to the StringBuffer
        if (nChar != -1)
          szBufferData.append((char) nChar);
        else
          // EOF
          break;
      }

      inputStream.close();
    } catch (Exception e) {
      System.err.println(e.toString());
      return null;
    }

    return szBufferData;
  }

  static public RotPosScaleTCBSplinePathInterpolator createSplinePathInterpolator(
      Alpha alpha, TransformGroup tg, Transform3D axis, URL urlKeyframes) {
    TCBKeyFrame[] keyFrames = readKeyFrames(urlKeyframes);

    if (keyFrames != null)
      return new RotPosScaleTCBSplinePathInterpolator(alpha, tg, axis,
          keyFrames);

    return null;
  }

  static public TCBKeyFrame[] readKeyFrames(URL urlKeyframes) {
    StringBuffer szBufferData = readFile(urlKeyframes);

    if (szBufferData == null)
      return null;

    Vector keyFramesVector = new Vector();

    // create a tokenizer to tokenize the input file at whitespace
    java.util.StringTokenizer tokenizer = new java.util.StringTokenizer(
        szBufferData.toString());

    // each keyframe is defined as follows
    // - knot (0 >= k <= 1)
    // - position (x,y,z)
    // - rotation (rx,ry,rz)
    // - scale (x,y,z)

    // - tension (-1 >= t <= 1)
    // - continuity (-1 >= c <= 1)
    // - bias (-1 >= b <= 1)
    // - linear (int - 0 or 1)

    while (true) {
      try {
        float knot = Float.parseFloat(tokenizer.nextToken());

        float posX = Float.parseFloat(tokenizer.nextToken());
        float posY = Float.parseFloat(tokenizer.nextToken());
        float posZ = Float.parseFloat(tokenizer.nextToken());

        float rotX = Float.parseFloat(tokenizer.nextToken());
        float rotY = Float.parseFloat(tokenizer.nextToken());
        float rotZ = Float.parseFloat(tokenizer.nextToken());

        float scaleX = Float.parseFloat(tokenizer.nextToken());
        float scaleY = Float.parseFloat(tokenizer.nextToken());
        float scaleZ = Float.parseFloat(tokenizer.nextToken());

        float tension = Float.parseFloat(tokenizer.nextToken());
        float continuity = Float.parseFloat(tokenizer.nextToken());
        float bias = Float.parseFloat(tokenizer.nextToken());

        int linear = Integer.parseInt(tokenizer.nextToken());

        TCBKeyFrame keyframe = new TCBKeyFrame(knot, linear,
            new Point3f(posX, posY, posZ),
            createQuaternionFromEuler(rotX, rotY, rotZ),
            new Point3f(scaleX, scaleY, scaleZ), tension,
            continuity, bias);

        keyFramesVector.add(keyframe);
      } catch (Exception e) {
        break;
      }
    }

    // create the return structure and populate
    TCBKeyFrame[] keysReturn = new TCBKeyFrame[keyFramesVector.size()];

    for (int n = 0; n < keysReturn.length; n++)
      keysReturn[n] = (TCBKeyFrame) keyFramesVector.get(n);

    // return the array
    return keysReturn;
  }
}