instantreality 1.0

Implementing Script nodes in Java

Keywords:
Script, Java
Author(s): Patrick Dähne
Date: 2011-04-12

Summary: This tutorial explains how to implement X3D Script nodes in Java.

Introduction

X3D, like many other 3D file formats, describes geometries of 3D objects that make up the scene and their materials in a hierarchical graph structure called the "Scene graph". Geometries and materials are defined as nodes in that scene graph that keep their data in fields. But X3D differs from most other 3D file formats in its ability to also describe the dynamic behaviour of the scene. X3D nodes are small "state machines". They can receive events via event-in slots, change their state according to the event, and send resulting events via event-out slots. Event-out slots and event-in slots are connected by "routes". As a result, the classical scene graph gets augmented by a data flow graph that describes the dynamic properties of the scene.

VRML/X3D already contains a large set of nodes that can be used to build typical VR applications, e.g. interpolator nodes that allow to define key frame animations. But of course the standard set of nodes cannot cover all possible applications. For that reason, you can extend the standard set of nodes by your own nodes. This can be done with a special kind of node, the "Script" node.

The Script node is some kind of node framework. It only has a minimal number of fields that control the operation of the Script node, and it does not have any behaviour of its own. The idea is that the application programmer defines additional fields, event-in slots and event-out slots when instantiating the Script node in the scene, i.e. he can define the interface of the respective Script node. Furthermore, he can define the behaviour of the respective Script node, i.e. how it reacts to incoming events, by using a piece of code written in a programming language that the browser supports. VRML/X3D defines how to use JavaScript and Java to program the behaviour of Script nodes. Instant Player supports both of these languages. This tutorial only explains how to use Java in Script nodes. There is another tutorial that explains how to use JavaScript in Script nodes.

Warning: Instant Player fully supports the old VRML Java API. The new X3D JAVA-SAI Script-interface is not implemented. Look at the API-Specification for details

Warning: The current implementation is unstable and might crash. When you create new threads, do not try to access the X3D scene graph from these thread - the scene graph is not yet thread safe!

For more information about Java in Script nodes, have a look into the VRML specification available from http://www.web3d.org/.

In the following sections, we will extend a simple scene consisting of a red sphere with a Script node with some simple behaviour written in Java: Whenever the user clicks onto the sphere, it should toggle its color between red and green. To detect click events, we have to add a "TouchSensor" to the scene. TouchSensors detect click events on neighbouring subtrees of the scene graph, i.e. on subtrees that are siblings of the TouchSensor. So usually you add the shape node you want to click on as well as the TouchSensor to a "Group" node. But in this example, the sphere is the only object in the scene, so we can omit the Group node. So the resulting example without the Script node looks like this when using classic VRML encoding:

Code: Scene used in the following examples (VRML encoding)

#VRML V2.0 utf8

Shape
{
  appearance Appearance
  {
    material DEF mat Material
    {
      diffuseColor 1 0 0
    } # Material
  } # Appearance
  geometry Sphere
  {
  } # Sphere
} # Shape

DEF ts TouchSensor
{
} # TouchSensor

In XML encoding, the same scene looks like this:

Code: Scene used in the following examples (XML encoding)

<?xml version="1.0" encoding="UTF-8"?>
<X3D profile='Full' version='3.0'>
  <Scene>

    <Shape>
      <Appearance>
        <Material DEF='mat' diffuseColor='1 0 0'/>
      </Appearance>
      <Sphere/>
    </Shape>

    <TouchSensor DEF='ts'/>

  </Scene>
</X3D>

You can download the complete example scenes at the end of this tutorial.

Defining the Interface

First of all, we have to define the interface of the Script node, i.e. its field, event-in slots and event-out slots. Like when instantiating other nodes in the scene, we start with the type of the node ("Script"). Because we have to connect the script node via routes with other nodes in the scene, we also have to give it a name ("script" in this example). So we start as always:

Code: The plain Script node (VRML encoding)

DEF script Script
{
  directOutput TRUE
  mustEvaluate TRUE
}

In XML encoding, the Script node looks like this:

Code: The plain Script node (XML encoding)

<Script DEF='script' directOutput='true' mustEvaluate='true'>
</Script>

As you can see in the code above, there are two SFBool fields of the Script node ("directOutput" and "mustEvaluate") that we set to TRUE. I won't go into detail what these fields are good for - actually, Instant Player ignores these fields, but you might run into problems on other players when you do not set them to TRUE. When you're curious, have a look at the Script node specification to find out what these fields are good for. As rule of thumb, start your own Script nodes as shown above.

