This Java tip illustrates some of the features of the SimpleUniverse class. It creates 3 views into a scengraph and associates a ViewerAvatar and PlatformGeometry with each Viewer object.

The example creates a very simple navigation environment where each Viewer of the environment can navigate using the keyboard and see the other viewer.

 import java.applet.Applet;
import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.GraphicsConfigTemplate;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.net.URL;
import java.util.Hashtable;

import javax.media.j3d.AmbientLight;
import javax.media.j3d.Appearance;
import javax.media.j3d.AudioDevice;
import javax.media.j3d.Background;
import javax.media.j3d.BoundingSphere;
import javax.media.j3d.Bounds;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Canvas3D;
import javax.media.j3d.DirectionalLight;
import javax.media.j3d.GraphicsConfigTemplate3D;
import javax.media.j3d.Group;
import javax.media.j3d.ImageComponent2D;
import javax.media.j3d.Locale;
import javax.media.j3d.Material;
import javax.media.j3d.PhysicalBody;
import javax.media.j3d.PhysicalEnvironment;
import javax.media.j3d.Shape3D;
import javax.media.j3d.Texture;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.media.j3d.View;
import javax.media.j3d.ViewPlatform;
import javax.media.j3d.VirtualUniverse;
import javax.vecmath.Color3f;
import javax.vecmath.Point2f;
import javax.vecmath.Point3d;
import javax.vecmath.Point3f;
import javax.vecmath.Vector3d;
import javax.vecmath.Vector3f;

import com.sun.j3d.audioengines.javasound.JavaSoundMixer;
import com.sun.j3d.utils.applet.MainFrame;
import com.sun.j3d.utils.behaviors.keyboard.KeyNavigatorBehavior;
import com.sun.j3d.utils.geometry.ColorCube;
import com.sun.j3d.utils.geometry.Cone;
import com.sun.j3d.utils.geometry.GeometryInfo;
import com.sun.j3d.utils.geometry.NormalGenerator;
import com.sun.j3d.utils.geometry.Primitive;
import com.sun.j3d.utils.image.TextureLoader;
import com.sun.j3d.utils.universe.MultiTransformGroup;
import com.sun.j3d.utils.universe.PlatformGeometry;
import com.sun.j3d.utils.universe.SimpleUniverse;
import com.sun.j3d.utils.universe.Viewer;
import com.sun.j3d.utils.universe.ViewerAvatar;
import com.sun.j3d.utils.universe.ViewingPlatform;

/**
 * This example illustrates some of the features of the SimpleUniverse class. It
 * creates 3 views into a scengraph and associates a ViewerAvatar and
 * PlatformGeometry with each Viewer object.
 * <p>
 * The example creates a very simple navigation environment where each Viewer of
 * the environment can navigate using the keyboard and see the other viewer.
 */
public class PlatformTest extends Applet implements ActionListener {
  // size of each Canvas3D
  static final int m_kWidth = 256;

  static final int m_kHeight = 256;

  // table used to map the name of a Viewer to its KeyNavigatorBehavior
  Hashtable m_KeyHashtable = null;

  BoundingSphere m_Bounds = null;

