Contributors

Taming VRML Scripts

by Bob Crispen
 

Part 1: VRML Objects and Events

In real life, I'm an embedded software analyst.  That means I have a pretty thorough grasp of Ada, C and more assemblers than I care to admit; I have a good familiarity with C++ and Java; and I know the arcana of realtime cold. I'm the guy they call when they need to write the software that makes the hardware work.

And yet when I started writing VRML, I struggled for months on scripting.  I'd write what I thought would work and scratched my head when nothing happened or the wrong thing happened. I'd try something else, and eventually I landed on the right way to do it (or often, just a way that the VRML browser didn't hate too much).  But I was shooting in the dark.

I now realize that, even though I knew more about programming and realtime programming than the average person, I still completely misunderstood the VRML event model, and the code I was trying to get to work, when I looked at in the light of my new understanding, was completely stupid.  Of course that wouldn't work -- I know now.  But I didn't realize it at the time.

This lesson is designed to save others the agony I went through.  Once you internalize these concepts, you'll find routing, scripts, and events are simple.  They just don't work the way you probably think they do now.


Classes

Let's borrow a little vocabulary from the object oriented (OO) folks.  A class is a template for a data structure and the things that act on it.  You can't do anything with a class itself, but as soon as you instantiate it (create an instance of it) then you have an object.  The way you access an object is through its methods.  Generally the methods associated with an object let you read the value of a piece of data, change that value, and control some other behaviors.  You can instantiate as many objects of a given class as you like.  Practically speaking, every instance is unique and independent of all the other instances.

Let's look at the spec definition for Sphere in the VRML spec:

It's been really useful to me to think of the definition in the spec as a definition of the Sphere class.  This is distinct from the Java classes for nodes.  But I want to make clear that the spec definition doesn't describe a specific object so much as it describes the class of Spheres, Cylinders, etc.  VRML has 54 of these class definitions.  It's legal inside a VRML file to create any (reasonable) number of instances of the Sphere class (that is, objects of the Sphere class), just as you can create any number of, say, Stacks in your favorite OO programming language.
 

Objects

How do you create a Sphere object?  By typing "Sphere{}" in a VRML file and sending that file to a VRML browser.

In OO programming languages, you customarily give names to your objects:

You give objects names so that you can operate on them through their visible methods.  Most of the time it would be pointless in a programming language to create an object that you didn't want to name, because if you just create an instance of a class and never do anything with it, usually nothing very interesting happens (unless you create a million instances and crash horribly).

VRML is different because it describes a scene.  Sometimes simply placing the object in the scene is good enough.  Of course, sometimes you do want to operate on the object through its methods, and in that case you can give the object a name:

Parameters

The Sphere class has one visible method: the constructor.  When you type "Sphere{}" in a VRML file and send it to the browser, you invoke this method.  The constructor method has one parameter: the radius.  There's a default value for the radius parameter, and it's shown in the spec: 1.  If you don't specify the parameter, the VRML browser will create an object with the default value for the radius parameter.

The spec also shows the legal values this parameter can take.  So if you try to create a Sphere object with a negative radius, that's an error.  Browsers may warn you of the error, or they may simply choose to ignore it and not render the erroneous sphere.

When you see in the spec that a class has a "field", as the Sphere class does, it means that the field is a parameter you can use when you create an instance of the class.  But a "field" does not describe a method of the class.  That is, once you've created the object and specified the parameter for the field, you can't ever change it again.

Let's start jotting down some of these ideas, and we'll call them heuristics (rules of thumb, statements that are only approximately true).  Why only approximately true?  Because, as opposed to rules, which come from the spec itself, these heuristics are ways of thinking about these elements; ways of thinking about them that I've found to be useful.  So here's our first:

 

Heuristic 1 (Rev. A):  A "field" is a parameter whose value you can set when you create an object.  Once you create the object, you can't read or write that parameter again.
 

We'll see another use for fields that may refine our thinking about them when we get to Script nodes, but for now it's a pretty good rule of thumb.
 
Methods

Let's turn to a more interesting example:

Group { 
  eventIn      MFNode  addChildren
  eventIn      MFNode  removeChildren
  exposedField MFNode  children      []
  field        SFVec3f bboxCenter    0 0 0     # (-,)
  field        SFVec3f bboxSize      -1 -1 -1  # (0,) or -1,-1,-1
}

Group nodes have two fields, and what are fields? Right: parameters, useful only when you create an instance of the Group class. Can you read those values or change those values once you've created the Group object? Nope.


Good. Let's go on to the new things that are there. You've got a couple of eventIns, and an exposedField. We'll get to the exposedField in a minute, but let's concentrate on the eventIns.

Heuristic 2:  An "eventIn" is a method.  It's the way you change an internal state or value of an object, and very often that change will be visible in the scene.

In a Group node, if you invoke the addChildren method, you'll generally change what the visitor to your virtual world sees.


So how do you invoke that method?

Variables and Events

You can change a VRML object through two mechanisms: one you're familiar with (variables) and one you probably aren't familiar with (events).

So let's talk about variables first.

VRML has no variables.

Sorry for the trickery, but most people need a little shock to realize that the paths between VRML objects are not simply dataflows or parameter passing like they're used to.  It's better to have that shock now than after you've struggled with VRML events for several months like I did.

So the only way VRML objects can communicate with one another is through events.  What's an event?

 

Heuristic 3:  An "event" consists of: 

  • a value
  • a timestamp, and
  • an action request
 

The value is a dataflow you're used to.  Like a dataflow (the argument of a function) in a conventional programming language, the value is of a given type.  For example, in the Group node, events in and out are of type MFNode.  Just as in any other language, you can't have type mismatches in VRML.  If an object is expecting an SFColor and gets an SFRotation, what would you expect it to do?  So basically, the rule says that your dataflows have to make sense.
 

Rule 1: Dataflows have to make sense:   If an eventIn is expecting an event of a given type, you have to send it an event of that type.

This is a rule, rather than a heuristic, because I don't care whether you think of it that way or not: if you violate the rule, the VRML browser or syntax checker will slap you upside the head.

The timestamp part of an event is the time on the VRML clock that the event occurred.  We'll talk about the VRML clock in more detail later, but right now if you think of it as a wall clock, you won't be far wrong.  You'll see some uses for the timestamp behind the scenes if you read the VRML spec section on event processing (Clause 4.10), but in the few cases where you use the timestamp, you'll use it in Scripts for finer control of time-dependent behaviors.

Now to the last part of the event: the action request.  Suppose you've got an event that you're sending to the addChildren eventIn (method) of a Group object.  Whenever the value of the event changes, the method will get called.  It's as though you had a variable in C or FORTRAN or Ada and that variable was being passed to a function.  If these other programming languages worked like VRML, every time you changed the value of that variable, the function would be called automatically. It works a lot like a mailbox interrupt or an address that's mapped to a port: write to it and it does something.

Now let's turn to the other thing our Group node has that our Sphere node doesn't: exposedFields.


ExposedFields

Whenever you see an exposedField in the spec definition of a class, that's shorthand for three different things:

Let's ignore the fields and eventIns in our Group node and pretend it has only the one exposedField:

SimplerGroup {
  exposedField MFNode  children      []
}

That's exactly equivalent to saying:

SimplerGroup {
  field    MFNode  children      []
  eventIn  MFNode  set_children
  eventOut MFNode  children_changed
}

These, in fact, are the names you use to refer to the field, the eventIn and the eventOut.
   

Heuristic 4:  An "exposedField" is a field (remember Heuristic 1), an eventIn, and an eventOut.  Nothing more.  Nothing less.

Objects of the SimplerGroup node class, then, have one field (used when you create the object), and one method: set_children.  What's children_changed?  We'll get to that next.


EventOuts

Heuristic 5:  An "eventOut" is the place an event comes from.

For most VRML objects, an event is generated in only two cases:

So now we can see what the three things in our SimplerGroup object are:

SimplerGroup { 
  field    MFNode children          # a parameter used to initialize the object 
  eventIn  MFNode set_children      # a method used to change the object 
  eventOut MFNode children_changed  # an event, generated when the object changes 
} 

So eventOuts are the faucets that events come out of.  Where do events go?  Well, the names probably tipped you off already: eventOuts go to eventIns.  That's where routing comes in.
 

Routing

Let's suppose we have two objects, each with a Material node. Let's write down the spec definition for the Material node class:

Material { 
  exposedField SFFloat ambientIntensity  0.2         # [0,1]
  exposedField SFColor diffuseColor      0.8 0.8 0.8 # [0,1]
  exposedField SFColor emissiveColor     0 0 0       # [0,1]
  exposedField SFFloat shininess         0.2         # [0,1]
  exposedField SFColor specularColor     0 0 0       # [0,1]
  exposedField SFFloat transparency      0           # [0,1]
}

Let's imagine that we have two spheres. Let's also imagine that we have some way to change the first sphere, and whenever we change that color we also want the color of the second sphere to change.  The first thing we need to do is give each of the Material objects associated with those spheres a name:

(We're ignoring where these statements would live in the actual VRML file and the fact that it may sometimes be more convenient to name the Shape nodes instead).

Now you use a ROUTE statement from Material_1's eventOut to Material_2's eventIn:

That's it.  Remember we said that whenever the value changes, you'll get an eventOut.  Whenever the diffuseColor in Material_1 changes, the eventOut will activate the eventIn on Material_2 and the diffuseColor in Material_2 will be set to match.

 

Heuristic 6:  When you use a route to connect an eventOut with an eventIn: 

  • you set up a path for the dataflow to be sent
  • you set up a path for the timestamp to be sent
  • you permit the event (whenever it occurs) to invoke the eventIn method, send the dataflow, and send the timestamp.
 

Let's be a little more precise on that "whenever the value changes".  If you change the value of the emissiveColor, you'll get an event from emissiveColor_changed, but you won't get an event from diffuseColor_changed or any of the other eventOuts.  Only the event corresponding to the change will be generated.
 

Rule 2: Whenever the part of an object's internal state corresponding to an eventOut changes, the object generates an event. 

E.g., if an object's diffuseColor changes, it will generate a diffuseColor_changed event (but it won't generate any other kind of event).
 

Interpolators

We started off assuming that there was some way to change the diffuseColor of Material_1 so that when that color changed it could be sent to Material_2.  How would we do that?

One way is from an interpolator.  There's a ColorInterpolator node (class), and if you look at the spec, you see that it has two exposedFields: key and keyValue, an eventIn: set_fraction, and an eventOut: value_changed:

ColorInterpolator { 
  eventIn      SFFloat set_fraction        # (-inf,inf) 
  exposedField MFFloat key           []    # (-inf,omf) 
  exposedField MFColor keyValue      []    # [0,1] 
  eventOut     SFColor value_changed 
}

ColorInterpolator objects have three eventOuts.  Let's list them:

eventOut MFFloat key_changed 
eventOut MFFloat keyValue_changed 
eventOut SFColor value_changed

Which one would you route to the eventIn on our Material node's eventIn?

eventIn  SFColor set_diffuseColor

Imagine that you haven't got a copy of the spec handy, because the spec describes exactly what all these fields, methods, and events are for.

Remember Rule 1, that if a method (eventIn) is expecting an event of a given type, it had better get an event of that type?  Well let's apply that rule backwards.  What eventOut of the ColorInterpolator will make the set_diffuseColor eventIn happy?

Just one: value_changed.  So the ROUTE statement from the interpolator (which we'll name MyColorInterpolator) to the material (which we already named Material_1) is:


Two things: first, our example just above was a little bit contrived.  You (usually) won't get anything useful out of any interface of a ColorInterpolator except the value_changed eventOut.  What you'll really do is look at the spec clause for each interpolator and pick an interpolator that sends an event of the type you need from its value_changed eventOut.  The spec writers wisely named the event you're looking for "value_changed" in every one of the interpolators to make that easy to do.

Second, if you're expecting the ROUTE statement you just wrote to be enough to make the color change, you'll be disappointed.  Following Rule 2, something has to change in the interpolator in order for it to generate a value_changed event, and nothing has done that yet.  A Material object can't just decide to change all by itself, and neither can an interpolator.

There's only one more step.  Set up some kind of sensor.  Unlike ordinary scene objects and interpolators, sensors can change because of some action by the visitor or the running of the VRML clock.  Let's make it a TimeSensor which we'll call MyTimeSensor.  And that ROUTE statement will look like this:

ROUTE MyTimeSensor.fraction_changed TO MyColorInterpolator.set_fraction

Let's take a look at the three routes together:

ROUTE MyTimeSensor.fraction_changed TO MyColorInterpolator.set_fraction 
ROUTE MyColorInterpolator.value_changed TO Material_1.set_diffuseColor 
ROUTE Material_1.diffuseColor_changed TO Material_2.set_diffuseColor

Something in MyTimeSensor changes (the value of the clock, which is running) causing it to send an event to MyColorInterpolator.  MyColorInterpolator responds by doing the interpolation and sending an event that changes the diffuseColor of Material_1.  Material_1 reacts to its diffuseColor being changed by sending an event to Material_2.  And finally, Material_2 reacts to the event by changing its diffuseColor.

That's called an event cascade.

So one little bit of tidying up and we're done with this section.  We said that events consist of dataflows, timestamps, and action requests.  The dataflows in this cascade are pretty straightforward.  Looking back to the spec, MyTimeSensor sends an SFFloat to MyColorInterpolator, which sends an SFColor to Material_1, and Material_1 sends an SFColor to Material_2.

Similarly the action requests are spelled out in the ROUTE statements of the cascade: MyTimeSensor is routed so that when the clock changes, it causes the set_fraction method of MyColorInterpolator to execute.  That in turn causes the set_diffuseColor method of Material_1 to execute, which causes the set_diffuseColor method of Material_2 to execute.

But what about the timestamp?

 

Rule 3:  All events in a cascade have the same timestamp.

That means that you can think of an event cascade as occurring in order all at the same time. Yes, that's a contradiction.  The spec has just gobs of stuff to say about timestamps and event ordering, but most of the words in the spec deal with unusual and even degenerate cases.  Since (by the time we're done with you) you're going the be the kind of VRML world builder who never writes degenerate code or routes, you can simply ignore all that difficulty.
 
Heuristic 7:  All the events in a cascade happen pretty much in the order you specify them.  If you write such a tangled series of routes that you can't understand what that order is, you're probably headed for trouble.
 

Summary

VRML objects (the objects in your world created from one of the 54 node classes) don't have variables.  Instead, they have:

Next lesson: Scripts