SAI Tutorial

This tutorial shows how to use the SAI - Scene Access Interface. This interface allows a programmer to change or build X3D worlds.

External Access - These are examples of modifying a X3D world from outside the world, similar to VRML97's EAI code.

Internal Access - These are examples of modifying a X3D world from inside the world, similar to VRML97's JSAI code.

All of these examples have been combined into a zip file. This file contains an Ant script that will automatically run Xj3D to show these examples. Please unzip this file into your Xj3D installation. It will make a directory called examples/sai. You can download Ant here: http://ant.apache.org/

The combined zip file is here: SAI Examples

To run the examples:

   cd examples/sai

How to load a world

This example will show how to write a simple file loader. It will go through the basics of creating an X3D browser component and show how to load a file.

Adding the 3D Window

Adding a 3D window to your application is easy. You first create an X3D component using the SAI BrowserFactory class. Then you add this component to your application. The following code shows those steps:

import org.web3d.x3d.sai.*;

public class SimpleSAIDemo extends JFrame {

    public SimpleSAIDemo() {
        Container contentPane = getContentPane();

        // Create an SAI component
        X3DComponent x3dComp = BrowserFactory.createX3DComponent(null);

        // Add the component to the UI
        JComponent x3dPanel = (JComponent)x3dComp.getImplementation();
        contentPane.add(x3dPanel, BorderLayout.CENTER);

All the classes needed for sai access are located in the package org.web3d.x3d.sai. A handy reference when writing SAI code is the Xj3D javadoc. You can access the online versions:

That's all there is to adding a 3D window to your application. The next section will show you how to a load a file.

Load the file

In order to a load a file we need to introduce the concept of a browser. A Browser in SAI is the basic interface to interacting with the system. It allows you to do tasks like loading files, creating new nodes and getting the frame rate. The Browser class is is available to both internal and external SAI programs.

The ExternalBrowser interface allows external SAI programs to access further functionality like pausing the rendering and adding browser listeners. You can only do these operations from external code. In order to load a file we will get a Browser reference from the X3DComponent we created.

        // Get an external browser
        ExternalBrowser x3dBrowser = x3dComp.getBrowser();

Once we have a browser reference we can use this to load a file into the browser. There a few methods of doing this. For this first example we will use the simplist version. Its a blocking call, createX3DFromURL which will not return till the file has been downloaded and is ready to display. This method return's an X3DScene object which encapsulates an X3D scene. We will then replace the current scene which is blank with this newly loaded scene.

        // Create an X3D scene by loading a file
        X3DScene mainScene = x3dBrowser.createX3DFromURL(new String[] { "moving_box.x3dv" });

        // Replace the current world with the new one
        x3dBrowser.replaceWorld(mainScene);
You can find the complete code for this example here SimpleSAIDemo.

URL Handling

One concept to understand here is how URL's are handled in X3D. The createX3DFromURL method takes an array of URL's as its input. This area is searched till one of the URL's resolves and downloads successfully. A URL provides a way of specifying the location of an object on the internet or your local machine. If you just provide a filename or a partial directory path and filename(as in "examples/foo,x3dv") then you have provided a relative URL. The X3D specification says these relative URL's should be based from the current working directory. See 3.3.4.2 of the X3D Java binding specification(ISO/IEC 19777-2:2005) for more details.

How to change a field value

In this section you will learn to change an X3D nodes field value. An example of this would be to turn a boxes color from red to blue.

Once a world is loaded its typical to change something dynamically. If the node is DEFed in the X3D file then its easy to access it from SAI.

In this example we will find the Material named "MAT" and change its diffuseColor to blue. The file we will be modifying looks like this, basically a red moving box:

#X3D V3.0 utf8

PROFILE Interactive

DEF TS TimeSensor {
  cycleInterval 10
  loop TRUE
}

DEF TG Transform {
  rotation 0 1 0 0.78
  children Shape {
    geometry Box {}
    appearance Appearance {
      material DEF MAT Material {
        diffuseColor 1 0 0
      }
    }
  }
}

DEF PI PositionInterpolator {
  key [ 0 0.25 0.5 0.75 1 ]
  keyValue [
     0 0 0
    -1 0 0
    -1 1 0
     0 1 0
     0 0 0
  ]
}

ROUTE TS.fraction_changed TO PI.set_fraction
ROUTE PI.value_changed TO TG.translation
Notice the material node is DEFed as MAT, and its currently red. We can use the following code to change the diffuseColor field from red to blue.

First we will find a node named MAT in the current scene. In this example we will use the main scene loaded from moving_box.x3dv. The getNamedNode method searches the specified scene and returns the named node or null if its not found.

        // Find a node named MAT
        X3DNode mat = mainScene.getNamedNode("MAT");
        if (mat == null) {
            System.out.println("Couldn't find material named: MAT");
            return;
        }
After we have the node we need to find the diffuseColor field and change its value. To get a field of a node we call the getField method on the node. We then type that field to its X3D datatype, SFColor in this case. Once we have the field we can query or set its value. Here we will create a local variable named blue and set the fields value with it.

The X3D abstract specification defines the field names for each node. You must follow the X3D access model. This means that initializeOnly fields are only changeable before the node is realized. A node is realized once its added to a scenegraph or when its realized method is called. You can modify inputOnly and inputOutput fields anytime. outputOnly fields cannot be modified using the SAI.

        // Get the diffuseColor field
        SFColor color = (SFColor) mat.getField("diffuseColor");

        // Set its value to blue
        float[] blue = {0,0,1};
        color.setValue(blue);
You can find the complete example in FieldAccessDemo.java

How to create nodes

This section will show how to create new nodes. Instead of loading a file you can create the whole world dynamically node by node. The example will create a very simple world containing a Box.

The first task when dynamically creating a world is to create a new X3DScene. Each X3DScene must pre-declare what Profile and Components it will use. When requesting a profile you must consider that the browser may not support that profile. If the profile you request is not supported it will throw a NotSupportedException. This example will exit if the Immersive profile is not supported.

        ProfileInfo profile = null;

        try {
            profile = x3dBrowser.getProfile("Immersive");
        } catch(NotSupportedException nse) {
            System.out.println("Immersive Profile not supported");
            System.exit(-1);
        }
Once you have the ProfileInfo and ComponentInfo instances you need then you are ready to create the scene. The createScene method creates an empty scene. It returns an X3DScene object.
        X3DScene mainScene = x3dBrowser.createScene(profile, null);
We can now begin to create nodes and add them to our world. The createNode method is used to create new nodes. If the node is not known it will throw a InvalidNodeException. This snippet shows a Shape node being created. Then a Box node is created and set as the geometry field of the Shape. The shape node is then added to the root of our newly created scene.
        X3DNode shape = mainScene.createNode("Shape");
        SFNode shape_geometry = (SFNode) (shape.getField("geometry"));
        X3DNode box = mainScene.createNode("Box");

        shape_geometry.setValue(box);

        mainScene.addRootNode(shape);

You can find the complete example in CreateNodeSAIDemo.java

How to listen for field changes

A SAI program can listen for changes on any field. This allows your application to make changes in response to what's going on in the 3D simulation. For this example we want to know when a box with a touch sensor is touched. The X3D scene for this example is very simple.

#X3D V3.0 utf8

PROFILE Interactive

DEF TG Transform {
  rotation 0 1 0 0.78
  children [
     Shape {
    geometry Box {}
    appearance Appearance {
      material Material {
        diffuseColor 1 0 0
      }
    }
  }
  DEF TOUCH_SENSOR TouchSensor {}
  ]
}
The SAI code will use the addX3DEventListener method to listen for changes on the TouchSensor's touchTime field. We will also need to implement the X3DFieldEventListener interface.
        // Create an X3D scene by loading a file
        X3DScene mainScene = x3dBrowser.createX3DFromURL(new String[] { "touchy_box.x3dv" });

        // Replace the current world with the new one
        x3dBrowser.replaceWorld(mainScene);

        // Find a TouchSensor named TOUCH_SENSOR
        X3DNode touch = mainScene.getNamedNode("TOUCH_SENSOR");
        if (touch == null) {
            System.out.println("Couldn't find TouchSensor named: TOUCH_SENSOR");
            return;
        }

        SFTime ttime = (SFTime) touch.getField("touchTime");

        // Listen for changes on the touchTime field
        ttime.addX3DEventListener(this);
    }

