Moving worlds: behaviors for VRML

Gavin Bell, gavin@sgi.com
Chee Yu
Chris Marrin
Rob Meyers
Silicon Graphics

This document can be found at http://reality.sgi.com/employees/gavin/vrml/Behaviors.Dec1.html. It was last updated on Dec. 1, 1995.


Our goal is to design a set of extensions to VRML that give the user a much richer experience than is possible in VRML 1.0. We believe that the ability to interact with intelligent objects with simple behaviors, the ability to create animated 3D objects, and the addition of sound into 3D worlds will provide the rich, interactive experience that will enable a completely new set of applications of VRML.

Goals

Our design was guided by the following constraints (in rough order of priority):

Performance
We believe that speed is a key to a good interactive experience, and that it is important to design the system so that VRML browsers will be able to optimize the VRML world.
Scalability
Our goal is to allow the creation of very large virtual worlds. Any feature which limits scalability is unacceptable, and in several areas of our design we have purposely made certain things difficult to encourage the creation of scalable VRML worlds.
Composability
Related to scalability, we want to be able to compose VRML worlds or objects to create larger worlds. We assume that we will be able to compose worlds that are created by different people simply by creating a 'meta-world' that refers to the other worlds.
Authoring
We assume that sophisticated VRML authoring tools will be created, and wish to make it possible to perform most of the tasks necessary to create an interesting, interactive VRML world using a graphical user interface. We believe that VRML will not be successful until artists and creative people that are not interested in programming are able to create compelling, interactive VRML content.
Power
We must allow programmers to seamlessly extend VRML's functionality, by allowing them to create arbitrary scripts/applets/code that can then be easily re-used by the non-programmer.
Multi-user potential
We expect VRML to evolve into a multi-user shared experience in the near future, allowing complete collaboration and communication in interactive 3D worlds. We have attempted to anticipate the needs of multi-user VRML in our design, considering the possibility that VRML browsers might eventually need to support synchronization of changes to the world, locking, persistent distributed worlds, event rollback and dead reckoning in the future.

We will not know if our goals and constraints have been met until we have implemented this system in both a browser and an authoring system. However, we have considerable experience with both browsers (WebSpace Navigator) and authoring systems (WebSpace Author) for VRML, and believe that this design will strike a good balance between speed for the VRML spectator, power for the VRML hacker and ease of use for the VRML artist.

Overview

The essence of simple behaviors is changes to the world over time. With a single spectator interacting with the world, we identify three sources of change:

  1. User events from some device, translated into affects on the 3D world.
  2. Time.
  3. "Real-world" sources of input, accessed through some scripting/logic language, perhaps by a process running asynchronously to the VRML browser.

We are proposing a model for describing how changes are communicated between objects in the VRML scene. Objects are defined to send and/or receive "events" or "messages"; the names and types of events that can be sent or received is defined by each object. A generic object (the Logic node) is defined to allow user-defined processing and generation of events.

This change communication model is combined with a new prototyping capability that allows the encapsulation and re-use of objects, behaviors, or both.

For some applications of VRML (such as database visualization, scientific visualization, and possibly shared, multi-user, distributed worlds) it is desirable to allow the creation and modification of VRML scene graphs. We address this need by allowing arbitrary changes to the well-defined parts of the world.

Issue: allowing arbitrary changes may limit the optimizations a browser can perform, and may force a browser to maintain the VRML scene structure. I'll say more about this later in this document; having an intermediate version of VRML that does NOT allow arbitrary changes in response to an event may make it easier for some browser implementors to implement this proposal.

Specification

The following describe in detail what is proposed:

Routes

We propose that many of the current VRML nodes be redefined such they may receive events with names and types corresponding to their fields, with the effect that the corresponding field is changed to the value of the event received. For example, the Transform node may receive "setTranslation" events (of type SFVec3f) that change the Transform's translation (it may also receive "setRotation", "setScaleFactor", ...etc events).

In Gavin's opinion, some fields should not be allowed to change after file-read; for many browsers, allowing things like the fields of IndexedFaceSet or ShapeHints to change over time will be hard to implement (perhaps IndexedFaceSets are tesselated into triangles at file-read time, or normals calculated based on the creaseAngle of ShapeHints, or...).

A node that produces events of a given name (and a given type) may be routed to a node that receives events of the same type using the following syntax:

ROUTE NodeName.msgOutName -> NodeName.msgInName

The NodeNames are defined using the existing DEF mechanism; ROUTE follows the same rules for resolving references to nodes as USE (the ROUTE must occur after the nodes are DEF'ed).

The route is separate from either of the nodes to allow:

  1. Both event fan-out and event fan-in
  2. The easy creation of loops
  3. The creation of re-usable components that encapsulate behavior

Routes are not nodes; ROUTE is merely a syntactic construct for establishing event paths between nodes.

Issues regarding loops and fan-in of values are covered in the evaluation order section.

The issue of whether or not all routes are established when the world is read-in is covered below in the section discussing how scripts should be allowed to edit the scene.

Issue: Should you be allowed to ROUTE inputs to inputs as a way of 'slaving' inputs, so I'm connected to whatever is connected to something else? Gavin thought that was a good idea but changed his mind, because there are some nasty cases, like:
ROUTE foo.in -> bar.in
ROUTE bar.in -> foo.in # Uh-oh, what does this do...

Prototyping

Prototyping is a mechanism that allows the set of node types to be extended from within a VRML file.

PROTO

A prototype is defined using the PROTO keyword, as follows:

PROTO typename [ eventIn fieldtypename name
                    IS nodename.eventInName nodename.eventInName ... ,
                 eventOut fieldtypename name
                    IS nodename.eventOutName nodename.eventOutName ...,
                 field fieldtypename name IS nodename.fieldName,
                 ... ]
      node { ... }

A prototype is NOT a node; it merely defines a prototype (named 'typename') that can be used later in the same file as if it were a built-in node. The implementation of the prototype is contained in the scene graph rooted by node. The implemenation may not be DEF'ed or USE'ed.

The eventIn and eventOut declarations export events inside the scene graph given by node. Specifying the type of each event in the prototype is intended to prevent errors when the implementation of prototypes are changed, and to provide consistency with external prototypes. Specifying a name for each event allows several events with the same name to be exported with unique names.

Fields hold the persistent state of VRML objects. Allowing a prototype to export fields allows the initial state of a prototyped object to be specified by prototype instances.

The node names specified in the event and field declarations must be DEF'ed inside the prototype implementation. The first node DEF'ed in lexical (not traversal) order will be exported. It is an error (and results are undefined) if there is no node with the given name, or the first node found does not contain a field of the appropriate type with the given field name.

Prototype declarations have file scope, and prototype names must be unique in any given file.

A prototype is instantiated as if typename were a built-in node. A prototype instance may be DEF'ed or USE'ed. For example, a simple chair with variable colors for the leg and seat might be prototyped as:

PROTO TwoColorChair [ field SFColor legColor IS leg.diffuseColor,
        field SFColor seatColor IS seat.diffuseColor ]
     Separator {
        Separator {
            DEF seat Material { diffuseColor .6 .6 .1 }
            Cube { ... }
        }
        Separator {
            Transform { ... }
            DEF leg Material { diffuseColor .8 .4 .7 }
            Cylinder { ... }
        }
    }
# Prototype is now defined.  Can be used like:
DEF redGreenChair TwoColorChair { legColor 1 0 0  seatColor 0 1 0 }

USE redGreenChair # Regular DEF/USE rules apply

We're making distinctions between fields, which can be given an initial value but cannot be changed except by the node that they're contained in, and events, which (at least for the built-in nodes) are requests to change fields. So, if we want our TwoColorChair to have colors that can be changed, we'd need to expose the leg.setDiffuseColor 'eventIn' and seat.diffuseColor 'eventIn' events. All of which may make for confusing and wordy prototype declarations. Are there ever cases where you might want to ONLY allow initial values to be set, and NOT allow them to be changed later?

Would the PROTO syntax be clearer if we added curly braces after the prototype name that surrounded the interface declaration and the implementation? E.G:

PROTO TwoColorChair { [ eventIn ... IS ... ] Separator { ... } }

Also, does a prototype implementation that consists of more than one node make sense? For shapes, you can always bundle several up using a Separator. For properties... well, if Group doesn't go away you can just bundle up (say) a Material and a Texture inside a Group node... but Groups are supposed to go away...

Note: PROTO sort of gives people their non-instantiating DEF: PROTO foo [] Cube { } is roughly equal to DEF foo Cube { }, except that foo is now a type name instead of an instance name (and you say foo { } to get another cube instead of USE foo). Smart implementations will automatically share the unchanging stuff in prototype implementations, so the end result will be the same.

NodeReference

What if we wanted a prototype that could be instantiated with arbitrary geometry? For example, we might want to define a prototype chair that allowed the geometry for the legs to be defined, with the default (perhaps) being a simple cylinder.

VRML 1.1 will include the SFNode field type-- a field that contains a pointer to a node. Using SFNode, it is easy to write the first part of the PROTO definition:

PROTO Chair [ field SFNode legGeometry 

... but then we get stuck when we try to define the IS part of the prototype. We need some way of taking an SFNode field and inserting it into the scene. This can be accomplished with a new node, the NodeReference node:

NodeReference {
    nodeToUse NULL  # SFNode field  (NULL is valid syntax for SFNode)
    # eventIn SFNode setNodeToUse
    # -- may also receive SFNode setNodeToUse events to set nodeToUse
}

Functionally, NodeReference is a "do-nothing" node-- it just behaves exactly like whatever 'nodeToUse' points to (unless nodeToUse is NULL, of course, in which case NodeReference does nothing). For example, this would be a verbose way to add a Sphere to the scene:

NodeReference { nodeToUse Sphere { } }

NodeReference is only interesting if its nodeToUse field is exposed in a prototype (or it receives a nodeToUse event). So, for example, our Chair with arbitrary leg geometry (with a Cylinder default if none is specified) can be filled out as:

PROTO Chair [ field SFNode legGeometry  IS  NR.nodeToUse ]
     Separator {
        Separator {
            Transform { ... }
            DEF NR NodeReference { nodeToUse Cylinder { } }
        }
        Separator {
            Transform { ... }
            USE NR
        }
        ... would re-use leg with a USE NR, would have
        geometry for seat/back/etc...
    }

Using the Chair prototype would look like:

Chair {
    legGeometry Separator { Coordinate3/IndexedFaceSet/etc }
}

It might also make sense to share the same geometry between several prototype instances; for example, you might do:

Chair {
    legGeometry DEF LEG Separator { Coordinate3/IndexedFaceSet/etc }
}
... somewhere later in scene...
Chair {
    legGeometry USE LEG
}

Note that SFNode fields follow the regular DEF/USE rules, and that SFNode fields contain a pointer to a node; using DEF/USE an SFNode field may contain a pointer to a node that is also a child of some node in the scene graph, is pointed to by some other SFNode field, etc.

The NodeReference node has nice, clean semantics, and allows a lot of flexibility and power for defining prototypes. It also has some nice implementation side effects:

Browsers that want to maintain a different internal representation for the scene graph can implement NodeReference so that nodeToUse is read and the different internal representation is generated. Optimizations might also be performed at the same time:

Browsers that optimize scene graphs can implement NodeReference such that whenever nodeToUse changes an optimized scene is created. When rendering, the optimized scene will be used instead of the unotimized scene.

A really smart browser will figure out that nobody is using the unoptimized scene and may free it from memory.

Something else to think about: should a prototype be allowed to expose the fields or events of an SFNode that is passed in? For example:

PROTO Foo [ field SFNode transform      IS NR.nodeToUse,
            eventIn SFVec3f setPosition IS NR.nodeToUse.translation ]
Separator { DEF NR NodeReference { nodeToUse Transform { } } Cube { } }

This could be pretty powerful, but might also be painful to implement, since the type-checking would have to be done at run-time when NR.nodeToUse changed.

EXTERNPROTO

A second form of the prototype syntax allows prototypes to be defined in external files:

EXTERNPROTO typename [ eventIn fieldtypename name,
                       eventOut fieldtypename name,
                       field fieldtypename,
                       ... ]
            URL

In this case, the implementation of the prototype is found in the given URL. The file pointed to by that URL must contain ONLY a single prototype implementation (using PROTO). That prototype is then given the name typename in this file's scope (allowing possible naming clashes to be avoided). It is an error if the eventIn/eventOut declaration in the EXTERNPROTO is not a subset of the eventIn/eventOut declaration specified in URL.

Note: The rules about allowing exporting only from files that contain a single PROTO declaration are consistent with the WWWInline rules; until we have VRML-aware protocols that can send just one object or prototype declaration across the wire, I don't think we should encourage people to put multiple objects or prototype declarations in a single file.

We need to think about scalability when using nested EXTERNPROTO's. EXTERNPROTOS don't have bounding boxes specified like WWWInlines, and they might need them. I'm starting to think that we might need to add bboxCenter/Size fields to Separator instead of having them only on WWWInline; with animations possible, pre-specifying maximum-possible bounding boxes could save a lot of work recalculating bounding boxes as things move.

Incorporate the URN proposal and allow multiple URL's (in which case we need more syntax-- MFString instead of SFString...)

Logic: scripting

Arbitrary, user-defined responses to events are supported by a new type of node: the Logic node. A Logic node instance may consists of:

  1. A description of the event names and types that it may receive
  2. A description of the event names and types that it may generate
  3. A script that defines how the Logic node reacts to events
  4. A set of fields with values that define persistent state

A distinction is made between synchronous Logic nodes, which are executed after one or more input events are received, updating their fields and generating one or more output events, and asynchronous Logic nodes, which may do everything synchronous Logic nodes may do, but may also spontaneously generate output events.

Syntax

A Logic node's behavior is specified by two fields; an SFString field called "language" which specifies which (computer) language the script is in, and an SFString field called "script" that is either an in-line implementation of the script or, if the language field is "REMOTE", a URL that specifies where to fetch the code for the script.

If it is necessary, the language field could contain something like "REMOTE:fortran" to specify both that a URL is being used, and the language contained in that URL (for transport methods like ftp that do not provide the MIME type of their content).

The descriptions of the events that may be generated or received must be specified as part of the Logic node. Although this isn't strictly necessary to make the system function, we believe that this requirement will prevent errors, and will make it possible to create graphical editors that intelligently deal with Logic nodes. The syntax chosen for declaring the messages that can be sent or received is:

Logic {
    eventIn fieldtypename name
    eventOut fieldtypename name
}

The persistent state of a Logic node is stored in fields, which are declared similarly but which MUST have an initial value:

Logic {
    field fieldtypename name value
}

There may be as many eventIn, eventOut and field declarations as needed, in any order inside the Logic node. For example, a Logic node performs a "look-at" function given a look-from and a look-to point (producing an orientation to look from the look-from point towards the look-at point, assuming a fixed "up" vector) would produce orientation events, receive lookFrom and lookAt events, and would need fields to store the "current" lookFrom and lookAt. Such a Logic node could be declared as:

Logic {
    eventIn   SFVec3f lookFrom
    field     SFVec3f lookFrom
    eventIn   SFVec3f lookTo
    field     SFVec3f lookTo
    eventOut  SFRotation orientation
}

I think it will be annoying if you can't have event names that are the same as field names. Of course, you can't have two fields with the same name (or two eventIn messages or two eventOut messages). I'm being lazy here, but the rules need to be clearly expressed.

Then again, it might be clearer if names aren't shared-- you'd receive "setLookFrom" messages which set you "lookFrom" field, and you'd generate "sendOrientation" messages...

If the events that a Logic node responded to were just implicit in the actions of the script, the ROUTE mechanism would not be able to do correct type-checking (to ensure that something that produced a Color message wasn't communicating with something that expected Float messages), and graphical interfaces would not know which messages an arbitrary Logic node could send or receive without parsing the script-- something which we assume will NOT be possible (the script might be at a temporarily unavailable URL, it might be Java byte-code, it might be in a language that the graphical interface does not support, ...).

The lookFrom and lookTo fields could also be omitted, assuming that the scripting language supported the concept of 'static' variables that are maintained across the execution of routines. See the script internal state section below for more discussion about this.

Issues: should incorporate the URN proposal here and make the script field an MFString that can contain multiple URLs to be searched. Should a scripting language be required-- e.g. language "Java" be required? What's the default value for language-- "REMOTE"?

Scripts

We've stated in general terms what a Logic node can do-- it can receive events, update its internal and/or persistent state, and may send out events. There are several possibilites for how to map these capabilities into a specific API for a particular scripting language. We present pseudo-code (in a C++-like language) for various possibilites:

One event per execution, one method per event
A script might consist of several methods, each of which handles one event. In our look from/to example, this might look something like:
// Helper method to broadcast orientation given lookFrom, lookTo:
computeOrientation(SFVec3f lookFrom, SFVec3f lookTo) {
    SFVec3f lookFrom = GET_VEC3F_FIELD("lookFrom");
    SFVec3f lookTo = GET_VEC3F_FIELD("lookTo");
    SFRotation result = ... do some math...
    SEND_SFROTATION_EVENT("orientation", result);
}

// Handle lookFrom events:
lookFrom(SFVec3f value) {
    SET_VEC3f_FIELD("lookFrom", value);
    computeOrientation();
}

// Handle lookTo events:
lookTo(SFVec3f value) {
    SET_VEC3F_FIELD("lookTo", value);
    computeOrientation();
}

There are two disadvantages to this approach:

1. If the script receives both "lookFrom" and "lookTo" messages at the same time, it will recompute its result unnecessarily and will generate two events when it really only needs to generate one.

2. Implementing a "helper" routine to avoid duplication of code can be awkward, and having a different routine to be called for each event requires more of the Logic node implementation. It is simpler if each script is a single routine. It is definitely simpler for the implementor, it is debatable whether or not it is simpler for the script creator.

One event per execution, one method for all events
processEvent(Event event) {
    if (event.name == "lookFrom") {
        SET_VEC3F_FIELD("lookFrom", event.value);
    } else if (event.name == "lookTo") {
        SET_VEC3F_FIELD("lookTo", event.value);
    }
    SFRotation result = ... GET_FIELD current from/to, do some math...
    SEND_SFROTATION_EVENT("orientation", result);
}

This is simpler, but may still result in unnecessary re-computation and may generate redundant events if multiple lookFrom/lookTo events occur.

Multiple events per execution, one method for all events
processEvents(EventList events) {
    for (i = 0; i < events.length; i++) {
        Event event = events[i];
        if (event.name == "lookFrom") {
            SET_VEC3F_FIELD("lookFrom", event.value);
        } else if (event.name == "lookTo") {
            SET_VEC3F_FIELD("lookTo", event.value);
        }
    }
    SFRotation result = ... GET_FIELD current from/to, do some math...
    SEND_SFROTATION_EVENT("orientation", result);
}

This is simpler, but is also more efficient-- if there are multiple lookFrom/lookTo events waiting, they are all processed before doing any calculations or sending out any events. It is also flexible; scripts are still free to send out events inside the for (.. each event..) loop, allowing the emulation of one-event/one-method or one-event/multiple-methods.

Logic API

By "API", I mean the actual code that the VRML world creator will type into her Logic nodes (and not a lower-level API used to communicate between a VRML implementation and a scripting language, although the lower level API will likely be very similar to what is described here).

At least the following will be necessary:

  1. Some kind of initialization, process events, and cleanup routines. If a language does not support asynchronous threads, then the initialization and cleanup routines may not be necessary.
  2. Methods to get/set the fields of the Logic node
  3. Methods to get information out of input events
  4. Methods to generate output events
  5. Methods that allow the manipulation of the standard VRML data types (all of the field types-- SFFloat, MFVec3f, SFRotation, etc). The "editing the scene" section discusses what should or should not be allowed once a Logic node gets a pointer to a node, either from an SFNode field or from an SFNode input event.
  6. When receiving an input event, some way of determining which node generated that event. This can be used in combination with fan-in and Logic nodes that edit the scene graph to do some interesting things.

What other API needs to be supported? It might be useful to be able to ask "ISROUTED("eventName") to see if anybody is listening to your "eventName" output events-- if not, the script might be able to save doing some work. Sensors might get simpler if events are time-stamped..

Script internal state

Logic nodes can maintain either state that the browser knows about ("public state", which is implemented simply as fields of the Logic node) or internal state that the browser is not aware of. Scalable implementations will load and unload Logic nodes as different parts of the virtual world are loaded and unloaded into and out of memory (we assume that virtual worlds will be larger than available disk or memory). To maintain the illusion of a continuous virtual world, when unloading part of the world browsers will need to save any state that has changed since that part of the world was loaded. For example, if the user walks into a room, turns the lights on, the walks out of the room, the browser must remember that the lights in that room are on even if the room is unloaded from memory (or disk). When the user later walks back into the room, the browser will read the room from disk (or over the network). If the on/off state of the lights is known to the browser (it is a field, eventIn or eventOut of a Logic node, an eventOut of a sensor, etc) then all of this can happen automatically.

To make this saving/restoring process fast and small, authors should try to minimize the amount of public state. Any state that can be easily recreated should be stored as private state inside the Logic node. For example, a Logic node that eventOut the last 100 trade prices for a specific stock on the stock market might have public state of:

Logic {
   field SFString tickerSymbol "SGI"
   field SFString stockServer "stock://www.sgi.com/stockfeed"
   # ... sends MFFloat events that are last 100 prices or whatever...
}

Given the stock server and stock symbol, the eventOut can be recreated, so this is all the public state needed even though internally more information may need to be cached.

Evaluation Order

To guarantee that a behavior produces similar results on different VRML implementations, rules are defined for determining the evaluation order of Logic nodes.

Authors need a well-defined, consistent mental model of how behaviors interact. The challenge is to describe this model precisely enough so that authors can be confident that they are creating behaviors that will work across implementations, but also to allow enough freedom so that different implementations of behaviors are possible.

FILL THIS IN:

-- processEvents called when there are events waiting
? is it Ok for an implementation to call processEvents if the eventList has zero entries?

-- If there are N events queued, an implementation may either call processEvents once, or may call processEvents N times, there are no guarantees.

-- Events generated from user-input are always ordered, with earlier events appearing first in message queues. Need to think hard about rules for events generated by Logic nodes in response to user-input events; something like events that are generated are time-stamped with the same time as the last event from the event list... or something... One of our goals is to be able to detect "bad fan-in"-- getting two events with the same name that sort to exactly the same place in the message queue. A warning that results are indeterminate is the appropriate response to detection of bad fan-in.

-- Loops must be broken; leaving it up to the author to break loops has the big disadvantage of making it more difficult to create re-usable components, but has the advantages of requiring less work for the implementor, and potentially faster execution since loops detection is not constantly happening, and allows authors to implement their own criteria for terminating loops. Automatic loop detection and termination rules can be added to the evaluation model later if authoring explicit loop termination is too error-prone or difficult.

-- All relevant event routes must be completely clear before results are presented to the user. "Relevant" event routes are those whose results the user can perceive. For example, if there are events waiting to change the translation and rotation fields of a Transform node, an implementation must not render a frame in which the translation field is changed but the rotation field is not. We feel that it is important to allow irrelevant events to be queued up by the system if doing so does not change the user experience (for example, if the user is waiting for the definition of an EXTERNPROTO to come across the network, a browser might allow the user to push buttons that affect the EXTERNPROTO'ed object, queuing up the input events to the EXTERNPROTO until its implementation was loaded and processing them when it arrived).

Asynchronous Logic

To support sources of input from outside the virtual world (Logic nodes that spontaneously produce events), it may be desireable to allow Logic nodes to run as asynchronous processes. In this case, the Logic nodes function very much like the built-in Sensor classes.

The evaluation model remains the same. The asynchronous process must be synchronized with the browser whenever it does any of the following:

The browser/script API will need at least the following methods:

start
Some start or init method, to allow the script to startup an asynchronous process.
There are some issues here; can the start method generate output events? Not a big issue, unless routes can be dynamically established.
processEvents
This is the same as a synchronous Logic node. Typically, the synchronous processEvents will arrange to communicate with the asynchronous process (started by the start method); either waiting for a response and sending out events based on info from the asynchronous process, or not-- the synchronous and asynchronous processes have complete flexibility in how they communicate and when the send messages.
end
The browser will call the end method when the Logic node is destroyed. It will clean up anything allocated in the start method, including terminating any asynchronous processes started.
sync/unsync/set/get/send
If running asynchronously, the anynchronous loop will probably look something like:
do forever:
  wait for input from somewhere in the real world OR (perhaps)
       for a message from the synchronous processEvents method
  sync with browser
  get state of fields
  perform some calculation
  set state of fields, send messages, based on VRML and real-world inputs
  unsync with browser

Exactly what all this looks like in the scripting language depends on which scripting language is being used. And exactly what this API looks like in the implementation of the browser and scripting language depends on which language the browser is implemented in, the calling conventions for the operating system, how asynchronous processes are implemented, etc.

Editing the scene

VRML 1.1 will introduce the notion of SFNode/MFNode fields-- fields that contain pointers to one or many nodes.

For other field types, it is pretty straightforward to come up with an API that allows a script to work with those types-- floating point numbers, booleans, integers and stings are pretty common concepts for scripting languages (2D/3D points, vectors, and rotations aren't as common, but I think that the successful VRML scripting language will have to have some notion of these; doing 3D graphics in TCL just won't work).

However, a big issue is what operations can a Logic node support once it has access to a pointer to a node?

Some possible answers (with Gavin's opinion on what should be allowed now/later/never in parentheses):
-- Send the pointer out as part of an SFNode/MFNode event (now)
-- Get the node's name (now)
-- Find out what fields the node has (never-- I wrote "maybe later", but changed my mind...)
-- Get field values (never)
-- Set field value (never)
-- Get the node's children (maybe later, again may need to distinguish public/private kids)
-- Adds/removes children (again, maybe later)
-- Find out what events the node can send and receive (now)
-- Establish/break event ROUTEs between the Logic node and the node it has a pointer to (now)
-- Establish/break event ROUTEs between two nodes (now)
-- Directly send/receive events (never-- is equivalent to establishing routes...)
-- Find out who may send/receive events to/from the node (later/never-- I'm not sure...)

Another issue: Can Logic nodes generate node pointers "from thin air"? I say yes, and the script API could support a couple of ways of doing this:
-- "Compile" a node given a string with VRML in it (now)
-- Copy a node the script already has a pointer to (later- issues of deep vs shallow copy)

Another big issue is how can Logic nodes get pointers to nodes? To allow scalability and composability, my opinions are:
-- SFNode/MFNode events received (now)
-- SFNode/MFNode fields of the Logic node (now)
-- Nodes the Logic node creates itself (now)
-- The root of the .wrl file that the Logic node was read from (never)
-- The root of the entire scene graph (never)
-- The nodes I'm receiving messages from (later/never)
-- The nodes I'm sending messages to (later/never)
-- "Pseudo-nodes" that provide global information (now-- but just from an aesthetics viewpoint, I'd lobby for these being done as new Sensor classes...)

I'm being lazy again, and glossing over the particular API and semantics. Please bear with me...

Combining the NodeReference and the Logic nodes results in scripts that can produce geometry or edit parts of the scene graph. Combining this with the prototyping mechanism makes some pretty interesting things possible; for example, to define a Torus node you might:

PROTO Torus [ field SFFloat radius1 IS GENTORUS.radius1,
              field SFFloat radius2 IS GENTORUS.radius2 ]
  Separator {
    DEF NODEREF NodeReference { }
    DEF GENTORUS Logic {
      field radius1 1.0
      field radius2 0.1
      eventOut SFNode geometry
      
      script "reads radius1/radius2, generates
               Separator/Coordinate3/IndexedFaceSet,
               generates geometry as eventOut event at startup"
    }
    ROUTE GENTORUS.geometry -> NODEREF.setNodeToUse
  }

A Torus prototyped this was will look exactly as if it is a built-in node. New property nodes (based on existing properties) can also be defined in this way.

Because an SFNode field stores a pointer to a node, a Logic node may decide to maintain a pointer to a node it is generating as part of either its public or internal state. This allows a Logic node to make arbitrary changes to one or more parts of the scene graph.

More implementation notes: for this to work, NodeReference will have to notice that scene graph that nodeToUse points to has been changed, and may need to re-optimize (or regenerate an internal rep) the next time it is rendered. Really smart optimizing browsers might keep track of how often a NodeReference is changing, and not bother doing the optimizations if it is changing too often-- authors should still try to minimize which parts of the scene they allow to be changed arbitrarily, since browsers with different internal representations will take a significant hit if the scene's that NodeReference nodes point to are constantly changing.

For example, a Logic node might receive scene graph changes from a server. The VRML file would look like:

Separator {
  DEF __N NodeReference { }
  DEF __L Logic { ... eventOut SFNode gometry ... }
  ROUTE __L.geometry -> __N.setNodeToUse
}

And the Logic node would have methods that did:

start
Started up an asynchronous process to listen to the server (whose name/address might be a field or eventIn of the Logic node), construct an initial scene graph based on information from the sever and set the geometry eventOut to that scene. Also maintain a pointer to the scene so that the asynchronous process can:
asynchronous process
Wait until the server sends information about changes to the scene, then:
Synchronize with the browser (making changes while it was in the middle of rendering would be bad)
Make changes to the scene
Tell the browser we're done making changes
end
Exit asynchronous process cleanly.

Sensors: input

General notes: Given a "thick enough" API for Logic nodes, Sensors could all be implemented as prototyped Logic nodes. Although it may make sense to define such a rich API eventually, in the short term we believe it makes more sense to build that functionality into pre-defined classes. Later versions of the VRML spec might choose to describe these as pre-define prototypes, along with their implementation using Logic nodes.

TimeSensor

TimeSensors generate events as time passes. TimeSensors remains inactive until their startTime is reached. At the first simulation tick where real time >= startTime, the TimeSensor will begin generating time and alpha events, which may be routed to other nodes to drive continuous animation or simulated behaviors. The length of time a TimeSensor generates events is controlled using cycleInterval and cycleCount; a TimeSensor stops generating time events at time startTime+cycleInterval*cycleCount. cycleMethod controls the mapping of time to alpha values (which are typically used to drive Interpolators). It may be set to FORWARD, causing alpha to rise from 0.0 to 1.0 over each interval, BACK which is equal to 1-FORWARD, or it may be set to SWING, causing alpha to alternate 0.0 to 1.0, 1.0 to 0.0, on each successive interval.

The minTick field specifies how often the TimeSensor will generate output events; output events are guaranteed to be generated no less than one minTick interval apart. For example, setting minTick to 1.0 will guarantee that t TimeSensor will generate output events at most once per second. By default, minTick is zero and a TimeSensor will generate time events as often as possible (typically, as quickly as the world can be redrawn).

pauseTime may be set to interrupt the progress of a TimeSensor. If pauseTime is greater than startTime, time and alpha events will not be generated after the pause time. pauseTime is ignored if it is less than or equal to startTime.

If cycleCount is <= 0, the TimeSensor will continue to tick continuously, without a cycle interval; in this case, cycleInterval and cycleMethod are ignored, and alpha events are not generated. This use of the TimeSensor should be used with caution, since it incurs continuous overhead on the simulation.

Setting cycleCount to 1 and cycleInterval to 0 will result in a single event being generated at startTime; this can be used to build alarms and timers that go off periodically.

No guarantees are made with respect to how often a TimeSensor will generate time events, but at least one event must be generated if it is later than startTime and startTime was set to a time in the future.

FILE FORMAT/DEFAULTS
     TimeSensor {
          startTime 0          # SFTime (double-precision seconds)
          pauseTime 0          # SFTime
          minTick 0            # SFTime
          cycleInterval 1      # SFTime
          cycleCount    1      # SFLong
          cycleMethod  FORWARD # SFEnum FORWARD | BACK | SWING
          # eventIn  SFTime  setStartTime
          # eventIn  SFTime  setPauseTime
          # eventIn  SFTime  setCycleInterval
          # eventIn  SFLong  setCycleCount
          # eventIn  SFEnum  setCycleMethod    # FORWARD | BACK | SWING
          # eventOut SFTime  time
          # eventOut SFFloat alpha
     }

Oops, need to add a description of the SFTime field. We could use SFFloat's for time, except that if we expect to be dealing with absolute times( from Jan 1 1970 0:00 GMT perhaps) then 32-bit floats don't give enough precision. Inventor writes SFTime fields as 64-bit double-precision values, but stores time values internally as two 32-bit integers

Proximity Sensors

Proximity sensors are nodes that generates events when the viewpoint enters, exits, and moves inside a space. A proximity sensor can be activated or inactivated by sending it an "enable" event with a value of TRUE/FALSE. enter and exit events are generated when the viewpoint enters/exits the region and contain the time of entry/exit (ideally, implementations will interpolate viewpoint positions and compute exactly when the viewpoint first intersected the volume). As the viewpoint moves inside the region, position and orientation events are generated that report the position and orientation of the viewpoint in the local coordinate system of the proximity sensor.

There are two types of proximity sensors: BoxProximitySensor and SphereProximitySensor, differing only in the shape of region that they detect.

Issue: Providing position and orientation when the user is outside the region would kill scalability and composability, so those should NOT be provided (authors can create proximity sensors that enclose the entire world if they want to track the viewpoint wherever it is in the world). Position and orientation at the time the user entered/exited the region might also be useful, but I'm not convinced they're useful enough to add (and besides, you could write a Logic node with internal state that figured these out...).

BoxProximitySensor

The BoxProximitySensor node reports when the camera enters and leaves the volume defined by the fields center and size (an object-space axis-aligned box).

A BoxProximitySensor that surrounds the entire world will have an enter time equal to the time that the world was entered, and can be used to start up animations or behaviors as soon as a world is loaded.

FILE FORMAT/DEFAULTS
     BoxProximitySensor {
          center          0 0 0 # SFVec3f
          size            0 0 0 # SFVec3f
          enabled  TRUE         # SFBool
          # eventIn SFBool setEnabled 
          # eventOut SFTime enter
          # eventOut SFTime exit
          # eventOut SFVec3f position
          # eventOut SFRotation orientation
     }

SphereProximitySensor

The PointProximitySensor reports when the camera enters and leaves the sphere defined by the fields center and radius.

FILE FORMAT/DEFAULTS
     PointProximitySensor {
          center          0 0 0 # SFVec3f
          radius          0     # SFFloat
          # eventIn SFBool setEnabled 
          # eventOut SFTime enter
          # eventOut SFTime exit
          # eventOut SFVec3f position
          # eventOut SFRotation orientation
     }

Pointing Device Sensors

A PointingDeviceSensor is a node which tracks the pointing device with respect to its child geometry. A PointingDeviceSensor can be made active/inactive by being sent enable events. There are two types of PointingDeviceSensors; ClickSensor and DragSensors.

Pointing device sensors may be nested, with the rule being that the sensors lowest in the scene hierarchy will have first change to 'grab' user input; if user input is 'grabbed', sensors higher in the hierarchy will not have a chance to process it.

ClickSensor

The ClickSensor generates events as the pointing device passes over its child geometry, and when the pointing device is over its child geometry will also generate button press and release events for the button associated with the pointing device. Typically, the pointing device is a mouse and the button is a mouse button.

An enter event is generated when the pointing device passes over any of the shape nodes contained underneath the ClickSensor and contains the time at which the event occured. Likewise, an exit event is generated when the pointing device is no longer over any of the ClickSensor's children. isOver events are generated when enter/exit events are generated; an isOver event with a TRUE value is generated at the same time as enter events, and an isOver FALSE event is generated with exit events.

Should we say anything about what happens if the cursor stays still but the geometry moves out from underneath the ClickSensor? If we do say something, we should probably be conservative and only require enter/exit events when the pointing device moves...

Issue: enter/exit is for locate-highlighting ( changing color or shape when the cursor passes over you to indicate that you may be picked). Is that too much to ask from implementations?

If the user presses the button associated with the pointing device while the cursor is located over its children, the ClickSensor will grab all further motion events from the pointing device until the button is released (other Click or Drag sensors will not generate events during this time). A press event is generated when the button is pressed over the ClickSensor's children, followed by a release event when it is released. isActive TRUE/FALSE events are generated along with the press/release events. Motion of the pointing device while it has been grabbed by a ClickSensor is referred to as a "drag".

As the user drags the cursor over the ClickSensor's child geometry, the point on that geometry which lies directly underneath the cursor is determined. When isOver and isActive are TRUE, hitPoint, hitNormal, and hitTexture events are generated whenever the pointing device moves. hitPoint events contain the 3D point on the surface of the underlying geometry, given in the ClickSensor's coordinate system. hitNormal events contain the surface normal at the hitPoint. hitTexture events contain the texture coordinates of that surface at the hitPoint, which can be used to support the 3D equivalent of an image map.

FILE FORMAT/DEFAULTS
     ClickSensor {
          enabled  TRUE  # SFBool
          # eventIn  SFBool    setEnabled
          # eventOut SFTime    enter
          # eventOut SFTime    exit
          # eventOut SFBool    isOver
          # eventOut SFTime    press
          # eventOut SFTime    release
          # eventOut SFBool    isActive
          # eventOut SFVec3f   hitPoint
          # eventOut SFVec3f   hitNormal
          # eventOut SFVec2f   hitTexture
     }

DragSensors

A DragSensor tracks pointing and clicking over its child geometry just like the ClickSensor; however, DragSensors track dragging in manner suitable for continuous controllers such as sliders, knobs, and levers. When the pointing device is pressed and dragged over the node's child geometry, the pointing device's position is mapped onto idealized 3D geometry.

DragSensors extend the ClickSensor's interface; enabled, enter, exit, isOver, press, release and isActive are implemented identically. hitPoint, hitNormal, and hitTexture events are only updated upon the initial click down on the DragSensors' child geometry. There are five types of DragSensors; LineSensor and PlaneSensor support translation-oriented interfaces, and DiscSensor, CylinderSensor and SphereSensor establish rotation-oriented interfaces.

LineSensor

The LineSensor maps dragging motion into a translation in one dimension, along the x axis of its local space. It could be used to implement the 3-dimensional equivalent of a 2D slider or scrollbar.

FILE FORMAT/DEFAULTS
     LineSensor {
          minPosition         0     # SFFloat
          maxPosition         0     # SFFloat
          enabled  TRUE  # SFBool
          # eventIn  SFBool    setEnabled
          # eventOut SFTime    enter
          # eventOut SFTime    exit
          # eventOut SFBool    isOver
          # eventOut SFTime    press
          # eventOut SFTime    release
          # eventOut SFBool    isActive
          # eventOut SFVec3f   hitPoint
          # eventOut SFVec3f   hitNormal
          # eventOut SFVec2f   hitTexture
          # eventOut SFVec3f   trackPoint
          # eventOut SFVec3f   translation
     }

minPosition and maxPosition may be set to clamp the translation events to a range of values as measured from the origin of the x axis. If minPosition is less than or equal to maxPosition, translation events are not clamped. trackPoint events provide unclamped drag position along the x axis.

PlaneSensor

The PlaneSensor maps dragging motion into a translation in two dimensions, in the x-y plane of its local space.

FILE FORMAT/DEFAULTS
     PlaneSensor {
          minPosition       0 0     # SFVec2f
          maxPosition       0 0     # SFVec2f
          enabled  TRUE  # SFBool
          # eventIn  SFBool    setEnabled
          # eventOut SFTime    enter
          # eventOut SFTime    exit
          # eventOut SFBool    isOver
          # eventOut SFTime    press
          # eventOut SFTime    release
          # eventOut SFBool    isActive
          # eventOut SFVec3f   hitPoint
          # eventOut SFVec3f   hitNormal
          # eventOut SFVec2f   hitTexture
          # eventOut SFVec3f   trackPoint
          # eventOut SFVec3f   translation
     }

minPosition and maxPosition may be set to clamp translation events to a range of values as measured from the origin of the x-y plane. If the x or y component of minPosition is less than or equal to the corresponding component of maxPosition, translation events are not clamped in that dimension. trackPoint events provide unclamped drag position in in the x-y plane.

DiscSensor

The DiscSensor maps dragging motion into a rotation around the z axis of its local space. The feel of the rotation is as if you were 'scratching' on a record turntable.

FILE FORMAT/DEFAULTS
     DiscSensor {
          minAngle           0       # SFFloat (radians)
          maxAngle           0       # SFFloat (radians)
          enabled  TRUE  # SFBool
          # eventIn  SFBool     setEnabled
          # eventOut SFTime     enter
          # eventOut SFTime     exit
          # eventOut SFBool     isOver
          # eventOut SFTime     press
          # eventOut SFTime     release
          # eventOut SFBool     isActive
          # eventOut SFVec3f    hitPoint
          # eventOut SFVec3f    hitNormal
          # eventOut SFVec2f    hitTexture
          # eventOut SFVec3f    trackPoint
          # eventOut SFRotation rotation
     }

minAngle and maxAngle may be set to clamp rotation events to a range of values as measured in radians about the z axis. If minAngle is less than or equal to maxAngle, rotation events are not clamped. trackPoint events provide unclamped drag position in the x-y plane.

CylinderSensor

The CylinderSensor maps dragging motion into a rotation around the y axis of its local space. The feel of the rotation is as if you were turning rolling pin.

FILE FORMAT/DEFAULTS
     CylinderSensor {
          minAngle           0       # SFFloat (radians)
          maxAngle           0       # SFFloat (radians)
          enabled  TRUE  # SFBool
          # eventIn  SFBool     setEnabled
          # eventOut SFTime     enter
          # eventOut SFTime     exit
          # eventOut SFBool     isOver
          # eventOut SFTime     press
          # eventOut SFTime     release
          # eventOut SFBool     isActive
          # eventOut SFVec3f    hitPoint
          # eventOut SFVec3f    hitNormal
          # eventOut SFVec2f    hitTexture
          # eventOut SFVec3f    trackPoint
          # eventOut SFRotation rotation
          # eventOut SFBool     onCylinder
     }

minAngle and maxAngle may be set to clamp rotation events to a range of values as measured in radians about the y axis. If minAngle is less than or equal to maxAngle, rotation events are not clamped.

Upon the initial click down on the CylinderSensors' child geometry, the hitPoint determines the radius of the cylinder used to map pointing device motion while dragging. trackPoint events always reflects the unclamped drag position on the surface of this cylinder, or in the plane perpendicular to the view vector if the cursor moves off of this cylinder. An onCylinder TRUE event is generated at the initial click down; thereafter, onCylinder FALSE/TRUE events are generated if the pointing device is dragged off/on the cylinder.

SphereSensor

The SphereSensor maps dragging motion into a free rotation about its center. The feel of the rotation is as if you were rolling a ball.

FILE FORMAT/DEFAULTS
     SphereSensor {
          enabled  TRUE  # SFBool
          # eventIn  SFBool     setEnabled
          # eventOut SFTime     enter
          # eventOut SFTime     exit
          # eventOut SFBool     isOver
          # eventOut SFTime     press
          # eventOut SFTime     release
          # eventOut SFBool     isActive
          # eventOut SFVec3f    hitPoint
          # eventOut SFVec3f    hitNormal
          # eventOut SFVec2f    hitTexture
          # eventOut SFVec3f    trackPoint
          # eventOut SFRotation rotation
          # eventOut SFBool     onSphere
     }

The free rotation of the SphereSensor is always unclamped.

Upon the initial click down on the SphereSensors' child geometry, the hitPoint determines the radius of the sphere used to map pointing device motion while dragging. trackPoint events always reflects the unclamped drag position on the surface of this cylinder, or in the plane perpendicular to the view vector if the cursor moves off of the sphere. An onSphere TRUE event is generated at the initial click down; thereafter, onSphere FALSE/TRUE events are generated if the pointing device is dragged off/on the cylinder.

VisibilitySensor

A visibility sensor generates visible/notVisible events, and can be used to make a script take up less CPU time when the things the script is controlling are not visible (for example, don't bother doing an elaborate walk-cycle animation if the character is not visible; instead, just modify its overall position/orientation).

There are some issues here about when visibility should be determined; I predict it will be a bit tricky to implement a browser that doesn't suffer from "off-by-one" visibility errors, but we might just leave that as a quality-of-implementation issue and not spec exact behavior (I'm sure we won't require exact visibility computation, anyway...).

FILE FORMAT/DEFAULTS
     VisibilitySensor {
          enabled  TRUE  # SFBool
          # eventIn  SFBool     setEnabled
          # eventOut SFBool     visible
     }

Interpolators : animation

Interpolators are nodes that are useful for doing keyframed animation. Given a sufficiently powerful scripting language, all of these interpolators could be implemented using Logic nodes (browsers might choose to implement these as pre-defined prototypes of appropriately defined Logic nodes). We believe that keyframed animation will be common enough to justify the inclusion of these classes as built-in types:

Issue: do we want to do automatic SF/MF type conversion? Or define different interpolators?

RotationInterpolator

This node interpolates among a set of SFRotation values. The values field must contain exactly as many rotations as there are keyframe times in the keys field, or an error will be generated and results will be undefined.

FILE FORMAT/DEFAULTS
     RotationInterpolator {
          keys            [ ]       # MFFloat
          values          [ ]       # MFRotation
          # eventIn  SFFloat    setAlpha
          # eventOut SFRotation outValue
     }

FloatInterpolator

This node interpolates among a set of SFFloat values. The values field must contain exactly as many numbers as there are keyframe times in the keys field, or an error will be generated and results will be undefined.

FILE FORMAT/DEFAULTS
     FloatInterpolator {
          keys            [ ] # MFFloat
          values          [ ] # MFFloat
          # eventIn  SFFloat    setAlpha
          # eventOut SFFloat    outValue
     }

ColorInterpolator

This node interpolates among a set of MFColor values, to produce MFColor outValue events. The number of colors in the values field must be an integer multiple of the number of keyframe times in the keys field; that integer multiple defines how many colors will be contained in the outValue events. For example, if 7 keyframe times and 21 colors are given, each keyframe consists of 3 colors; the first keyframe will be colors 0,1,2, the second colors 3,4,5, etc. The values are interpolated in RGB space.

The description of MF values in and out belongs in the general interpolator section above, or maybe we should split up the interpolators into single-valued and multi-valued sections.

FILE FORMAT/DEFAULTS
     FloatInterpolator {
          keys            0 # MFFloat
          values          0 # MFColor
          # eventIn  SFFloat    setAlpha
          # eventOut MFColor    outValue
     }

PositionInterpolator

This node interpolates among a set of Vec3f values, doing linear interpolation. This would be appropriate for interpolating a translation of a transformation.

FILE FORMAT/DEFAULTS
     PositionInterpolator {
          keys            0 # MFFloat
          values          0 # MFVec3f
          # eventIn  SFFloat    setAlpha
          # eventOut SFVec3f    outValue
     }

CoordinateInterpolator

This node interpolates among a set of multiple-valued Vec3f values, doing linear interpolation. This would be appropriate for interpolating vertex positions to do a geometric morph.

The number of colors in the values field must be an integer multiple of the number of keyframe times in the keys field; that integer multiple defines how many colors will be contained in the outValue events.

FILE FORMAT/DEFAULTS
     PositionInterpolator {
          keys            0 # MFFloat
          values          0 # MFVec3f
          # eventIn  SFFloat    setAlpha
          # eventOut MFVec3f    outValue
     }

NormalInterpolator

This node interpolates among a set of Vec3f values, suitable for transforming normal vectors.

The number of colors in the values field must be an integer multiple of the number of keyframe times in the keys field; that integer multiple defines how many colors will be contained in the outValue events.

FILE FORMAT/DEFAULTS
     VectorInterpolator {
          keys            0 # MFFloat
          values          0 # MFVec3f
          # eventIn  SFFloat    setAlpha
          # eventOut SFVec3f    outValue
     }

Need to add way to interpolate value of a Switch...

Examples

All of these examples are "pseudo-VRML" to save space (coordinates are not specified, etc). They also use a C or C++-like language for all Logic nodes.

A cube that changes color when picked

DEF CLICKSENSOR ClickSensor {
  DEF MATERIAL Material { }
  Cube { }

  DEF LOGIC Logic {
    eventIn SFBool isBeingClicked
    eventOut SFColor color
    script "processEvents(EventList events) {
              Color outputColor;
              for (int i = 0; i < events.getLength(); i++) {
                BoolEvent event = (BoolEvent) events[i];
                if (event.isTrue())
                  outputColor = Color(1,0,0);
                else
                  outputColor = Color(0,1,0);
              }
              SEND_COLOR_EVENT(\"color\", outputColor);
            }"
  }
  ROUTE LOGIC.color -> MATERIAL.setDiffuseColor
  ROUTE CLICKSENSOR.isActive -> LOGIC.isBeingClicked
}

The Logic node was defined as the first child of the ClickSensor. The colors it produces are hard-wired in the script, but could also have been provided as fields:

  DEF LOGIC Logic {
    field SFColor color1 0 0 0  # Defaults to black
    field SFColor color2 0 0 0
    eventIn SFBool isBeingClicked
    eventOut SFColor color
    script "... if (event.isTrue()) outputColor = GETFIELD(\"color1\");
                else outputColor = GETFIELD(\"color2\");"
    color1 1 0 0
    color2 0 1 0
  }
  ROUTE CLICKSENSOR.isActive -> LOGIC.isBeingClicked

Start a keyframe animation whenever a cube is picked

Separator {

  # This is the cube that will start the animation:
  DEF START ClickSensor {
    Cube { }
 
    # Need a time source and an interpolator:
    DEF TIME TimeSensor {
      # Animate for 10 seconds starting whenever the button is
      # released from the cube:
      interval 10.0
    } 
    DEF INTERP PointInterpolator {
        keys [ .... ]
        values [ .... ]
      }
    }

    # Wire things up:
    ROUTE START.release -> TIME.setStart
    ROUTE TIME.alpha -> INTERP.setAlpha
 }

  # And this is the object that will move:
  Separator {
    DEF TRANSFORM Transform { }
    ... objects to be animated ...

    ROUTE INTERP.outValue -> TRANSFORM.setTranslation
  }
}

This example just starts a 10-second keyframe animation that changes the translation of some objects when the user clicks and releases the mouse (or other pointing device) over the cube. To both animate and rotate the objects, we could just add a set of keyframes to the Transform's rotation field:

DEF ROTATE_INTERP RotationInterpolator {
       keys [ ... ] values [ ... ]
     }
}
ROUTE TIME.alpha -> ROTATE_INTERP.setAlpha
ROUTE ROTATE_INTERP.outValue -> TRANSFORM.setRotation

A Logic node could also be inserted between the Transform and the TimeSensor to (for example) start the animation 3 seconds after the cube is picked, or to only start an animation IF a button has been pressed or a puzzle has been solved, or to start a series of animations (e.g. animation #1 starts right away and lasts 14 seconds, animations #2 and #3 start 14 seconds from now and last 5 and 7 seconds, respectively, animation #4 starts when animation #2 ends (19 seconds from now), etc-- arbitrary scheduling and general modifications of time can be done using Logic nodes.

A simple toggle-button prototype

This is a very simple implementation of a button that alternately outputs TRUE/FALSE events when clicked:

PROTO ToggleButton [ field SFBool initialState  IS  LOGIC.state,
                     eventOut SFBool isOn  IS  LOGIC.eventOut ]
  DEF CLICKSENSOR ClickSensor {
    DEF LOGIC Logic {
      eventIn SFTime release
      field SFBool state FALSE
      eventOut SFBool state
      script "processEvents(Events events) {
                // Only need to change state if odd number of events:
                if ((events.length % 2) == 1) {
                   SFBool curState = GET_FIELD(\"state\");
                   curState = !curState;
                   SET_FIELD(curState);
                   SEND_EVENT(\"state\", curState);
              }"
    }
    ROUTE CLICKSENSOR.release -> LOGIC.release
    ... Geometry of button is here...
  }

... later, to use a toggle button:
DEF TB ToggleButton { initialState FALSE }
DEF LIGHT PointLight { }
ROUTE TB.state -> LIGHT.setOn

This is overly simple; a real implementation would also have a Switch node that changed the button's appearance depending on its state. A good implementation would also use the isOver eventOut of the ClickSensor to locate-highlight the button.

HSV color space Material node

This prototype demonstrates why considering properties first-class objects is powerful. An HSV equivalent of the Material node is defined:

PROTO HSVMaterial
  [ eventIn MFColor setAmbientColor IS CONVERT.ambientIn,
    field   MFColor ambientColor IS M.ambientColor,
    eventIn MFColor setDiffuseColor IS CONVERT.diffuseIn,
    field   MFColor diffuseColor IS M.diffuseColor,
    eventIn MFColor setSpecularColor IS CONVERT.specularIn,
    field   MFColor specularColor IS M.specularColor,
    eventIn MFColor setEmissiveColor IS CONVERT.emissiveIn,
    field   MFColor emissiveColor IS M.emissiveColor,
    eventIn MFFloat setShininess IS M.setShininess,
    field   MFColor shininess IS M.shininess,
    eventIn MFFloat setTransparency IS M.setTransparency,
    field   MFColor transparency IS M.transparency ]
# Implementation: Material hooked up to Logic:
  Group {
    DEF M Material { }
    DEF CONVERT Logic {
       eventIn MFColor ambientIn
       eventOut MFColor ambientOut
       ... etc ...
       script "processEvents(Events e) {
           For all events:
              switch on event name
                 convert HSV to RGB and send out event
           done."
    }
    ROUTE CONVERT.ambientOut -> M.setAmbientColor
    ROUTE CONVERT.diffuseOut -> M.setDiffuseColor
    ... etc, I'm being lazy again...
  }

Note that attaching a ColorInterpolator (which interpolates in RGB space) to an HSVMaterial gives color interpolation in HSV space (the "RGB" values that are being interpolated will be translated into HSV values as they're interpolated by the HSVMaterial's logic).

Useful properties such as a camera defined by a look-at and a look-up direction, or a Transform whose rotation is defined by yaw/pitch/roll values could also be implemented this way.

Issue: I had to use a Group node to group together the Logic and Material nodes as a single prototype-- but Group nodes are being removed from VRML. We could not allow new properties to be prototyped like this (I _like_ this feature, though), extend the PROTO syntax to allow multiple nodes for a prototype implementation, redefine Logic so that it is not a node but is a something-else... or just keep Group.

Talking to a multi-user Avatar motion server

There are two main parts to this example: Reporting "our" position to the Avatar server, and getting Avatar geometry and the positions of all Avatars from the server. The overall structure of the world looks like:

#VRML V1.1 utf8
Separator {
    DEF PositionReporter BoxProximitySensor {
       ...
    }
    DEF SEND_TO_SERVER Logic {
       eventIn SFVec3f setCurrentPosition
       script ...
    }
    ROUTE PositionReporter.position -> SEND_TO_SERVER.setCurrentPosition
    ...AvatarsGeometry...
    Separator { ... geometry of the world... }
}

So, first, the task of reporting "my" position to the server is accomplished by using a BoxProximitySensor that is big enough to surround the entire world, with my currentPosition inside that world-sized region being reported to the server by using a Logic node. The Logic script processEvents method just sends the last reported position to the server (that's all assuming that your scripting language supports that kind of communication). Details are left as an exercise for the hacker.

The second part of the problem is displaying the avatars. To make this easier to read, lets define a "FromServer" prototype that lets us refer to a node that we get from the server:

PROTO FromServer [ field SFString objectName IS L.objectName,
                   field SFString serverAddr IS L.serverAddr ]
    Separator {
      DEF NODEREF NodeReference { }
      DEF L Logic {
          field SFString objectName ""
          field SFString serverName ""
          eventOut SFNode geometry
          script "....something..."
      }
      ROUTE L.geometry -> NODEREF.setNodeToUse
    }

The script would ask the server (given in serverName) for the geometry for some object (whose name is given in objectName). If that object might change over time, the script will start an asynchronous process that sends geometry events whenever it gets a change message from the server.

So, in our example, the "...AvatarsGeometry..." would actually simply be:

FromServer {
    objectName "AllAvatars"
    serverName "...some protocol/address..."
}

When an Avatar moves, the server might tell the script to just modify some transformation in the AllAvatars scene graph. That will work well for browsers that don't optimize the scene and that use the scene graph as their internal representation; it won't work very well for other browsers. To be most efficient for those other browsers, the AllAvatars scene graph could itself contain FromServer NodeReference objects, like this:

# AllAvatars scene created by script (from server info):
Separator {
    # Avatar 1
    Separator {
        FromServer {
            objectName "Avatar1Transform"
            serverName "...vrtp://www.sgi.com/Q_14_12_server"
        }
        ... geometry for avatar 1
    }
    # Avatar 2
    Separator {
        FromServer {
           objectName "Avatar2Transform"
           serverName "...."
        }
    }
    ... etc, for each avatar in this world
}

Now, the server will frequently generate changes to the Avatar#Transform objects, but will rarely generate changes to the top-level AllAvatars object (e.g. perhaps that changes only when somebody decides to change how they look, or when a new avatar enters/leaves the world). Using NodeReferences in this way to mark the parts of the scene that will change often will allow implementations to be smarter about their optimization or conversion to their internal formats.