next up previous
Next: Virtual Reality Applications Up: VRML Previous: VRML Scripts

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:

Get the color into the script. We'll choose to do that with an eventIn, and let's call it oldColor. Get a place to store the color. A field in a Script node is a good place. Let's call it theColor. Set up an eventOut from the Script that you'll write back to the color. Call it newColor.

So let's build the skeleton for our script:

DEF Oops Script { 
  eventIn  SFColor oldColor 
  eventOut SFColor newColor 
  field    SFColor theColor 0 0 0 
  url "javascript: 
    function oldColor(value) { 
      // we'll figure out what goes here in a minute 
    } 
  " 
}

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 (Revised 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.

In the script method, fetch the color by reading the value of the Script node's eventIn:

theColor = value;

Modify the red field of the color:

theColor[0] += 0.1;

Send the modified color back to the Material node:

newColor = theColor;

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

However don't go typing that in JUST 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).

How can it be worse than not working at all:

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!

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:

if (value && (theColor[0] <= 0.9)) {

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.


next up previous
Next: Virtual Reality Applications Up: VRML Previous: VRML Scripts
Dave Marshall
10/4/2001