  public PlatformTest() {
    m_KeyHashtable = new Hashtable();
    m_Bounds = new BoundingSphere(new Point3d(0, 0, 0), 100);
    // get the graphics configuration for the graphics device
    GraphicsConfiguration config = SimpleUniverse
        .getPreferredConfiguration();

    // create the first canvas, this is the top-down view
    Canvas3D c = new Canvas3D(config);
    c.setSize(m_kWidth, m_kHeight);
    add(c);

    // create the second canvas, this is used for "Jim's" Viewer
    Canvas3D c2 = new Canvas3D(config);
    c2.setSize(m_kWidth, m_kHeight);
    add(c2);

    // create the third canvas, this is used for "Dan's" Viewer
    Canvas3D c3 = new Canvas3D(config);
    c3.setSize(m_kWidth, m_kHeight);
    add(c3);

    // Create the simple environment
    BranchGroup scene = createSceneGraph();

    // create the first Viewer, this is a static top-down view
    // create a ViewingPlatform with 2 TransformGroups above the
    // ViewPlatform
    ViewingPlatform vp = new ViewingPlatform(2);

    // create the Viewer and attach to the first canvas
    Viewer viewer = new Viewer(c);

    // rotate and position the first Viewer above the environment
    Transform3D t3d = new Transform3D();
    t3d.rotX(Math.PI / 2.0);
    t3d.setTranslation(new Vector3d(0, 0, -40));
    t3d.invert();

    MultiTransformGroup mtg = vp.getMultiTransformGroup();
    mtg.getTransformGroup(0).setTransform(t3d);

    // create a SimpleUniverse from the ViewingPlatform and Viewer
    SimpleUniverse u = new SimpleUniverse(vp, viewer);

    // add the geometry to the scenegraph
    u.addBranchGraph(scene);

    // add two more Viewers to the scenegraph
    u.getLocale().addBranchGraph(
        createViewer(c2, "Jim", new Color3f(0.1f, 1.0f, 1.0f), -5, 8));
    u.getLocale().addBranchGraph(
        createViewer(c3, "Dan", new Color3f(1.0f, 0.1f, 0.1f), 2, -8));

  }

  ViewingPlatform createViewer(Canvas3D c, String szName, Color3f objColor,
      double x, double z) {
    // create a Viewer and attach to its canvas
    // a Canvas3D can only be attached to a single Viewer
    Viewer viewer2 = new Viewer(c);

    // create a ViewingPlatform with 1 TransformGroups above the
    // ViewPlatform
    ViewingPlatform vp2 = new ViewingPlatform(1);

    // create and assign the PlatformGeometry to the Viewer
    vp2.setPlatformGeometry(createPlatformGeometry(szName));

    // create and assign the ViewerAvatar to the Viewer
    viewer2.setAvatar(createViewerAvatar(szName, objColor));

    // set the initial position for the Viewer
    Transform3D t3d = new Transform3D();
    t3d.setTranslation(new Vector3d(x, 0, z));
    vp2.getViewPlatformTransform().setTransform(t3d);

    // set capabilities on the TransformGroup so that the
    // KeyNavigatorBehavior
    // can modify the Viewer's position
    vp2.getViewPlatformTransform().setCapability(
        TransformGroup.ALLOW_TRANSFORM_WRITE);
    vp2.getViewPlatformTransform().setCapability(
        TransformGroup.ALLOW_TRANSFORM_READ);

    // attach a navigation behavior to the position of the viewer
    KeyNavigatorBehavior key = new KeyNavigatorBehavior(vp2
        .getViewPlatformTransform());
    key.setSchedulingBounds(m_Bounds);
    key.setEnable(false);

    // add the KeyNavigatorBehavior to the ViewingPlatform
    vp2.addChild(key);

    // set the ViewingPlatform for the Viewer
    viewer2.setViewingPlatform(vp2);

    // associate the name of the Viewer with its KeyNavigatorBehavior
    m_KeyHashtable.put(szName, key);

    // create a button to switch the Viewer ON.
    Button button = new Button(szName);
    button.addActionListener(this);
    add(button);

    return vp2;
  }