Now, how do we define the fields and slots of our Script node? Normally, we write a list of field names followed by their field values between the braces (when using VRML encoding), or we specify the field values as attribues of the opening XML tag. For script nodes, we do more or less the same. The only difference is that we also have to specify whether we have a field, an event-in slot or an event-out slot, as well as the X3D data type.

So, what do we need? First of all, we need to receive click events from the TouchSensor. The TouchSensor has an SFBool event-out slot "isActive" that provides these events. So we need an SFBool event-in slot. We'll call it also "isActive" (we could use any name here, but it's always a good idea to use names that make clear what the event-in slot is good for). Furthermore, we want to change the color of the sphere, so we need an SFColor event-out slot. We'll call that event-out slot "color_changed". Finally, we want to toggle the color, so we need an SFBool field that saves the current state (whether the sphere currently is red or green). We'll call that field "flag"). So the complete interface of our Script node looks like this:

Code: Script node with interface declarations (VRML encoding)

DEF script Script
{
  directOutput TRUE
  mustEvaluate TRUE

  eventIn SFBool isActive
  eventOut SFColor color_changed
  field SFBool flag FALSE
  exposedField SFBool foo FALSE
}

In XML encoding, the Script node with the interface declarations looks like this:

Code: Script node with interface declarations (XML encoding)

<Script DEF='script' directOutput='true' mustEvaluate='true'>
  <field accessType='inputOnly' name='isActive' type='SFBool'/>
  <field accessType='outputOnly' name='color_changed' type='SFColor'/>
  <field accessType='initializeOnly' name='flag' type='SFBool' value='false'/>
  <field accessType='inputOutput' name='foo' type='SFBool' value='false'/>
</Script>

As you can see, we have to specify an initial value for our "flag" field, in this case "FALSE". You have to specify initial values for all the fields of your Script node. On the other hand, you cannot specifiy initial values for event-in slots and event-out slots.

By the way, exposed fields (or inputOutput fields in X3D slang) where forbidden in the original VRML-Spec but are allowed in X3D Script nodes. Instant Player now (with Beta7) supports exposed/inputOutput-Fields but there are still incompatibilities between different browsers so you have to be carefull.

Before we finally write the code, we connect the Script node to the TouchSensor and the Material node:

Code: Connecting the Script node (VRML encoding)

ROUTE ts.isActive TO script.isActive
ROUTE script.color_changed TO mat.set_diffuseColor

In XML encoding, the routes look like this:

Code: Connecting the Script node (XML encoding)

<ROUTE fromNode='ts' fromField='isActive' toNode='script' toField='isActive'/>
<ROUTE fromNode='script' fromField='color_changed' toNode='mat' toField='set_diffuseColor'/>

Writing the Code

After defining the interface, we have to write the Java code that handles incoming events, changes the state of the node accordingly, and sends resulting events. In our case, we need code that listens to the "isActive" event-in slot, toggles the state of the "flag" field, and sends a red or green color to the "color_changed" event-out slot depending on the state of the "flag" field.

You have to specify the file that contains the code in the "url" field of the Script node (that is one of the native fields of the Script node). E.g. when your Java class code is saved in a file called "MyCode.class" next to your X3D file, you have to specify the (relative) URL to that file like this:

Code: Referencing the Java byte code from the Script node

Script
{
  # Your fields here
  url "MyCode.class"
}

Ok, let's start coding. As always when programming in Java, we have to create a new class. Our new class must extend a class provided by the X3D browser, "vrml.node.Script". That class defines some empty methods that get called by the X3D browser that you may override with your own implementations:

void initialize()
:
Gets called once after loading the VRML scene.

void shutdown()
:
Gets called once before leaving/unloading the VRML scene.

void processEvent(vrml.Event event)
:
Gets called whenever an event is received from any of the event-in slots of the Script node.

void processEvents(int count, vrml.Event events[])
:
This method is an alternative to the previous method. It allows some optimisation when there at a given point in the execution of the scene more than one event-in slot of the Script node received an event. The previous processEvent method gets nevertheless called for each event.

void eventsProcessed()
:
Get called after processing all available events.

The following pseudo code shows the sequence in which these methods get called:

script.initialize();
while (doRender == true)
{
  vrml.Event[] events = getEventsForScriptNode();
  int count = events.length;
  script.processEvents(count, events);
  for (int i = 0; i < count; ++i)
    script.processEvent(events[i]);
  script.eventsProcessed();
}
script.shutdown();

An empty template you can use when you start implementing your own scripts looks like this:

Code: Empty skeleton of a Java Script node implementation

