Java 1.1 Unleashed
- 45 -
Building VRML 2 Behaviors in Java
by Justin Couch revised by Bruce Campbell
IN THIS CHAPTER
- Making the World Behave
- An Overview of VRML
- The VRML Script Node
- VRML Data Types in Java
- Integrating Java Scripts with VRML
- The Browser Class
- The Script Execution Model
- Creating Efficient Behaviors
- Dynamic Worlds--Creating VRML on the Fly
- Creating Reusable Behaviors
- The Future: VRML, Java, and AI
You've probably had enough of buttons, menus, and creating a hundred and one pictures
for animations. Now you're looking for something a little different. If you have
been following the computing press, you may have noticed discussions about another
hot 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 an appropriate viewer. VRML is similar to
HTML in that it can be created with a simple ASCII editor such as Notepad and delivered
across the Web by the same Web server that delivers HTML documents.
The first version of the standard, VRML 1, produced static scenes that could be
visited by anyone with a VRML 1 browser or appropriate VRML 1 plug-in viewer created
for use with the Netscape Navigator or Internet Explorer browser. VRML 1 is a derivative
of Silicon Graphic's Open Inventor file format. In that first version, a user could
visit a 3D scene by walking or flying around, but there was 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. VRML 1 was enough to get the art
community interested in creating some beautiful virtual places.
In December 1995, the VRML community decided to drop planned revisions to version
1 and head straight to a fully interactive version, VRML 2. One of the prime requirements
for VRML 2 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 2,000
members of the VRML mailing list. Contained in a draft proposal for VRML 2 was a
Java API for creating behaviors for objects in a VRML 2 scene.
Effectively combining VRML and Java requires 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. For specifics about VRML
2, Teach Yourself VRML 2 in 21 days, published by Sams.net, is an excellent reference
book.
Interestingly enough, Sony has implemented a Java API in its CommunityPlace VRML
2 viewer. This Java API is somewhat different than the Java API implemented by SGI
in its CosmoPlayer VRML 2 viewer. Although they use the same basic class structure,
you can think of Sony's approach as adding Java scripting to the VRML 2 node structure
and SGI's approach as providing VRML 2 3D graphics output capabilities to any Java
program. Each of these approaches to integrating Java with VRML has its strengths
and weaknesses. Both methods are covered in this chapter with appropriate code examples.
SGI has promoted a VRML-specific scripting language called VRMLscript, which you
can use instead of Java when you are scripting behaviors; VRMLscript is similar to
Sony's interpretation of intranode scripting. Consult a VRML 2 reference for the
specifics of VRMLscript. VRMLscript is an appropriate alternative for Java scripting
on many occasions, but is more similar to JavaScript than it is to the Java language
itself.
-
NOTE: You can learn more about Sony's
CommunityPlace viewer at http://vs.spiw.com/vs/;
you'll find additional information about SGI's CosmoPlayer at http://vrml.sgi.com/cosmoplayer/.
Making the World Behave
Within the virtual reality environment, any dynamic change in a virtual world
is regarded as a behavior. This change can be something as simple as an object changing
color when it is touched or something as complex as autonomous, semi-intelligent
agents that look and act like humans, such as Neal Stephenson's Librarian from his
visionary, sci-fi novel Snow Crash.
To understand how to create behaviors for virtual objects, you have to understand
how VRML works. Although this section won't delve into a lengthy discussion of VRML,
a few basic concepts are reviewed. To start with, VRML is a separate language from
the Java programming language used in scripts. The Java VRML classes interact only
with a preexisting VRML scene contained in a file with a .wrl extension.
The scene is developed using the VRML language to create a collection of VRML nodes;
the scene is then associated with a rectangular region within the browser window.
The VRML browser then provides a consistent method for moving around in the scene
and interacting with it. Although it is possible to create a VRML browser exclusively
in Java, both Sony and SGI have compiled machine-dependent browsers for the purposes
of rendering speed. Even with a VRML browser written in Java, a scene must be created
using VRML, which the Java VRML classes can then dynamically change over time.
Within Sony's CommunityPlace Java API, each virtual object can have its own script
attached to it. Creating a highly complex world in this manner requires writing many
short scripts. For more interesting behaviors, a longer, heavyweight script can combine
the VRML API classes with Java's thread and networking classes.
To minimize the amount of external programming, the VRML specification contains
a number of nodes to create common behaviors within the .wrl file itself.
These nodes are of two types: sensors and interpolators. Sensor nodes initiate events
when the user interacts with the sensor. Sensors are associated with virtual objects,
timers, and locations in three-dimensional space. A location is a 3D volume defined
by a cylinder, disk, plane, sphere, or point. Sensors initiate events based on touch
(such as a mouse click), time, or proximity (to the user's viewpoint). Interpolators
change a virtual object over time between specific end-point values defined by the
VRML author. Interpolators are available for changing color, scale, position, and
orientation over time. By using the specifics of VRML 2, a sensor can turn on a timer
that then interpolates the value of a virtual object's characteristic over time.
An example is a virtual doorbell that senses a user's mouse click and turns on an
orientation interpolator that opens a door slowly over a specific time period. This
door example is easily included in a VRML scene without Java.
An Overview of VRML
The VRML world description uses a traditional hierarchical scene-graph approach
reminiscent of PEX/PHIGS and other 3D toolkits. A scene graph is a document
used to express the hierarchical relationships between all the objects in a scene.
The word graph as used in the term scene graph refers to an organization of objects
described by certain mathematical characteristics. In a graph, each object in the
scene is called a node, and each connecting line is called an edge. Each node within
a scene graph has a parent, and each parent can have multiple children. As an example
of a hierarchical VRML scene graph, a body Transform node can include two
leg Transform nodes, each of which can contain a foot Transform
node, which can contain five toe Transform nodes. The toe Transform
node can have children nodes that define its geometry, texture, and color as well
as a sensor to make it interactive.
Surprisingly, VRML nodes can be represented in a semiobject-oriented manner that
meshes well with Java. Each node has a number of fields. These fields can be accessed
by other nodes only if they are explicitly declared to be accessible. You can declare
a field as read or write only, or you can define it to require a specific method
to access its value. In VRML syntax, the four types of field access are described
as follows:
- field: Hidden from general access--a private field
- eventIn: Sends a value to a node--a write-only fiel
- eventOut: Sends a value from a node--a read-only fiel
- exposedField: Publicly accessible for both read and write
The official VRML 2 node definition specifies the standard accessibility of each
field in a node; you must then consider accessibility when you are writing behavior
scripts. Most scripts are written to process a value being passed to the script in
the form of an eventIn field, which then passes the result back through
an eventOut field. Any internal values are kept in fields. The
Script can be defined as a node within the scene graph. These Script
nodes are not permitted to have exposedFields because of the updating and
implementation ramifications within the event system.
Although a node can consist of a number of input and output fields, these fields
do not all have to be connected. Usually, the opposite is the case--only a few of
the available connections are made. VRML requires explicit connection of nodes using
the ROUTE keyword, as shown here:
ROUTE fromNode.fieldname1 TO toNode.fieldname2
The only restriction when connecting fields is that the two fields be of the same
type. No casting of types is permitted.
This route mechanism can be very powerful when combined with scripting. Basically,
both a VRML ROUTE statement and a Java script can send an eventIn
field or process an eventOut field. The specification allows both fan in
and fan out of events. Fan in occurs when many nodes send an event to a single eventIn
field of a node. Fan out is the opposite: one eventOut object is connected
to many other eventIn objects. Fan out is handy when you want one script
to control a number of different objects at the same time; for example, a light switch
eventOut object turning on multiple lights simultaneously.
If two or more events fan in on a particular eventIn object, the results
are undefined. You should be careful to avoid such situations unless the ambiguity
is intended. An example of this situation is when two separate animation scripts
set the position of the same virtual object. To avoid this situation in a complicated
animated scene, create an event graph with arrows showing the direction of events
firing from one node to another. Then make sure that any two or more events coming
in to the same node cannot possibly fire at the same time.
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
handling 3D graphics such as points, vectors, image, and color. To deal with the
extra requirements of the VRML scene, node and time types have been added. The node
data type contains an instance pointer to a particular node in the VRML scene graph
(such as a virtual toe or foot). Individual fields within a node are not accessible
directly. Individual field references in behaviors programming is rarely necessary
because communication is on an event-driven model. When field references are needed
within the API, a node instance and field string description pair are used.
Except for the boolean and time types, which are always single,
values can be either single or multivalued. The distinction is made in the field
name. An SF prefix is used for single-valued fields and an MF prefix
is used for multivalued fields. For example, type SFInt32 defines a single
integer; type MFInt32 defines an array of integers. 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 controlling
a condition.
The VRML Script Node
The Script node provides the way 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 final specification of VRML
2, sample APIs were provided for Java, C, and also VRML's own scripting language,
VRMLscript--a derivative of Netscape's JavaScript. The Script node is defined
as shown here:
Script {
exposedField MFString url []
field SFBool directOutput FALSE
field SFBool mustEvaluate FALSE
# And any number of:
eventIn eventTypeName eventName
field fieldTypeName fieldName initialValue
eventOut eventTypeName eventName
}
Unlike standard HTML, VRML enables multiple target files of an open location request
to be specified in order of preference. The url field contains any number
of strings specifying URLs or URNs to the desired behavior script. For compiled Java
scripts, this would be the URL of the .class file, but the url
list is not limited to just one script type.
Apart from specifying the behavior file, VRML also enables control over how the
Script node performs within the scene graph. The mustEvaluate field
tells the browser how often the script should be run. If this 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 required by the browser. A TRUE setting is most likely to cause browser
performance to degrade because of the constant context-swapping needed; a FALSE
setting queues events to keep the context-swapping to a minimum. Unless you are performing
something the browser is not aware of (such as using a networking or database functionality),
you should set the mustEvaluate field to FALSE.
The directOutput 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 Transform node, and the directOutput 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 directOutput is set to FALSE, it requires the Script
node to 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 ROUTE statements) is much simpler.
However, in a scene in which objects are being generated on the fly, static routing
and events do not work and the first approach is required.
VRML Data Types in Java
The API is built around two Java interfaces defined in the package vrml.
The eventIn and Node interfaces are defined as follows:
interface eventIn {
public String getName();
public SFTime getTimeStamp();
public ConstField getValue();
}
interface Node {
public ConstField getValue(String fieldName)
throws InvalidFieldException;
public void postEventIn(String eventName, Field eventValue)
throws InvalidEventInException;
}
In addition to these two interfaces, each of the VRML field types also has two
class definitions that are subclasses of Field: a standard version and a
restricted, read-only version. The Const* definitions are used only in the
eventIn fields defined in individual scripts. Unless that field class has
an exception explicitly defined, these class definitions are guaranteed not to generate
exceptions.
For nonconstant fields, each class has at least the 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, which enables the caller to
set an individual element.
SFString and MFString need special attention. Java defines them
as being Unicode characters, but VRML defines them as a subset of the Unicode character
set--UTF-8. Ninety-nine percent of the time, this difference between Java and VRML
should not present any problems. Listing 48.1 includes a complete list of the available
field types as part of the VRML API class hierarchy as implemented by SGI's CosmoPlayer
VRML viewer. Check out http://vrml.sgi.com/moving-worlds/spec/ExternalInterface.html#Hierarchy
for any new developments. Note that the classes are divided into three packages:
vrml, vrml.field, and vrml.node. This code is also located
on the CD-ROM that accompanies this book.
Listing 45.1. The VRML API class hierarchy.
java.lang.Object
|
+- vrml.Event
+- vrml.Browser
+- vrml.Field
| +- vrml.field.SFBool
| +- vrml.field.SFColor
| +- vrml.field.SFFloat
| +- vrml.field.SFImage
| +- vrml.field.SFInt32
| +- vrml.field.SFNode
| +- vrml.field.SFRotation
| +- vrml.field.SFString
| +- vrml.field.SFTime
| +- vrml.field.SFVec2f
| +- vrml.field.SFVec3f
| |
| +- vrml.MField
| | +- vrml.field.MFColor
| | +- vrml.field.MFFloat
| | +- vrml.field.MFInt32
| | +- vrml.field.MFNode
| | +- vrml.field.MFRotation
| | +- vrml.field.MFString
| | +- vrml.field.MFTime
| | +- vrml.field.MFVec2f
| | +- vrml.field.MFVec3f
| |
| +- vrml.ConstField
| +- vrml.field.ConstSFBool
| +- vrml.field.ConstSFColor
| +- vrml.field.ConstSFFloat
| +- vrml.field.ConstSFImage
| +- vrml.field.ConstSFInt32
| +- vrml.field.ConstSFNode
| +- vrml.field.ConstSFRotation
| +- vrml.field.ConstSFString
| +- vrml.field.ConstSFTime
| +- vrml.field.ConstSFVec2f
| +- vrml.field.ConstSFVec3f
| |
| +- vrml.ConstMField
| +- vrml.field.ConstMFColor
| +- vrml.field.ConstMFFloat
| +- vrml.field.ConstMFInt32
| +- vrml.field.ConstMFNode
| +- vrml.field.ConstMFRotation
| +- vrml.field.ConstMFString
| +- vrml.field.ConstMFTime
| +- vrml.field.ConstMFVec2f
| +- vrml.field.ConstMFVec3f
|
+- vrml.BaseNode
+- vrml.node.Node
+- vrml.node.Script
java.lang.Exception
java.lang.RuntimeException
vrml.InvalidRouteException
vrml.InvalidFieldException
vrml.InvalidEventInException
vrml.InvalidEventOutException
vrml.InvalidExposedFieldException
vrml.InvalidNavigationTypeException
vrml.InvalidFieldChangeException
vrml.InvalidVRMLSyntaxException
Integrating Java Scripts with VRML
At some point, each VRML Script node is connected with a list of instructions
that brings the VRML objects to life. For simple animations, VRMLscript is an appropriate
language to use for coding behaviors. If you need more complexity, the thread and
networking features of the Java environment make Java a more capable language for
coding behaviors efficiently. Java can be used to create VRML behaviors by extending
the Script class or by tying VRML-aware classes together with a VRML scene
in an HTML document.
The Script Class Definition
Sony's CommunityPlace VRML viewer includes the Script class as part of
its Java API. Remember that SGI's CosmoPlayer Java API does not include the Script
class because you are expected to use VRMLscript for your intranode scripting. The
definition of Sony's Java Script class follows; as this book goes to press,
SGI has not yet included a Script class in its API.
Class Script implements Node {
public void processEvents(Events [] events)
throws Exception;
public void eventsProcessed()
throws Exception
protected Field getEventOut(String eventName)
throws InvalidEventOutException;
protected Field getField(String fieldName)
throws InvalidFieldException
}
When you create a script, you are expected to subclass the Script class
definition to provide the necessary functionality. The class definition deliberately
leaves the definition of the codes for the exceptions up to you so that you can create
tailored exceptions and handlers.
The getField() method returns the value of the field nominated by the
given string. This method is how the Java script gets the values from the VRML Script
node fields. This method is used for all fields and exposedField fields.
To the Java script, an eventOut looks just like another 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
Every eventIn field specified in the VRML Script node definition
requires a matching public method in the Java implementation. The method
definition takes this form:
public void <eventName>(Const<eventTypeName> <variable name>, SFTime <variable name>);
The method must have the same name as the matching eventIn field in the
VRML script description. The second field corresponds to the timestamp of when the
event was generated. The SFTime field is particularly useful when the mustEvaluate
field is set to FALSE, meaning that an event may be queued for some time
before finally being processed.
Because Script is an implementation of the Node interface, it
contains the postEventIn() method. Earlier in this chapter, you learned
that you should not call the eventIn methods of other scripts directly.
To facilitate direct internode communication, the postEventIn() method enables
you to send information to other nodes while staying within the VRML event-handling
system. The arguments are a String specifying the eventIn field
name and a Field containing the value. The value is a VRML data type cast
to Field. The use of PostEventIn(), as available in Sony's CommunityPlace
API, is shown in the following example and is also used later in this chapter when
a simple dynamic world is constructed.
//The node we are getting is a translation
Node translation;
float[3] translation_details;
translation[0] = 0.0f;
translation[1] = 2.3f;
translation[2] = -0.4f;
translation.postEventIn("translation", (Field)translation);
SGI's CosmoPlayer API does not use a postEventIn() method. Instead, it
uses the following syntax that, in effect, does the same thing:
Node translation = browser.getNode("box");
EventInSFVec3f set_translation = (EventInSFVec3f) material.getEventIn("translation");
float[] val = new float[3];
val[0] = 0.0f;
val[1] = 2.3f;
val[2] = -0.4f;
set_translation.setValue(val);
Remember that SGI's API does not contain the Script class. The event-processing
methods, processEvents() and eventsProcessed(), are described later
in this chapter.
The First Behavior--A Color-Changing Box
Now you are ready to put this all together to create a color-changer behavior
that toggles a box's color between red and blue. Figure 45.1 shows a simple VRML
box that has turned blue as a result of a user's mouse click (trust me; although
the figure is shown in black and white, the box really is blue!). The following sections
show you how to create the example for both the CommunityPlace and CosmoPlayer VRML
viewers. The CommunityPlace example requires five components: a Box primitive,
a TouchSensor object, a Material node, the Script node,
and the Java script. The CosmoPlayer example requires a Box primitive, a
TouchSensor object, a Material node, the Java script, and an HTML
document to associate the Java script with the VRML scene.
Figure 45.1.
The color-changing box example.
The CommunityPlace Version
For the CommunityPlace viewer, the basic VRML scene consists of a TouchSensor-enabled,
red box placed at the scene origin (0,0,0) with an associated Script node
and a couple of ROUTE statements. Listing 45.2 shows the complete VRML scene,
which was created using a simple text editor. In fact, all the VRML scenes in this
chapter were typed by hand into the Notepad text editor that is a part of the Windows
operating system. You can find the code in Listing 45.2 on the CD-ROM that accompanies
this book.
Listing 45.2. The VRML scene graph for CommunityPlace.
#VRML V2.0 utf8
#filename: s_box.wrl
Transform {
bboxSize 1 1 1
children [
Shape {
appearance Appearance {
material DEF cube_material Material {
diffuseColor 1 0 0 #start red.
}
}
geometry Box {size 1 1 1}
} # end of shape definition
# Now define a TouchSensor node. This node takes in the
# geometry of the parent transform. Default behavior OK.
DEF cube_sensor TouchSensor {}
]
}
DEF color_script Script {
url "s_color_changer.class"
# now define our needed fields
field SFBool isRed TRUE
eventIn SFBool clicked
eventOut SFColor color_out
}
ROUTE cube_sensor.isActive TO color_script.clicked
ROUTE color_script.color_out TO cube_material.set_diffuseColor
The Script node acts as the color changer. It takes input from the TouchSensor
object and outputs the new color to the Material node. The Script
node must also keep internal track of the color. This is done by reading in 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.
Finally, the Java script shown in Listing 45.3 is compiled to produce the color_changer.class
file that is referenced in Listing 45.2 in the Script node's behavior
field. You can find this code on the CD-ROM that accompanies this book.
Listing 45.3. The sample Java script for CommunityPlace.
//filename: s_color_changer.java
import vrml.field.*;
import vrml.node.*;
import vrml.*;
class s_color_changer extends Script {
// declare the field
private SFBool isRed = (SFBool)getField("isRed");
// declare the eventOut
private SFColor color_out = (SFColor)getEventOut("color_out");
// declare color float array
float[] color;
// declare eventIns
public void clicked(ConstSFBool isClicked, ConstSFTime ts) {
// called when the user clicks or touches the cube 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 cube is red or blue
if(isRed.getValue() == true)
isRed.setValue(false);
else
isRed.setValue(true);
}
}
// finally the event processing call
public void eventsProcessed() {
if(isRed.getValue() == true) {
color = new float[3];
color[0] = 0.0f;
color[1] = 0.0f;
color[2] = 1.0f;
color_out.setValue(color);
}
else {
color = new float[3];
color[0] = 1.0f;
color[1] = 0.0f;
color[2] = 0.0f;
color_out.setValue(color);
}
}
}
The CosmoPlayer Version
For the CosmoPlayer viewer, the basic VRML scene consists of a TouchSensor-enabled,
red box placed at the scene origin (0,0,0) but does not include a Script
node or the ROUTE statements used with the CommunityPlace example. As you
see in Listing 45.4 (which is also located on the CD-ROM that accompanies this book),
the routing is done within the Java script itself.
Listing 45.4. The VRML scene graph for CosmoPlayer.
#VRML V2.0 utf8
#filename: sgi_box.wrl
Transform {
bboxSize 1 1 1
children [
Shape {
appearance Appearance {
material DEF cube_material Material {
diffuseColor 1.0 0 0 #start red.
}
}
geometry Box {size 1 1 1}
} # end of shape definition
# Now define a TouchSensor node. This node takes in the
# geometry of the parent transform. Default behavior OK.
DEF cube_sensor TouchSensor {}
]
}
Instead of connecting the Java script to the VRML scene within the VRML file,
the connection is made in an HTML document. This approach has one big advantage:
You can use the Java AWT to control your VRML scene interaction. The HTML code is
provided in the file Color_changer.htm, as shown in Listing 45.5, and can
be found on the CD-ROM.
Listing 45.5. The HTML document for the color-changer example
in CosmoPlayer.
<HTML>
<HEAD>
<TITLE>Color Changer Example</TITLE>
</HEAD>
<BODY>
<CENTER>
<EMBED SRC="sgi_box.wrl" BORDER=0 HEIGHT="400" WIDTH="400">
</CENTER>
<APPLET CODE="sgi_color_changer.class" MAYSCRIPT>
</APPLET>
</BODY>
</HTML>
In the HTML file, you connect the box.wrl VRML file to the Color_changer.class
Java class using the <EMBED> and <APPLET> HTML tags.
Note that you can set the HEIGHT and WIDTH of the VRML scene to
whatever size you want; any Java controls you include in the .class file
appear in an area following your VRML scene.
Finally, the Java sgi_color_changer class source code is shown in Listing
45.6 (this code can also be found on the CD-ROM). This code creates the Java controls
and connects them to the VRML scene for visitors to use to interact with the scene.
The code in Listing 45.6 allows a user to interact with the VRML scene using Java
AWT controls such as buttons and checkboxes, which are made available in a separate
control panel. The code in Listing 45.7 allows a user to interact with the VRML scene
by clicking on VRML objects in the scene itself.
Listing 45.6. The sample Java script for CosmoPlayer.
//filename: sgi_color_changer.java
import java.awt.*;
import java.applet.*;
import vrml.external.field.EventOut;
import vrml.external.field.EventInSFColor;
import vrml.external.Node;
import vrml.external.Browser;
import vrml.external.exception.*;
import netscape.javascript.JSObject;
public class sgi_color_changer extends Applet {
TextArea output = null;
Browser browser = null;
Node material = null;
EventInSFColor diffuseColor = null;
boolean red = true;
boolean error = false;
public void init() {
add(new Button("Change Color"));
JSObject win = JSObject.getWindow(this);
JSObject doc = (JSObject) win.getMember("document");
JSObject embeds = (JSObject) doc.getMember("embeds");
browser = (Browser) embeds.getSlot(0);
// Now we've got the handle to the VRML Browser.
try {
// Get the material node...
material = browser.getNode("cube_material");
// Get the diffuseColor EventIn
diffuseColor = (EventInSFColor) material.getEventIn("set_diffuseColor");
}
catch (InvalidNodeException ne) {
showStatus("Failed to get node:" + ne);
error = true;
}
catch (InvalidEventInException ee) {
showStatus("Failed to get EventIn:" + ee);
error = true;
}
catch (InvalidEventOutException ee) {
showStatus("Failed to get EventOut:" + ee);
error = true;
}
}
public boolean action(Event event, Object what) {
if (error)
{
showStatus("Problems! Had an error during initialization");
return true; // Uh oh...
}
if (event.target instanceof Button)
{
Button b = (Button) event.target;
if (b.getLabel() == "Change Color") {
if (red) {
showStatus ("Blue!");
float[] val = new float[3];
val[0] = 0.2f;
val[1] = 0.2f;
val[2] = 0.8f;
diffuseColor.setValue(val);
red = false;
}
else {
showStatus("Red!");
float[] val = new float[3];
val[0] = 0.8f;
val[1] = 0.2f;
val[2] = 0.2f;
diffuseColor.setValue(val);
red = true;
}
}
}
return true;
}
}
So that you can click the box and change the color from within the VRML scene
instead of from a Java AWT button, SGI's browser sets up an event callback in the
Java class by implementing EventOutObserver. The EventOutObserver
sets up event awareness for the VRML scene. If you want to interact with the cube
within the VRML itself instead of from a Java AWT button, just replace sgi_color_changer.class
with Color_changer.class, compiled from the source code in Listing 45.7.
This code can also be found on the CD-ROM that accompanies this book.
Listing 45.7. An alternative Java script for CosmoPlayer.
//filename: Color_changer.java
import java.awt.*;
import java.applet.*;
import vrml.external.field.EventOut;
import vrml.external.field.EventInSFColor;
import vrml.external.field.EventOutSFColor;
import vrml.external.field.EventOutSFTime;
import vrml.external.field.EventOutObserver;
import vrml.external.Node;
import vrml.external.Browser;
import vrml.external.exception.*;
import netscape.javascript.JSObject;
public class Color_changer extends Applet implements EventOutObserver {
TextArea output = null;
Browser browser = null;
Node material = null;
EventInSFColor diffuseColor = null;
EventOutSFColor outputColor = null;
EventOutSFTime touchTime = null;
boolean red = true;
boolean error = false;
public void init() {
JSObject win = JSObject.getWindow(this);
JSObject doc = (JSObject) win.getMember("document");
JSObject embeds = (JSObject) doc.getMember("embeds");
browser = (Browser) embeds.getSlot(0);
// Now we've got the handle to the VRML Browser.
try {
// Get the material node...
material = browser.getNode("cube_material");
// Get the diffuseColor EventIn
diffuseColor = (EventInSFColor) material.getEventIn("set_diffuseColor");
// Get the Touch Sensor
Node sensor = browser.getNode("cube_sensor");
// Get its touchTime EventOut
touchTime = (EventOutSFTime) sensor.getEventOut("touchTime");
// Set up the callback
touchTime.advise(this, new Integer(1));
// Get its diffuseColor EventOut
outputColor = (EventOutSFColor) material.getEventOut("diffuseColor");
// Set up its callback
outputColor.advise(this, new Integer(2));
}
catch (InvalidNodeException ne) {
add(new TextField("Failed to get node:" + ne));
error = true;
}
catch (InvalidEventInException ee) {
add(new TextField("Failed to get EventIn:" + ee));
error = true;
}
catch (InvalidEventOutException ee) {
add(new TextField("Failed to get EventOut:" + ee));
error = true;
}
}
public void callback(EventOut who, double when, Object which) {
Integer whichNum = (Integer) which;
if (whichNum.intValue() == 1) {
if (red) {
showStatus ("Blue!");
float[] val = new float[3];
val[0] = 0.2f;
val[1] = 0.2f;
val[2] = 0.8f;
diffuseColor.setValue(val);
red = false;
}
else {
showStatus("Red!");
float[] val = new float[3];
val[0] = 0.8f;
val[1] = 0.2f;
val[2] = 0.2f;
diffuseColor.setValue(val);
red = true;
}
}
if (whichNum.intValue() == 2) {
// Make the new color of the sphere and timestamp
// show up in the textarea.
float[] val = outputColor.getValue();
showStatus("Got color " + val[0] + ", " + val[1] + ", " +
val[2] + " at time " + when + "\n");
}
}
}
Of course, you can combine both the Java controls and the VRML node callback routine
in the same Java file.
That's it. Now you have a cube that changes color when you click it. The code
looks almost identical for changing a virtual object's translation, rotation, or
scale in the VRML scene. All other eventIns can be accessed in similar fashion--including
the current scene viewpoint. Creating more complex behaviors is just a variation
of this scheme, with more Java code and fields. Although the basic user input usually
comes from sensors, in the SGI approach the events can come from any Java program--including
a program that embeds all kinds of physics to handle interactions between the objects
drawn to the screen in the VRML scene. This approach creates many scientific visualization
opportunities.
Even with Sony's approach, scripts are not restricted to input methods based on
eventIn fields. One example is a stock market tracker that runs as a separate
thread. It can constantly receive updates from the network, process them, and then
send the results through a public method to the script, which then puts the appropriate
results into the 3D world.
The Browser Class
Behaviors using the methods presented in the color-changer box examples 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. As a thought experiment, consider a virtual taxi: A user should be
able to step inside and instruct the cab where to go. Using the techniques from the
preceding examples, the cab would move 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 specification defines a series of actions the programmer can access
to set and retrieve information about the virtual world. Within the Java implementation
of the API, world information is provided in the Browser class. The Browser
class provides all the functions a programmer needs that are not specific to any
particular part of the scene graph.
To define a system-specific behavior, the first functions you must define are
these:
public static String getName();
public static 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 static float getCurrentFrameRate();
In systems that use prediction, two more handy pieces of information to know are
what mode the user is navigating the scene in, and at what speed the user is traveling.
Similar to the getName() method, the string returned to describe the navigation
type is browser dependent. VRML defines that, at a minimum, the following four navigation
types must be supported: WALK, EXAMINE, FLY, and NONE.
However, if you are building applications for an intranet and you know what type
of browser is used, this navigation information can be quite handy for varying the
behavior, depending on how the user approaches the object of interest. Information
about navigation is available from the following methods:
public static String getNavigationType();
public static void setNavigationType(String type)
throws InvalidNavigationTypeException;
public static float getNavigationSpeed();
public static void setNavigationSpeed(float speed);
public static float getCurrentSpeed();
The difference between navigation speed and current speed is in the definition.
VRML 2 defines a navigationInfo 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; these units are defined for each browser by the individual browser
developers. There is no specification about what this speed represents, only hints.
A reasonable estimate of the navigation speed is the movement speed in WALK
and FLY mode and the movement speed used in panning and dollying in EXAMINE
mode, encountered when a user has not selected any speed controls. The current speed
is the actual speed at which the user is traveling at that point in time. This is
the speed that the user has set with the browser controls. Speed controls, when they
are provided by the browser developer, allow the user to vary from the default navigation
speed. For example, a browser with a default speed of 70 pixels/second may slow down
to 40 pixels/second when the user selects the slow speed control from an available
menu.
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 that is only 0.001 units across, this speed would
be ridiculously fast. The navigation speed value can be used to scale speeds to something
reasonable for the particular world.
Also contained in the navigationInfo node is a boolean field for a headlight.
The headlight is a directional light that points in the direction the user is facing.
Where the scene creator has used other lighting effects (such as radiosity), the
headlight is usually turned off. In the currently available browsers, the headlight
field has led to software bugs (for example, turning off the headlight results in
the whole scene going black). It is recommended that you do not use the headlight
feature within the behaviors because the browser includes logic to determine when
best to use the headlight. For example, the headlight is usually disabled when the
browser is showing the effects of a single point of light. If you have to access
the headlight, the following functions are provided by the Browser class:
public static boolean getHeadlight();
public static void setHeadlight(boolean onOff);
The methods described in this section enable you to change individual components
of the world. The other approach is to completely replace the world with some internally
generated one. This approach enables you to use VRML to generate new VRML worlds
on the fly--assuming that you already are part of a VRML world (you cannot use this
approach in an application to generate a 3D graphics front-end). Use the following
statement to replace the current world with an internally generated one:
public static void replaceWorld(node nodes[]);
This is a nonreturning call that unloads the current scene graph and replaces
it with a new one.
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. You can dramatically change a VRML scene while a user is visiting
it. The fact that nodes are embedded within other nodes in a scene graph makes VRML
very flexible for changing just the specific part of the scene you want to change.
You can query the current world to find out the URL from which it was originally
loaded. Your Java code can then contain different paths based on the current world:
public static 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 that are included in the world--a technique called
inlining in VRML parlance.
You can change your VRML scene dynamically by using the following three browser
methods:
public static void loadWorld(String[] url);
public static Node createVrmlFromString(String vrmlSyntax);
public static void createVrmlFromURL(String[] url,
Node node,
String eventInName);
To completely replace the scene graph, you call the loadWorld() method.
As with all URL references in VRML, an array of strings are passed. These strings
are a list of URLs and URNs to be loaded in order of preference. Should the load
of the first URL fail, the browser attempts to load the second, and so on until a
scene is loaded or the browser reaches the end of the list. If the load fails, the
VRML viewer can notify the user as the developer sees fit. The specification also
states that it is up to the browser whether the loadWorld() call blocks
or starts a separate thread when loading a new scene.
In addition to replacing the whole scene, you may want to add bits at a time to
a scene. You can do this 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()
call. The node that is returned can be added to the scene to produce dynamic results.
Perhaps the most useful of the preceding three functions is the createVrmlFromURL()
method. From the definition, you may have noticed that, along with a list of URLs,
the method 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 into the VRML viewer's internal representation,
and then finally sends the newly created list of nodes to the specified node's eventIn
field. The eventIn field type must 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 new nodes directly to the scene graph without having
to write extra functionality in the script.
With both of the create methods, the returned nodes do not become visible until
they have been added to some preexisting node in the scene. Although it is possible
to create an entire scene on the fly within a standalone applet, there is no way
to make the scene visible unless there is an exiting node instance to which you can
add the dynamically generated scene.
Once you have created a set of new nodes, you also want to be able to link them
together to get a behavior system similar to the one in the original world. The Browser
class defines methods for dynamically adding and deleting ROUTE statements
between nodes:
public void addRoute(Node fromNode, String fromEventOut,
Node toNode, String toEventIn)
throws InvalidRouteException;
public void addRoute(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 field. Exceptions
are thrown if either of the nodes or fields does 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 acquire the wisdom
to create responsive worlds that won't get bogged down in Java code.
The Script Execution Model
When tuning the behaviors in a virtual world, the methods used depend on the execution
model. The VRML API gives you a lot of control over exactly how scripts are executed
and how events passed to it are distributed.
The arrival of an eventIn field at a Script node causes the
execution of the matching method. There is no other way to invoke a method. A script
can start an asynchronous thread, which in turn can call another non-eventIn
method of the script or can even send events directly to other nodes. The VRML 2
specification makes no mention about scripts containing non-eventIn public
methods. Although it is possible to call an eventIn method directly, it
is in no way encouraged. Such programming interferes with the script execution model
by preventing browser optimization and can affect the running of other parts of the
script. Calling an eventIn method directly can also cause performance penalties
in other parts of the world, not to mention reentrancy problems within the eventIn
method itself. If you find it necessary to call an eventIn method of the
script, use the postEventIn() method so that the operation of the browser's
execution engine is not affected.
Unless the mustEvaluate field is set, all the events are queued in timestamp
order from oldest to newest. For each queued event, the corresponding eventIn
method is called. Each eventIn field calls exactly one method. If an eventOut
fans out to a number of eventIns, multiple eventIns are generated--one
for each node. Once the queue is empty, the eventsProcessed() method for
that script is called. The eventsProcessed() method enables any post-event
processing to be performed.
A typical use of this post-processing was shown in the example of the color-changing
cube, earlier in this chapter. In that example, the eventIn method just
took the data and stored it in an internal variable. The eventsProcessed()
method took the internal value and generated the eventOut. This approach
was overkill for such simple behavior. Normally, such simplistic behavior uses VRMLscript
instead of Java. However, the separation of data processing from data collection
is very effective in a high-traffic environment, in which event counts are very high
and the overhead of data processing is best absorbed into a single, longer run instead
of many short runs.
Once the eventsProcessed() method has completed execution, any eventOuts
generated as a result are sent as events. If the script generates multiple eventOuts
for a single eventOut field, only one event is sent. All eventOuts
generated during the execution of the script have the same timestamp.
If your script has spawned a thread, and that script is removed from the scene
graph, the browser is required to call the shutdown() method for each active
thread to facilitate a graceful exit.
If you want to maintain static data between invocations of the script, it is recommended
that your VRML Script node have fields to hold the values. Although it is
possible to use static variables within the Java class, VRML makes no guarantees
that these variables will be retained, especially if the script is unloaded from
memory.
If you are a hardcore programmer, you probably want to keep track of all the event-handling
mechanisms yourself. VRML provides the facility to do this with the processEvents()
method. This method is called when the browser decides to process the queued eventIns
for a script. The method is sent an array of the events waiting to be processed,
which you can then do with as you please. Graphics programmers should already be
familiar with event-handling techniques from the Microsoft Windows, Xlib, or Java
AWT system.
Circular Event Loops
The ROUTE syntax makes it very easy to construct circular event loops.
Circular loops can be quite handy. The VRML specification states that if the browser
finds event loops, it only processes each event once per timestamp. Events generated
as a result of a change are given the same timestamp as the original change because
events are considered to happen instantaneously. When event loops are encountered
in this situation, the browser enforces a breakage of the loop. The following sample
script from the VRML specification uses VRMLscript to explain this process:
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 input b. After the initial event with a=1,
the script leaves the eventOut c with the value of 1.
This causes a cascade effect, in which b is set to 1. Normally,
this generates an eventOut on c with the value 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.
Creating Efficient Behaviors
For 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 keeping 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 may sound a little
strange in 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 your viewers may be people using low-end PCs.
Because most VRML browsers specify that a minimum of 16M of RAM is required (and
32M is recommended), also loading the Java VM into memory requires lots of swapping
to keep the behaviors going. The inevitable result is bad performance. For this reason,
the interpolator nodes and VRMLscript were created--built-in nodes for common basic
calculations and a small, light language to provide basic calculation abilities.
Use of Java should be limited to the times when you require the capabilities of a
full programming language, such as for 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 and then return as soon as possible.
In VR systems, frame rate is king. Don't aim to have a one-hundred percent correct
behavior if it leads to half the frame rate when a ninety-percent correct behavior
will do. It is amazing how users don't notice an incorrect behavior, but as soon
as the picture update slows 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, your goal should be
to maintain at least 20fps. 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 is up to the individual programmer and the requirements of
the application. Your user will typically accept that a door does not open smoothly
as long as he or she can move around without watching individual frames redraw.
Don't play with the event-processing loop unless you really must. 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 virtual reality, you don't have control over
the whole system. The processEvents() method applies only to the individual
script, not as a common method across all scripts. So although you think you are
optimizing the event handling, you are doing so only for one script. In a reasonably
sized world, another few hundred scripts may also be running, so the optimization
of an individual script isn't generally worth the effort.
Changing the Scene
Add to the scene graph only what is necessary. If you can modify existing primitives,
do so instead of adding new ones. Every primitive you add to a scene requires the
renderer to convert the scene to its internal representation and then reoptimize
the scene graph to take the new objects into account. When it modifies existing primitives,
the browser is not required to resort the scene graph structure, saving computation
time. 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 just delete them from the scene graph. It is better
to remove them from a node and keep an instance pointer to them so that they can
be reinserted at a later time. At the expense of a little extra memory, you save
time. If you don't take the time now, you may later have to access the objects from
a network or construct them from the ground up from a string representation.
Another trick is to create VRML objects but not add them to the visual scene graph.
VRML scripting enables objects to be created but not added to the scene graph. Any
object not added isn't drawn. For node types such as sensors, interpolators, and
scripts, there is no need for these objects to be added because they are never drawn.
Doing so causes extra events to be generated, resulting in a slower system. Normal
Java garbage collection rules apply 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
ROUTE statements, and then exits, the node stays allocated and it functions
properly. You can use this approach to set up a VRML node that does not use events
for its visualization.
There are dangers in this approach. Once you lose the node instance pointer, you
have no way to delete the node. You need this pointer if you are to delete the ROUTE.
Deleting ROUTE statements 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 ROUTE statements
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 to
a floating node that also contains an eventOut to a section of an undeleted
section. This situation 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
referenced by the eventOut.
Dynamic Worlds--Creating VRML on the Fly
This section develops a framework for creating worlds on the fly. Dynamically
changing worlds add tremendous potential to your VRML scenes. You can develop cyberspace
protocol-based, seamless worlds in which a visitor can take an object from one world
and use or leave it in another world. You can provide a VRML toolkit with which a
visitor creates new VRML objects from existing ones and saves them as separate VRML
files. But for starters, you may just want to add a few new objects or eliminate
objects as a visitor interacts with your scene. The next sections provide an example
of a dynamic world for both CommunityPlace and CosmoPlayer. The example primarily
familiarizes you with the createVrmlFromString(String vrmlSyntax),
createVrmlFromURL(String[] url, Node node, String event),
and loadURL(String[] url, String[] parameter) methods
of the Browser class. Figure 45.2 shows the sample VRML scene before it
dynamically changes.
Figure 45.2.
A simple VRML scene based on the VRML logo.
Creating the VRML Source File
If you are to add new objects to a VRML scene, a placeholder node must already
exist in the .wrl file. This placeholder need not be anything more sophisticated
than the following empty Transform node:
DEF root_node Transform { }
In fact, this statement can be the entire starting world if the world is to be
built using Java AWT controls. More typically, however, other objects are already
in the world when it is loaded and the Transform node becomes just another
potential node to which you can add new objects. The Transform node has
two eventIn fields--addChildren and removeChildren--that
can be used to add or delete multiple children from within the node.
In the following code example, three objects exist in the VRML scene when the
world is first loaded. Each object enables an event that dynamically changes the
world. Using the three primitive shapes that form the VRML logo, the red cube enables
the createVrmlFromURL() method, the green sphere enables the createVrmlFromString()
method, and the blue cone takes the user to another VRML world by using the loadURL()
method. The cube, sphere, and cone are each children of Transform nodes
to make sure that they are located in different parts of the world (all objects are
located at the Transform node's origin by default). The code that creates
the cube Transform node follows:
DEF cube Transform {
children [
DEF cube_sensor TouchSensor{}
Shape {
appearance Appearance {
material Material {
diffuseColor 1 0 0
}
}
geometry Box { size 1 1 1}
}
# note: script node goes here for CommunityPlace
]
bboxSize 1 1 1
translation -2 0 0
}
Notice that the TouchSensor itself has been defined (with DEF)
as has the whole Transform node. The TouchSensor is the object
that triggers events. Without a defined sensor, the cube has no way to interact with
the rest of the scene. Any mouse click (or touch, if the user has a dataglove) on
the cube does nothing. As you soon see, the other two nodes are similar in definition.
The Shape node contains the appearance of the cube (in this case, a bright
red color) as well as the geometry of the cube (which is 1 unit wide, 1 unit tall,
and 1 unit deep). The bboxSize field defines a bounding box that helps the
VRML viewer render the cube efficiently. The translation field places the
cube two units to the right from the (0,0,0) origin of the VRML scene.
Keeping this VRML primer in mind, you are ready to follow the example for both
the CommunityPlace and CosmoPlayer VRML viewers.
The CommunityPlace Version of the Dynamic World Example
The following sections break down the process for creating the dynamically changing
world for the CommunityPlace browser.
NOTE: For demonstration purposes, the
separate scripts are described and grouped with their appropriate objects. It makes
no difference if you have lots of small scripts or one large one. If you are a VR
scene creator, it is probably better to have one large script to keep track of the
scene graph if you want to save a VRML file with the changes. If you are creating
a virtual factory of reusable, plug-and-play component VRML objects, you may prefer
to use many small scripts, perhaps with some "centralized" script to act
as the system controller.
Defining the Script Nodes
Once the basic VRML 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().
The example presented does not have any real-time constraints, so the mustEvaluate
field for each object is left with the default setting of FALSE. For the
cone object, no outputs are sent directly to nodes, so the directOutputs
field is left at FALSE. For the sphere object, outputs are sent directly
to the Group node, so the directOutputs field is set to TRUE.
The directOutputs field for the cube object must also be set to TRUE
for reasons explained in the next section.
In addition to eventIn, the box_script example also needs an
eventOut to send the new object to the Group node that is acting
as the scene root. Good behavior is desirable if the user clicks the cube 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 "box_script.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. Because some processing must be
done on the node returned from the createVrmlFromURL() method, you must
provide an eventIn for the argument. If you do not have to process the returned
nodes, use the root_node.add_children() method instead.
The other interesting point to note is that the script declaration includes a
field that is a reference to itself. At the time this chapter was written, the draft
specifications for VRML 2 did not specify how a script was to refer to itself when
calling its own eventIns. To play it safe, the method in the preceding declaration
is guaranteed to work. However, it should be possible for the script to specify the
this keyword as the node reference when referring to itself. Check the most
current version of the specification, available at http://vag.vrml.org/,
for more information.
To show 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; when this copy is resolved
in Java, it essentially becomes an instance pointer to the node. Using direct writing
to nodes means that you no longer require the eventOut from the cube's script,
but you do 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 the user clicks the cone, all
it does is fetch some named URL and set the URL as the new scene graph: a simple
cylinder.
DEF cone_script Script {
url "cone_script.class"
eventIn SFBool isClicked
field MFString target_url ["cylinder.wrl"]
}
Completing the VRML Description
Now that you have defined the scripts, you must wire them together. A number of
routes are added between the sensors and scripts, as shown in Listing 45.8.
Listing 45.8. The main world VRML description for CommunityPlace.
#VRML V2.0 utf8
#filename: s_dynamic.wrl
# first the pseudo root
DEF root_node Transform { bboxSize 1000 1000 1000}
# The cube
Transform {
children [
DEF cube_sensor TouchSensor{}
Shape {
appearance Appearance {
material Material {
diffuseColor 1 0 0
}
}
geometry Box { size 1 1 1}
}
DEF box_script Script {
url "box_script.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 ["cylinder.wrl"]
}
]
bboxSize 1 1 1
translation 2 0 0
}
ROUTE cube_sensor.isActive TO cube_script.isClicked
ROUTE cube_script.childlist TO root_node.add_children
# The sphere
Transform {
children [
DEF sphere_sensor TouchSensor {}
Shape {
appearance Appearance {
material Material {
diffuseColor 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
}
]
# no translation needed as it the origin already
bboxSize 1 1 1
}
ROUTE sphere_sensor.isActive TO sphere_script.isClicked
# The cone
Transform {
children [
DEF cone_sensor TouchSensor {}
Shape {
appearance Appearance {
material Material {
diffuseColor 0 0 1
}
}
geometry Cone {
bottomRadius .5
height 1
}
}
DEF cone_script Script {
url "cone_script.class"
eventIn SFBool isClicked
}
]
bboxSize 1 1 1
translation -2 0 0
}
ROUTE cone_sensor.isActive TO cone_script.isClicked
# end of file
The box sensor adds objects to the scene graph from an external file. This external
file, shown in Listing 45.9, contains a Transform node with a single Cylinder
as a child. Because the API does not permit you to create node types and you have
to place the newly created box at a point other than the origin, you must use a Transform
node. Although you can just load a box from the external scene and then create a
Transform node with the createVrmlFromString() method, this approach
requires more code and slows execution speed. Remember that behavior writing is about
getting things done as quickly as possible; the more you can move to external static
file descriptions, the better.
Listing 45.9. The external VRML world file.
#VRML V2.0 utf8
#filename: cylinder.wrl
Transform {
children
Shape {
appearance Appearance {
material Material {
diffuseColor 1 1 .85
}
}
geometry Cylinder {}
}
translation 0 4 0
}
# end of file
Organizing 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 example like the one in Listing 45.8, there are two ways to arrange the
scripts. Imagine what can happen in a moderately complex file of two or three thousand
objects!
All the scripts in this example are simple. When the node is received back in
newNodes eventIn, the node must be translated to the new position. Ideally,
you would like to do this directly by setting the translation field, but
you are not able to do so because the translation field is encapsulated
within the received node. The only way to translate the node to the new position
is to post an event to the node, naming that field as the destination (which is the
reason you set directOutputs to TRUE). After this is done, you
can then call the add_children() method. Because all the scripts are short,
the processEvents() method is not used because the risk to browser execution
interruption is minimal. Shorter scripts are less likely to significantly impair
the browser's usual processing. Listing 45.10 shows the complete source code for
the cube script; this code can also be found on the CD-ROM that accompanies this
book.
Listing 45.10. Java source for the cube script.
//filename: box_script.java
import vrml.field.*;
import vrml.node.*;
import vrml.*;
class box_script extends Script {
private SFInt32 zPosition = (SFInt32)getField("zPosition");
private SFNode thisScript = (SFNode)getField("thisScript");
private MFString newUrl = (MFString)getField("newUrl");
// declare the eventOut field
private MFNode childList = (MFNode)getEventOut("childList");
// now declare the eventIn methods
public void isClicked(ConstSFBool clicked, SFTime ts)
{
// check to see if picking up or letting go
if(clicked.getValue() == false)
// Note: as of the writing of this book, Sony's CommunityPlace
Java API had yet to implement
// the createVrmlFromURL and postEventIn methods
Browser.createVrmlFromUrl(newUrl.getValue(),
thisScript, "newNodes");
}
public void newNodes(ConstMFNode nodelist, SFTime ts)
{
Node[] nodes = (Node[])nodelist.getValue();
float[] translation={0.0f,0.0f,0.0f};
// 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);
}
}
Listing 45.11 (the code can also be found on the CD-ROM) shows the sphere_script
class, which is similar to the cube_script class, except that you have to
construct the text-string equivalent of the cylinder.wrl file. This is a
straightforward string buffer problem. All you have to do is make sure that the Transform
node of the newly added object has an appropriate value for the translation
field to avoid a collision with the existing world objects.
Listing 45.11. Java source for the sphere script.
//filename: sphere_script.java
import vrml.field.*;
import vrml.node.*;
import vrml.*;
class sphere_script extends Script {
private SFInt32 zPosition = (SFInt32)getField("zPosition");
private SFNode root = (SFNode)getField("root");
// now declare the eventIn methods
public void isClicked(ConstSFBool clicked, SFTime ts)
{
StringBuffer vrml_string = new StringBuffer();
MFNode nodes=null;
// 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 { bboxSize 1 1 1 ");
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.createVrmlFromUrl(vrml_string));
// Note: as of the writing of this book, Sony's CommunityPlace
Java API had yet to implement
// the createVrmlFromURL and postEventIn methods
root.postEventIn("add_children", (Field)nodes);
}
}
}
The cone_script class is the easiest of the lot. As soon as it receives
a confirmation of a touch, it starts to load another world specified by the URL.
Listing 45.12 reveals the source code; the code is also located on the CD-ROM that
accompanies this book.
Listing 45.12. Java Source for the cone script.
//filename: cone_script.java
import vrml.field.*;
import vrml.node.*;
import vrml.*;
class cone_script extends Script {
SFBool isClicked = (SFBool)getField("isClicked");
// The eventIn method
public void isClicked(ConstSFBool clicked, SFTime ts)
{
if(clicked.getValue() == false) {
String s[] = new String[1] ;
s[0] = "cylinder.wrl";
String t[] = new String[1] ;
t[0] = "target=info_frame";
getBrowser().loadURL( s,t );
}
}
}
By compiling the preceding Java code samples 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. The rest of the world will get the same behavior as you do--regardless
of what system individual users are running.
The CosmoPlayer Version of the Dynamic World Example
The following sections break down the process for creating the dynamically changing
world for the CosmoPlayer browser.
The Complete VRML Description
As Listing 45.13 shows, the VRML 2 file for CosmoPlayer contains no Script
nodes or ROUTE statements. Instead, all scripting is done in the Java class,
which is tied to the .wrl file in the HTML document, Dynamic.htm.
This code is also located on the CD-ROM that accompanies this book.
Listing 45.13. The main world VRML description for CosmoPlayer.
#VRML V2.0 utf8
#filename: sgi_dynamic.wrl
DEF root_node Transform { },
DEF cube Transform {
children [
DEF cube_sensor TouchSensor{}
Shape {
appearance Appearance {
material Material {
diffuseColor 1 0 0
}
}
geometry Box { size 1 1 1}
}
]
bboxSize 1 1 1
translation -2 0 0
},
DEF sphere Transform {
children [
DEF sphere_sensor TouchSensor{}
Shape {
appearance Appearance {
material Material {
diffuseColor 0 1 0
}
}
geometry Sphere { radius .5 }
}
]
bboxSize 1 1 1
translation 0 0 0
},
DEF cone Transform {
children [
DEF cone_sensor TouchSensor{}
Shape {
appearance Appearance {
material Material {
diffuseColor 0 0 1
}
}
geometry Cone {
bottomRadius .5
height 1
}
}
]
bboxSize 1 1 1
translation 2 0 0
}
The HTML File
The HTML file in Listing 45.14 is almost identical to the one used in the first
behavior example in Listing 45.5, earlier in this chapter. We only have to change
the document title to Dynamic World Example, the SRC tag value
to Dynamic.wrl, and the CODE tag value to Dynamic.class.
With these changes, the HTML document will load the VRML file in a 400x400 pixel
area within the browser window. The Java class is associated with the VRML scene
and even provides an area for any AWT controls instantiated in the Java source code.
Listing 45.14. The HTML document.
<HTML>
<HEAD>
<TITLE>Dynamic World Example</TITLE>
</HEAD>
<BODY>
<CENTER>
<EMBED SRC="sgi_dynamic.wrl" BORDER=0 HEIGHT="400" WIDTH="400">
</CENTER>
<APPLET CODE="sgi_dynamic.class" MAYSCRIPT>
</APPLET>
</BODY>
</HTML>
The Java File
The source code in Listing 45.15 compiles without any warnings when you use the
JDK 1.1 from Sun and the beta 3 version of CosmoPlayer 1.0. In the listing, I have
commented out the lines that have not yet been implemented by SGI so that you can
compare them to the VRML Consortium's suggested Java API at http://vrml.vag.org.
As of this writing, neither the createVrmlFromURL() nor the loadURL()
method had been implemented for the Windows 95/NT 4.0 CosmoPlayer viewer. Note that
SGI has implemented a method declared as replaceWorld(Node node),
which works like the loadURL() method when you use a string parameter that
refers to the URL of a .wrl file. As usual, you can find the code presented
in Listing 45.15 on the CD-ROM that accompanies this book.
Listing 45.15. The Java source code for CosmoPlayer.
//filename: sgi_dynamic.java
import java.awt.*;
import java.applet.*;
import vrml.external.field.*;
import vrml.external.Node;
import vrml.external.Browser;
import vrml.external.exception.*;
import netscape.javascript.JSObject;
public class sgi_dynamic extends Applet implements EventOutObserver{
boolean error = false;
// Browser we're using
Browser browser;
// Root of the scene graph (to which we add our nodes)
Node root=null;
Node sensor[] = {null,null,null};
// Shape group hierarchy
Node[] shape[] = {null,null};
Node[] scene = null;
// EventIns of the TouchSensors
EventOutSFTime touchTime[] = {null,null,null};
// EventIns of the root node
EventInMFNode addChildren;
EventInMFNode removeChildren;
public void init() {
JSObject win = JSObject.getWindow(this);
JSObject doc = (JSObject) win.getMember("document");
JSObject embeds = (JSObject) doc.getMember("embeds");
browser = (Browser) embeds.getSlot(0);
try {
// Get root node of the scene, and its EventIns
root = browser.getNode("root_node");
sensor[0] = browser.getNode("cube_sensor");
sensor[1] = browser.getNode("sphere_sensor");
sensor[2] = browser.getNode("cone_sensor");
for(int x=0;x<3;x++) {
touchTime[x] = (EventOutSFTime) sensor[x].getEventOut("touchTime");
touchTime[x].advise(this, new Integer(x));
}
addChildren = (EventInMFNode) root.getEventIn("addChildren");
removeChildren = (EventInMFNode) root.getEventIn("removeChildren");
// Create shapes to be added on the fly --
can re-assign new strings at any time
//NOTE: as of beta 3 of CosmoPlayer 1.0 for Windows95/NT 4.0,
the createVRMLFromURL method
//was not implemented:
//shape[0] = browser.createVrmlFromURL("cylinder.wrl",root,"addChildren");
shape[0] = browser.createVrmlFromString("Transform {\n" +
" children\n" +
" Shape {\n" +
" appearance Appearance {\n" +
" material Material {\n" +
" diffuseColor 0 1 1\n" +
" }\n" +
" }\n" +
" geometry Cylinder {}\n" +
" }\n" +
" translation 0 -4 0\n" +
"}\n");
shape[1] = browser.createVrmlFromString("Transform {\n" +
" children\n" +
" Shape {\n" +
" appearance Appearance {\n" +
" material Material {\n" +
" diffuseColor 1 1 .85\n" +
" }\n" +
" }\n" +
" geometry Cylinder {}\n" +
" }\n" +
" translation 0 4 0\n" +
"}\n");
Node[] scene = browser.createVrmlFromString("Transform {\n" +
" children\n" +
" Shape {\n" +
" appearance Appearance {\n" +
" material Material {\n" +
" diffuseColor 1 1 .85\n" +
" }\n" +
" }\n" +
" geometry Sphere {radius 2}\n" +
" }\n" +
" translation 0 0 0\n" +
"}\n");
}
catch (InvalidNodeException e) {
showStatus("PROBLEMS!: " + e + "\n");
error = true;
}
catch (InvalidEventInException e) {
showStatus("PROBLEMS!: " + e + "\n");
error = true;
}
catch (InvalidVrmlException e) {
showStatus("PROBLEMS!: " + e + "\n");
error = true;
}
if (error == false)
showStatus("Ok...\n");
}
public void callback(EventOut who, double when, Object which) {
Integer whichNum = (Integer) which;
if (whichNum.intValue() == 0) {
addChildren.setValue(shape[0]);
}
else if (whichNum.intValue() == 1) {
addChildren.setValue(shape[1]);
}
else if (whichNum.intValue() == 2) {
//NOTE: as of beta3 of CosmoPlayer 1.0 for Windows95, the loadURL method
//was not implemented:
//loadURL("cylinder.wrl","");
browser.replaceWorld(scene);
}
}
}
The Java source code contains a object and event declaration and an initialization
section that connects the Java variables to the VRML scene graph nodes and sets up
the callback. Then it defines three nodes: shape[0] is a cyan cylinder,
shape[1] is a yellow cylinder, and scene is another yellow cylinder
centered at the origin. The callback listens for any events generated by the user.
When the cube's sensor is activated, shape[0] is added to the VRML scene
graph with the addChildren() method. When the sphere's sensor is activated,
shape[1] is added to the VRML scene graph with the addChildren()
method. When the cone's sensor is activated, the current VRML scene graph is replaced
with the scene node through the replaceWorld() method of the browser
object. Although not used in this example, the removeChildren() method is
called in exactly the same way as the addChildren() method when you want
to remove a single node from the VRML scene graph.
Creating Reusable Behaviors
It would be problematic if you had to rewrite this code every time you wanted
to use it in another file. Although you could reuse the Java bytecodes, this means
that you would need identical copies of the script declaration every time you wanted
to use it. Redundancy is not a particularly nice practice from the software engineering
point of view, either. Eventually, you will be caught by the cut-and-paste error
of having extra pieces of ROUTE statements (and extra fields) floating around
that could accidentally be connected to nodes in the new scene, resulting in difficult-to-trace
bugs.
VRML 2 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 tradition,
provide interfaces to only 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 then add a behavior to a VRML file by using just 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 representing 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 the second until it gets a cab.
A taxi can have many attributes (such as speed and direction) that the user of
the cab need not really discriminate. 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, get
in, tell it where to go, pay the fare, and then get 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 shape 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 the following:
#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 have a control that your avatar
uses to query a nearby cab for its isAvailable field. (An avatar is the
virtual body used to represent you in the virtual world.) If the isAvailable
field 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's creator and users can always be sure of getting the latest version of
the taxi implementation and that the taxi will exhibit uniform behaviors regardless
of which world the users are in.
CommunityPlace's approach to the VRML/Java API is set up to take advantage of
using Java within prototyped intranode behaviors. CosmoPlayer, on the other hand,
leaves intranode behavior scripting to VRMLscript. You can take advantage of Java
class reusability directly within the CosmoPlayer VRML/Java API. For example, you
can create a bounce class in Java that many different types of VRML objects
could inherit in the Java source code itself. The bounce class would define
how to deform an object as it hits an obstacle and changes direction; the class would
be written only once, yet take advantage of full reusability.
The Future: VRML, Java, and AI
The information in this chapter has so far relied on static, predefined behaviors
available either within the original VRML file or retrievable from somewhere on the
Internet.
One exciting goal of VR worlds is to be able to create 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 VR world can take advantage of self-modifying
code.
Stephenson's Librarian from Snow Crash is just one example of how an independent
agent can act in a VR world. His model was very simple--a glorified version of today's
2D HTML-based search engine that, when requested, searched the U.S. Library of Congress
for information related to a desired topic (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 go the next step further--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. By combining VRMLscript and Java behaviors, you
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. Although doing so is probably not
feasible with current Pentium-class machines, the next generation of processors will
probably make it so.
Summary
With the tools presented in this chapter, you should be able to create whatever
you require of cyberspace. There is only so much you can do with a 2D screen in terms
of new information-presentation techniques. The third dimension of VRML enables you
to create experiences that are far beyond what you expect of today's Web pages. 3D
representation of data and VR behaviors programming is still very much in its infancy--so
much so that, at the time of this writing, only Sony's CommunityPlace, SGI's CosmoPlayer,
and Netscape's Live3D viewers were available for testing, and even then, many parts
of these viewers were not implemented. In fact, the Live3D API works only with VRML
1 and is quite different from what is suggested by the VRML 2 standard.
If you are serious about creating behaviors, you must learn VRML thoroughly. Many
little problems can catch the unwary, particularly the peculiarities of VRML syntax
when it comes to 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.
For a good reference on VRML 2, check out Teach Yourself VRML 2 in 21 Days, by Chris
Marrin and Bruce Campbell (published by Sams.net Publishing).
Whether you are 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 is catching.
|