  // create a tiled environment from -12 to +12. The environment
  // is created from a QuadArray. The environment is surrounded by a ColorCube
  // "wall" that is 2 units high (from Z = -1 to Z = 1).
  public BranchGroup createSceneGraph() {
    final int LAND_WIDTH = 12;
    final float LAND_HEIGHT = -1.0f;
    final int LAND_LENGTH = 12;
    final int nTileSize = 2;

    // calculate how many vertices we need to store all the "tiles"
    // that compose the QuadArray.
    final int nNumTiles = ((LAND_LENGTH / nTileSize) * 2)
        * ((LAND_WIDTH / nTileSize) * 2);
    final int nVertexCount = 4 * nNumTiles;
    Point3f[] coordArray = new Point3f[nVertexCount];
    Point2f[] texCoordArray = new Point2f[nVertexCount];

    // create an Appearance and load a texture
    Appearance app = new Appearance();
    Texture tex = new TextureLoader("land.jpg", this).getTexture();
    app.setTexture(tex);

    // create the parent BranchGroup
    BranchGroup bg = new BranchGroup();

    int nItem = 0;

    // loop over all the tiles in the environment
    for (int x = -LAND_WIDTH; x <= LAND_WIDTH; x += nTileSize) {
      for (int z = -LAND_LENGTH; z <= LAND_LENGTH; z += nTileSize) {
        // if we are on the border of the environment create a
        // TransformGroup to position a ColorCube to create a "wall"
        if (x == -LAND_WIDTH || x == LAND_WIDTH || z == -LAND_LENGTH
            || z == LAND_LENGTH) {
          TransformGroup tg = new TransformGroup();
          Transform3D t3d = new Transform3D();
          t3d.setTranslation(new Vector3d(x, 0, z));
          tg.setTransform(t3d);
          tg.addChild(new ColorCube(nTileSize / 2));
          bg.addChild(tg);
        }

        // if we are not on the last row or column create a "tile"
        // and add to the QuadArray. Use CCW winding and assign texture
        // coordinates.
        if (z < LAND_LENGTH && x < LAND_WIDTH) {
          coordArray[nItem] = new Point3f(x, LAND_HEIGHT, z);
          texCoordArray[nItem++] = new Point2f(0, 0);
          coordArray[nItem] = new Point3f(x, LAND_HEIGHT, z
              + nTileSize);
          texCoordArray[nItem++] = new Point2f(1, 0);
          coordArray[nItem] = new Point3f(x + nTileSize, LAND_HEIGHT,
              z + nTileSize);
          texCoordArray[nItem++] = new Point2f(1, 1);
          coordArray[nItem] = new Point3f(x + nTileSize, LAND_HEIGHT,
              z);
          texCoordArray[nItem++] = new Point2f(0, 1);
        }
      }
    }

    // create a GeometryInfo and generate Normal vectors
    // for the QuadArray that was populated.
    GeometryInfo gi = new GeometryInfo(GeometryInfo.QUAD_ARRAY);

    gi.setCoordinates(coordArray);
    gi.setTextureCoordinates(texCoordArray);

    NormalGenerator normalGenerator = new NormalGenerator();
    normalGenerator.generateNormals(gi);

    // wrap the GeometryArray in a Shape3D
    Shape3D shape = new Shape3D(gi.getGeometryArray(), app);

    // add the Shape3D to the parent BranchGroup
    bg.addChild(shape);

    // create some lights for the scene
    Color3f lColor1 = new Color3f(0.7f, 0.7f, 0.7f);
    Vector3f lDir1 = new Vector3f(-1.0f, -1.0f, -1.0f);
    Color3f alColor = new Color3f(0.2f, 0.2f, 0.2f);

    AmbientLight aLgt = new AmbientLight(alColor);
    aLgt.setInfluencingBounds(m_Bounds);
    DirectionalLight lgt1 = new DirectionalLight(lColor1, lDir1);
    lgt1.setInfluencingBounds(m_Bounds);

    // add the lights to the parent BranchGroup
    bg.addChild(aLgt);
    bg.addChild(lgt1);

    // create a light gray background
    Background back = new Background(new Color3f(0.9f, 0.9f, 0.9f));
    back.setApplicationBounds(m_Bounds);
    bg.addChild(back);

    // compile the whole scene
    //bg.compile();

    return bg;
  }