import vrml.*;
import vrml.node.*;
import vrml.field.*;

public class ScriptTemplate extends vrml.node.Script
{
  public void initialize()
  {
  }

  public void processEvents(int count, vrml.Event events[])
  {
  }

  public void processEvent(vrml.Event event)
  {
  }

  public void eventsProcessed()
  {
  }

  public void shutdown()
  {
  }
}

Usually, you'll just need to override the "initialize", "processEvent" and "shutdown" methods. "processEvents" and "eventsProcesses" are only needed under rare circumstances.

Back to our example. Well call our class "Tutorial" and save it in a file called "Tutorial.java". We want to react to mouse-click events from our TouchSensor, so we have to override the "processEvent" method:

Code: Tutorial Java implementation

public class MyCode extends vrml.node.Script
{
  public void processEvent(vrml.Event event)
  {
  }
}

The first thing we have to do in our "processEvent" method is to check from which event-in slot we received the event. In our example we just have one event-in slot, so we actually do not to do that, but for demonstration purposes we'll nevertheless check if we got the event from our "isActive" event-in slot:

Code: Checking for "isActive" events

if (event.getName().equals("isActive"))
{
}

Now that we are sure that we actually got an event from the "isActive" event-in slot, we have to check whether we got a "TRUE" or "FALSE" value. The "vrml.Event" class has a method "getValue()" which returns the event-in slot that received the event (the name of the method is misleading - you get the event-in slot, not the value). "getValue()" returns an object of the generic type "vrml.ConstField" which we have to cast into the concrete type of our event-in slot, "vrml.field.ConstSFBool". To get the actual value of the event, we have to call the "getValue()" method of the event-in slot:

Code: Checking for "TRUE" events

vrml.field.ConstSFBool isActive = (vrml.field.ConstSFBool)event.getValue();
if (isActive.getValue() == true)
{
}

Now that we now that we got a "TRUE" event on the "isActive" event-in slot, we have to toggle the "flag" field and send either a red or a green color to the "color_changed" event-out slot. Before we can do that, we need to get references to the "flag" field and the "color_changed" event-out slot. A good place to do that is the "initialize()" method, so we also override that method in our class:

Code: Getting the "flag" field and the "color_changed" event-out slot

private vrml.field.SFBool flag;
private vrml.field.SFColor color_changed;

public void initialize()
{
  flag = (vrml.field.SFBool)getField("flag");
  color_changed = (vrml.field.SFColor)getEventOut("color_changed");
}

Now we are able to finish the implementation of our "processEvent" method:

Code: Sending red or green color events

if (flag.getValue() == false)
{
  flag.setValue(true);
  color_changed.setValue(0, 1, 0);
}
else
{
  flag.setValue(false);
  color_changed.setValue(1, 0, 0);
}

The complete code looks like that:

Code: Complete Java implementation

public class MyCode extends vrml.node.Script
{
  private vrml.field.SFBool flag;
  private vrml.field.SFColor color_changed;

  public void initialize()
  {
    flag = (vrml.field.SFBool)getField("flag");
    color_changed = (vrml.field.SFColor)getEventOut("color_changed");
  }

  public void processEvent(vrml.Event event)
  {
    if (event.getName().equals("isActive"))
    {
      vrml.field.ConstSFBool isActive = (vrml.field.ConstSFBool)event.getValue();
      if (isActive.getValue() == true)
      {
        if (flag.getValue() == false)
        {
          flag.setValue(true);
          color_changed.setValue(0, 1, 0);
        }
        else
        {
          flag.setValue(false);
          color_changed.setValue(1, 0, 0);
        }
      }
    }
  }
}

Compiling the Code

To compile the Java code, you need the Java archive "instantreality.jar". The location of "instantreality.jar" depends on where on your machine you have installed Instant Player:

  • Under Windows, "instantreality.jar" is located in the "bin" folder of Instant Player's installation folder. By default, Instant Player gets installed under "C:\Program Files\Instant Reality" (on english versions of Windows), so the location of "instantreality.jar" is usually "C:\Program Files\Instant Reality\bin\instantreality.jar".
  • Under Mac OS, "instantreality.jar" is located inside the application bundle. When you installed Instant Player into your "Applications" folder, the location of "instantreality.jar" is "/Applications/Instant Player.app/Contents/MacOS/instantreality.jar". Please note that the Finder hides the contents of application bundles. To access them, right-click on the application icon and choose "Show Package Contents" from the context menu.

