All Categories :
VRML
Chapter 21
Using Java to Add Behaviors to VRML
-by Justin Couch
CONTENTS
In the previous chapter, you were introduced to scripting and
the basics of JavaScript. Once you start playing with it, you'll
realize some of its limitations. It's fine for doing basic mathematical
work and input response, but it can't do any of the more juicy
bits you need for doing really interesting things. So you need
to use something that's designed for the task: Enter Java.
The VRML 2.0 specification deliberately did not require support
for any one language for VRML scripting. At the time of Draft
3's release, there were two language bindings for the VRML API:
JavaScript and Java. If you've made it this far into the chapter,
then I expect you either already know Java or have enough interest
that you want to learn it. Compared to the rest of the book, this
chapter will really start to dig into the heart of VRML. Java
isn't a scripting language, so you really want to know the effects
of your code on what the world does.
You'll start by taking up where the last chapter left off and
cover the following topics:
- First, you'll see how to create scripts in Java by transforming
the example from the previous chapter into Java.
- Once you have the basics down, then you'll see where you should
use Java and where you should JavaScript, examining their strengths
and weaknesses.
- The final section looks at using other Java classes to produce
exciting, dynamic, moving worlds.
The first thing that's assumed here is that you're already set
up to use Java. You have either the JDK from Sun Microsystems
(http://www.javasoft.com/)
or you have another implementation that comes with a Java compiler,
like Borland C++ 5.0 or Symantec Cafe.
- You need to get hold of the Java classes for VRML; they're
found with your browser. Start with Sony's CyberPassage. You've
been using CosmoPlayer for most of the examples, but it doesn't
support Java for the scripting.
- Add the directory path to the VRML class files to the CLASSPATH
variable so that the Java compiler can find them. Assuming you
have installed it in the directory C:\Program Files\Cyberpassage,
then you need to add C:\Program Files\Cyberpassage\vrml to CLASSPATH.
Now you should be able to compile your Java scripts with no problems.
Caution |
Early versions of CyberPassage put classes in a PKZipped file called classes.zip. This required you to create a directory called vrml and unzip the class files into that directory for it to work. Check the information with the latest version of the software.
|
- Start to write and compile your Java scripts in your favorite
environment.
The difference between Java and JavaScript is that you can't write
Java source code directly in the file, which means you must compile
every single script, too. Compared to JavaScript, this does slow
down the development cycle a bit, but the end result is scripts
that execute much faster than the interpreted JavaScript. Each
class will be a separate file to be downloaded, but if you're
using the same script many times in the world, then it needs to
be downloaded only once.
Assuming you understood what was happening in the scripting in
the last chapter, you'll now convert it to Java. To start with,
you need to change the url to point to the Java class file. The
VRML specification says only that the Java byte code needs to
be supported. In this case, you'll call the source file replace_script.java
to match with the name used in the DEF
keyword.
The vrml Packages
In VRML, Java does not run as an applet; it runs as a special
Java class called Script. Script is a class that's extended for
the individual scripts. VRML-specific Java classes are available
in the vrml packages, which have three main parts: vrml, vrml.node,
and vrml.field. There's also a class specifically for the browser,
which is in the vrml package.
The Field class is empty so that individual classes can be created
that mimic the VRML field types. There are two types of Field
classes: read-only and unlimited access. The read-only versions
start with Const<fieldtype>,
and the unlimited access versions have the same name as the field
type. The types returned by these classes are standard Java types,
with a few exceptions. MF types return an array of that type,
so the call to the getValue()
method of an MFString would
return an array of type String.
The basic outline of a Field type class is demonstrated by the
MFString class:
public class MFString extends MField
{
public MFString(String s[]);
public void getValue(String s[]);
public void setValue(String s[]);
public void setValue(int size, String s[]);
public void setValue(ConstMFString s);
public String get1Value(int index);
public void set1Value(int index, String s);
public void set1Value(int index, ConstSFString s);
public void set1Value(int index, SFString s);
public void addValue(String s);
public void addValue(ConstSFString s);
public void addValue(SFString s);
public void insertValue(int index, String s);
public void insertValue(int index, ConstSFString s);
public void insertValue(int index, SFString s);
}
The method names are pretty straightforward. You can set values
using both the standard VRML type as well as the read-only field
value, which comes in handy when you're setting values based on
the arguments presented.
The other half of the vrml Java API is the Script itself. Script
is based on the Node interface, which is defined only for VRML
scripts. This interface serves as the basis for representing the
individual nodes. VRML 2.0 defines Script class as the only implementation
of the Node interface, which is shown below:
public abstract class Script extends BaseNode {
// This method is called before any event is generated
public void initialize();
// Get a Field by name.
// Throws an InvalidFieldException if fieldName isn't a valid
// event in name for a node of this type.
protected final Field getField(String fieldName);
// Get an EventOut by name.
// Throws an InvalidEventOutException if eventOutName isn't a valid
// event out name for a node of this type.
protected final Field getEventOut(String fieldName);
// processEvents() is called automatically when the script receives
// some set of events. It should not be called directly except by its subclass.
// count indicates the number of events delivered.
public void processEvents(int count, Event events[]);
// processEvent() is called automatically when the script receives
// an event.
public void processEvent(Event event);
// eventsProcessed() is called after every invocation of processEvents().
public void eventsProcessed()
// shutdown() is called when this Script node is deleted.
public void shutdown();
}
Every script is a subclass of the Script class. However, you can't
just go out and write your own script now. You need some more
introduction to how it works.
- Since you must use the vrml packages to write scripts, then
you have to tell the compiler to use it by importing it. Most
of the time, you'll use all the different areas of the VRML interface,
so you will need to import all three packages. Start the class
by declaring its name and what classes it uses:
import vrml.*;
import vrml.field.*;
import vrml.node.*;
class replace_script extends Script {}
- Next, create the initialize()
method and use the getField()
method from the Script class definition to get the variables belonging
to the VRML Script node into Java. To do this, simply pass the
method a text string with the name of the field you want. Because
getField() returns a Field,
you need to cast it to be the correct field type so you can call
the right methods. For example, use the following to get the strings
corresponding to the external file:
class myscript extends Script {
private MFString externalFile;
void initialize(void)
{
externalFile = (MFString)getField("externalFile");
}
}
By convention, all the script internal variables are declared
private. You could make them public, but there's no point because
nothing accesses them from outside. These internal variables can
be named whatever you want, but they usually have the same name
as the corresponding VRML field.
Tip |
The getField() method is used only for retrieving the field type values from the script. To get the eventOut type values, use the getEventOut() method in the same way, remembering to cast the returned value. In JavaScript you could just assign a value to that eventOut reference to generate the event. You can do the same in Java, once you have retrieved the eventOut reference.
|
- This leaves the eventIns. Now the Java model varies radically
from the JavaScript one. In JavaScript, each eventIn in Script's
VRML declaration requires a corresponding method. Java uses a
different approach that has a single method taking a list of the
current events to be processed; then you call the appropriate
internal methods to deal with the information. If you have done
any programming with the AWT package, then you should be fairly
used to this approach. In the next section, you'll take a look
at Java event handling.
Caution |
In early drafts of the Java API, it was designed so that you wrote direct eventIn methods, just like the JavaScript way of doing things. Beta 1 and 2 versions of Sony's CyberPassage version 2.0 implemented this early form, and several early books on VRML 2.0 outlined this previous version of the Java API. This version is no longer current, however.
The change was made so that browsers could be written purely in Java. With the old draft versions, this wasn't possible.
|
The previous section mentioned that the event-handling mechanism
for Java scripts uses a single function as the interface point.
To deal with events in a script, you need to take a few steps
to get there:
- Declare the initialize()
method, and set the internal values using the getField()
method. (This is the same as Step 2 in the previous task section.)
- Create a series of private methods that you want to call for
processing individual events.
- Create one of the event-handling functions and use it to call
the internal methods created in Step 1.
Events are passed to the script's event handler as an Event object
type:
class Event {
public String getName();
public ConstField getValue();
public double getTimeStamp();
}
As with all Java programming, if you want these methods to be
called from outside, you must declare them public. An event coming
into the script is like an external class calling methods from
your class. In the Java API, event processing is done by one of
two methods. The processEvents()
method is used when there's more than one event generated at a
particular timestamp, but the processEvent()
method is called when there's only one event to be taken care
of at that time.
The processEvents() method
takes an array of event objects that you then analyze and pass
to the various methods. This is no different from the way the
AWT event-handling system works. A typical segment of code is
demonstrated in Listing 21.1.
To see what a complete Java script source file looks like, declare
just the isOver method for
the moment so you can see the difference between the Java and
JavaScript ways of handling the incoming events.
Listing 21.1. Converted version of one of the eventIn handlers
from JavaScript to Java.
Import
vrml.*;
import vrml.field.*;
import vrml.node.*;
class replace_script extends Script
{
// now we get all the class variables
private SFBool pointerOver;
//initialization
public void initialize(void)
{
pointerOver = (SFBool)getField("pointerOver");
}
// now the eventIn declarations - only do the isClicked event for now
private void isOver(ConstSFBool value)
{
if(value.getValue() == false)
pointerOver.setValue(false);
else
pointerOver.setValue(true);
}
// now the event handling function
public void processEvents(int count, Event events[])
{
int i;
for(i=0; i < count; I++)
{
if (events[i].getName().equals("isOver"))
isOver(events[i].getValue());
// collection of other else if statements here
}
}
The second event handler method is processEvent();
since it deals with just a single event, the argument is only
a single Event object. Therefore,
the only difference between this method and the processEvents()
method is that you don't need the for
loop. The big if...else ladder
of string comparisons remains, though.
When should you use the different event-handling functions? Take
the following piece of VRML code as an example (this comes straight
from the VRML specification):
Transform {
children [
DEF TS TouchSensor {}
Shape { geometry Cone {} }
]
}
DEF SC Script {
url "Example.class"
eventIn SFBool isActive
eventIn SFTime touchTime
}
ROUTE TS.isActive TO SC.isActive
ROUTE TS.touchTime TO SC.touchTime
Whenever the TouchSensor is touched, it generates two simultaneous
events, so the script receives two. In this case, you need the
processEvents() method that
deals with a number of simultaneous events. If you were interested
only in the isActive event,
then you could use just the processEvent()
method.
If you're not sure whether the script will receive more than one
simultaneous event, then you can declare both methods. To save
duplicating large amounts of code, you can put all the code to
call the internal methods in the processEvent()
method and just put a for
loop that calls processEvent()
with each individual event object in processEvents().
If this has confused you, then have a look at the following code
fragment:
void public processEvent(Event e)
{
if(e.getValue().equals("someEvent"))
// call internal method
else if ......
}
void public processEvents(int count, Event events[])
{
int i;
for (i=0; i < count; i++)
processEvent(events[i]);
}
Notice that a bit more work needs to be done to get an initial
Java class file running. One advantage is that you declare only
the fields you need to use. In Listing 21.1 you just wanted to
use the pointerOver field from the VRML definition, so you left
the rest out. The Java code is compiled independently of VRML
source code, allowing you to take a staged approach to developing
the code, adding variables and event handlers only when they're
needed.
The Browser API defined in the previous chapter is also applicable
to Java, but naturally there are Java methods for accessing the
browser functionality. These methods can be accessed through the
Browser class, so the syntax is almost the same as that in the
previous chapter. The only thing that differs is what the various
VRML types are in Java.
VRML types relate back to standard Java in the following manner
(MF versions are just arrays of the SF type):
Table 21.1. Relationship of VRML types to Java types.
VRML Type | Java Type
|
SFString
| String
|
SFFloat
| float
|
SFInt32
| int |
MSString
| String[]
|
MFNode
| Node[]
|
Look again at the example of loading a world on demand and the
newNodesReady() method that
it called in Listing 21.2. You'll create the strings by using
the Java String type.
Listing 21.2. Using the Browser class under Java.
public void newNodesReady(ConstMFNode node_list)
{
String shape = "Shape { appearance Appearance {} }";
String material = "Material { emissiveColor .2 .2 .2 }";
String box = "Box { size 0.5 0.5 0.5 }";
String sensor = "TouchSensor {}";
Node shape_node;
Node sensor_node;
// create some nodes
shape_node = Browser.createVrmlFromString(shape);
sensor_node = Browser.createVrmlFromString(sensor);
// assign some properties
shape_node.postEventIn("material",
(Field)Browser.createVrmlFromString(material));
shape_node.postEventIn("geometry",
(Field)Browser.createVrmlFromString(box));
// update the internal field with the newly created
// list of nodes
newWorld.setValue(node_list.getValue());
// now add the nodes to the scene
secondObject.getValue().postEventIn("add_children",
(Field)shape_node);
secondObject.getValue().postEventIn("add_children",
(Field)sensor_node);
// finally add routes between the newly formed
// touchsensor and the inputs to this script
Browser.addRoute(sensor_node, "isActive",
this, "isClicked_new");
Browser.addRoute(sensor_node, "isOver",
this, "isOver_new");
}
Java has a completely different structure. First, there's no way
to directly assign values to the nodes' fields, as there was in
JavaScript. If the node contains ordinary field types, then you must set them in the text string before you actually
create the node; otherwise, you won't have access to it again.
To write values to the fields that are eventIns or exposedFields,
you must post an event to that field. You may remember from the
last chapter that exposedFields can act just like eventIns and
eventOuts combined. In the case of Java scripts, this is exactly
how you must treat them.
Notice how much more object-oriented Java is than the JavaScript
scripting; you can be sure Java is passing the correct types to
the different nodes. There's more work involved to get the script
up and running, but once you do, you can explore many other goodies.
The Beginning and the End
When you first add a behavior to a world, you might need to initialize
some internal values. VRML allows the normal method of using the
constructor method to perform any initialization that needs to
be done internally.
The problem is that at the time the constructor is called, you
probably won't have all the external access to the world enabled;
even the values of the fields may not be valid yet because the
world is still loading. To overcome this, the initialize()
method was created. This function is, by default, empty, but can
be overridden. The initialize()
method is guaranteed to be called just after the entire world
is created but before any external events are generated. It's
called only once, and when it is, you know that you can read valid
values for each of the script fields (using the getField()
method) and can send events to other parts of the scene graph.
This means everything in your world should function normally.
In the next section, you'll look at creating multithreaded scripts.
When a node is about to be removed, you should clean up any extras
that have been left lying around. The shutdown()
method is a predefined method for all Script nodes that's called
just before your script is removed. In this method, you place
any cleanup code you need, such as killing threads you might have
created.
JavaScript is fairly limited in what it can do. Besides some basic
math and fetching files with http calls, you can't do much else.
Basically, you can use it to write quick code to fulfill short-term
needs. When you want to do something fairly complex with your
world, then you need to switch to Java.
A good place to use Java is when you want to feed live data to
the world. Java scripts can make use of any of the standard Java
classes, which means the networking and threads classes are available.
One typical use would be an external server sending data to your
scene, which you then represent as 3D objects. The next section
will outline how to do this.
So where do you use Java and where do you use JavaScript? It depends
on what you want to do. Unfortunately, at this stage of the game,
you're limited more by the browser than anything else. In time,
you can expect to see browsers that support both Java and JavaScript,
as well as other languages. For the moment, just put on rose-colored
glasses and assume an ideal world exists.
JavaScript is very handy for doing short little tasks when the
compile-test cycle is too much effort. A good example is the toggle
type of switch created in the previous chapter. Creating and compiling
code for a three-line script isn't worth the trouble.
When you need greater flexibility and speed, then you should be
using Java. The precom-piled code combined with JIT compilers
should make the code execution much faster, particularly in large
complex worlds with many behaviors running continuously.
Developing a full networked and threaded example is beyond the
scope of this book, but I can give you some outlines on how to
go about it. A typical example is the stock market ticker that
does a real-time display in 3D of your favorite stocks. At a stock
market site, a server process would broadcast the information
to whoever requests it. At the client end (your VRML world), you
need to establish a link to receive the information.
The problem is that you want to keep the time spent in the script
to an absolute minimum; also, the timing of when the script will
be called is completely out of your control. The script may not
be called at the same time that data is ready to be read from
the network. What do you do? Well, since I have just been talking
about threads, that should trigger something. Of course-you set
up an independent thread containing all the networking code and
then just report back to the world when there's more information
to update.
When you first enter the world, the tracking should start. To
do this, you take advantage of the creation method and start up
the thread there. The thread code then starts up and installs
any code it needs to monitor the network connection to the server.
Such tasks as establishing the initial connection would all be
handled in this separate task, so that the script can return to
the browser as quickly as possible.
Having a separate thread that listens for new data is fine, but
this thread needs to get information back to the VRML world. There
are two ways to do that; in each method, you'll pass an instance
of the Script node to the thread. First, you could just create
another method that isn't an eventIn method, then call that method.
Second, you could post events to that script just as any other
node or script would. Using non-eventIn methods is questionable
at the moment because there are no rules as to whether it's permitted.
The safe bet is to post events back to the script so that they
get dispatched and looked after in the same manner as all the
other events.
Java scripting issues are not as complete as the JavaScript interface;
for instance, there are no classes to represent the individual
node types that exist in VRML. During the writing of the VRML
2.0 specification, there wasn't enough time to define these classes,
so they were left out.
Many things, because of incompleteness, were left out of the specification,
but will be left to the revised version 2.1 of the specification.
There was a whole section on an external API that allowed outside
programs to talk directly with the browser, and also the VRML
world itself, that was left out. The external API was left out
completely; it was not just the Java version.
This is a limited look at what can be done with Java. Because
Java is a full programming language, the scope for experimentation
is wide, and the possibilities are endless. Keep in mind that
you should be careful to not put too much code into the scripts.
Every millisecond spent in the script is less time spent actually
drawing the world onscreen. Make good use of threads where needed,
and keep the scripts small.
One tool that hasn't been covered is the Liquid Reality toolkit
from DimensionX. It has a complete set of classes that mimic the
VRML nodes. It doesn't create a link for the scripts to interface
with a particular browser; instead, you basically create your
own browser by defining the nodes and their relationships, which
are then drawn to the screen within the Java code itself. How
Liquid Reality works, though, is more of a programming issue rather
than a VRML topic, so it wasn't covered in depth in this book.
Next Steps
So what's left to do?
- In Chapter 22, "Adding Interactivity:
The Future of VRML," you'll do a bit of crystal-ball gazing
to see what's just around the corner for VRML. This chapter examines
some of the higher level issues, like what you can and can't do
with VRML and multi-user topics.
- In Chapter 23, "Real-Life Examples:
A 3D Gallery: An Advanced VRML World," you finish developing
the VRML gallery you started in Chapter 17.
This chapter combines everything you've learned and adds a few
extras for that touch of class.
Q&A
Q: | What if I don't want to write my own Java behaviors but just view worlds that have behaviors? What do I need to have?
|
A: | You still need to use Sony's CyberPassage to view them. No doubt there will probably be a few more browsers to add to the list as more companies release their offerings. You don't need to set the CLASSPATH variable, however, to use the Java behaviors.
|
Q: | After compiling my code, it runs fine when I load it from the local drive. However, when I run it from the Web server, it doesn't work at all.
|
A: | There may be two answers to your problem. First, the Web server might not be configured to handle the .class files. If so, then get your Web server administrator to add the following MIME type:
application/octet-stream.class
Second, CyberPassage has the same security rules as Sun's HotJava browser. It's possible you don't have the correct permissions set up to allow your browser to download the Java binaries from the Web server.
|