by Bernie Roehl
This chapter examines the relationship between Java and VRML, the Virtual Reality Modeling Language.
VRML is a large and powerful language, and there'll be several places throughout this chapter where a detailed description of some VRML feature is beyond the scope of this book. The best place to look for more detailed information is the VRML Repository (http://sdsc.edu/vrml). That's also the place to find VRML browsers such as Sony's Community Place or Dimension X's Liquid Reality that will be needed to run the examples in this chapter.
To run the examples in this chapter, you'll need a VRML browser that supports the final 2.0 version of the specification. If you're using a recent enough version of Netscape or Internet Explorer, they'll already have VRML 2.0 support built in.
VRML is the Virtual Reality Modeling Language, the standard file format for creating 3-D graphics on the World Wide Web. VRML files are stored on ordinary Web servers and are transferred using HTTP.
VRML files have a MIME type of x-world/x-vrml (although this is expected to change to model/vrml in the near future) and have an extension of .Wrl. A three-character extension is used to avoid the confusion which might be caused by PC-based servers that truncate extensions at three characters (as happened with .Htm versus .Html).
When a user retrieves a VRML file (by clicking a link in an HTML document, for example), it's transferred onto their machine and a VRML browser is invoked. In most cases, the VRML browser is implemented as a plug-in. Once the scene has been loaded, the VRML browser enables the user to travel through it without the server having to transfer any more data.
Starting with Netscape Navigator 3.0, VRML support is included as part of the standard distribution so VRML will soon be on many desktops.
To understand how VRML interacts with Java, it's necessary to
have a basic understanding of VRML.
| NOTE |
As this book goes to press, the final version of the VRML 2.0 specification has just been released. VRML browsers compliant with version 2.0 are not yet available. Please note that many of the examples towards the end of this chapter (in particular, the Towers of Hanoi) have only been verified to work with early beta versions of the Sony CyberPassage browser (recently renamed "Community Place"). Be sure to refer to the full VRML specification for definitive information about the language. The details for locating the specification on the Web are found at the end of this chapter. N |
A VRML file describes a three-dimensional scene. The basic VRML data structure is an inverted tree composed of "nodes," as shown in Figure 55.1.
Figure 55.1 : The basic VRML scene structure resembles an inverted tree.
There are two basic types of nodes-Leaf nodes and Grouping nodes. Each Grouping node can contain Leaf nodes and additional Grouping nodes.
Leaf nodes generally correspond to three-dimensional shapes, sounds, lights, and so forth. A table or chair might be represented by a Shape node and the ticking of a clock by a Sound node, and a scene is made visible using one or more lighting nodes.
Grouping nodes, on the other hand, are completely invisible. You can't see a Grouping node when you view a VRML world, but it's there and it has an effect on the positioning and visibility of the Leaf nodes below it in the tree. The most common type of Grouping node is a Transform, which is used to position shapes, sounds, and lights in the virtual world.
A node that has another node attached to it is referred to as a parent. Nodes that are attached to the parent are referred to as the children of that node, and nodes sharing a common parent are siblings to each other. Note that in VRML 2.0, the order of children is generally irrelevant because sibling nodes don't affect each other the way they did in VRML 1.0. However, the ordering of children is still important in certain types of Grouping nodes such as Switch or LOD, which are beyond the scope of this chapter.
There are also nodes that are not really "in" the tree structure, although they're stored there for the sake of convenience. Among these nodes is the Script node, which provides the connection between VRML and Java. We'll be looking at the Script node in detail later in this chapter.
There are a number of different types of nodes in VRML 2.0, and it's possible to define new nodes using the "prototype" mechanism. Each of these nodes does something specific; fortunately, you don't have to learn very many of them in order to start building simple VRML worlds.
Each type of node has a set of fields that contain values. For example, a lighting node would have a field that specifies the intensity of the light. If you change the value of that field, the brightness of the light changes accordingly. That's the essence of what behavior in VRML is all about: changing the values of fields in nodes.
VRML files use the Unicode character set (described elsewhere
in this book)and are readable by users. That means you can print
them out, modify them with a text editor, and so forth.
| NOTE |
IBM, Apple, and Paragraph International recently announced that they are working together on a binary format for VRML. The format would make use of IBM's advanced geometry compression algorithms in order to drastically reduce the size of a VRML file and the time it takes to download it. Check the VRML Repository (http://sdsc.edu/vrml) for current information about the binary format. The binary format will be fully compatible with the ASCII format, so you shouldn't need to know much about it in order to make use of VRML. |
Everything after a # on any line of a VRML file is treated as a comment and ignored. The only exception is when a # appears inside a quoted string. The # works just like // in a Java program.
The first line of every VRML 2.0 file is a special comment that looks like the following:
#VRML V2.0 utf8
The V2.0 means that this file conforms to version 2.0 of the VRML specification. The utf8 refers to the character set encoding.
As previously described, the rest of the file consists mostly of nodes. Each node contains a number of fields that contain the node's data, and each field has a specific type. For example, Listing 55.1 shows a typical PointLight node.
Listing 55.1 A Typical PointLight Node
PointLight
{
on TRUE
intensity 0.75
location 10 -12 7.5
color 0.5 0.5 0
}
This node contains four fields. The fact that they're on separate lines is irrelevant; VRML is completely free-format and anywhere a space appears, you can also have a tab or a newline. We could just as easily have said
PointLight { on TRUE intensity 0.75 location 10 -12 7.5 color 0.5 0.5 0 }
but it would have been harder to read.
The word PointLight indicates what type of node this is. The words on, intensity, location, and color are field names, and each is followed by a value. Notice that the values are different for each field; the on field is a boolean value (called an SFBool in VRML), and in this case it has the value TRUE. The intensity field is a floating-point number (an SFFloat in VRML terminology). The location is a vector, a set of x, y, and z values (called an SFVec3f in VRML), and the color is an SFColor containing the red, green, and blue components of the light.
In other words, the point light source is turned on, at 75% of its maximum intensity. It's located at 10 meters along the positive x-axis (right), 12 meters along the negative y-axis (down), and 7.5 meters along the positive z-axis (towards us). It's a reddish-green color because the red and green values are each at 50% of their maximum value and the blue value is set to zero.
Note that any fields that aren't given for a particular type of node will have default values assigned to them, as described in the VRML specification. For example, we could have left out the on TRUE because the on field has TRUE as its default value.
You can assign a name to a node using the DEF (for "define") syntax. For example,
DEF Fizzbin PointLight { intensity 0.5 }
would create a PointLight and assign it the name Fizzbin. We'll see later how these names get used.
VRML supports a number of different types of fields, many of which
correspond to data types in Java. The following table shows the
correspondence between Java types and VRML types.
| Java Type | VRML Type |
| boolean | SFBool |
| float | SFFloat |
| int | SFInt32 |
| String | SFString |
As previously mentioned, there are also special data types for 3-D vectors (SFVec3f), colors (SFColor), and rotations (SFRotation). There are also 2-D vectors (SFVec2f). There's a special data type that's used for time (SFTime) and one for bitmapped images (SFImage).
In addition to these single-valued fields, which is what the "SF" prefix stands for, there are multiple-valued versions of most of them, which begin with "MF." These multiple-valued fields are arrays of values; for example, an array of vectors would be an MFVec3f. If more than one value is specified for a particular field, the values are surrounded by square brackets, like the following:
point [ 0 0 0, 1.3 2.57 -14, 12 17 4.2 ]
There's one other useful field type, SFNode, which allows fields to have a node as their value. There's also an MFNode for fields whose value is an array of nodes.
The complete list of VRML 2.0 field types is shown in Table 55.1.
| Field Type Name | Type of Data |
| SFBool | TRUE or FALSE value |
| SFInt32 | 32-bit integer value |
| SFFloat | Floating-point number |
| SFString | A character string in double quotes |
| SFTime | A floating-point number giving the time in seconds |
| SFVec2f | A two-element vector (used for texture map coordinates) |
| SFVec3f | A three-element vector (locations, vertices, and so on) |
| SFRotation | Four numbers: a three-element vector plus an angle |
| SFColor | Three numbers: the Red, Green, and Blue components |
| SFImage | A bitmapped image |
| SFNode | A node |
| MFInt32 | An array of 32-bit integers |
| MFFloat | An array of floating-point numbers |
| MFString | An array of double-quoted strings |
| MFVec2f | An array of two-element vectors |
| MFVec3f | An array of three-element vectors |
| MFRotation | An array of four-element rotations |
| MFColor | An array of colors |
| MFNode | An array of nodes |
Figure 55.2 illustrates the Cartesian coordinate system used by VRML.
Figure 55.2 : The coordinate system used by VRML.
The basic transformations that are used in VRML include translation, scaling, and rotation.
Every point in 3-D space can be specified using three numbers that correspond to the coordinates along the x-, y-, and z-axes. In VRML, distances are always represented in meters; a meter is about three feet. If a particular point in a VRML world is at (15.3, 27.2, -4.2), then it's 15.3 meters along the x-axis, 27.2 meters along the y-axis, and 4.2 meters backwards along the z-axis. This is illustrated in Figure 55.3.
Moving a point in space is referred to as translation. This is one of the three basic operations you can perform with a Transform node; the other two are scaling and rotation.
Scaling means changing the size of an object. Just as you can translate objects along the x-, y-, and z-axes, you can also scale them along each of those axes. Figure 55.4 shows a sphere as it might appear in a VRML browser.
Figure 55.4 : A sphere in VRML looks like this.
Figure 55.5 shows the same sphere scaled by a factor of 2 in the y direction and a factor of 0.5 in the x direction.
Figure 55.5 : A sphere scaled by (0.5, 2, 1).
Scaling is always represented by three numbers, which are the amount to stretch the object along the x-, y-, and z-axes, respectively. A value greater than 1.0 makes the object larger along that axis, and a value less than 1.0 makes it smaller. If you don't want to stretch or shrink an object along a particular axis, use a factor of 1.0 (as we did for the z-axis in our sphere example).
Rotation is more complex than scaling or translation. Rotation always takes place around an axis, but the axis doesn't have to be aligned with one of the axes of the coordinate system. Any arbitrary vector pointing in any direction can be the axis of rotation, and the angle is the amount to rotate the object around that axis. The angle is measured in radians. Since there are 3.14159 radians in 180¡, you convert degrees to radians by multiplying by 3.14159/180 or about 0.01745.
Translation, rotation, and scaling are all transformations. VRML stores these transformations in a type of node called a Transform. A single Transform can store a translation, a rotation, a scaling operation, or any combination of the three. That is, a Transform node can either scale the nodes below it in the tree, rotate them, or translate them, or any combination of these. The sequence of operations is always the same: The objects in the subtree are first scaled, then rotated, and finally translated to their new location. For example, a typical Transform node is shown in Listing 55.2.
Listing 55.2 A Typical Transform Node
Transform
{
scale 1 2 3
rotation 0 1 0 0.7854
translation 10 0.5 -72.1
children
[
PointLight { }
Shape { geometry Sphere { } }
]
}
This particular Transform node has four fields: scale, translation, rotation, and children. The scale and translation fields are vectors (SFVec3f), and the rotation is an SFRotation that consists of a three-element vector and a floating-point rotation in radians.
Because Transform is a grouping node, it has children stored in its children field. The children are themselves nodes, in this case, a point light source and a shape whose geometry is a sphere (more about these things later). Both the light and the shape have their location, orientation, and scale set by the fields of the Transform. For example, the sphere is scaled by (1, 2, 3), then rotated by 0.7854 radians around the y-axis (0, 1, 0), and translated 10 meters along x, half a meter along y, and negative 72.1 meters along z.
The full Transform node is actually more complex than this because it can specify a center of rotation and an axis for scaling. Please note that these features are beyond the scope of this chapter. There's also a version of Transform called Group, which simply groups nodes together without performing any transformations on them.
Each Transform node defines a new coordinate system, or frame of reference. The scaling, rotation, and translation are all relative to the parent coordinate system (see Figure 55.6).
Figure 55.6 : Transformations and coordinate systems are essential concepts in VRML.
A typical VRML world has a number of different coordinate systems within it. There's a world coordinate system and a coordinate system for each Transform node in the world. To understand how this works, take a look at Figure 55.7.
Figure 55.7 : The transformation hierarchy for a pool table looks like this.
The top-level Transform node is used to position the pool table itself in the world coordinate system. This might involve scaling the table, rotating it to a different orientation, and trans-lating it to a suitable location. Each of the balls on the table has its own Transform node to position the ball on the table. Each ball, therefore, has its own little coordinate system embedded within the coordinate system of the pool table. As the balls move, they move relative to the table's frame of reference. Similarly, the table's coordinate system is embedded within the coordinate system of the room.
Each of these coordinate systems has its own origin. The coordinate system for each ball might have its origin at the geometric center of the ball. The coordinate system of the table might have its origin at the geometric center of the table. The coordinate system of the room might have its origin in the corner near the door. The Transform nodes define the relationship between these coordinate systems. This transformation hierarchy, as it would appear in a VRML file, is shown in Listing 55.3.
Listing 55.3 A Pool Table
#VRML V2.0 utf8
DirectionalLight { direction -1 -1 -1 }
DirectionalLight { direction 1 1 1 }
Transform {
translation 5 1 2 # location of pool table in room
children [
Shape { # Pool table
appearance Appearance {
material Material { diffuseColor 0 1 0 }
}
geometry Box { size 6 0.1 4 }
}
Transform {
translation 0 0.35 0.75
children [
Shape {
appearance Appearance {
material Material { diffuseColor 1 0 0 }
}
geometry Sphere { radius 0.3 }
}
]
}
Transform {
translation 1.5 0.35 0
children [
Shape {
appearance Appearance {
material Material { diffuseColor 0 0 1 }
}
geometry Sphere { radius 0.3 }
}
]
}
Transform {
translation -0.9 0.35 0.45
children [
Shape {
appearance Appearance {
material Material { diffuseColor 1 0 1 }
}
geometry Sphere { radius 0.3 }
}
]
}
]
}
Notice that there are Transform nodes in the children field of another Transform node; this is how the transformation hierarchy is represented.
The Shape node is used to create visible objects; everything you see in a VRML scene is created with a Shape node.
The Shape node has only two fields: geometry and appearance. The geometry field specifies the geometric description of the object, while the appearance field gives its surface properties. Listing 55.4 shows a typical Shape node.
Listing 55.4 A Typical Shape Node
Shape
{
geometry Sphere { radius 2 }
appearance Appearance { material Material { diffuseColor 1 0 0 } }
}
This example creates a red sphere with a radius of two meters. The geometry field has a type of SFNode and, in this case, it has a Sphere node as its value. The Sphere has a radius field with a value of 2.0 meters.
The appearance field can only take one type of node as its value: an Appearance node. The Appearance node has several fields. One field, the material field, is illustrated here. The material field can only take a Material node as its value. These appearance Appearance and material Material sequences may seem very odd and redundant, but as we'll see later, they actually turn out to be useful. The other fields of the Appearance node allow us to specify a texture map to use for the shape, which includes information about how the texture map should be scaled, rotated, and translated. Later, we'll be looking at the Appearance node in more detail.
The Material node specifies only one field in this example: the diffuseColor of the sphere. In this case, it has a red component of 1.0 and a value of 0.0 for each of the green and blue components. As we'll see later, the Material node can also specify the shininess, transparency, and other surface properties for the shape.
There are ten geometric nodes in VRML. Four of them are straightforward: Sphere, Cone, Cylinder, and Box. There's also a Text node that creates large text in a variety of fonts and styles, an ElevationGrid node that's handy for terrain, and an Extrusion node that allows surfaces of extrusion or revolution to be created. Finally, the PointSet, IndexedLineSet, and IndexedFaceSet nodes let you get right down to the point, line, and polygon level.
Sphere, Cone, Cylinder, and Box The Sphere node has a radius field that gives the size of the sphere in meters. Remember that this is a radius, not a diameter; the default 1.0 value produces a sphere that's two meters in diameter.
A Cone has a bottomRadius field that gives the radius of the base of the cone. It also has a height and a pair of flags (side and bottom) that indicate whether the sides or bottom should be visible.
Like the Cone, the Cylinder node has fields that indicate which parts are visible: bottom, side, and top. It also has a height and a radius.
The Box node is simple: It just has a size field, which is a three-element vector (an SFVec3f) that gives the x-, y-, and z-dimensions of the box.
Figure 55.8 shows these four basic geometric primitives.
Figure 55.8 : The Sphere, Cone, Cylinder, and Box nodes are the simplest geometric primitives.
ElevationGrid, Extrusion, and Text The ElevationGrid node is useful for creating terrain. It stores an array of heights (y-values) that you can use to generate a polygonal representation of the landscape. This is sometimes referred to as a heightfield.
The Extrusion node takes a 2-D cross-section and extrudes it along an open or closed path to form a 3-D shape.
The Text node creates flat, 2-D text that can be positioned and oriented in the 3-D world.
Figure 55.9 shows the Text node in action.
Figure 55.9 : The Extrusion, ElevationGrid, and Text nodes are very useful.
Points, Lines, and Faces The PointSet node is useful for creating a cloud of individual points, and the IndexedLineSet node is handy for creating geometry that consists entirely of line segments.
The IndexedFaceSet node allows you to specify any arbitrary shape by listing the vertices of which it's composed and the faces (also called "polygons") that join the vertices together. Figure 55.10 shows an object made from an IndexedFaceSet.
Figure 55.10 : An IndexedFaceSet allows arbitrary geometric shapes to be created.
The Appearance node has three fields and is only found in the appearance field of a Shape node. One field is used to specify a material for the shape, a second provides a texture map, and the third gives texture transform information.
Listing 55.5 makes this clearer.
Listing 55.5 The Appearance Node in Action
#VRML V2.0 utf8
DirectionalLight { direction -1 -1 -1 }
DirectionalLight { direction 1 -1 -1 }
DirectionalLight { direction 0 0 -1 }
Shape {
geometry Sphere { }
appearance Appearance {
material Material {
diffuseColor 0 0 0.9
shininess 0.8
transparency 0.6
}
texture ImageTexture {
url "brick.bmp"
}
textureTransform TextureTransform { scale 5 3 }
}
}
This creates a shiny blue sphere that is partially transparent. It applies a brick texture, loaded from a BMP file out on the Web, to the surface of the sphere. The two-dimensional texture coordinates are scaled up, which makes the texture smaller, and it gets repeated (or tiled) across the surface as needed. The finished sphere is shown in Figure 55.11.
Figure 55.11 : A texture-mapped sphere looks more realistic.
In addition to the diffuseColor, shininess, and transparency, a Material node specifies the emissiveColor for objects that appear to glow, the specularColor for objects that have a metallic highlight, and an ambientIntensity factor, which indicates what fraction of the scene's ambient light should be reflected.
The example above shows an ImageTexture that loads the texture from an image map (in this case, a Windows BMP file). Another alternative would be to use a MovieTexture node to specify an MPEG file that produces an animated texture on the surface. You could also use a PixelTexture node to generate the texture map with Java. Generating texture maps is beyond the scope of this chapter.
The TextureTransform node enables you to scale the texture coordinates, shift them, and rotate them. It's like a 2-D version of the Transform node. When you scale the texture coordinates up, they're farther apart; this makes the texture seem compressed because there's more of it between any given pair of texture coordinates.
In VRML, it's possible to reuse parts of the scene by creating additional "instances" of nodes or complete subtrees. We've seen how it's possible to assign a name to a node using DEF. Once you've done that, you can create another instance of the node by using USE. Listing 55.6 shows an example.
Listing 55.6 Multiple Instancing
#VRML V2.0 utf8
DirectionalLight { direction -1 -1 -1 }
DirectionalLight { direction 1 -1 -1 }
DEF Ball Shape {
appearance Appearance { material Material { diffuseColor 1 0 0 } }
geometry Sphere { }
}
Transform {
translation -8 0 0
children [
USE Ball
]
}
Transform {
translation 8 0 0
children [
USE Ball
]
}
The sphere is created once and then instanced twice: once inside a Transform that shifts it 8 meters to the left, and once inside a Transform that shifts it 8 meters to the right.
Note that USE does not create a copy of a node, it simply reuses the node in memory. As you'll see later, this does make a difference. If a behavior alters the color of the ball, it affects all three instances. This relationship is shown in Figure 55.12.
Figure 55.12 : Instancing of nodes saves memory and download time.
VRML supports three different types of light sources: PointLight, SpotLight, and DirectionalLight. One important thing to keep in mind is the more light sources you add to a scene, the more work the computer has to do to compute the lighting on each object. You should avoid having more than a few lights turned on at the same time.
All the lights have the same basic set of fields: intensity, color, and on. They also have an ambientIntensity, which indicates how much of their light contributes to the ambient illumination in the room, as well as some attenuation factors (which are beyond the scope of this chapter).
A PointLight has a location field that indicates the placement of the light source within its parent's coordinate system. PointLights radiate equally in all directions.
SpotLights are similar to PointLights except they also have a direction field, which indicates which way they're pointing (again, relative to their parent's coordinate system), and some additional information (beamWidth and cutOffAngle) that describe the cone of light that they produce.
Unlike PointLight and SpotLight, a directional light has no location. It appears to come from infinitely far away, and the light it emits travels in a straight line. DirectionalLights put less of a burden on the rendering engine, which results in improved performance.
One of the most important additions to VRML 2.0 is support for sound. Two nodes are used for this purpose: Sound and AudioClip.
A Sound node is like a SpotLight except that it emits sound instead of light. It has a location, a direction vector, and an intensity. It also contains an AudioClip node to act as a source for the sound.
An AudioClip node gives the url of the sound source (a WAV file or MIDI data), a readable description of the sound for users with no sound capabilities, a pitch adjustment, and a flag that indicates whether or not the sound should loop.
The Viewpoint node allows the author of a world to specify a location and orientation from which the scene can be viewed. If only one Viewpoint is specified, the user starts off at that location and orientation. The Viewpoint is part of the transformation hierarchy, and the user is "attached" to it. This means you can move the user around the environment by altering the values in the Transform nodes above the Viewpoint.
There are a number of other nodes in VRML, which are beyond the scope of this chapter. The Fog node creates fog in the environment, and the Background node allows you to specify a background image as well as the colors for the sky and ground. The NavigationInfo node lets you control the speed and movement style of the user, and the WorldInfo node lets you embed arbitrary information (author's name, copyright, and so forth) in a way that won't get eliminated when comments are stripped out. The Billboard node is a type of Transform that always keeps its local Z axis pointing towards the user. This is particularly useful for geometry that must always be seen head-on.
There's an Anchor node that allows you to make any object or group of objects in your scene work as a link to other VRML worlds or HTML documents. The Inline node lets you bring other VRML worlds into yours (much like the "include" mechanism in the C programming language). There are grouping nodes for automatically switching level of detail (LOD) or selecting any of several different subtrees (Switch). There's a Collision node that enables or disables collision detection for its subtrees, which allows you to make some of the shapes "solid" to prevent the user from passing through them.
For details about these and other nodes, see the full VRML specification
online.
| ON THE WEB |
http://sdsc.edu/vrml This site, the VRML Repository, has links to the full VRML specification and much, much more including VRML browsers and authoring tools |
There are a number of nodes that detect various types of events that take place in the virtual environment. These nodes are referred to as sensors.
At the moment, there are seven such sensors: CylinderSensor, PlaneSensor, ProximitySensor, SphereSensor, TimeSensor, TouchSensor, and VisibilitySensor.
All sensors are able to generate events, which contain a timestamp (indicating the time at which the event occurred), an indication of the type of event, and event-specific data. All sensors can generate more than one type of event from a single interaction.
A complete description of all the sensors and how they work is beyond the scope of this chapter. However, two sensors in particular are worth taking a closer look at: TouchSensor and TimeSensor.
A TouchSensor is a node that detects when the user has touched some geometry in the scene. The definition of "touch" is general enough to support immersive environments with 3-D pointing devices as well as more conventional desktop metaphors that use a 2-D mouse. Touching in a desktop environment is usually done by clicking the object on-screen.
The TouchSensor node enables contact detection for all its siblings. In other words, if the TouchSensor is a child of a Transform, it detects contact with any shape under that same Transform.
Listing 55.7 shows how a TouchSensor would be used.
Listing 55.7 Using a TouchSensor
#VRML V2.0 utf8
Transform {
children [
TouchSensor { }
Shape { geometry Sphere { } }
Shape { geometry Box { } }
]
}
A TouchSensor generates several events, but the two most important ones are isActive and touchTime. The isActive event is an SFBool value that is sent when contact is first made; touchTime is an SFTime value that indicates the time the contact was made.
A TouchSensor can be used for operating a light switch or a doorknob, or for triggering any event based on user input.
Clicking either the sphere or the box in the preceding example causes the TouchSensor to send both an isActive event and a touchTime event as well as several other events, which are beyond the scope of this chapter.
The TimeSensor node is the only sensor that doesn't deal with user input. Instead, it generates events based on the passage of time.
A TimeSensor has a startTime and a stopTime. When the current time reaches the startTime, the TimeSensor starts generating events. It continues until it reaches the stopTime (assuming the stopTime is greater than the startTime). You can enable or disable a TimeSensor by using its enabled field.
Sometimes, you want to generate continuous time values. Other times, you want to generate discrete events, for example, once every five seconds. Still other times, you want to know what fraction of the total time has elapsed. A TimeSensor is able to do all three of these things, and do them simultaneously. It does this by generating four different kinds of events, one for each of the three situations described previously and one that indicates when the TimeSensor goes from active to inactive.
The first type of event is simply called time. It gives the system time when the TimeSensor generates an event. Bear in mind that although time flows continuously in VRML, TimeSensor nodes only generate events sporadically. Even though most VRML browsers cause the TimeSensors to send events once for each rendered frame, there's no guarantee that this will always be the case. The time value output by a TimeSensor is always correct, but there's no way to be sure you're going to get values at any particular time.
The second type of event is called cycleTime. The TimeSensor has a cycleInterval field that causes a cycleTime event to be generated whenever a cycleInterval has elapsed. There are no guarantees that the cycleTime event will be generated at any particular time, only that it will be generated after the cycle elapses. The cycleTime is useful for events that happen periodically. With loop set to TRUE, the timer runs until it reaches the stopTime and multiple cycleTime events are generated. If the stopTime is less than the startTime (it defaults to zero) and loop is TRUE, the timer would run continuously forever and generate a cycleTime event after every cycleInterval.
The third type of event is called fraction_changed. It's a floating-point number between 0.0 and 1.0 that indicates what fraction of the cycleInterval has elapsed. It's generated at the same time that time events are, and again is not guaranteed to be generated at any particular time.
The final type of event is isActive, which is an SFBool
that is set to TRUE when the TimeSensor starts
generating events (such as when the startTime is reached)
and is set to FALSE when the TimeSensor stops
generating events.
| NOTE |
The TimeSensor is the most complex node in the entire VRML specification. If you run into problems, you can post questions to the comp.lang.vrml newsgroup |
Figure 55.13 shows how to conceptualize a TimeSensor node.
Figure 55.13 : The TimeSensor node is used to mark the passage of time.
A ROUTE is not a node. It's a statement that tells the VRML browser to connect a field in one node to a field in another node. For example, we could connect a TimeSensor's fraction_changed event output to a light's intensity field as shown in Listing 55.8.
Listing 55.8 Using ROUTE
#VRML V2.0 utf8
Viewpoint { position 0 -1 5 }
DEF Fizzbin TimeSensor { loop TRUE cycleInterval 5 }
DEF Bulb PointLight { location 2 2 2 }
Shape { geometry Sphere { } }
ROUTE Fizzbin.fraction TO Bulb.intensity
This would cause the light intensity to vary continuously, increasing from 0.0 to 1.0, and then jumping back down to zero again.
Notice what's happening in this example. The default value for the enabled field of the TimeSensor is TRUE, so the timer is ready to run. Because the default value for startTime is zero, and the current time is greater than that, the TimeSensor will generate events. Because loop is TRUE and the default value for stopTime is zero (which is less than or equal to the startTime), the timer will run continuously. The cycleInterval is 5 seconds, so the fraction_changed value increases from 0.0 to 1.0 over that interval.
The ROUTE statement is what connects the fraction_changed
value in the Fizzbin TimeSensor to the intensity
field in the PointLight named Bulb. Both ROUTE
and TO should be all uppercase.
| NOTE |
Not all fields can be routed to or routed from. For example, the radius field of a Sphere node can't be the source or destination of a ROUTE. You can, however, change the size of a sphere by altering the scale field of the surrounding Transform node. Check the VRML specification for details |
The types of values in the fields referenced in a ROUTE must match. In this example, we were able to route the TimeSensor's fraction_changed value (an SFFloat) to the PointLight's intensity field (also an SFFloat); however, routing an SFBool (like a TimeSensor's isActive field) to the PointLight's intensity field would have been an error.
An interpolator computes a series of values for some field to animate the objects in the scene. Every interpolator node in VRML has two arrays: key and keyValue. Each interpolator also has an input (called set_fraction) and an output (called value_changed). If you imagine a 2-D graph with the keys along the x-axis and the key values along the y-axis, you'll have an idea of how an interpolator works (see Figure 55.14).
Figure 55.14 : Interpolator nodes use linear interpolation.
The keys and the key values have a one-to-one relationship: There's a corresponding keyValue for every key. When an interpolator receives a set_fraction event, the incoming fraction is compared to all the keys. The two keys on either side of the incoming fraction are found, along with the corresponding key values, and a value is computed that's the same percentage of the way between the key values as the incoming fraction is between the keys.
There are half a dozen different interpolators in VRML: ColorInterpolator, CoordinateInterpolator, NormalInterpolator, OrientationInterpolator, PositionInterpolator, and ScalarInterpolator. Each serves a purpose of some kind, but we're only going to look at the PositionInterpolator.
In a PositionInterpolator, the key values (and value_changed) are of type SFVec3f; that is, they're 3-D vectors. Listing 55.9 shows an example of a PositionInterpolator at work.
Listing 55.9 A Flying Saucer
#VRML V2.0 utf8
DEF Saucer-Transform Transform {
scale 1 0.25 1
children [
Shape {
geometry Sphere { }
}
]
}
DEF Saucer-Timebase TimeSensor { loop TRUE cycleInterval 5 }
DEF Saucer-Mover
PositionInterpolator {
key [ 0.0, 0.2, 0.4, 0.6, 0.8, 1.0 ]
keyValue [ 0 0 0, 0 2 7, -2 2 0, 5 10 -15, 5 5 5, 0 0 0 ]
}
ROUTE Saucer-Timebase.fraction_changed TO Saucer-Mover.set_fraction
ROUTE Saucer-Mover.value_changed TO Saucer-Transform.set_translation
The saucer is just a sphere that's been squashed along the y-axis using a scale in the surrounding Transform node. The translation field for the Transform isn't given, so it defaults to (0, 0, 0). The TimeSensor is just like the one we looked at earlier.
The Saucer-Mover is a PositionInterpolator. It has six keys, going from 0.0 to 1.0 in steps of 0.2. There's no reason why we had to go in fixed-size steps; we could just as easily have used any set of values, as long as they steadily increase.
There are six values corresponding to the six keys. Each one is a three-element vector, giving a particular position value for the saucer.
We create the routes after the nodes are defined. The first ROUTE connects the TimeSensor's fractional output to the PositionInterpolator's fractional input. As the TimeSensor runs, the input to the PositionInterpolator increases steadily from 0.0 to 1.0. (which it reaches after five seconds, the cycleInterval). The second ROUTE connects the value_changed output of the PositionInterpolator to the translation field of the saucer's Transform node; this is what lets the interpolator move the saucer. Figure 55.15 shows the relationship between these nodes.
Figure 55.15 : The routes between nodes for the flying saucer example look like this.
Notice that the saucer doesn't jump from one value to another; its location is linearly interpolated between entries in the PositionInterpolator's keyValue field.
Scripts are the mechanism by which Java programs communicate with a VRML world, and vice versa. There's a special kind of node called Script that makes this communication possible.
The Script node is a type of nexus. Events flow in and out of the node, just as they do for interpolators or other types of nodes. However, the Script node is special: It allows an actual program, written in Java, to process the incoming events and generate the outgoing events. Figure 55.16 shows the relationship between the Script node in VRML and the Java code which implements it.
Figure 55.16 : How Java accesses VRML through a Script node.
The Script node has only one built-in field which you have to worry about at this stage: url, which gives the URL of a Java bytecode file (that is, a .Class file) somewhere on the Internet. There are a couple of other fields, but we don't need to worry about them here.
The Script node can also have a number of declarations for incoming and outgoing events, as well as fields that are accessible only by the script. For example, Listing 55.10 shows a Script node that can receive two incoming events (an SFBool and an SFVec3f), send three outgoing events, and has two local fields.
Listing 55.10 A Typical Script Node
#VRML V2.0 utf8
Script {
url "bigbrain.class"
eventIn SFBool recomputeEverything
eventIn SFVec3f spotToBegin
eventOut SFBool scriptRan
eventOut MFVec3f computedPositions
eventOut SFTime lastRanAt
field SFFloat rateToRunAt 2.5
field SFInt32 numberOfTimesRun
}
The eventIn, eventOut, and field designators are used to identify incoming events, outgoing events, and fields that are private to the Script node.
The Java bytecode file Bigbrain.class would be loaded in, and the constructor for the class would be called. Before any events are sent to the class, the initialize() method of the class is called.
As events arrive at the Script node, they're passed to the processEvent() method of the class. That method looks like the following:
public void processEvent(Event ev);
where ev is an incoming event. An event is defined as follows:
class Event {
public String getName();
public ConstField getValue();
public double getTimeStamp();
}
The getName() method returns the name of the incoming event, which is the name the event was given in the Script node in the VRML file. The getTimeStamp() method returns the time that the Script node received the event. The getValue() method returns a ConstField that should then be cast to the actual field type (such as ConstSFBool or ConstMFVec3f).
There are Java classes for each type of VRML field. Each of these classes defines methods for reading (and possibly writing) their values. These classes will be contained in a package called vrml (not java.vrml) that should be included with your VRML browser.
Let's say you want to have a light change to a random intensity whenever the user touches a sphere. VRML itself doesn't have any way to generate random numbers, but of course, Java does (the java.util.Random class). Listing 55.11 shows how you would construct your VRML world.
Listing 55.11 RandLight.wrl-A Random Light in VRML
#VRML V2.0 utf8
Viewpoint { position 0 -1 5 }
NavigationInfo { headlight FALSE }
DEF RandomBulb DirectionalLight { -1 -1 -1 }
Transform {
children [
DEF Touch-me TouchSensor { }
Shape {
geometry Sphere { } # something for the light to shine on
}
]
}
DEF Randomizer Script {
url "RandLight.class"
eventIn SFBool click
eventOut SFFloat brightness
}
ROUTE Touch-me.isActive TO Randomizer.click
ROUTE Randomizer.brightness TO RandomBulb.intensity
The DirectionalLight is given the name RandomBulb using a DEF. A Sphere shape and a TouchSensor are grouped as children of a Transform. This means that touching the Sphere triggers the TouchSensor.
The Script node is given the name Randomizer, and it has one input (an SFBool called click) and one output (an SFFloat called brightness).
When the RandLight class first loads, its constructor is called, followed by its initialize() method.
Whenever you click the sphere, the TouchSensor's isActive field is set to TRUE and routed to the script's click eventIn. This, in turn, causes an event to be sent to the processEvent() method of the RandLight class. The event would have a name of click, and a value that would be cast to a ConstSFBool. The ConstSFBool has a value of true which is returned by its getValue() method. When you release the button, another event is sent that's identical to the first, but this time with a value of false in the ConstSFBool.
When any of the methods in the RandLight class sets the brightness value (as described later), that event is routed to the intensity field of the PointLight called RandomBulb.
Now that we've seen how the VRML end of things works, let's look at things from the Java perspective. We'll be returning to our random light project shortly, but first let's take a little detour through the VRML package.
import vrml.*; import vrml.field.*; import vrml.node.*;
The vrml package defines a number of useful classes. There's a class called Field (derived from Object) that corresponds to a VRML field. From Field there are a number of derived classes, one for each of the basic VRML data types such as SFBool and SFColor. There are also "read-only" versions of all those classes; they have a "Const" prefix, as in ConstSFBool.
The read-only versions of the fields provide a getValue() method that returns a Java data type corresponding to the VRML type. For example, the ConstSFBool class looks like the following:
public class ConstSFBool extends ConstField {
public boolean getValue();
}
The read-write versions of the fields provide the getValue() method as well, but they also have a setValue() method that takes a parameter (such as a boolean) and sets it as the value of the field. Doing this causes an event to be sent from the Script node.
There are classes that correspond to multiple-valued VRML types such as MFFloat. These have the getValue() and setValue() methods, but they also have a method for setting a single element of the array: set1Value(). Listing 55.12 shows what the MFVec3f class looks like.
Listing 55.12 The MFVec3f Class
public class MFVec3f extends MField
{
public MFVec3f(float vecs[][]);
public MFVec3f(float vecs[]);
public MFVec3f(int size, float vecs[]);
public void getValue(float vecs[][]);
public void getValue(float vecs[]);
public void setValue(float vecs[][]);
public void setValue(int size, float vecs[]);
public void setValue(ConstMFVec3f vecs);
public void get1Value(int index, float vec[]);
public void get1Value(int index, SFVec3f vec);
public void set1Value(int index, float x, float y, float z);
public void set1Value(int index, ConstSFVec3f vec);
public void set1Value(int index, SFVec3f vec);
public void addValue(float x, float y, float z);
public void addValue(ConstSFVec3f vec);
public void addValue(SFVec3f vec);
public void insertValue(int index, float x, float y, float z);
public void insertValue(int index, ConstSFVec3f vec);
public void insertValue(int index, SFVec3f vec);
}
An MFVec3f contains an array of three-element vectors (the three elements being the x-, y-, and z-components). A single entry is a float[], and an MFVec3f is a float[][] type in Java.
Notice that there are three versions of setValue(), one which takes a two-dimensional array of floats, one which takes an array of floats plus a count, and one which takes another MFVec3f.
Not only is there a class in the VRML package corresponding to a field in a VRML node, there's also a class for VRML nodes themselves. The Node class provides methods for accessing exposedFields, eventIns, and eventOuts by name. For example, the name of a field in the node is passed to getExposedField(), which returns a reference to the field. As we'll see later, that return value needs to be cast as an appropriate type.
There's also a Script class that is related to Node. When you write Java code to support a Script node, you create a class that's derived from the Script class. The Script class provides a getField() method for accessing a field, and a similar getEventOut() method. It also has an initialize() method and a processEvent() method. There's also a shutdown() method that gets called just before the Script node is discarded to allow the class to clean up after itself.
The Script class also defines two other methods: processEvents() (not to be confused with processEvent()) which is given an array of events and a count so that they can be processed more efficiently than by individual processEvent() calls, and an eventsProcessed() method, which is called after a number of events have been delivered.
Finally, there's a Browser class that provides methods for finding such things as the name and version of the VRML browser that's running, the current frame rate, the URL of the currently loaded world, and so on. You can also add and delete ROUTEs and even load additional VRML code into the world either from a URL or directly from a String.
Listing 55.13 shows the Java source for the RandLight class, which would be stored in a file called RandLight.java.
Listing 55.13 RandLight.java-The Java Code for RandLight
// Code for a VRML Script node to set a light to a random intensity
import vrml.*;
import vrml.field.*;
import vrml.node.*;
import java.util.*;
public class RandLight extends Script {
Random generator = new Random();
SFFloat brightness;
public void initialize() {
brightness = (SFFloat) getEventOut("brightness");
brightness.setValue(0.0);
}
public void processEvent(Event ev) {
if (ev.getName().equals("click")) {
ConstSFBool value = (ConstSFBool) ev.getValue()
if ((value.getValue() == false) { // touch complete
brightness.setValue(generator.nextFloat());
}
}
}
}
The RandLight.java file defines a single class, called RandLight, which extends the Script class defined in the VRML package as described earlier.
The RandLight class contains a random number generator, and it also has an SFFloat called brightness. As we saw earlier, the Script class has a method called getEventOut() that retrieves a reference to an eventOut in the Script node in the VRML file using the name of the field (in this case, "brightness"). Because the type of eventOut (SFBool, SFVec3f, and so on) is unknown, the getEventOut() method simply returns a Field which is then cast to be a field of the appropriate type (using (SFFloat)). This is assigned to the variable called brightness, which has a type of SFFloat. We didn't have to call the variable brightness, but it's a good idea to keep the field name in the Script node consistent with its corresponding variable in the class that supports that Script node.
Like all read-write classes that correspond to VRML fields, the SFFloat class has a method called setValue(). It takes a float parameter and stores it as the value of that field, which causes the Script node in VRML to generate an outgoing event. That event may, in turn, be routed somewhere.
The rest of the code is straightforward. The initialize() method sets the brightness to zero. The processEvent() method, which gets called when an event arrives at the Script node in VRML, checks for click events and sets the brightness to a random value on false clicks (releases of the mouse button). That's all there is to it.
The Towers of Hanoi is a very simple puzzle, yet intriguing to watch. There are three vertical posts, standing side by side. On one of the posts is a stack of disks. Each disk has a different diameter, and they're stacked so that the largest disk is on the bottom, the next largest is on top of it, and so on until the smallest disk is on top.
The goal is to move the entire stack to another post. You can only move one disk at a time, and you are not allowed to place a larger disk on top of a smaller one. Those are the only rules.
If you were doing it by hand, you would start by taking the topmost (smallest) disk from the first post and placing it on the second post. You would then take the next largest disk and place it on the third post. Then you'd take the disk from the second post and place it on the third one. This process would continue until you'd moved all the disks.
Building a VRML/Java application to do this is a multi-stage process. You'll start by building the posts and base along with some lighting and a nice viewpoint. Then you'll add the disks, and finally the script that animates them. You're going to use everything you've learned about in this chapter, including TouchSensors, TimeSensors, PositionInterpolators, Scripts, ROUTE statements, and basic VRML nodes.
The three posts are created using Cylinder nodes and the base is a Box. You'll position the base first (see Listing 55.14).
Listing 55.14 The Base
#VRML V2.0 utf8
# Base
Transform {
translation 0 0.0625 0
children [
Shape {
appearance Appearance {
material Material { diffuseColor 0.50 0.50 0 }
}
geometry Box { size 1.5 0.125 0.5 }
}
]
}
The box is 1.5 meters wide (x-axis), 0.125 meters high (y-axis), and 0.5 meters deep (z-axis). Because you want it resting on the "ground" (the x-z plane), you need to position its lowest point at Y=0. Because the origin of the Box is at its geometric center, you need to shift it vertically by half its height (0.125Ö2 = 0.0625), which is why you have a translation of (0, 0.0625, 0)-no translation in x or z and a 0.0625-meter translation in y.
The next step is to add the first post, as shown in Listing 55.15.
Listing 55.15 The First Post
# Posts
Transform {
translation 0 0.375 0
children DEF Cyl Shape { geometry Cylinder { height 0.5 radius 0.035 } }
}
The first post is a Cylinder, half a meter high with a radius of 0.035 meters. You assign this shape the name Cyl because we'll be making USE of it later. You want the bottom of the post to rest on top of the box. Because the origin of a Cylinder is at its geometric center, you need to shift it vertically by half its height (that is, 0.25 meters) plus the height of the base (0.125 meters). Because 0.25 plus 0.125 equals 0.375, you give this shape a translation of (0, 0.375, 0). The post is centered over the middle of the box, because there's no x or z translation. Figure 55.17 shows what you have so far.
Figure 55.17 : The base and the first (middle) post look like this.
Rather than create two more cylinders, you'll make use of instancing. Listing 55.16 shows how this works.
Listing 55.16 The Other Two Posts
Transform {
translation -0.5 0.375 0
children USE Cyl
}
Transform {
translation 0.5 0.375 0
children USE Cyl
}
The USE Cyl creates another instance of the post shape we created earlier. The first Transform moves the post to the left (x equals -0.5 meters), and the second moves the post to the right(x equals 0.5 meters). They both move the posts to the same y equals 0.375 location as the first post.
We'll also add a WorldInfo node to store author information and a title for the world, and a NavigationInfo node to put the user's VRML browser in FLY mode and turn off their headlight. We'll also add a TouchSensor to the base to give the user a way to start and stop the movement of the disks. Finally, we'll thrown in some lights. Listing 55.17 shows our world so far; Figure 55.18 shows what it looks like in a VRML browser.
Figure 55.18 : This is how our world-in-progress looks.
Listing 55.17 The World So Far
#VRML V2.0 utf8
WorldInfo {
title "Towers of Hanoi"
info "Created by Bernie Roehl (broehl@ece.uwaterloo.ca), July 1996"
}
NavigationInfo { type "FLY" headlight FALSE }
PointLight { location 0.5 0.25 0.5 intensity 6.0 }
PointLight { location -0.5 0.25 0.5 intensity 6.0 }
DirectionalLight { direction -1 -1 -1 intensity 6.0 }
Viewpoint { position 0 0.5 2 }
# Base
Transform {
translation 0 0.0625 0
children [
DEF TOUCH_SENSOR TouchSensor { }
Shape {
appearance Appearance {
material Material { diffuseColor 0.50 0.50 0 }
}
geometry Box { size 1.5 0.125 0.5 }
}
]
}
# Posts
Transform {
translation 0 0.375 0
children DEF Cyl Shape { geometry Cylinder { height 0.5 radius 0.035 } }
}
Transform {
translation -0.5 0.375 0
children USE Cyl
}
Transform {
translation 0.5 0.375 0
children USE Cyl
}
The static part of our world is done. Now it's time to add the moving parts-the disks themselves.
For our example, we'll be using five disks. The definition of each disk is pretty simple, as shown in Listing 55.18.
Listing 55.18 A Disk
DEF Disk1
Transform {
translation -0.5 0.305 0
children [
Shape {
appearance Appearance {
material Material { diffuseColor 0.5 0 0.5 }
}
geometry Cylinder { radius 0.12 height 0.04 }
}
]
}
The disks are just cylinders. All the disks will be the same,
except for the value of the translation (they're stacked
vertically, so the y-component will be different), the value of
the radius (each disk is smaller than the one below it),
and the diffuseColor of the disk.
| NOTE |
This book is being written at a very early stage of VRML 2.0, and there are no fully-compliant browsers available. Early testing of the examples in this chapter was done using Sony's Community Place, which did not have support for Prototypes. That's why PROTO is not being used to create the disks |
We'll be adding some additional nodes for each disk, but for now, we'll just leave it at the geometry. Figure 55.19 shows the posts with the disks stacked in their starting position.
Figure 55.19 : The posts and the disks look like this.
We're going to be using a PositionInterpolator for each disk to handle its movement, and we'll be driving those interpolators from TimeSensor nodes. Let's look at the interpolator first; the one for the first disk is shown in Listing 55.19.
Listing 55.19 An Interpolator for One of the Disks
DEF Disk1Inter
PositionInterpolator {
key [ 0, 0.3, 0.6, 1 ]
}
There are four keys, spaced roughly 0.3 units apart. Each disk is going to move from its current location to a point immediately above the post it's on. It then moves to a point immediately above the destination post, then finally down into position. Four locations, four keys. Notice that no key values are specified; they'll be filled in later by our Java code.
The timer associated with each disk is a TimeSensor, as shown in Listing 55.20.
Listing 55.20 A Timer for One of the Disks
DEF Disk1Timer
TimeSensor {
loop FALSE
enabled TRUE
stopTime 1
}
The timer is designed to run once each time it's started, which is why its loop field is FALSE. It starts off being enabled. The startTime is not specified; again, that's because it will be filled in from our Java code.
The next step is to connect the TimeSensor to the PositionInterpolator, and the PositionInterpolator to the Transform node for the disk. A pair of ROUTE statements does the trick:
ROUTE Disk1Timer.fraction_changed TO Disk1Inter.set_fraction ROUTE Disk1Inter.value_changed TO Disk1.set_translation
We're going to be adding a Script node. It will need to be able to update the keyValue field of the PositionInterpolator and the startTime field of the TimeSensor, so we'll add a couple of additional ROUTEs:
ROUTE SCRIPT.disk1Start TO Disk1Timer.startTime ROUTE SCRIPT.disk1Locations TO Disk1Inter.keyValue
The Script node called SCRIPT will have a disk1Start field into which it will write the start time for the interpolation, and a disk1locations field into which it will write the four locations where this disk will move (current location, above the current post, above the destination post, and final location).
The complete VRML source for a single disk, therefore, looks like Listing 55.21.
Listing 55.21 A Single Disk
DEF Disk1
Transform {
translation -0.5 0.305 0
children [
Shape {
appearance Appearance {
material Material { diffuseColor 0.5 0 0.5 }
}
geometry Cylinder { radius 0.12 height 0.04 }
}
]
}
DEF Disk1Inter PositionInterpolator { key [ 0, 0.3, 0.6, 1 ] }
DEF Disk1Timer TimeSensor { loop FALSE enabled TRUE stopTime 1 }
ROUTE SCRIPT.disk1Start TO Disk1Timer.startTime
ROUTE Disk1Timer.fraction TO Disk1Inter.set_fraction
ROUTE Disk1Inter.value_changed TO Disk1.set_translation
ROUTE SCRIPT.disk1Locations TO Disk1Inter.keyValue
This complete sequence is replicated for each of the five disks. Of course, Disk1 is replaced with Disk2, Disk3, and so on.
To keep things simple, we're going to have a single Script node to drive the entire simulation. It has a large number of inputs and outputs, as shown in Listing 55.22.
Listing 55.22 The Script Node
DEF SCRIPT Script {
url "Hanoi.class"
eventIn SFBool clicked
eventIn SFTime tick
eventOut MFVec3f disk1Locations
eventOut SFTime disk1Start
eventOut MFVec3f disk2Locations
eventOut SFTime disk2Start
eventOut MFVec3f disk3Locations
eventOut SFTime disk3Start
eventOut MFVec3f disk4Locations
eventOut SFTime disk4Start
eventOut MFVec3f disk5Locations
eventOut SFTime disk5Start
}
The script is loaded from a file called Hanoi.class, which is the result of compiling Hanoi.java. It will be described in detail later in this chapter. The clicked eventIn is used to let the script node know when the user clicked the base of the posts (to start or stop the simulation). The tick eventIn is used to advance the simulation.
For each disk, there's the set of locations that get routed to the PositionInterpolator's keyValue field as we saw earlier, and a start time that gets routed to the disk's TimeSensor's startTime value.
We need a ROUTE to connect the TouchSensor on the base to the clicked field of the ROUTE:
ROUTE TOUCH_SENSOR.isActive TO SCRIPT.clicked
We'll also add a TimeSensor to drive the simulation, as shown in Listing 55.23.
Listing 55.23 The Master TimeSensor
DEF TIMEBASE TimeSensor {
cycleInterval 1.5
enabled TRUE
loop TRUE
}
This sends a cycleTime event every 1.5 seconds, indefinitely. Each of these cycleTime events triggers the moving of one disk.
And finally, there's a ROUTE to connect this timer to the Script node's tick field:
ROUTE TIMEBASE.cycleTime TO SCRIPT.tick
Figure 55.20 shows an overall diagram of how the nodes are connected to each other.
Figure 55.20 : The routing relationships in the Towers of Hanoi example look like this.
The complete listing for Hanoi.wrl is found on the CD-ROM that accompanies this book.
We'll be using the initialize() method of our Hanoi class to generate the complete sequence of moves and store them in an array. Whenever we get a message from the TimeSensor, we'll carry out the next step in the sequence. We'll also handle the click message to allow the user to turn us on or off.
The moves themselves will be stored in three arrays: disks[], startposts[], and endposts[]. The disks[] array stores the number of the disk (0 through 4 because there are five disks) that's supposed to be moved. The startposts[] and endposts[] arrays store the starting and ending post numbers (0 through 2 because there are three posts).
There's also a postdisks[] array that keeps track of the number of disks on each post. We'll be using it to compute the height of the topmost disk on each post in order to make the moves.
We'll begin with the standard header and declarations for our data, as shown in Listing 55.24.
Listing 55.24 The Start of Hanoi.java
import vrml.*;
import vrml.field.*;
import vrml.node.*;
public class Hanoi extends Script {
// the following three arrays record the moves to be made
int disks[] = new int[120]; // which disk to move
int startposts[] = new int[120]; // post to move it from
int endposts[] = new int[120]; // post to move it to
int nmoves = 0; // number of entries used in those three arrays
int current_move = 0; // which move we're on now
boolean forwards = true; // initially, move from
// post 0 to post 2
int postdisks[] = new int[3]; // number of disks on each
// of the posts
Next comes our initialize() method. It just calls a recursive routine called hanoi_r() to do the actual work and then initializes the number of disks on each post. Because all the disks are on the first post to begin with, and the entries in postdisks[] are all zero initially, this is pretty easy. Listing 55.25 shows the initialize() method.
Listing 55.25 The initialize() Method
/***** initialize() builds table of moves *****/
public void initialize() {
int number_of_disks = 5;
postdisks[0] = number_of_disks; // first post has all the disks
hanoi_r(number_of_disks, 0, 2); // generate the sequence of moves
}
Next, we define a flag that indicates whether we're running or not. We also define a processEvent() method to handle events coming into the script. These are shown in Listing 55.26.
Listing 55.26 The processEvent() Method
boolean running = false; // true if we're running
/***** clicking on the base starts and stops the action *****/
public void processEvent(Event ev) {
if (ev.getName().equals("click")) {
ConstSFBool value = (ConstSFBool) ev.getValue();
if (value.getValue() == false) {
running = running ? false : true; // toggle
}
else if (ev.getName().equals("tick"))
tick(ev.getTime());
}
}
This code fragment is similar to our earlier RandLight example. Recall that all fields have a getValue() method that returns a standard Java value. In the case of a ConstSFBool field, the getValue() method returns a boolean type value. If that value is true, the user touched the object (by clicking it with the mouse), and if the value is false, the user "untouched" the object (for example, by releasing the mouse). In such a case, the running flag is toggled true or false.
If the incoming event is a tick rather than a click, the next move in sequence executes. When we hit the end of the list of moves, all the disks are at their destination post. At that point, we replay the sequence backwards to return to the original configuration. We then play the sequence forwards again, and so on. The tick() method is shown in Listing 55.27.
Listing 55.27 The tick() Method
/***** at each tick (cycleTime), make the next
move in the sequence *****/
void tick(double time) {
if (running == false)
return; // do nothing if we're not running
if (forwards) // moving from source to destination
{
make_move(disks[current_move], startposts[current_move],
endposts[current_move], time);
if (++current_move >= nmoves) {
current_move = nmoves-1;
forwards = false;
}
}
else { // moving in the other direction
make_move(disks[current_move], endposts[current_move],
startposts[current_move], time);
if (--current_move < 0) {
current_move = 0;
forwards = true;
}
}
}
The tick() method does nothing if we're not running. If we're running the sequence forward, it makes the move and increments the current_move counter. When we hit the last move, we make the last move our next one and reverse directions.
If we're running backwards, we make the opposite move; we move from the endposts[current_move] post to the startposts[current_move] post. We decrement the current move; when we hit the first move, we make it our next one and again reverse directions.
The make_move() method is where we do most of our talking to VRML. We start by defining some constants to use for array indexing:
static final int X = 0, Y = 1, Z = 2; // elements of an SFVec3f
This lets us say (for example) vector[Y] to refer to the y component of the three-element vector, instead of vector[1].
To make a move, we have to fill in the four-element array of locations, each of which is itself an array of three elements (x, y, and z). Listing 55.28 shows how we compute the first position for the disk.
Listing 55.28 The make_move() Routine
/**** Routine to make an actual move *****/
void make_move(int disk, int from, int to, ConstSFTime now) {
float four_steps[][] = { { 0, 0, 0 }, { 0, 0, 0 },
{ 0, 0, 0 }, { 0, 0, 0 } };
// compute starting location for disk
// center post is at x=0, left post is x=-0.5
// and right post is x=0.5
four_steps[0][X] = (from - 1) * 0.5f;
// vertical position is height of disk (0.04) times
// the number of disks on source post, plus height of base
four_steps[0][Y] = 0.04f * postdisks[from] + 0.145f;
// disk is centered on post in Z axis
four_steps[0][Z] = 0f;
Because the center post is at x equals 0, the left post is at x equals -0.5, and the right post is at x equals 0.5, the expression (from-1) * 0.5f gives the x-coordinate of the from post. Because each disk is 0.04 meters high, and there are postdisks[from] disks on the from post, and the base is 0.145 units tall, it's easy to compute the current y-component of the disk's location. The z-component is easy: It's zero because the disk is centered on the post along that axis.
Computing the destination location is almost exactly the same, as shown in Listing 55.29.
Listing 55.29 Computing the Destination Location
// compute ending location for disk
// center post is at x=0, left post is x=-0.5 and
// right post is x=0.5
four_steps[3][X] = (to - 1) * 0.5f;
// vertical position is height of disk (0.04) times
// number of disks on four_steps[0] post, plus height of base
four_steps[3][Y] = 0.04f * postdisks[to] + 0.145f;
// disk is centered on post in Z axis
four_steps[3][Z] = 0f;
The intermediate locations are the same except that the y-coordinates will be one meter up, as shown in Listing 55.30.
Listing 55.30 The Remaining Steps
// now fill in the missing steps
// one meter above the source post
four_steps[1][X] = four_steps[0][0];
four_steps[1][Y] = 1f;
four_steps[1][Z] = 0f;
// one meter above the destination post
four_steps[2][X] = four_steps[3][0];
four_steps[2][Y] = 1f;
four_steps[2][Z] = 0f;
The next step is to adjust the count of the number of disks on each post:
--postdisks[from]; // one less disk on source post
++postdisks[to]; // one more disk on destination post
Finally, we make the move by updating the eventOuts in the Script (which are routed to the disk's PositionInterpolator and TimeSensor). The code to do this is shown in Listing 55.31.
Listing 55.31 Making the Move
// now move the disk
MFVec3f locations = (MFVec3f) getEventOut("disk"
+ (disk+1) + "Locations");
locations.setValue(four_steps);
SFTime timerStart = (SFTime) getEventOut("disk"
+ (disk+1) + "Start");
timerStart.setValue(now);
}
We build the name of the eventOut based on the disk number. Notice that we add 1 to the disk-that's because in the VRML file we counted our disks starting from 1 instead of 0. The eventOut that we found using getEventOut() is routed to the keyValue field of a PositionInterpolator for the disk in question.
The timer is found in a similar fashion. The value now, which is the timestamp of the event that caused this routine to run, is set as the start time for the timer. This starts the timer, which drives the interpolator, which moves the disk.
So far, so good. All we need now is the actual recursive routine for generating the moves. This is shown in Listing 55.32.
Listing 55.32 The Recursive Move Generator
/***** hanoi_r() is a recursive routine for
generating the moves *****/
// freeposts[starting_post][ending_post] gives which post is unused
static final int[][] freeposts = { { 0, 2, 1 }, { 2, 0, 0 },
{ 1, 0, 0 } };
void hanoi_r(int number_of_disks, int starting_post, int goal_post) {
if (number_of_disks > 0) { // check for end of recursion
int free_post = freeposts[starting_post][goal_post];
hanoi_r(number_of_disks - 1, starting_post, free_post);
// add this move to the arrays
disks[nmoves] = number_of_disks - 1;
startposts[nmoves] = starting_post;
endposts[nmoves] = goal_post;
++nmoves;
hanoi_r(number_of_disks - 1, free_post, goal_post);
}
}
The freeposts[] array determines which post to use to make the move. If we're moving from post 0 to post 2, then post 1 is free. This is represented by freeposts[0][2] having the value 1. Note that the main diagonal of this little matrix (the [0][0], [1][1], and [2][2] elements) will never be used because the starting_post and goal_post will never be the same.
The complete Hanoi.java source code is included on the CD-ROMs that come with this book.
All the examples listed in the text of this chapter should work with any final release (not beta) VRML 2.0 browser that supports scripting in Java. However, because no such browsers were available at the time this chapter was written, there are no guarantees.
Just to be on the safe side, I'll be maintaining an "errata" sheet for this chapter, just off of my Web page (http://ece.uwaterloo.ca/ ~broehl/bernie.html).
Also, be sure to check the VRML Repository (http://sdsc.edu/vrml) for a complete listing of VRML resources, including links to the complete specification and lots of examples and tools. See you online!