Chapter 21

Using Java to Add Behaviors to VRML

-by Justin Couch


CONTENTS


In the previous chapter, you were introduced to scripting and the basics of JavaScript. Once you start playing with it, you'll realize some of its limitations. It's fine for doing basic mathematical work and input response, but it can't do any of the more juicy bits you need for doing really interesting things. So you need to use something that's designed for the task: Enter Java.

The VRML 2.0 specification deliberately did not require support for any one language for VRML scripting. At the time of Draft 3's release, there were two language bindings for the VRML API: JavaScript and Java. If you've made it this far into the chapter, then I expect you either already know Java or have enough interest that you want to learn it. Compared to the rest of the book, this chapter will really start to dig into the heart of VRML. Java isn't a scripting language, so you really want to know the effects of your code on what the world does.

You'll start by taking up where the last chapter left off and cover the following topics:

Setting Up to Use Java

The first thing that's assumed here is that you're already set up to use Java. You have either the JDK from Sun Microsystems (http://www.javasoft.com/) or you have another implementation that comes with a Java compiler, like Borland C++ 5.0 or Symantec Cafe.

  1. You need to get hold of the Java classes for VRML; they're found with your browser. Start with Sony's CyberPassage. You've been using CosmoPlayer for most of the examples, but it doesn't support Java for the scripting.
  2. Add the directory path to the VRML class files to the CLASSPATH variable so that the Java compiler can find them. Assuming you have installed it in the directory C:\Program Files\Cyberpassage, then you need to add C:\Program Files\Cyberpassage\vrml to CLASSPATH. Now you should be able to compile your Java scripts with no problems.

    Caution
    Early versions of CyberPassage put classes in a PKZipped file called classes.zip. This required you to create a directory called vrml and unzip the class files into that directory for it to work. Check the information with the latest version of the software.

  3. Start to write and compile your Java scripts in your favorite environment.

The difference between Java and JavaScript is that you can't write Java source code directly in the file, which means you must compile every single script, too. Compared to JavaScript, this does slow down the development cycle a bit, but the end result is scripts that execute much faster than the interpreted JavaScript. Each class will be a separate file to be downloaded, but if you're using the same script many times in the world, then it needs to be downloaded only once.

Using Java in Scripts

Assuming you understood what was happening in the scripting in the last chapter, you'll now convert it to Java. To start with, you need to change the url to point to the Java class file. The VRML specification says only that the Java byte code needs to be supported. In this case, you'll call the source file replace_script.java to match with the name used in the DEF keyword.

The vrml Packages

In VRML, Java does not run as an applet; it runs as a special Java class called Script. Script is a class that's extended for the individual scripts. VRML-specific Java classes are available in the vrml packages, which have three main parts: vrml, vrml.node, and vrml.field. There's also a class specifically for the browser, which is in the vrml package.

The Field class is empty so that individual classes can be created that mimic the VRML field types. There are two types of Field classes: read-only and unlimited access. The read-only versions start with Const<fieldtype>, and the unlimited access versions have the same name as the field type. The types returned by these classes are standard Java types, with a few exceptions. MF types return an array of that type, so the call to the getValue() method of an MFString would return an array of type String. The basic outline of a Field type class is demonstrated by the MFString class:

public class MFString extends MField
{
   public MFString(String s[]);

   public void getValue(String s[]);

   public void setValue(String s[]);
   public void setValue(int size, String s[]);
   public void setValue(ConstMFString s);

   public String get1Value(int index);

   public void set1Value(int index, String s);
   public void set1Value(int index, ConstSFString s);
   public void set1Value(int index, SFString s);

   public void addValue(String s);
   public void addValue(ConstSFString s);
   public void addValue(SFString s);

   public void insertValue(int index, String s);
   public void insertValue(int index, ConstSFString s);
   public void insertValue(int index, SFString s);
}

The method names are pretty straightforward. You can set values using both the standard VRML type as well as the read-only field value, which comes in handy when you're setting values based on the arguments presented.

The other half of the vrml Java API is the Script itself. Script is based on the Node interface, which is defined only for VRML scripts. This interface serves as the basis for representing the individual nodes. VRML 2.0 defines Script class as the only implementation of the Node interface, which is shown below:

public abstract class Script extends BaseNode { 
   // This method is called before any event is generated
  public void initialize();
  // Get a Field by name.
  //   Throws an InvalidFieldException if fieldName isn't a valid
  //   event in name for a node of this type.
  protected final Field getField(String fieldName);
  // Get an EventOut by name.
  //   Throws an InvalidEventOutException if eventOutName isn't a valid
  //   event out name for a node of this type.
  protected final Field getEventOut(String fieldName);
  // processEvents() is called automatically when the script receives 
  //   some set of events. It should not be called directly except by its subclass.
  //   count indicates the number of events delivered.
  public void processEvents(int count, Event events[]);
  // processEvent() is called automatically when the script receives 
  // an event. 
  public void processEvent(Event event);
  // eventsProcessed() is called after every invocation of processEvents().
  public void eventsProcessed()
  // shutdown() is called when this Script node is deleted.
  public void shutdown(); 
}

Every script is a subclass of the Script class. However, you can't just go out and write your own script now. You need some more introduction to how it works.

Outlining the Script

  1. Since you must use the vrml packages to write scripts, then you have to tell the compiler to use it by importing it. Most of the time, you'll use all the different areas of the VRML interface, so you will need to import all three packages. Start the class by declaring its name and what classes it uses:
    import vrml.*;
    import vrml.field.*;
    import vrml.node.*;
    class replace_script extends Script {}
    
  2. Next, create the initialize() method and use the getField() method from the Script class definition to get the variables belonging to the VRML Script node into Java. To do this, simply pass the method a text string with the name of the field you want. Because getField() returns a Field, you need to cast it to be the correct field type so you can call the right methods. For example, use the following to get the strings corresponding to the external file:
    class myscript extends Script {
        private MFString externalFile;
        void initialize(void)
        {
            externalFile = (MFString)getField("externalFile");
        }
    }
    

    By convention, all the script internal variables are declared private. You could make them public, but there's no point because nothing accesses them from outside. These internal variables can be named whatever you want, but they usually have the same name as the corresponding VRML field.

    Tip
    The getField() method is used only for retrieving the field type values from the script. To get the eventOut type values, use the getEventOut() method in the same way, remembering to cast the returned value. In JavaScript you could just assign a value to that eventOut reference to generate the event. You can do the same in Java, once you have retrieved the eventOut reference.

  3. This leaves the eventIns. Now the Java model varies radically from the JavaScript one. In JavaScript, each eventIn in Script's VRML declaration requires a corresponding method. Java uses a different approach that has a single method taking a list of the current events to be processed; then you call the appropriate internal methods to deal with the information. If you have done any programming with the awt package, then you should be fairly used to this approach. In the next section, you'll take a look at Java event handling.

Caution
In early drafts of the Java API, it was designed so that you wrote direct eventIn methods, just like the JavaScript way of doing things. Beta 1 and 2 versions of Sony's CyberPassage version 2.0 implemented this early form, and several early books on VRML 2.0 outlined this previous version of the Java API. This version is no longer current, however.
The change was made so that browsers could be written purely in Java. With the old draft versions, this wasn't possible.

Dealing with Events

The previous section mentioned that the event-handling mechanism for Java scripts uses a single function as the interface point. To deal with events in a script, you need to take a few steps to get there:

  1. Declare the initialize() method, and set the internal values using the getField() method. (This is the same as Step 2 in the previous task section.)
  2. Create a series of private methods that you want to call for processing individual events.
  3. Create one of the event-handling functions and use it to call the internal methods created in Step 1.

Events are passed to the script's event handler as an Event object type:

class Event {
      public String getName();
      public ConstField getValue();
      public double getTimeStamp();
   }

As with all Java programming, if you want these methods to be called from outside, you must declare them public. An event coming into the script is like an external class calling methods from your class. In the Java API, event processing is done by one of two methods. The processEvents() method is used when there's more than one event generated at a particular timestamp, but the processEvent() method is called when there's only one event to be taken care of at that time.

The processEvents() method takes an array of event objects that you then analyze and pass to the various methods. This is no different from the way the awt event-handling system works. A typical segment of code is demonstrated in Listing 21.1.

To see what a complete Java script source file looks like, declare just the isOver method for the moment so you can see the difference between the Java and JavaScript ways of handling the incoming events.


Listing 21.1. Converted version of one of the eventIn handlers from JavaScript to Java.

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

class replace_script extends Script
{
    // now we get all the class variables
    private SFBool pointerOver;

    //initialization
    public void initialize(void)
    {
        pointerOver = (SFBool)getField("pointerOver");
    }

    // now the eventIn declarations - only do the isClicked event for now
    private void isOver(ConstSFBool value)
    {
        if(value.getValue() == false)
            pointerOver.setValue(false);
        else
            pointerOver.setValue(true);
    }

    // now the event handling function
    public void processEvents(int count, Event events[])
    {
        int    i;
        for(i=0; i < count; I++)
        {
            if (events[i].getName().equals("isOver"))
                isOver(events[i].getValue());
            // collection of other else if statements here
        }
}

The second event handler method is processEvent(); since it deals with just a single event, the argument is only a single Event object. Therefore, the only difference between this method and the processEvents() method is that you don't need the for loop. The big if...else ladder of string comparisons remains, though.

When should you use the different event-handling functions? Take the following piece of VRML code as an example (this comes straight from the VRML specification):

Transform {
    children [
        DEF TS TouchSensor {}
        Shape { geometry Cone {} }
    ]
}
DEF SC Script {
    url     "Example.class"
    eventIn SFBool isActive
    eventIn SFTime touchTime
}
ROUTE TS.isActive  TO SC.isActive
ROUTE TS.touchTime TO SC.touchTime

Whenever the TouchSensor is touched, it generates two simultaneous events, so the script receives two. In this case, you need the processEvents() method that deals with a number of simultaneous events. If you were interested only in the isActive event, then you could use just the processEvent() method.

If you're not sure whether the script will receive more than one simultaneous event, then you can declare both methods. To save duplicating large amounts of code, you can put all the code to call the internal methods in the processEvent() method and just put a for loop that calls processEvent() with each individual event object in processEvents(). If this has confused you, then have a look at the following code fragment:

void public processEvent(Event e)
{
    if(e.getValue().equals("someEvent"))
        // call internal method
  else if ......
}
void public processEvents(int count, Event events[])
{
    int    i;
    for (i=0; i < count; i++)
        processEvent(events[i]);
}

Notice that a bit more work needs to be done to get an initial Java class file running. One advantage is that you declare only the fields you need to use. In Listing 21.1 you just wanted to use the pointerOver field from the VRML definition, so you left the rest out. The Java code is compiled independently of VRML source code, allowing you to take a staged approach to developing the code, adding variables and event handlers only when they're needed.

Outline of the VRML Java API

The Browser API defined in the previous chapter is also applicable to Java, but naturally there are Java methods for accessing the browser functionality. These methods can be accessed through the Browser class, so the syntax is almost the same as that in the previous chapter. The only thing that differs is what the various VRML types are in Java.

VRML types relate back to standard Java in the following manner (MF versions are just arrays of the SF type):

Table 21.1. Relationship of VRML types to Java types.

VRML TypeJava Type
SFString String
SFFloat float
SFInt32 int
MSString String[]
MFNode Node[]

Look again at the example of loading a world on demand and the newNodesReady() method that it called in Listing 21.2. You'll create the strings by using the Java String type.


Listing 21.2. Using the Browser class under Java.

public void newNodesReady(ConstMFNode node_list)
{
    String shape = "Shape { appearance appearance {} }";
    String material = "Material { emissiveColor .2 .2 .2 }";
    String box = "Box { size 0.5 0.5 0.5 }";
    String sensor = "TouchSensor {}";
    Node shape_node;
    Node sensor_node;

    // create some nodes
    shape_node = Browser.createVrmlFromString(shape);
    sensor_node = Browser.createVrmlFromString(sensor);

    // assign some properties
    shape_node.postEventIn("material",
                (Field)Browser.createVrmlFromString(material));
    shape_node.postEventIn("geometry",
                (Field)Browser.createVrmlFromString(box));

    // update the internal field with the newly created
    // list of nodes
    newWorld.setValue(node_list.getValue());

    // now add the nodes to the scene
    secondObject.getValue().postEventIn("add_children",
                                        (Field)shape_node);
    secondObject.getValue().postEventIn("add_children",
                                        (Field)sensor_node);

    // finally add routes between the newly formed
    // touchsensor and the inputs to this script
    Browser.addRoute(sensor_node, "isActive",
                     this, "isClicked_new");
    Browser.addRoute(sensor_node, "isOver",
                     this, "isOver_new");
}

Java has a completely different structure. First, there's no way to directly assign values to the nodes' fields, as there was in JavaScript. If the node contains ordinary field types, then you must set them in the text string before you actually create the node; otherwise, you won't have access to it again. To write values to the fields that are eventIns or exposedFields, you must post an event to that field. You may remember from the last chapter that exposedFields can act just like eventIns and eventOuts combined. In the case of Java scripts, this is exactly how you must treat them.

Notice how much more object-oriented Java is than the JavaScript scripting; you can be sure Java is passing the correct types to the different nodes. There's more work involved to get the script up and running, but once you do, you can explore many other goodies.

The Beginning and the End

When you first add a behavior to a world, you might need to initialize some internal values. VRML allows the normal method of using the constructor method to perform any initialization that needs to be done internally.

The problem is that at the time the constructor is called, you probably won't have all the external access to the world enabled; even the values of the fields may not be valid yet because the world is still loading. To overcome this, the initialize() method was created. This function is, by default, empty, but can be overridden. The initialize() method is guaranteed to be called just after the entire world is created but before any external events are generated. It's called only once, and when it is, you know that you can read valid values for each of the script fields (using the getField() method) and can send events to other parts of the scene graph. This means everything in your world should function normally.

In the next section, you'll look at creating multithreaded scripts. When a node is about to be removed, you should clean up any extras that have been left lying around. The shutdown() method is a predefined method for all Script nodes that's called just before your script is removed. In this method, you place any cleanup code you need, such as killing threads you might have created.

Why Use Java?

JavaScript is fairly limited in what it can do. Besides some basic math and fetching files with http calls, you can't do much else. Basically, you can use it to write quick code to fulfill short-term needs. When you want to do something fairly complex with your world, then you need to switch to Java.

A good place to use Java is when you want to feed live data to the world. Java scripts can make use of any of the standard Java classes, which means the networking and threads classes are available. One typical use would be an external server sending data to your scene, which you then represent as 3D objects. The next section will outline how to do this.

So where do you use Java and where do you use JavaScript? It depends on what you want to do. Unfortunately, at this stage of the game, you're limited more by the browser than anything else. In time, you can expect to see browsers that support both Java and JavaScript, as well as other languages. For the moment, just put on rose-colored glasses and assume an ideal world exists.

JavaScript is very handy for doing short little tasks when the compile-test cycle is too much effort. A good example is the toggle type of switch created in the previous chapter. Creating and compiling code for a three-line script isn't worth the trouble.

When you need greater flexibility and speed, then you should be using Java. The precom-piled code combined with JIT compilers should make the code execution much faster, particularly in large complex worlds with many behaviors running continuously.

Using Other Java Classes

Developing a full networked and threaded example is beyond the scope of this book, but I can give you some outlines on how to go about it. A typical example is the stock market ticker that does a real-time display in 3D of your favorite stocks. At a stock market site, a server process would broadcast the information to whoever requests it. At the client end (your VRML world), you need to establish a link to receive the information.

The problem is that you want to keep the time spent in the script to an absolute minimum; also, the timing of when the script will be called is completely out of your control. The script may not be called at the same time that data is ready to be read from the network. What do you do? Well, since I have just been talking about threads, that should trigger something. Of course-you set up an independent thread containing all the networking code and then just report back to the world when there's more information to update.

When you first enter the world, the tracking should start. To do this, you take advantage of the creation method and start up the thread there. The thread code then starts up and installs any code it needs to monitor the network connection to the server. Such tasks as establishing the initial connection would all be handled in this separate task, so that the script can return to the browser as quickly as possible.

Having a separate thread that listens for new data is fine, but this thread needs to get information back to the VRML world. There are two ways to do that; in each method, you'll pass an instance of the Script node to the thread. First, you could just create another method that isn't an eventIn method, then call that method. Second, you could post events to that script just as any other node or script would. Using non-eventIn methods is questionable at the moment because there are no rules as to whether it's permitted. The safe bet is to post events back to the script so that they get dispatched and looked after in the same manner as all the other events.

Future Enhancements

Java scripting issues are not as complete as the JavaScript interface; for instance, there are no classes to represent the individual node types that exist in VRML. During the writing of the VRML 2.0 specification, there wasn't enough time to define these classes, so they were left out.

Many things, because of incompleteness, were left out of the specification, but will be left to the revised version 2.1 of the specification. There was a whole section on an external API that allowed outside programs to talk directly with the browser, and also the VRML world itself, that was left out. The external API was left out completely; it was not just the Java version.

Workshop Wrap-up

This is a limited look at what can be done with Java. Because Java is a full programming language, the scope for experimentation is wide, and the possibilities are endless. Keep in mind that you should be careful to not put too much code into the scripts. Every millisecond spent in the script is less time spent actually drawing the world onscreen. Make good use of threads where needed, and keep the scripts small.

One tool that hasn't been covered is the Liquid Reality toolkit from DimensionX. It has a complete set of classes that mimic the VRML nodes. It doesn't create a link for the scripts to interface with a particular browser; instead, you basically create your own browser by defining the nodes and their relationships, which are then drawn to the screen within the Java code itself. How Liquid Reality works, though, is more of a programming issue rather than a VRML topic, so it wasn't covered in depth in this book.

Next Steps

So what's left to do?

Q&A

Q:
What if I don't want to write my own Java behaviors but just view worlds that have behaviors? What do I need to have?
A:
You still need to use Sony's CyberPassage to view them. No doubt there will probably be a few more browsers to add to the list as more companies release their offerings. You don't need to set the CLASSPATH variable, however, to use the Java behaviors.
Q:
After compiling my code, it runs fine when I load it from the local drive. However, when I run it from the Web server, it doesn't work at all.
A:
There may be two answers to your problem. First, the Web server might not be configured to handle the .class files. If so, then get your Web server administrator to add the following MIME type:
application/octet-stream.class
Second, CyberPassage has the same security rules as Sun's HotJava browser. It's possible you don't have the correct permissions set up to allow your browser to download the Java binaries from the Web server.