by Justin Couch
So far in this book, you have seen Java being used to create whole standalone applications or applets to put in Web pages. But Java has other uses as well.
If you have been closely reading the computing press, you may have noticed sections creeping in about another Web technology called VRML-the Virtual Reality Modeling Language. VRML is designed to produce the 3D equivalent of HTML: a three-dimensional scene defined in a machine-neutral format that can be viewed by anyone with the appropriate viewer.
Until recently, VRML has not really lived up to its name. The first version of the standard produced only static scenes and was a derivative of Silicon Graphic's Open Inventor file format. A user could wander around in a 3D scene, but had no way to interact with the scene apart from clicking on the 3D-equivalent of hypertext links. This was a deliberate decision on the part of the designers. In December 1995, the VRML mailing list decided to drop planned revisions to version 1.0 and head straight to the fully interactive version 2.0.
One of the prime requirements for VRML 2.0 was the ability to support programmable behaviors. Of the seven proposals, the Moving Worlds submission by Sony and SGI came out as the favorite among the 2000 members of the VRML mailing list. Contained in what has now become the draft proposal for VRML 2.0 was a Java API for creating behaviors.
To effectively combine VRML and Java, you need a good understanding of how both languages work. This chapter introduces the Java implementation of the VRML API and shows you how to get the most from a dynamic virtual world.
Within the virtual reality environment, any dynamic change in the scenery is regarded as a behavior. A behavior can be something as simple as an object changing color when it is touched or as complex as autonomous agents that look and act like humans (such as Neal Stephenson's Librarian from Snow Crash).
To understand how to integrate behaviors, you have to understand how VRML works. Although this section doesn't provide a lengthy discussion of VRML, it does cover a few basic concepts. To start, VRML is a separate language from the Java used in the scripts. S"pL provides a class to interact only with a preexisting scene, which means that you cannot use VRML as a 3D toolkit. A standalone application can use the VRML class libraries to create a collection of VRML nodes, but without a preexisting browser open, there is no way of making these nodes visible on-screen. In the future, you will be able to write a browser using Java3D that is responsible for the visualization of the VRML file structure, but there is no method to do so currently.
The second concept to understand is that within VRML there is no such thing as a monolithic application. Java started the ball rolling by introducing individual classes that could be loaded on the fly and VRML takes it one step further. Each object has its own script attached to it. Creating a highly complex world means writing lots of short scripts.
Scripting under VRML has a defined set of functionality for which
any language can be used. When the final 2.0 specification was
released, there were appendixes for Java and JavaScript. Much
of the lightweight work can be performed with JavaScript. Although
this is the preferred method for short calculations, when more
complex work must be done, the world creator uses Java-based scripting.
Typically, such heavyweight operations combine the VRML API with
the thread and networking classes. However, at the time of this
writing, VRML browsers understood either JavaScript or Java, but
not both (the browsers don't take advantage of the built-in Java
and JavaScript interpreters provided by Netscape Navigator and
MS Internet Explorer).
Note |
It is expected that Netscape's version of Live3D that supports VRML 2.0 will handle both JavaScript and Java because the browser already does. |
To keep down the amount of programming, the VRML specification writers added a number of nodes to take care of commonly required functionality. These nodes can divided into two groups: interpolators and sensors. Interpolators are available for color, scalar values, points (morphing), vectors, position, and orientation. Sensors cover a more varied range: geometric shapes (cylinder, disk, plane, and sphere), proximity, time, and touch. These all can be directly inserted into a scene and connected to the various primitives to create effects without having to write a line of code. Simple effects, such as an automatically opening door, can be created by adding a sensor, interpolator, and primitives to the scene.
To start compiling the example code in this chapter, you must obtain the VRML packages. These come with the individual browser you are using. The examples in this chapter were compiled and tested using Sony's Community Place. Normally, Community Place puts the class files in a directory under itself; typically, the directory is as follows:
C:\Program Files\Sony\Community Place Browser\lib\java
If you add this directory to your CLASSPATH
statement, you should be able to compile.
Caution |
Although Netscape's Live3D browser also includes VRML class files, they are not VRML 2.0 files. The version of Live3D that ships with Navigator 3.0 does not support VRML 2.0. If you have the Netscape directory already in your CLASSPATH statement, you must remove it to compile the examples in this chapter. Also watch out for any other VRML classes in your directory-particularly because each browser comes with its own library. Multiple VRML classes caused me several days of frustration as I tried to sort out where all the conflicts were. |
The VRML world description uses a traditional scene-graph approach reminiscent of PEX/PHIGS and other 3D toolkits. This description applies not only to the file structure but also to the inner workings. Each node within the scene has a parent and many nodes can have children. For a complete structural description of VRML, I recommend that you purchase a good book on VRML, especially if you intend to undertake serious behavioral programming. (Naturally, I recommend Laura Lemay's Web Workshop: 3D Graphics and VRML 2, authored by yours truly.)
When I say that VRML uses a scene graph structure, I talk about the way information is constructed in the file. The text in the file consists of a number of different things.
Each object within the scene is represented by what is called a node. The node can be a geometrical object (such as a sphere), a color or texture, or even something to modify the children of this node by scaling or rotating it. In the text file, you can pick out the nodes because their names all begin with capital letters.
After you have declared the node you want to use, you give it some properties. These properties are termed fields in VRML parlance. A field controls one little property of that node. A sphere has only one property-its radius. If you want to give the sphere a color, you must use another node to describe the color properties. You also need another node that joins these two pieces of information together (a color does not need to belong to a shape, nor does a shape have to have a color). You see some examples of this use of nodes later in this chapter.
The arrangement of these parent/child relationships is governed very heavily by the specification. For example, you cannot give a sound node a cone node for the child.
Fields are probably the most important thing you must understand in this chapter. Any behavior that you will be creating depends on the script modifying these field values. When you read the VRML specification, you can see that each node is defined to have a collection of fields; each field has a default value. If you do not declare a value for that field in the VRML file, the field uses the default value.
To make sure that the browser knows when one node finishes and the next starts, VRML uses curly braces { } to delimit the node. This arrangement is the same as bracketing a block of code in Java.
As you discover shortly, if you want to create behaviors, you must make a series of explicit connections between various fields and nodes. A scene can well consist of a couple hundred boxes, so how do you know which box you are trying to connect to? Very simple: VRML provides a method of giving explicit names to nodes.
To name a node, you simply use the VRML keyword DEF, specify a word for its name, and then provide the node type itself. To create a sphere called mysphere, for example, you use the following code:
DEF mysphere Sphere {}
Whenever you refer to mysphere, you now refer to this node. You can refer to mysphere where ever you like in the file after it has been declared. You can name a node whatever you like-including using the names of other node types (although this is definitely not recommended). You can name another node with the same name in the file. In this case, the last node declared is the winner-all references relate to it.
Surprisingly-for a language that was first created before Java
became popular-the VRML nodes can be represented in a semi-object-oriented
manner that meshes well with Java. Each node has a number of fields
that can be accessible to other nodes only if explicitly declared
so. The nodes can also be declared read-only or write-only or
have defined methods to access their values. In VRML syntax, the
four types of access are described as follows:
Access | Description |
field | Hidden from general access. |
eventIn | Sends a value to a node-a write-only field. |
eventOut | Sends a value from a node-a read-only field. |
exposedField | Publicly accessible for both read and write. |
Apart from seeing these in the definitions of the nodes defined by VRML, you will deal with them in the writing of the behavior scripts. Most scripts are written to process a value being passed to it in the form of an eventIn, which then passes the result back through the eventOut. Any internal values are kept in field values. Script nodes are not permitted to have an exposedField because of the updating and implementation ramifications within the event system.
Although a node may consist of a number of input and output fields, it does not insist that they all be connected. Usually the opposite is true-only a few of the available connections are made. By connecting only the required fields, you can create a more general-purpose script and share it with a number of nodes.
VRML requires explicit connection of nodes using the ROUTE keyword as follows:
ROUTE fromNode.fieldname1 TO toNode.fieldname2
You can connect a route from any field to any other field. The only restriction is that the two fields must be of the same type. No casting of types is permitted. FromNode and toNode are the names of nodes you have declared previously with the DEF keyword.
This route mechanism can be very powerful when combined with scripting. The specification allows both fan in and fan out of ROUTEs. Fan in occurs when many nodes have ROUTEs to a single eventIn field of a node. Fan out is the opposite: one eventOut is connected to many other eventIns. This arrangement enables sensors and interpolators to feed the one script with information, thus saving coding effort. Currently, the only problem is that there is no way to find out which node generated an event for an eventIn. Fan out is also handy when one script controls a number of different objects at once (for example, a light switch that turns on multiple lights simultaneously).
If two or more events cause a fan-in clash on a particular eventIn, the results are undefined. The programmer should be careful to avoid such situations. A typical situation in which this occurs is when two animation scripts set the position of an object.
All VRML data types follow the standard programming norms. There are integer, floating point, string, and boolean standard types as well as specific types for dealing with 3D graphics such as points, vectors, image, and color. To deal with the extra requirements of the VRML scene-graph structure, behavior nodes and time types have been added. The node data type contains an instance pointer to a particular node in the scene graph. Individual fields within a node are not accessible directly. Individual field references in behaviors programming is rarely needed because communication is based on an event-driven model. When field references are needed within the API, a node instance and field string description pair are used.
Apart from the boolean and
time types, these values
can be either single or multivalued. The distinction is made in
the field name: use the SF
prefix for single-valued fields and MF
for multivalued fields. A SFInt32
field contains a single integer; a MFInt32
field contains an array of integers. For example, the script node
definition in the next section contains an MFString
and a SFBool. The MFString
is used to contain a collection of URLs, each kept in its own
separate substring, but the SFBool
contains a single boolean
flag that controls a condition.
Caution |
One small thing to watch for is the use of booleans. In VRML, they are declared in uppercase letters but in Java, they are declared in lowercase letters. |
The Script node provides the means to integrate a custom behavior into VRML. Behaviors can be programmed in any language supported by the browser and for which an implementation of the API can be found. In the draft versions of the VRML 2.0 specification, sample APIs were provided for Java and JavaScript. The Script node is defined as follows:
Script { field MFString behavior [] field SFBool mustEvaluate FALSE field SFBool directOutputs FALSE # any number of the following eventIn eventTypeName eventName eventOut eventTypeName eventName field fieldTypeName fieldName initialValue }
Unlike a standard HTML, VRML enables multiple target files to be specified in order of preference. The behavior field contains any number of strings specifying URLs or URNs to the desired behavior script. For Java scripts, this is the URL of the .class file but is not limited to just one script type.
Apart from specifying what the behavior script is, VRML also lets you control how the script node performs within the scene graph. The mustEvaluate field tells the browser about how often the script should be run. If the field is set to TRUE, the browser must send events to the script as soon as they are generated, forcing an execution of the script. If the field is set to FALSE, in the interests of optimization, the browser may elect to queue events until the outputs of the script are needed by the browser. A TRUE setting is most likely to cause browser performance to degrade because of the constant context-swapping needed rather than batching to keep it to a minimum. Unless you are performing something that the browser is not aware of (such as using the networking or database functionality), you should set the mustEvaluate field to FALSE.
The directOutputs field controls whether the script has direct access for sending events to other nodes. Java methods require the node reference of other nodes when setting field values. If, for example, a script is passed an instance of a group node, and the directOutputs field is set to TRUE, the script can send an event directly to that node. To add a new default box to this group, the script would contain the following code:
SFNode group_node = (SFNode)getField("group_node"); group_node.postEventIn("add_children", (Field)CreateVRMLfromString("Box{}"));
If directOutputs is set to TRUE, the script node must have an eventOut field with the corresponding event type specified (an MFNode, in this case), and a ROUTE connecting the script with the target node.
There are advantages to both approaches. When the scene graph is static in nature, the second approach (using known events and ROUTEs) is much simpler. However, for a scene in which objects are being generated on the fly, static routing and events do not work and the first approach is required.
The whole of the API is built around two Java abstract classes
defined in the VRML packages: vrml,
vrml.node, and vrml.field.
The API also has a class specifically for the browser, which is
in the vrml package.
Note |
An interface is a class that has been declared but that has no body (the local platform provides the implementation). A package is a collection of classes grouped together for convenience. |
The Field class is empty so that individual classes can be created to mimic the VRML field types. There are two types of Field classes: read-only and unlimited access. The read-only version starts with Const<fieldtype>; the unlimited-access version has 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-an arrangement that comes in handy when you're setting values based on the arguments presented.
For the nonconstant fields, each class has at least setValue()
and getValue() methods that
return the Java equivalent of the VRML field type. For example,
a SFRotation class returns
an array of floats mapping to the x, y, z and orientation, but
the MFRotation class returns
a two-dimensional array of floats. The multivalued field types
also have a set1value() method
that enables the caller to set an individual element.
Caution |
SFString and MFString field types need special attention. Java defines character strings as Unicode characters but VRML defines characters as UTF-8. Unicode and UTF-8 are different encodings of the ISO 10646 standard for international text. Ninety-nine percent of the time, this difference should not present any problems, but it pays to be aware of it. |
So far, we have been looking almost entirely at VRML. But this is a Java book, right? So I had better get back to the Java. In previous sections of this chapter, you met the VRML Script node. The Script node allows you to say to the browser, "Hey, browser! Here is my script that I want to execute." Now we have to define what the browser is supposed to execute.
The other half of the VRML Java API is the Script
class itself. Now we are talking about the Java Script
class rather than the VRML Script
node. Confusing, isn't it? The Script
class is based on the Node
interface, which is defined only for VRML scripts at the moment.
This interface serves as the basis for representing the individual
nodes. VRML 2.0 defines the Script
class as the only implementation of the Node
interface, as shown in Listing 36.1 (this code is also provided
on the CD-ROM that accompanies this book).
Note |
It is expected that later versions of VRML should have individual classes for each node type, just as there are individual classes for each field type. |
Listing 36.1. The Script
abstract class definition.
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 right now. You need some more introduction to how it works.
The getField() method returns the value of the field nominated by the given string. This is how the Java script gets the values from the VRML Script node fields. The getField() method is used for all fields and exposedFields. To the Java script, eventOut looks like any other field. There is no need to write an eventOut function-the value is set by calling the appropriate field type's setValue() method.
Earlier in this chapter, the VRML event system was introduced.
Somehow, you need to get the event from the scene graph into the
Java script.
Note |
In early draft versions of the VRML specification, you were simply able to declare a public method that had the same name as the eventIn declared in the VRML Script node. Problems occurred because you could not write a VRML browser in Java alone. So the approach was changed to the event-handling style you are familiar with from the AWT classes. |
First, VRML defines its own Event class as follows:
class Event { public String getName(); public ConstField getValue(); public double getTimeStamp(); public Object clone(); }
Caution |
The VRML Event class is not the same as the AWT Event class. Although it is not possible to put both into a script file (the browser would ignore the AWT calls), things could get very confusing. However, the AWT and VRML event handling systems do share a common philosophy. |
Now you need a method that is passed an event when it happens. VRML gives you a choice of two: The processEvents() method is used when there's more that one event generated at a particular timestamp, and 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 shown in Listing 36.2 (this code can also be found on the CD-ROM that accompanies this book).
Listing 36.2. An example Java class for a script.
import vrml.*; import vrml.field.*; import vrml.node.*; class replace_script extends Script { // now we get all the class variables private SFBool pointerOver; //initialisation public void initialize() { 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(); because 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, however.
When should you use the different event-handling functions? Take the following piece of VRML code as an example (this example comes straight from the VRML specification):
Transform { children [ DEF TS TouchSensor {}, Shape { appearance Appearance { material Material { emissiveColor 0 0 1 } } 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 TouchSensor is touched, it generates two simultaneous events, so the script receives the two events. In this case, you need the processEvents() method to deal with a number of simultaneous events. If you were interested only in the isActive event, you could use the processEvent() method.
If you're not sure whether the script will receive more than one simultaneous event, 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 confuses you, have a look at the following code fragment:
void public processEvent(Event e) { if(e.getName().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 must 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 36.2 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.
In the preceding code fragment, you may have noticed that an extra method was declared.This initialize() method is where you do all the initialization of values used by the script. VRML has some interesting problems: The time at which the class is first initialized may be different than the time at which values in the scene graph are ready.
If you used the constructor function to initialize the field values, you cannot guarantee that the values will be valid. The initialize() method is called when the scene graph contains valid data: that is, after the whole VRML scene has been loaded but before any events are created.
One of the first things that must be done during initialization is to match the fields declared in the VRML script node definition to the Java variables. The Script class contains two methods: one to get the fields and another to get eventOuts. These methods are passed a string to return the information in a form suitable for Java. Look back at the initialize() method from Listing 36.2:
public void initialize() { pointerOver = (SFBool)getField("pointerOver"); }
The getEventOut() method call would look the same. To Java, sending an event to another node looks the same as simply assigning a value to that variable. The Java internals and the browser take care of the rest.
The first behavior can now be defined by putting all that you have learned so far together: A box that, when touched, toggles color between red and blue. This sample behavior requires five components: a box primitive, a touch sensor, a material node, the script node, and the Java script. In this example, the static connections between the script and the other nodes are used because the scene stays the same and we change just a property.
The basic input scene consists of a box placed at the origin with a color and touch sensor around it:
Transform { children [ Shape { geometry Box {size 1 1 1} appearance Appearance { DEF box_material Material { diffuseColor 1.0 0. 0. #start red. } } } # end of shape definition # Now define a TouchSensor node. This node takes in the # geometry of the parent transform. Default behavior OK. DEF box_sensor TouchSensor {} ] }
Now you have to define a script to act as the color changer. You
have to take input from the touch sensor and output the new color
to the Material node. You
also have to internally keep track of the color. You can do this
by reading the value from the Material
node, but for demonstration purposes, an internal flag is included
in the script. No fancy processing or event sending to other nodes
is necessary, so both the mustEvaluate
and directOutputs fields
can be left at the default setting of null.
Our completed VRML Script
definition looks
like this:
DEF color_script Script { behavior "color_changer.class" # now define our needed fields field SFBool isRed TRUE eventIn SFBool clicked eventOut SFColor color_out }
Now you have to connect the preceding two pieces of code (the script and the box) together using ROUTEs:
ROUTE box_sensor.isOver TO color_script.clicked ROUTE color_script.color_out TO box_material.diffuseColor
Finally, you add the script to make everything work. Listing 36.3 shows the complete source code for the color-changing box. This code can also be found on the CD-ROM that accompanies this book.
Listing 36.3. Java source for the color-changing box.
import vrml.*; import vrml.field.*; import vrml.node.*; class color_changer extends Script { // declare the field private SFBool isRed; // declare the eventOut private SFColor color_out; //initialization public void initialize() { isRed = (SFBool)getField("isRed"); color_out = (SFColor)getEventOut("color_out"); } // declare eventIns private void clicked(ConstSFBool isClicked) { float red[] = {1.0, 0, 0}; float blue[] = {0, 0, 1.0}; // called when the user clicks or touches the box or // stops touching/click so first check the status of the // isClicked field. We will only respond to a button up. if(isClicked.getValue() == false) { // now check whether the box is red or green if(isRed.getValue() == true) { isRed.setValue(false); color_out.setValue(red); } else { isRed.setValue(true); color_out.setValue(blue); } } } // finally the event processing call public void processEvent(Event e) { clicked((ConstSFBool)e.getValue()); } }
That's it. You now have a box that changes color when you click it. Creating more complex behaviors is just a variation on this scheme using more Java code and fields. User input usually comes from sensors or interpolators, which are usually directly wired to a series of other event-generating and event-receiving structures.
More complex input from external systems is also possible. Scripts are not restricted to input methods based on eventIns. One example is a stock market tracker that runs as a separate thread. The tracker could constantly receive updates from the network, process them, and then send the results through a public method to the script, which would put the appropriate results into the 3D world.
Behaviors using the method presented in the color-changing box example in the preceding section work for many simple systems. Effective virtual reality systems, however, require more than just being able to change the color and shape of objects that already exist in the virtual world. Consider a virtual taxi: A user must step inside and instruct the cab where to go. The cab moves off, leaving the user in the same place. The user does not "exist" as part of the scene graph-the user is known to the browser but not to the VRML scene-rendering engine. Clearly, a greater level of control is needed.
The VRML 2.0 specification defines a series of actions that must be provided if the programmer is to set and retrieve information about the world. Within the Java implementation of the API, this functionality is provided as the Browser class. This class provides all the functions a programmer needs that are not specific to any particular part of the scene graph.
To define system-specific behaviors, the first functions you must define are these:
public String getName(); public String getVersion();
These strings are defined by the browser writer and identify the browser in some unspecified way. If this information is not available, empty strings are returned.
If you are programming expensive calculations, you may want to know how they affect the rendering speed (frame rate) of the system. The getCurrentFrameRate() method returns the value in frames per second. If this information is not available, the return value is 100.0.
public float getCurrentFrameRate(); public float getCurrentSpeed();
The difference between navigation speed and current speed is in
the definition. VRML 2.0 defines NavigationInfo
as a node that contains default information about how to act if
given no other external cues. The navigation speed is the
default speed in units per second. There is no specification about
what this speed represents, only hints. A reasonable assumption
is the movement speed in WALK
and FLY navigation modes
and in panning and dollying in EXAMINE
mode. The current speed is the actual speed at which the
user is travelling at that point in time. The current speed is
the speed the user has set with the browser controls.
Note |
For a complete description of the different navigation types (WALK, FLY, and so on), see the VRML specification for the NavigationInfo node. |
Having two different descriptions of speed may seem wasteful, but it comes in quite handy when moving between different worlds. The first world may be a land of giants, where traveling at 100 units per second is considered slow, but in the next world, which models a molecule only 0.001 units across, this speed would be ridiculous. The navigation speed value can be used to scale speeds to something that is reasonable for the particular world.
There is only so much you can do with what is already available in a scene. Complex worlds use a mix of static and dynamically generated scenery to achieve their impressive special effects.
The first thing you may want to do is find out where you are from the URL:
public String getWorldURL();
GetWorldURL() returns the URL of the root of the scene graph rather than the URL of the currently occupied part of the scene. VRML enables a complex world to be created using a series of small files included into the world-a process called inlining in VRML parlance.
The following four methods allow you to modify the scene in some way. To completely replace the scene graph with a new file, call the loadURL() method. As with all URL references within VRML, an array of strings is passed. The array of strings is a list of URLs and URNs to be loaded in order of preference. If the load of the first URL fails, the method attempts to load the second, and so on until the method is successful or the end of the list is reached. If the load fails, the method should notify the user in some browser-specific manner.
As this book goes to press, the exact specification of URNs is still being debated. URNs are legal within fields that contain strings for URLs; the VRML specification states that if the browser is not capable of supporting the URNs, they are to be silently ignored. The specification also states that it is up to the browser whether the loadURL() call blocks or starts a separate thread when loading a new URL. This URL may be another VRML world or any other valid URL such as an HTML page.
So far, the methods described enable the programmer to change individual components of the world. The other requirement is to completely replace the world with some internally generated one. Doing so enables you to use VRML to generate new VRML worlds on the fly. This still assumes that you already are part of a VRML world-you cannot use this approach in an application to generate a 3D graphics front-end.
If you have generated an array of nodes within your script (see the createVRML calls in the next section), ReplaceWorld()is what you have to call. This is a non-returning call that unloads the old scene and replaces it with the given collection of nodes:
public static void loadURL(String[] url); public void replaceWorld(Node[] nodes); public static Node[] createVrmlFromString(String vrmlSyntax); throws InvalidVRMLSyntaxException; public static void createVrmlFromURL(String[] url, Node node, String event); throws InvalidVRMLSyntaxException;
In addition to replacing the whole scene, you may want to add bits at a time. You can do so in one of two ways. If you are very familiar with VRML syntax, you can create strings on the fly and pass them to the createVrmlFromString() method. The node that is returned can then be added into the scene as required.
Perhaps the most useful of the above methods is the createVrmlFromURL() method. You may notice from the definition that in addition to a list of URLs, createVrmlFromURL() also takes a node instance and a string that refers to an eventIn field name. This call is a nonblocking call that starts a separate thread to retrieve the given file from the URL, converts it to the internal representation, and then sends the newly created list of nodes to the specified node's eventIn field. The eventIn type is required to be an MFNode. The Node reference can be any sort of node, not just a part of the script node. This arrangement enables the script writer to add these new nodes directly to the scene graph without having to write extra functionality in the script.
With both the create methods, the returned nodes do not become visible until they have been added to some preexisting node that already exists within the scene. To add them to the scene, you can either pass them to a grouping node (for example, Transform or Group) or call the replaceWorld() method that we looked at a little earlier. Although it is possible to create an entire scene on the fly within a standalone applet, there is no way to make it visible because this applet does not have a previous node instance to which you can add the dynamically generated scene.
Once you have created a set of new nodes, you want to be able to link them together to get the same behaviors system as the original world. The Browser class defines these methods for dynamically adding and deleting ROUTEs between nodes:
public void addRoute(Node fromNode, String fromEventOut, Node toNode, String toEventIn) throws InvalidRouteException; public void delRoute(Node fromNode, String fromEventOut, Node toNode, String toEventIn) throws InvalidRouteException;
For each of these methods, you must know the node instance for both ends of the ROUTE. In VRML, you cannot obtain an instance pointer to an individual field in a node. It is also assumed that if you know you will be adding a route, you also know what fields you are dealing with, so a string is used to describe the field name corresponding to an eventIn or eventOut. Exceptions are thrown if either of the nodes or fields do not exist or an attempt to delete a nonexistent ROUTE is made.
You now have all the tools required to generate a world on the fly, respond to user input, and modify the scene. The only thing that remains is to add the finesse to create responsive worlds that don't get bogged down in Java code.
In all animation programming, the ultimate goal is to keep the frame rate as high as possible. In a multithreaded application like a VRML browser, the less time spent in behaviors code, the more time that can be spent rendering. Virtual reality behavior programming in VRML is still very much in its infancy. This section outlines a few common-sense approaches to keep up reasonable levels of performance, not only for the renderer, but also for the programmer.
The first technique is to use Java only where necessary. This many sound a little strange from a book about Java programming, but consider the resources required to have not only a 3D-rendering engine but a Java VM loaded to run even a simple behavior; also consider that the majority of viewers will be people using low-end PCs. Because most VRML browsers specify that a minimum of 16MB of RAM is required (with a recommendation of 32MB), also loading the Java VM into memory would require lots of swapping to keep the behaviors going. The inevitable result is bad performance. For this reason, the interpolator nodes and JavaScript were created: built-in nodes for common, basic calculations and a small, light language to provide basic calculation capabilities. Limit your use of Java to the times when you require the capabilities of a full programming language, such as multithreading and network interfaces.
When you do have to use Java, keep the amount of calculation in the script to a minimum. If you are producing behaviors that require either extensive network communication or data processing, these behaviors should be kept out of the script node and sent off in separate threads. The script should start the thread as either part of its constructor or in response to some event; it should then return as soon as possible.
In VR systems, frame rate is king. Don't aim to have a 100-percent-correct behavior if doing so leads to twice the frame rate. Will a 90-percent-correct behavior do? It is quite amazing how users don't notice an incorrect behavior, but as soon as they notice the picture update slowing down, they start to complain. Every extra line of code in the script delays the return of the CPU back to the renderer. In military simulations, the goal is to achieve 60fps; even for Pentium-class machines, the goal should be to maintain at least 10fps. Much of this comes down not only to how detailed the world is, but also to how complex the behaviors are. As always, the tradeoff between accuracy and frame rate depends on the individual programmer and the application requirements. Users usually accept that a door does not open smoothly as long as they can move around without watching individual frames redraw.
Be careful with the event-processing loop. Your behaviors code will be distributed on many different types of machines and browsers. Each browser writer knows best how to optimize the event-handling mechanism to mesh with its internal architecture. With windowing systems, dealing with the event loop is a must if you are to respond to user input, but in VR, you no longer have control over the whole system. The processEvents() method applies only to an individual script; you cannot use it as a common method across all scripts. Although you may think you are optimizing the event handling, you are only doing so for a single script. In a reasonably sized world, you may have another few hundred scripts running, so the optimization of an individual script generally isn't worth the effort.
Add to the scene graph only what is necessary. If it is possible to modify existing primitives, do so instead of adding new ones. Every primitive added to a scene requires the renderer to convert it to its internal representation and then reoptimize the scene graph to take account of the new objects. When it modifies existing primitives, the browser is not required to re-sort the scene graph structure, saving computation time. For example, a cloudy sky is better simulated using a multiframed texture map image format (such as MJPEG or PNG) on the background node than using lots of primitives that are constantly modified or dynamically added.
If your scene requires objects to be added and removed on the fly, and many of these objects are the same, don't simply delete the objects from the scene graph. It is better to remove them from a node but keep an instance pointer to them so that they can be reinserted at a later time. At the expense of a little extra memory, this approach saves time. If you don't take the time now, you may have to access the objects later from a network or construct them from the ground up from a string representation.
Another trick is to create objects but not add them to the scene graph. VRML lets you create objects but not add them to the scene graph. Any object not added isn't drawn. Node types such as sensors, interpolators, and scripts have no need to be added. Doing so causes extra events to be generated, resulting in a slower system. Normal Java garbage collection rules apply for when these nodes are no longer referenced. VRML, however, adds one little extra: Adding a ROUTE to any object is the same as keeping a reference to the object. If a script creates a node, adds one or more ROUTEs, and then exits, the node stays allocated and functions as though it were a normal part of the scene graph.
There are dangers in this approach. Once you have lost the node instance pointer, there is no way to delete it. You need this pointer if you are to delete the ROUTE. Deleting ROUTEs to the object is the only way to remove these floating nodes. Therefore, you should always keep the node instance pointers for all floating nodes you create so that you can delete the ROUTEs to them when they're no longer needed. You must be particularly careful when you delete a section of the scene graph that has the only routed eventIn field to a floating node that also contains an eventOut to a undeleted section of the scene graph. This arrangement creates the VRML equivalent of memory leaks. The only way to remove this node is to replace the whole scene or to remove the part of the scene that the eventOut references.
The ROUTE syntax makes it very easy to construct circular event loops. Circular loops can be quite handy. The VRML specifications state that if the browser finds event loops, it processes each event only once per timestamp. Events generated as a result of a change are given the same timestamp as the original change. This happens because events are considered to happen instantaneously. When event loops are encountered in this situation, the browser enforces a breakage of the loop. The sample script from the VRML specification using JavaScript shows this:
DEF S Script { eventIn SFInt32 a eventIn SFInt32 b eventOut SFInt32 c field SFInt32 save_a 0 field SFInt32 save_b 0 url "data:x-lang/x-vrmlscript, TEXT; function a(val) { save_a = val; c = save_a+save_b;} function b(val) { save_b = val; c = save_a+save_b;} } ROUTE S.c to S.b
S computes c=a+b with the ROUTE, completing a loop from the output c back to the input b. After the initial event with a=1, S leaves the eventOut c with a value of 1. This causes a cascade effect, in which b is set to 1. Normally, this should generate an eventOut on c with the value of 2, but the browser has already seen that the eventOut c has been traversed for this timestamp and therefore enforces a break in the loop. This leaves the values save_a=1, save_b=1, and the eventOut c=1.
Earlier in this chapter, you learned that it was not possible to create a world from a completely standalone application. Although it would be nice to have this facility, it would be the same as being able to create a whole HTML page in the same manner. To create an HTML page applet, you must first start it from an <APPLET> tag. A Java-enabled page may consist of no more than an opening <HTML> tag followed by an <APPLET> tag pair and a closing </HTML> tag. VRML is no different. You can enclose a whole 3D application based on VRML in a similar manner.
Although this approach is not quite as efficient as creating a 3D application using a native 3D toolkit such as Java3D, VRML can be considered an abstraction that enables programmable behaviors in a simplified manner-much like using a GUI builder to create an application rather than writing it all by hand.
The next section develops a framework for creating worlds on the fly. Such a framework can have quite a few different applications-from developing cyberspace protocol-based seamless worlds, to acting as a VR-based scene editor, to generating VRML or other 3D format output files. The explanations of the development process in the next sections assume that you are familiar with at least VRML 1.0 syntax.
There are three ways to create a world on the fly. These are given by the three methods in the Browser class described earlier in the chapter: createVrmlFromURL(), createVrmlFromString(), and loadURL(). The example we develop in the following sections is very simple but demonstrates each of these calls. The VRML scene consists of three objects: the sphere, the cone, and the cube of the VRML logo. Touching each of the objects causes a different action to happen (which is explained as we go along).
Just as in HTML, you must start with a skeleton file in which you include the Java application. In VRML, however, you have to do a little more than just include an applet and a few PARAM tags.
The first thing you need in the VRML file is at least one node to which you can add things. Remember that there is no method of adding a primitive to the root of the scene graph, so a pseudo root to which objects are added is required. For simplicity, a Group node is used. There is nothing that has to be specified, so we can leave the fields alone. The Group node has two eventIn fields that are used later: add_children and remove_children. The definition is shown here:
DEF root_node Group {}
A few objects have to be put into the scene that are representative of the three methods of adding an object to the world. Taking the three primitives that form the VRML logo, the box will represent creating objects from a downloaded file, the sphere will create and add an object from an internal text description, and the cone takes the user to another VRML world by using the call to loadURL(). These primitives are surrounded in a Transform node to make sure that they are located in different parts of the world (all objects are located at the origin by default). The box definition follows:
Transform { translation 2 0 0 children [ DEF box_sensor TouchSensor{} Box { size 1 1 1} # script node will go here ] }
Notice that DEF is used only for the TouchSensor itself and not the whole object. The TouchSensor is the object from which events are taken. If there was no sensor, the box would exist as itself. Any mouse click (or touch, if you are using a data glove) on the box does nothing. The other two nodes are similar in definition.
For demonstration purposes, separate scripts have been put with each of the objects. It makes no difference if you have lots of small scripts or one large one. For a VR scene creator, it is probably better to have one large script to keep track of the scene graph for the output file representation, but a virtual factory can have many small scripts, perhaps with some "centralized" script acting as the system controller.
Once the basic file is defined, you must add behaviors. The VRML file stands on its own at this point. You can click objects, but nothing happens. Because each object has its own behavior, the requirement for each script is different. Each script requires one eventIn, which is the notification from its TouchSensor.
Because the example presented does not have any real-time constraints, the mustEvaluate field is left with the default setting of FALSE. For the cone, no outputs are sent directly to nodes, so the directOutputs field is left at FALSE. For the sphere, outputs are sent directly to the Group node, so it is set to TRUE. The box must be set to TRUE as well, for reasons explained in the next section.
Besides the eventIn, the box's script also requires an eventOut to send the new object to the Group node acting as the scene root. Good behavior is desirable if the user clicks on the box more than once, so an extra internal variable is added, keeping the position of the last object that was added. Each new object added is translated two units along the z axis from the previous one. A field is also needed to store the URL of the sample file that will be loaded. The box script definition follows:
DEF box_script Script { url "boxscript.class" directOutputs TRUE eventIn SFBool isClicked eventIn MFNode newNodes eventOut MFNode childlist field SFInt32 zposition 0 field SFNode thisScript USE box_script field MFNode newUrl [] }
Notice that there is an extra eventIn.
Processing must be done on the node returned from the createVrmlFromURL()
method, so you must provide an eventIn
for the argument. If you do not need to process the returned nodes,
you can use the root_node.add_children
eventIn
instead as the target eventIn
for the create call.
The other interesting point to note is that the script declaration includes a field that is a reference to itself. A Java class can always refer to itself using this when it needs an event.
To explain the use of direct outputs, the sphere uses the postEventIn method to send the new child directly to root_node. To do this, a copy of the name that was defined for the Group is taken, which, when resolved in Java, essentially becomes an instance pointer to the node. Using direct writing to nodes means that you no longer require the eventOut from the box's script but you keep the other fields:
DEF sphere_script Script { url "sphere_script.class" directOutputs TRUE eventIn SFBool isClicked field SFNode root USE root_node field SFInt32 zposition 0 }
The script for the cone is very simplistic. When you click the cone, all it does is fetch a named URL and set it as the new scene graph. In this case, the URL being used belongs to the independent virtual community called Terra Vista, of which I am a member. At the time of this writing, Terra Vista is a complete VRML 1.0c distributed community that is starting to move towards version 2.0. By the time you read this, it should give you many examples of how to use both simple and complex behaviors.
DEF cone_script Script { url "cone_script.class" eventIn SFBool isClicked field MFString target_url ["http://www.alaska.net/~pfennig/flux/flux.wrl"] }
Now that the scripts are defined, you must wire them together.
A number of ROUTEs are added
between the sensors and scripts, as shown in the complete code
in List-
ing 36.4. This code can also be found on the CD-ROM that accompanies
this book.
Listing 36.4. The main world VRML description.
#VRML V2.0 utf8 # # Demonstration dynamically created world # Created by Justin Couch 1996 # first the pseudo root DEF root_node Group {} # The box Transform { translation 2 0 0 children [ DEF box_sensor TouchSensor{}, Shape { appearance Appearance { material Material { emissiveColor 1 0 0 } } geometry Box { size 1 1 1} }, DEF box_script Script { url "boxscript.class" directOutputs TRUE eventIn SFBool isClicked eventIn MFNode newNodes eventOut MFNode childlist field SFInt32 zPosition 0 field SFNode thisScript USE box_script field MFString newUrl ["sample_world.wrl"] } ] } ROUTE box_sensor.isActive TO box_script.isClicked ROUTE box_script.childlist TO root_node.addChildren # The sphere Transform { # no translation needed as it is at the origin already children [ DEF sphere_sensor TouchSensor {}, Shape { appearance Appearance { material Material { emissiveColor 0 1 0 } } geometry Sphere { radius 0.5 } }, DEF sphere_script Script { url "sphere_script.class" directOutputs TRUE eventIn SFBool isClicked field SFNode root USE root_node field SFInt32 zPosition 0 } ] } ROUTE sphere_sensor.isActive TO sphere_script.isClicked # The cone Transform { translation -2 0 0 children [ DEF cone_sensor TouchSensor {}, Shape { appearance Appearance { material Material { emissiveColor 0 0 1} } geometry Cone { bottomRadius 0.5 height 1 } }, DEF cone_script Script { url "cone_script.class" eventIn SFBool isClicked field MFString targetUrl ["http://www.alaska.net/~pfennig/Âflux/flux.wrl"] } ] } ROUTE cone_sensor.isActive TO cone_script.isClicked
The box sensor adds objects to the scene graph from an external file. This external file (defined in Listing 36.5 and found on the CD-ROM that accompanies this book) contains a Transform node with a single box as a child. Because the API does not permit users to create node types, and because you have to place the newly created box at a point other than the origin, you have to use a Transform node. Although you could just load in a box from the external scene and then create a Transform node with the createVrmlFromString() method, doing so requires more code and slows down execution speed. Remember that behavior writing is about getting things done as quickly as possible, so the more you move to external static file descriptions, the better.
Listing 36.5. The external VRML world file.
#VRML V2.0 utf8 # # Demonstration sample world to be loaded # Created by Justin Couch May 1996 Transform { children [ Shape { appearance Appearance { material Material { emissiveColor 0.4 0.41 0.1 } } geometry Box { size 1 1 1} } ] }
Probably the most time-consuming task for someone writing a VRML scene with behaviors is deciding how to organize the various parts in relation to the scene graph structure. In a simple file like this, there are two ways to arrange the scripts. Imagine what could happen in a moderately complex file of two or three thousand objects.
All the scripts in this example are simple. Listing 36.6 is the source for the script that belongs to the box (and the source can be found on the CD-ROM that accompanies this book). When the node is received back in newNodes eventIn, the node must be translated to the new position. Ideally, you should be able to do this directly by setting the translation field but you cannot. The only way to do this is to post an event to the node, naming that field as the destination-the reason for setting directOutputs to TRUE. After this is done, you can then call the add_children eventIn. Because each of the scripts is short, the processEvents() method is not used.
Listing 36.6. Java source for the box script.
import vrml.*; import vrml.node.*; import vrml.field.*; class box_script extends Script { private SFInt32 zPosition; private SFNode thisScript; private MFString newUrl; // declare the eventOut field private MFNode childList; // initialization public void initialize() { zPosition = (SFInt32)getField("zPosition"); thisScript = (SFNode)getField("thisScript"); newUrl = (MFString)getField("newUrl"); childList = (MFNode)getEventOut("childList"); } // now declare the eventIn methods private void isClicked(ConstSFBool clicked) { // check to see if picking up or letting go if(clicked.getValue() == FALSE) Browser.createVrmlFromURL(newUrl.getValue(), thisScript, "newNodes"); } private void newNodes(ConstMFNode nodelist, SFTime ts) { Node[] nodes = (Node[])nodelist.getValue(); float[3] translation; // Set up the translation zPosition.setValue(zPosition.getValue() + 2); translation[0] = zPosition.getValue(); translation[1] = 0; translation[2] = 0; // There should only be one node with a transform at the // top. No error checking. nodes[0].postEventIn("translation", (Field)translation); // now send the processed node list to the eventOut childList.setValue(nodes); } //event handling public void processEvents(int count, Event events[]) { int i; for(i = 0; i < count; i++) { if(e.getName().equals("isClicked")) isClicked((ConstSFBool)e.getValue()); else if(e.getName().equals("newNodes")) newNodes((ConstMFNode)e.getValue()); } } }
The sphere class in Listing 36.7 is similar to the previous code, except that you have to construct the text string equivalent of the sample_world.wrl file. This is a straightforward string buffer problem. All you have to do is make sure that the Transform node has the correct value for the translation field. You can find this code on the CD-ROM that accompanies this book.
Listing 36.7. Java source for the sphere script.
import vrml.*; import vrml.field.*; import vrml.node.*; class sphere_script extends Script { private SFInt32 zPosition; private SFNode root; //initialization public void initialize() { zPosition = (SFInt32)getField("zPosition"); root = (SFNode)getField("root"); } // now declare the eventIn methods public void processEvent(Event e) { StringBuffer vrml_string = new StringBuffer(); MFNode nodes; ConstSFBool clicked = (ConstSFBool)e.getValue(); // set the new position zPosition.setValue(zPosition.getValue() + 2); // check to see if picking up or letting go if(clicked.getValue() == FALSE) { vrml_string.append("Transform {"); vrml_string.append("translation "); vrml_string.append(zPosition.getValue()); vrml_string.append(" 0 0 "); vrml_string.append("children [ "); vrml_string.append("sphere { radius 0.5} ] }"); nodes.setValue(Browser.createVrmlFromString(vrml_string)); root.postEventIn("addChildren", (Field)nodes); } } }
The cone_script class in Listing 36.8 (and found on the CD-ROM that accompanies this book) is the easiest of the lot. As soon as it receives confirmation of a touch, it starts to load the world with the provided URL.
There is only one interesting part: When retrieving the value of the boolean passed to it, we use two getValue() methods. The first returns the ConstField from the event and the second returns the boolean value that was actually passed with the event. To make sure that everything is understandable, I used an intermediate variable to keep from confusing the two methods.
Listing 36.8. Java source for the cone script.
import vrml.*; import vrml.field.*; import vrml.node.*; class cone_script extends Script { SFBool isClicked; MFString targetUrl; // initialization public void initialize() { isClicked = (SFBool)getField("isClicked"); targetUrl = (MFString)getField("targetUrl"); } // The eventIn method public void processEvent(Event e) { ConstSFBool val = (ConstSFBool)e.getValue(); if(val.getValue() == FALSE) Browser.loadURL(targetUrl.getValue()); } }
By compiling the preceding Java code and placing these and the two VRML source files in your Web directory, you can serve this basic dynamic world to the rest of the world and everyone will get the same behavior as you-regardless of the system they're running.
It would be problematic if you had to rewrite the code in the preceding example every time you wanted to use it in another file. You could always just reuse the Java bytecodes, but this means that you would need identical copies of the script declaration every time you wanted to use it. Reusing the bytecodes is not a particularly nice practice from the software-engineering point of view, either. Eventually, you will be caught in the cut-and-paste error of having extra pieces of ROUTEs floating around (and extra fields) that could accidentally be connected to nodes in the new scene, resulting in bugs that are difficult to trace.
VRML 2.0 provides a mechanism similar to the C/C++ #include directive and typedef statements all rolled into one: the PROTO and EXTERNPROTO statement pair. The PROTO statement acts like a typedef: you use PROTO with a node and its definition and then you can use that name as though it were an ordinary node within the context of that file.
If you want to access that prototyped node outside of that file, you can use the EXTERNPROTO statement to include it in the new file and then use it as though it were an ordinary node.
Although this approach is useful for creating libraries of static parts, where it really comes into its own is in creating canned behaviors. A programmer can create a completely self-contained behavior and in the best object-oriented traditions, provide only the interfaces to the behaviors he or she wants. The syntax of the PROTO and EXTERNPROTO statements follow:
PROTO prototypename [ # any collection of eventIn eventTypeName eventName eventOut eventTypeName eventName exposedField fieldTypeName fieldName initialValue field fieldTypeName fieldName initialValue ] { # scene graph structure. Any combination of # nodes, prototypes, and ROUTEs } EXTERNPROTO prototypename [ # any collection of eventIn eventTypeName eventName eventOut eventTypeName eventName exposedField fieldTypeName fieldName field fieldTypeName fieldName ] "URL" or [ "URN1" "URL2"]
You can add a behavior to a VRML file just by using the prototypename in the file. For example, if you have a behavior that simulates a taxi, you may want to have many taxis in a number of different worlds that represent different countries. The cabs are identical except for their color. Note again the ability to specify multiple URLs for the behavior. If the browser cannot retrieve the first URL, it tries another until it gets a cab.
A taxi can have many behaviors (such as speed and direction) that users of a cab do not really care about when they want to use it. (Well, if they were going in the wrong direction once they got in, they might care!) To incorporate a virtual taxi into your world, all you really care about is a few things, such as being able to signal a cab, getting in, telling it where to go, paying the fare, and then getting out when it has reached its destination. From the world authors' point of view, how the taxi finds its virtual destination is unimportant. A declaration of the taxi prototype file might look like the following:
#VRML V2.0 utf8 # # Taxi prototype file taxi.wrl PROTO taxicab [ exposedField SFBool isAvailable TRUE eventIn SFBool inCab eventIn SFString destination eventIn SFFloat payFare eventOut SFFloat fareCost eventOut SFInt32 speed eventOut SFVec3f direction field SFColor color 1. 0 0 # rest of externally available variables ] { DEF root_group Transform { # Taxi geometry description here } DEF taxi_script Script { url ["taxi.class"] # rest of event and field declarations } # ROUTE statements to connect it altogether }
To include the taxi in your world, the file would look something like this:
#VRML V2.0 utf8 # # myworld.wrl EXTERNPROTO taxi [ exposedField SFBool isAvailable eventIn SFBool inCab eventIn SFString destination eventIn SFFloat payFare eventOut SFFloat fareCost eventOut SFInt32 speed eventOut SFVec3f direction field SFColor color # rest of externally available variables ] [ " http://myworld.com/taxi.wrl", "http://yourworld.com/taxi.wrl"] # some scene graph #.... Transform { children [ # other VRML nodes. Then we use the taxi DEF my_taxi taxi { color 0 1. 0 } ] }
Here is a case in which you are likely to use the postEventIn() method to call a cab. Somewhere in the scene graph, you would have a control that your avatar queries a nearby cab for its isAvailable field. (An avatar is the virtual body used to represent you in the virtual world.) If it is TRUE, the avatar sends the event to flag the cab. Apart from the required mechanics to signal the cab with the various instructions, the world creator does not care how the cab is implemented. By using the EXTERNPROTO call, the world creator and its users can always be sure that they are getting the latest version of the taxi implementation and that there will be uniform behavior regardless of which world they are in.
The information presented in this chapter relied on static predefined behaviors available either within the original VRML file or from somewhere on the Internet.
The ultimate step in creating VR worlds is autonomous agents that have some degree of artificial intelligence. Back in the early days of programming, self-modifying code was common, but it faded away as more resources and higher-level programming languages removed the need. A true VR world brings this kind of coding back.
The Librarian from Stephenson's cyberpunk novel Snow Crash is just one example of how an independent agent can act in a VR world. Stephenson's model is very simple-a glorified version of today's 2D HTML-based search engine that, when requested, would search the U.S. Library of Congress for information on the desired and related topics (the Librarian also has speech recognition and synthesis capabilities). The next generation of intelligent agents will include learning behavior as well.
The VRML API enables you to take the next step-a virtual assistant that can modify its own behavior to suit your preferences. This is not just a case of loading in some canned behaviors. With the combination of JavaScript and Java behaviors, a programmer can create customized behaviors on the fly by concatenating the behavior strings and script nodes, calling the createVrmlFromString() method, and adding it to the scene graph in the appropriate place. This sort of work takes a lot of CPU time just to react to other users in the environment, so it is probably not feasible with current Pentium-class machines; the next generation of processors will probably make it so.
What you have learned so far in this chapter is fairly restricted. These scripts cannot interact with anything outside the VRML browser. If you have a multiframed document, there is no way an applet can communicate with the internal scripts. During the drafting of the VRML 2.0 specification, there were some moves to get an external interface as well. Although this was dropped from the final specification, design of the external interface is starting to happen.
At the time of this writing, there was no firm decision on which of the proposals was favored, but a decision should be reached by the time you read this. If you want to know more about this, check out the latest version of the VRML specification, which can be found at this site:
http://vag.vrml.org/
With the tools presented in this chapter, you should be able to create whatever you require of the real cyberspace. There is only so much you can do with a 2D screen in terms of new information-presentation techniques. The third dimension provided by VRML enables you to create experiences that are far beyond that of the Web page. 3D representation of data and VR behavior programming is still very much in its infancy. The limits are set only by your imagination-and CPU horsepower, of course!
Sony's Community Place (http://www.spiw.com/vs) and DimensionX's Liquid Reality (http:// www.dimensionx.com/products/lr) were the only products available at the time of this writing and did not contain a complete implementation of the final spec. So take everything with a grain of salt and test everything properly. I'm bound to have got something wrong!
If you are serious about creating behaviors, learning VRML thoroughly is a must. There are many little problems that catch the unwary, particularly in the peculiarities of the VRML syntax when you are ordering objects within the scene graph. An object placed at the wrong level severely restricts its actions. A book on VRML is a must for this work-my introductory book, Laura Lemay's Web Workshop: 3D Graphics and VRML 2 fits nicely with this chapter.
Whether it is creating reusable behavior libraries, an intelligent postman that brings the mail to you wherever you are, or simply a functional Java machine for your virtual office, the excitement of behavior programming awaits you.