    //----------------------------------------------------------
    // Methods defined by X3DFieldEventListener
    //----------------------------------------------------------

    public void readableFieldChanged(X3DFieldEvent evt) {
        System.out.println("Stop touching me!");
    }
The complete example is located here: FieldEventDemo.java

How to add a Route

Routes are used in X3D to send messages to nodes. A typical example is routing output of an interpolator to the transform above some geometry. This example starts with a Box with a TouchSensor. When it is clicked the SAI code will wire up a TimeSensor to an interpolator and the interpolator to a Transform above a Box. The box will then start to animate.

This snippet of code finds the TouchSensor named TOUCH_SENSOR. It then gets its touchTime field and adds a X3DFieldEventListener. When this touchSensor is activated it will trigger the field listener.

        X3DNode ts = mainScene.getNamedNode("TOUCH_SENSOR");
        if (ts == null) {
            System.out.println("Couldn't find TouchSensor named: TOUCH_SENSOR");
            return;
        }

        // Get TOUCH_SENSOR.touchTime
        SFTime touchTimeField = (SFTime) ts.getField("touchTime");

        touchTimeField.addX3DEventListener(this);
Here we actually add the dynamic route. To add a route you need the two nodes to wire together and the field names to use. Here we use getNamedNode to get the TimeSensor, PositionInterpolator and Transform that we will route to. Two routes are added, one from the TimeSensor to the Interpolator. The other from the Interpolator to the Transform. If you try to get an invalid route you will get one of the following exceptions: InvalidReadableFieldException, InvalidWriteableFieldException or InvalidNodeException.
    /**
     * An X3D field has changed.
     *
     * @param evt The event.
     */
    public void readableFieldChanged(X3DFieldEvent evt) {
        // Find a timesensor named TIME_SENSOR
        X3DNode TS = mainScene.getNamedNode("TIME_SENSOR");
        X3DNode PI = mainScene.getNamedNode("PI");
        X3DNode TG = mainScene.getNamedNode("TG");

        mainScene.addRoute(TS,"fraction_changed",PI,"set_fraction");
        mainScene.addRoute(PI,"value_changed",TG,"translation");
    }
The complete example is located here: AddRouteSAIDemo.java

How to walk an X3D scenegraph

Another way to change the X3D scenegraph is to walk the graph and look for nodes of a specific type. This example will show how to walk a Scenegraph looking for Material nodes.

The main code of interest is in the findMaterial method. This method recursively walks the scengraph looking for Materal nodes.