  // creates and positions a simple Cone to represent the Viewer.
  // The Cone is aligned and scaled such that it is similar to a
  // 3D "turtle".... Aaah good old Logo.
  ViewerAvatar createViewerAvatar(String szText, Color3f objColor) {
    ViewerAvatar viewerAvatar = new ViewerAvatar();

    // rotate the Cone so that it is lying down and
    // points sharp-end towards the Viewer's field of view.
    TransformGroup tg = new TransformGroup();
    Transform3D t3d = new Transform3D();
    t3d.setEuler(new Vector3d(Math.PI / 2.0, Math.PI, 0));
    tg.setTransform(t3d);

    // create appearance and material for the Cone
    Appearance app = new Appearance();
    Color3f black = new Color3f(0.4f, 0.2f, 0.1f);
    app.setMaterial(new Material(objColor, black, objColor, black, 90.0f));

    // create the Primitive and add to the parent BranchGroup
    tg.addChild(new Cone(1, 3, Primitive.GENERATE_NORMALS, app));
    viewerAvatar.addChild(tg);

    return viewerAvatar;
  }

  // create a simple Raster text label used to help
  // identify the viewer.
  PlatformGeometry createPlatformGeometry(String szText) {
    PlatformGeometry pg = new PlatformGeometry();
    pg.addChild(createLabel(szText, 0f, 2f, 0f));

    return pg;
  }

  // creates a simple Raster text label (similar to Text2D)
  private Shape3D createLabel(String szText, float x, float y, float z) {
    BufferedImage bufferedImage = new BufferedImage(25, 14,
        BufferedImage.TYPE_INT_RGB);
    Graphics g = bufferedImage.getGraphics();
    g.setColor(Color.white);
    g.drawString(szText, 2, 12);

    ImageComponent2D imageComponent2D = new ImageComponent2D(
        ImageComponent2D.FORMAT_RGB, bufferedImage);

    // create the Raster for the image
    javax.media.j3d.Raster renderRaster = new javax.media.j3d.Raster(
        new Point3f(x, y, z), javax.media.j3d.Raster.RASTER_COLOR, 0,
        0, bufferedImage.getWidth(), bufferedImage.getHeight(),
        imageComponent2D, null);

    return new Shape3D(renderRaster);
  }

  // Enables the KeyNavigatorBehavior associated with the
  // AWT button that was pressed for the Viewer. Disables all other
  // KeyNavigatorBehaviors for non-active Viewers.
  public void actionPerformed(ActionEvent event) {
    KeyNavigatorBehavior key = (KeyNavigatorBehavior) m_KeyHashtable
        .get(event.getActionCommand());
    Object[] keysArray = m_KeyHashtable.values().toArray();

    for (int n = 0; n < keysArray.length; n++) {
      KeyNavigatorBehavior keyAtIndex = (KeyNavigatorBehavior) keysArray[n];
      keyAtIndex.setEnable(keyAtIndex == key);

      if (keyAtIndex == key)
        System.out.println("Enabled: " + event.getActionCommand());
    }
  }

  public static void main(String[] args) {
    new MainFrame(new PlatformTest(), (int) (m_kWidth * 3.5),
        (int) (m_kHeight * 1.1));
  }
}

/*******************************************************************************
 * Copyright (C) 2001 Daniel Selman
 * 
 * First distributed with the book "Java 3D Programming" by Daniel Selman and
 * published by Manning Publications. http://manning.com/selman
 * 
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, version 2.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * The license can be found on the WWW at: http://www.fsf.org/copyleft/gpl.html
 * 
 * Or by writing to: Free Software Foundation, Inc., 59 Temple Place - Suite
 * 330, Boston, MA 02111-1307, USA.
 * 
 * Authors can be contacted at: Daniel Selman: This email address is being protected from spambots. You need JavaScript enabled to view it.
 * 
 * If you make changes you think others would like, please contact one of the
 * authors or someone at the www.j3d.org web site.
 ******************************************************************************/

//*****************************************************************************
/**
 * Java3dApplet
 * 
 * Base class for defining a Java 3D applet. Contains some useful methods for
 * defining views and scenegraphs etc.
 * 
 * @author Daniel Selman
 * @version 1.0
 */
