All Categories :
Java
Chapter 36
Building VRML 2.0 Behaviors in
Java
by Justin Couch
CONTENTS
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.)
How Does VRML Work?
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.
Giving a Node a Name
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.
Understanding Field Types
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.
VRML Data Types
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 Script
Class Definition
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.
Dealing with Event Input
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.
Setting Up the Class to Run
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.
Changing the Current Scene
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.
Modifying the Scene
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.
Changing the Scene
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.
Circular Event Loops
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).
The VRML Source File
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.
Defining the Script Nodes
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}
}
]
}
The Java Behaviors
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.
Contact
reference@developer.com with questions or comments.
Copyright 1998
EarthWeb Inc., All rights reserved.
PLEASE READ THE ACCEPTABLE USAGE STATEMENT.
Copyright 1998 Macmillan Computer Publishing. All rights reserved.