When you have located "instantreality.jar", you can either specify its path on the command line when calling the Java compiler, our you can add it to the "CLASSPATH" environment variable (see Java documentation for more information about that). When you specify the path on the command line, the compiler call looks like this (don't forget to adjust the path to "instantreality.jar" when you enter this command):

javac -classpath "C:\Program Files\Instant Reality\bin\instantreality.jar" MyCode.java

The result of this call (when everything went ok) is the compiled Java byte code in the file "Tutorial.class". When you get an error message telling you that the packages "vrml" or "vrml.node" do not exist, the compiler did not find "instantreality.jar". Check the path again you specified on the command line!

Now we are finished with the Java code for our example. We just have to add the Script node the our X3D file "Java.wrl". You can load the X3D scene in your X3D browser and check if it actually works as expected.

Pitfalls, Tipps and Tricks

Even though programming Script nodes in Java is straightforward and easy, there are some gotchas. In this section I'll keep a list of things that you should keep in mind when developing Java code:

Debugging

Debugging X3D Java code is a little bit uncomfortable because none of the existing X3D players has an integrated Java debugger. But many X3D players have some kind of error console where they print warnings and error messages. Instant Player also has such a console - you get it when you click onto the "Console" item in the "Window" menu. You can print your own message to that console by using the standard output stream "System.out" or the standard error output stream "System.err". You'll also find information about all uncatched Java exceptions in the console.

For example, in our "processEvent()" event handler we've developed above, we could add a "System.out.println()" call that prints the value of the incoming event to the console as well as the outgoing color values:

Code: processEvent event handler with debug output

public void processEvent(vrml.Event event)
{
  if (event.getName().equals("isActive"))
  {
    vrml.field.ConstSFBool isActive = (vrml.field.ConstSFBool)event.getValue();
    System.out.println("isActive(" + isActive.getValue() + ")"); // Debug output
    if (isActive.getValue() == true)
    {
      if (flag.getValue() == false)
      {
        flag.setValue(true);
        System.out.println("color is green"); // Debug output
        color_changed.setValue(0, 1, 0);
      }
      else
      {
        flag.setValue(false);
        System.out.println("color is red"); // Debug output
        color_changed.setValue(1, 0, 0);
      }
    }
  }
}

Keep in mind that event though the "System.out.println()" method takes a single String object parameter, you can create arbitrary complex messages by concatenating strings with the "+" operator. Due to the fact that Java automatically converts all objects to strings, you can also print arbitrary objects - when printing your own Java objects, you have to provide "toString()" methods for your objects. All standard VRML Java objects that represent VRML data types already have such a "toString()" method, so can directly print all VRML data types.

Load that code in your browser. Click onto the sphere several times, and you'll get some debug output like this in the console:

Code: Debug output in the console

isActive(true)
color is green
isActive(false)
isActive(true)
color is red
isActive(false)
isActive(true)
color is green
isActive(false)

The Sandbox - or: How to cope with AccessControlExceptions

For security reasons, all Java byte code is executed in a restricted sandbox, i.e. the code cannot execute potentially harmful operations like reading or writing files or opening network connections. Your code will throw an "AccessControlException" whenever it tries to execute a forbidden operation. In this case, you'll have to grant your code additional permissions using a special tool, the "policytool". There is a special tutorial that explains how to break out of the Java sandbox.

Converting between X3D timestamps and Java Date objects

X3D timestamps in Java are double values that represent the number of seconds since January 1, 1970, 00:00:00 GMT. That might be a good representation for a computer, but not for a human - it is little bit difficult to deal with X3D timestamps directly. Java has a special class called "java.util.Date" to work with dates and times. But how do you convert a X3D timestamp into a Java Date object and vice versa?

Converting a X3D timestamp into a Date object is straightforward. One variant of the Date constructor takes the number of milliseconds since January 1, 1970, 00:00:00 GMT. So you simply have to multiply the X3D timestamp by 1000:

Code: Converting a X3D timestamp into a Java Date object

java.util.Date date = new java.util.Date((long)(timestamp * 1000.0));

The other way round is straightforward, too. The "getTime()" method of the Date class returns the number of milliseconds since January 1, 1970, 00:00:00 GMT. So you simply have to divide that value by 1000:

Code: Converting a Java Date object into a X3D timestamp

double timestamp = (double)date.getTime() / 1000.0;

Example files

Here you can download the complete example we developed in this tutorial. The test scene is available in classic VRML encoding ("Java.wrl") as well as XML encoding ("Java.x3d"). The Java source is in "MyCode.java" and the compiled byte code in "MyCode.class". "ScriptTemplate.java" contains the code skeleton you can use for your own implementations.

Files: