Contributors

Taming VRML Scripts

by Bob Crispen
 
Part 2: Scripts

In our first lesson we talked about VRML objects (individual nodes) and how eventIns were handles for methods that execute when events arrive.  Until now we've had to assume that there was some underlying code that, for example, changes the appearance of an object when the method "set_diffuseColor" executes.  That was kind of abstract, and you may have wondered why I bothered to mention that when it all happens behind the scenes.

Here's why: in a Script node, you get to specify the methods, in a programming language like Java or ECMAScript.  In our examples we'll be using ECMAScript exclusively, but any programming language that the browser supports will do.

So let's write a script.  Here's one:

DEF Pointless Script { 
  eventIn  SFColor colorIn 
  eventOut SFColor colorOut 
  url "javascript: 
    function colorIn(value, timestamp) { 
      colorOut = value; 
    } 
  " 
}

Here's some routes that will plug this script into the scene and make it execute:

ROUTE Material_1.diffuseColor_changed TO Pointless.colorIn 
ROUTE Pointless.colorOut TO Material_2.set_diffuseColor

This is, of course, a totally useless script.  It merely copies the input to the output.  So we could just as easily have forgotten about the script and written the following route:

ROUTE Material_1.diffuseColor_changed TO Material_2.set_diffuseColor

But right now we aren't worried about writing a killer script.  We're interested in finding out how scripts work.

The first thing you should notice is that Pointless has an eventIn called "colorIn" and it has a function of the same name.  Any event arriving on that eventIn will execute the function of the same name.  We said earlier that eventIns were methods.  Now that you've written one, you know why.  What would happen if you forgot (or misspelled) the function name?  Nothing.  There's no method corresponding to the eventIn, so nothing happens.

The second thing you should notice is that the function (method) colorIn has two arguments: value and timestamp.  Remember when we said that an event consisted of a dataflow, a timestamp and a run request?  Now you know why: the "value" is the dataflow.  Whatever data gets routed to the colorIn eventIn will show up in the "value" argument.  And the timestamp will show up in the "timestamp" argument.

There's nothing magic about the names "value" and "timestamp".  You can call your function arguments "foo" and "bar" or anything else that strikes your fancy.  The first argument will be the value of the dataflow, and the second, whatever you call it, will be the timestamp.  If you don't use the timestamp (as most of the time you won't) you can simply declare the function without it:

function colorIn(value) {

and the VRML browser (and its ECMAScript interpreter) will simply pretend there's only one argument.

The final thing you should notice about the Pointless script is that your script methods (in this case, we only have one: colorIn) are responsible for writing any eventOuts.

Once again, what happens if you forget to write to an eventOut (or misspell it)?  Nothing.  If you misspell an eventOut name, the ECMAScript interpreter will simply assume that you meant to declare a local variable of that name and will obediently create that variable and write to it.

In all our examples we will explicitly declare variables.  So if you see an example where we write to a misspelled eventOut name, you can assume the same thing as you'd assume about your own code: we made a mistake.

Now let's write a useful script:

DEF Diode Script { 
  eventIn  SFBool boolIn 
  eventOut SFBool boolOut 
  url "javascript: 
    function boolIn(value) { 
      if (value) 
        boolOut = value; 
    } 
  " 
}

Why is this a useful script when it seems to do little more than Pointless did?  Because this little guy only passes a true value.  That's useful for all sorts of things we'll talk about later, but there's one important thing it illustrates about scripts: if a FALSE event comes into boolIn, then the boolIn method won't write anything to boolOut.  And if it doesn't write anything to boolOut, then that event won't get generated this time.  In general:

One more thing along the lines of dusting and cleaning.  In all the other nodes except the Script node, the value of an internal state variable has to change for the event to be generated on the corresponding eventOut.  In a Script node, it doesn't matter whether the value you write to an eventOut is different from the value you wrote last time.  The act of writing to the eventOut is what matters, not that the value has changed.

You can think of the people who implemented the methods for all the other 53 nodes as following a design rule in their (invisible) methods: don't write to an eventOut unless the value has changed from last time.  Since those people were plenty smart, it's probably a good idea to keep in mind for the script methods you write.

Postponed till later

There are three other things you can do in Script nodes:

We'll introduce the former when we absolutely can't avoid it, the middle one some time soon, but we'll save the latter for the very end.  For now, we want you to have the main script paradigm firmly in your mind: you route to a script's eventIn methods, and you route from the script's eventOuts.  Data comes in the form of events, and, apart from one exception we'll mention very soon, there are no variables.  Only after this paradigm is thoroughly pounded into your head, so that there's no possibility of your being confused by these shortcuts, will we suggest you let your guard down a little.

A broken script

Now we know enough to look at a really horrible script.  And just for laughs, we'll make the script worse before we finally fix it.

Here's some code I actually wrote once in a script method:

Material_1.diffuseColor[0] += 0.1;

(Picky people will complain that you can't access the names of nodes directly in a Script, but I wanted to show you the effect of what I was trying to do, not the grammar.)

Now what in the world was I thinking?  What I wanted to do was increment the value of the red component of a color by 0.1.  Obviously, I was thinking that diffuseColor (which after all is an exposedField) was a variable.

But as we learned in our last chapter, it isn't.  VRML doesn't have any variables.  The only way to increment the red portion of a color that makes any sense given what we now understand about VRML's event model is:

and somewhere in the file we'll have:

ROUTE Material_1.diffuseColor_changed TO Oops.oldColor 
ROUTE Oops.newColor TO Material_1.set_diffuseColor

The reason for naming that script "Oops" will become clear in a minute.


It's worth taking a breath here before we go on to the next step, because we just introduced something very important in passing.  In a Script node you can have a field that you use to store values.  And you can access that field, but only from inside the same Script node.

So remember how we said earlier that a field was a parameter and wasn't of any use once you'd instantiated an object?  Well now we can think of it as working a little bit more subtly.

Imagine that fields have little doors on them.  When you instantiate a VRML object the door opens up long enough to let you write to that field.  But then the door slams shut again, and only the VRML object itself can access the field.
 

Heuristic 1 (Rev. B -- final version):  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 the field, but the object itself can read and write the field.
 

OK, back to our "improved" script to increment the red value of a color.

That's it.  Here's our Script node:
 

DEF Oops Script { 
  eventIn  SFColor oldColor 
  eventOut SFColor newColor 
  field    SFColor theColor 0 0 0 
  url "javascript: 
    function oldColor(value) { 
      theColor = value; 
      theColor[0] += 0.1; 
      newColor = theColor; 
    } 
  " 
}

And we'll have these routings:

ROUTE Material_1.diffuseColor_changed TO Oops.oldColor 
ROUTE Oops.newColor TO Material_1.set_diffuseColor

Now for heaven's sakes, don't go typing that in yet, because there are two very bad things about that script that we'll have to talk about.  We did in fact make it worse than the first version (which didn't work at all).  Worse than not working at all?  Sure.

The first problem is easy.  Experienced programmers will have noticed that we haven't guarded against overflow.  As soon as that red value exceeds 1.0 we can expect something bad to happen.  Either the browser is going to complain, or it's going to ignore the error and pretend the value is 1.0.  The second is much worse than the first; you won't catch the error, but somebody else will, and you'll look a whole lot less like a VRML master than you intended.

Our second problem is subtler, but just as important to understand: what's going to make the methods in this script execute?  There's only one, oldColor, and because it's attached to an eventIn of the same name, it'll execute whenever an event arrives on that eventIn.

When will the eventIn arrive?  We told it, through the ROUTE statement, that we wanted it to execute every time we get an eventOut from Material_1.diffuseColor_changed.  And when does that eventOut get generated?  Whenever the diffuseColor of Material_1 changes.  Clear so far?

Now when does the diffuseColor of Material_1 change?  There's only one way: when it receives an event on its set_diffuseColor eventIn.  And finally, when does it get that event?  Via the other ROUTE statement, whenever Oops's oldColor method writes to it.

Wait a minute.  That's an infinite loop!

The diffuseColor changes, causing an eventOut; the script executes and changes the diffuseColor; the diffuseColor changes, causing an eventOut....  And because the spec says that every node generates all its eventOuts when it initializes, you're guaranteed to execute that infinite loop.

Now if you're a masochist, you can think about the timestamps and puzzle over the spec words to figure out whether this will execute forever at initialization (or until it spits up on the overflow) or whether it'll take 11 cycles before it maxes out and breaks or whether the browser will break the loop and the color won't increment.  But why bother?  This script is broken, and the lesson you should learn from it is don't do that!

How do we fix this script?  First, we add a little logic:

if (theColor[0] <= 0.9) 
  theColor[0] += 0.1;

That should take care of the overflow.  Now let's fix the infinite loop.  Since we haven't talked about any way to get data and events into a Script besides eventIns, we'll simply add another eventIn.  The eventIn we already have, oldColor, will just write the value of the color it receives into the field, theColor.  But it won't do anything else.

The new eventIn, which we'll call incrementColor, will take care of writing the eventOut that we're routing to Material_1.  That breaks the infinite loop.

Here's a non-broken version of this Script:

DEF Better Script { 
  eventIn  SFColor oldColor 
  eventIn  SFBool  incrementColor 
  eventOut SFColor newColor 
  field    SFColor theColor 0 0 0 
  url "javascript: 
    function oldColor(value) { 
      theColor = value; 
    } 
    function incrementColor(value) { 
      if (value) { 
        if (theColor[0] <= 0.9) 
          theColor[0] += 0.1; 
        newColor = theColor; 
      } 
    } 
  " 
} 
... 
ROUTE Material_1.diffuseColor_changed TO Better.oldColor 
ROUTE Better.newColor TO Material_1.set_diffuseColor

Nothing makes incrementColor() execute yet, but that's OK.  Set up a TouchSensor (we'll call it Touched) and

ROUTE Touched.isActive TO Better.incrementColor

Easy.

Now there's still a little bit of crud left in the script.  First, let's talk about the obvious piece, then the less obvious one.

Obvious cleanup: we probably don't need to send out an event if we don't increment the color, so we can make our incrementColor method look like:

    function incrementColor(value) { 
      if (value) { 
        if (theColor[0] <= 0.9) { 
          theColor[0] += 0.1; 
          newColor = theColor; 
        } 
      } 
    }

There's another way of writing that, with just one if:

but some interpreters may generate code that executes a trifle more slowly than doing it with two ifs.  No big deal either way.

Less obvious cleanup: we're still going to get an eventIn from that route every time the diffuseColor changes, and executing incrementColor will change the diffuseColor.  The result is that we'll execute oldColor every time we execute incrementColor, and oldColor will set theColor to the very value incrementColor just finished setting it to!

For now, let's leave it.  We haven't got the tools at hand yet to finish cleaning it up.  But keep it in mind.  Every time you execute a script method that you don't need to execute, you waste cycles and slow down your frame rate needlessly.
 
To be continued.