//*****************************************************************************

abstract class Java3dApplet extends Applet {
  public static int m_kWidth = 300;

  public static int m_kHeight = 300;

  protected String[] m_szCommandLineArray = null;

  protected VirtualUniverse m_Universe = null;

  protected BranchGroup m_SceneBranchGroup = null;

  protected Bounds m_ApplicationBounds = null;

  //  protected com.tornadolabs.j3dtree.Java3dTree m_Java3dTree = null;

  public Java3dApplet() {
  }

  public boolean isApplet() {
    try {
      System.getProperty("user.dir");
      System.out.println("Running as Application.");
      return false;
    } catch (Exception e) {
    }

    System.out.println("Running as Applet.");
    return true;
  }

  public URL getWorkingDirectory() throws java.net.MalformedURLException {
    URL url = null;

    try {
      File file = new File(System.getProperty("user.dir"));
      System.out.println("Running as Application:");
      System.out.println("   " + file.toURL());
      return file.toURL();
    } catch (Exception e) {
    }

    System.out.println("Running as Applet:");
    System.out.println("   " + getCodeBase());

    return getCodeBase();
  }

  public VirtualUniverse getVirtualUniverse() {
    return m_Universe;
  }

  //public com.tornadolabs.j3dtree.Java3dTree getJ3dTree() {
  //return m_Java3dTree;
  //  }

  public Locale getFirstLocale() {
    java.util.Enumeration e = m_Universe.getAllLocales();

    if (e.hasMoreElements() != false)
      return (Locale) e.nextElement();

    return null;
  }

  protected Bounds getApplicationBounds() {
    if (m_ApplicationBounds == null)
      m_ApplicationBounds = createApplicationBounds();

    return m_ApplicationBounds;
  }

  protected Bounds createApplicationBounds() {
    m_ApplicationBounds = new BoundingSphere(new Point3d(0.0, 0.0, 0.0),
        100.0);
    return m_ApplicationBounds;
  }

  protected Background createBackground() {
    Background back = new Background(new Color3f(0.9f, 0.9f, 0.9f));
    back.setApplicationBounds(createApplicationBounds());
    return back;
  }

  public void initJava3d() {
    //  m_Java3dTree = new com.tornadolabs.j3dtree.Java3dTree();
    m_Universe = createVirtualUniverse();

    Locale locale = createLocale(m_Universe);

    BranchGroup sceneBranchGroup = createSceneBranchGroup();

    ViewPlatform vp = createViewPlatform();
    BranchGroup viewBranchGroup = createViewBranchGroup(
        getViewTransformGroupArray(), vp);

    createView(vp);

    Background background = createBackground();

    if (background != null)
      sceneBranchGroup.addChild(background);

    //    m_Java3dTree.recursiveApplyCapability(sceneBranchGroup);
    //  m_Java3dTree.recursiveApplyCapability(viewBranchGroup);

    locale.addBranchGraph(sceneBranchGroup);
    addViewBranchGroup(locale, viewBranchGroup);

    onDoneInit();
  }

  protected void onDoneInit() {
    //  m_Java3dTree.updateNodes(m_Universe);
  }

  protected double getScale() {
    return 1.0;
  }

  public TransformGroup[] getViewTransformGroupArray() {
    TransformGroup[] tgArray = new TransformGroup[1];
    tgArray[0] = new TransformGroup();

    // move the camera BACK a little...
    // note that we have to invert the matrix as
    // we are moving the viewer
    Transform3D t3d = new Transform3D();
    t3d.setScale(getScale());
    t3d.setTranslation(new Vector3d(0.0, 0.0, -20.0));
    t3d.invert();
    tgArray[0].setTransform(t3d);

    return tgArray;
  }

  protected void addViewBranchGroup(Locale locale, BranchGroup bg) {
    locale.addBranchGraph(bg);
  }