    /**
     * Find all the material nodes and change them to red.
     */
    private void findMaterial(X3DNode[] nodes) {
        int len = nodes.length;
        int types[];

        X3DFieldDefinition[] decls;
        int dlen;
        X3DNode node;

        for(int i=0; i < len; i++) {
            node = nodes[i];

            if (typeEquals(node, X3DNodeTypes.X3DMaterialNode)) {

                // Get the diffuseColor field
                SFColor color = (SFColor) node.getField("diffuseColor");

                // Set its value to blue
                float[] blue = {0,0,1};
                color.setValue(blue);
            }

The above code looks at each node and determines if its a Material node. If it is then it changes its material color to blue.

            decls = node.getFieldDefinitions();
            dlen = decls.length;
            int ftype;
            int atype;
            MFNode mfnode;
            SFNode sfnode;

            X3DNode[] snodes;

            for(int j=0; j < dlen; j++) {
                ftype = decls[j].getFieldType();
                atype = decls[j].getAccessType();

                if (atype == X3DFieldTypes.INPUT_OUTPUT || atype == X3DFieldTypes.INITIALIZE_ONLY) {
                    if (ftype == X3DFieldTypes.MFNODE) {
                        mfnode = (MFNode) node.getField(decls[j].getName());
                        snodes = new X3DNode[mfnode.size()];

                        mfnode.getValue(snodes);
                        findMaterial(snodes);
                    } else if (ftype == X3DFieldTypes.SFNODE) {
                        sfnode = (SFNode) node.getField(decls[j].getName());
                        snodes = new X3DNode[1];

                        snodes[0] = sfnode.getValue();

                        if (snodes[0] != null) {
                            findMaterial(snodes);
                        }
                    }
                }
            }
        }
    }
The above code gets the field declarations of a node. It determines that the field is readable(inputOutput or initializeOnly) using the getFieldAccess method. Fields which are MFNode or SFNode are recursively traversed for further processing. The getFieldType method returns a constant which is defined in the X3DFieldTypes interface.

The complete code for this example is located here: SAISGWalkDemo.java

How to use SAI inside a world

This example shows how to use SAI inside a world. It uses a TimeSensor to clock the world occassionally. Every cycle of the TimeSensor the box will be moved.

X3DScriptionImplementation Interface

An internal SAI script must implement the X3DScriptImplementation interface. This interface has the following methods:

Xj3D Component Example
Method Summary
 void eventsProcessed()
          Notification that all the events in the current cascade have finished processing.
 void initialize()
          Notificatoin that the script has completed the setup and should go about its own internal initialisation.
 void setBrowser(Browser browser)
          Set the browser instance to be used by this script implementation.
 void setFields(X3DScriptNode externalView, java.util.Map fields)
          Set the listing of fields that have been declared in the file for this node.
 void shutdown()
          Notification that this script instance is no longer in use by the scene graph and should now release all resources.
 

These methods are called at the appropriate time in the X3D event model.

In this example we have defined the following interface to our script:

   DEF S Script {
     inputOnly SFTime pulse
     outputOnly SFVec3f location
     outputOnly SFRotation orientation

     url [ "MoveObjectScript.class" ]
   }
Here only the setFields and shutdown methods are needed. The setFields method gives the script access to its fields. We will register interest for changes on the pulse field.
    //----------------------------------------------------------
    // Methods defined by X3DScriptImplementation
    //----------------------------------------------------------

    public void setBrowser(Browser browser) {
        // ignored as we don't need any browser functionality
    }

    public void setFields(X3DScriptNode externalView, Map fields) {
        // Get the fields we need for processing

        location = (SFVec3f)fields.get("location");
        rotation = (SFRotation)fields.get("orientation");

        inputField = (SFTime)fields.get("pulse");

        // Express interest in pulse changes
        inputField.addX3DEventListener(this);
    }

    public void initialize() {
    }

    public void eventsProcessed() {
    }

    public void shutdown() {
        inputField.removeX3DEventListener(this);
    }
Every time the pulse field is changed we will flip between two locations. The following code shows this logic. Notice that this interface is the same regardless whether you are internal or external to an X3D world.
    //----------------------------------------------------------
    // Methods defined by X3DFieldEventListener
    //----------------------------------------------------------

    public void readableFieldChanged(X3DFieldEvent evt) {
        if(flip) {
            flip = false;
            location.setValue(otherLocation);
            rotation.setValue(otherRotation);
        }
        else {
            flip = true;
            location.setValue(homeLocation);
            rotation.setValue(homeRotation);
        }
    }
You can find the complete code for this example here MoveObjectScript using this main world: move_object.x3dv

How to write per-frame logic

TBD

How to turn external access code into internal

TBD

Professional Training

Yumetech offers training on X3D and how to use Xj3D in your application. Details available at: Yumetech Training
[ Xj3D Homepage | Xj3D @ Web3d | Download | Screenshots | Dev docs | Dev Releases | Conformance | Contributors | Getting Started | Bug Tracking ]

Author: Alan Hudson
(c) 2005 by Yumetech, Inc.   All rights reserved
Last updated: $Date: 2007-09-19 20:47:58 $