  protected Locale createLocale(VirtualUniverse u) {
    return new Locale(u);
  }

  protected BranchGroup createSceneBranchGroup() {
    m_SceneBranchGroup = new BranchGroup();
    return m_SceneBranchGroup;
  }

  protected View createView(ViewPlatform vp) {
    View view = new View();

    PhysicalBody pb = createPhysicalBody();
    PhysicalEnvironment pe = createPhysicalEnvironment();

    AudioDevice audioDevice = createAudioDevice(pe);

    if (audioDevice != null) {
      pe.setAudioDevice(audioDevice);
      audioDevice.initialize();
    }

    view.setPhysicalEnvironment(pe);
    view.setPhysicalBody(pb);

    if (vp != null)
      view.attachViewPlatform(vp);

    view.setBackClipDistance(getBackClipDistance());
    view.setFrontClipDistance(getFrontClipDistance());

    Canvas3D c3d = createCanvas3D();
    view.addCanvas3D(c3d);
    addCanvas3D(c3d);

    return view;
  }

  protected PhysicalBody createPhysicalBody() {
    return new PhysicalBody();
  }

  protected AudioDevice createAudioDevice(PhysicalEnvironment pe) {
    JavaSoundMixer javaSoundMixer = new JavaSoundMixer(pe);

    if (javaSoundMixer == null)
      System.out.println("create of audiodevice failed");

    return javaSoundMixer;
  }

  protected PhysicalEnvironment createPhysicalEnvironment() {
    return new PhysicalEnvironment();
  }

  protected float getViewPlatformActivationRadius() {
    return 100;
  }

  protected ViewPlatform createViewPlatform() {
    ViewPlatform vp = new ViewPlatform();
    vp.setViewAttachPolicy(View.RELATIVE_TO_FIELD_OF_VIEW);
    vp.setActivationRadius(getViewPlatformActivationRadius());

    return vp;
  }

  protected Canvas3D createCanvas3D() {
    GraphicsConfigTemplate3D gc3D = new GraphicsConfigTemplate3D();
    gc3D.setSceneAntialiasing(GraphicsConfigTemplate.PREFERRED);
    GraphicsDevice gd[] = GraphicsEnvironment.getLocalGraphicsEnvironment()
        .getScreenDevices();

    Canvas3D c3d = new Canvas3D(gd[0].getBestConfiguration(gc3D));
    c3d.setSize(getCanvas3dWidth(c3d), getCanvas3dHeight(c3d));

    return c3d;
  }

  protected int getCanvas3dWidth(Canvas3D c3d) {
    return m_kWidth;
  }

  protected int getCanvas3dHeight(Canvas3D c3d) {
    return m_kHeight;
  }

  protected double getBackClipDistance() {
    return 100.0;
  }

  protected double getFrontClipDistance() {
    return 1.0;
  }

  protected BranchGroup createViewBranchGroup(TransformGroup[] tgArray,
      ViewPlatform vp) {
    BranchGroup vpBranchGroup = new BranchGroup();

    if (tgArray != null && tgArray.length > 0) {
      Group parentGroup = vpBranchGroup;
      TransformGroup curTg = null;

      for (int n = 0; n < tgArray.length; n++) {
        curTg = tgArray[n];
        parentGroup.addChild(curTg);
        parentGroup = curTg;
      }

      tgArray[tgArray.length - 1].addChild(vp);
    } else
      vpBranchGroup.addChild(vp);

    return vpBranchGroup;
  }

  protected void addCanvas3D(Canvas3D c3d) {
    setLayout(new BorderLayout());
    add(c3d, BorderLayout.CENTER);
    doLayout();
  }

  protected VirtualUniverse createVirtualUniverse() {
    return new VirtualUniverse();
  }

  protected void saveCommandLineArguments(String[] szArgs) {
    m_szCommandLineArray = szArgs;
  }

  protected String[] getCommandLineArguments() {
    return m_szCommandLineArray;
  }
}