VRML Behaviors - an API

This document is part of a collection of VRML2.0 history at http://www.mitra.biz/vrml/vrml2/mw_history.html
This version was last updated on 12 Nov 95.
Feedback, and comments are welcome to: Mitra mitra@mitra.biz


Overview and Goals

The goal of this document is to present a straightforward API that is reasonable to implement, and efficient to run across a variety of platforms and languages.

This proposal presents a simple "thin" API consisting of the minimum set of calls that enable a language interpreter and browser to interact, while allowing for appropriate extensability.

An API for VRML must be.

  1. Implementable - to ensure support across a variety of browsers
  2. Work across multiple platforms
  3. Support a number of languages
  4. Avoid a M platforms by N languages implementation burden.
  5. Work with new nodes, without changing the interpreters or browsers.
  6. Work well with networking, while not being dependant upon the network protocol.

Dependencies

This proposal depends on the behaviors proposal http://www.mitra.biz/vrml/vrml2/vrml-behaviors.html Which defines nodes to specify the behavior, event handlers etc.

API - Object Model

Overview - two parts

The model taken here has three distinct elements.
  1. Browser
    Reads behavior files via HTTP or whatever, loads interpreters, and passes behavior files to them, handles callbacks from running Applets including those that manipulate the scene graph, and passes events to them. Passes messages between Applets.
  2. Interpreter
    A library or DLL that implements the language and can manage one or more running Applets passed to it by the browser. It will also translate calls from the browser into calls to specific Applets, and calls from the Applets into calls to the Browser.
  3. Applets
    Applications running in the interpreter, executing code supplied in the behavior files. Think of these as being equivalent to C++ objects or Java classes.

What is NOT specified

In order to give language writers as much flexibility as possible this spec does not specify:

Few simple calls

The API breaks down into a very few calls. This document will assume C++ for defining the API although this does not need to be a requirement. The calls take two forms, one form for calling from the browser to the library, or DLL or whatever that implements the interpreter, and the second form for the call from the interpreter to the Applet. In most cases we are specifying calls to the Applet - calls to the Interpreter are specified in a (slightly) platform dependant manner.

Starting and stopping the Applet

Starting the Applet is seperated into three stages, locating the interpreter, loading the program, and running the program.
Interpreter* LoadInterpreter(char* LanguageName)
Loads an interpreter (see below for what might happen if the interpreter isn't found), returns a pointer to the interpreter. The implementation of this method starts off in the browser code, where in a machine dependant way it will locate and load an interpreter. For example on Windows it could have a rule to find "java.dll".
This call should then call "LoadInterpreter" on the interpreter, to allow it to initialise or whatever is appropriate, for example some languages will want all calls to go through the same interpreter-object, others will want a seperate interpreter-object or OS thread for each behavior. This is hidden inside this call.
Note that this call happens once for EVERY applet loaded, however it may be a no-op after the first call.

Applet* Interpreter::Load(Node*, char* Program)
Load the program, sanitize it (e.g. remove calls to "eval"). The Node is passed so that the interpreter can create the appropriate pointers to the Node.
This call returns a pointer to an Applet, which is used to send messages to the Applet later, it returns NULL if the interpreter cannot run the program.

Applet::Init(Node*)
This has the interpreter run the program for the first time, it calls the "Init" function in the script. The Node* gives the program a reference, so that for example the same program can be run for a number of different Nodes, and so that the Applet can call back to the browser and tell it fields etc to change.

void Applet::End(Node*)
Is used to signal to the Applet that it should clean up and stop, after this point the Browser should not send any more calls to the Applet.

void Browser::Ended(Applet*)
Is used to signal the Browser that the Applet has cleaned up and exited.

Resolution

Original this was all written using strings being passed back and forwards across the API, however as Chee Yu pointed out, this makes optimisation hard. So, in this draft, Strings are explicitly turned into Cookies by the browser allowing them to be cached by the Applet. Note these are called "Cookies" to distinguish them from pointers to make it explicit that the behavior should not use these as addresses of something it can manipulate or read. So in examples here "Field*" refers to a Cookie for a Field.

Open: It is an open issue as to what an Applet should be able to resolve, opinions varied between "anything" and "only-fields" in the Script node. I believe consensus is emerging on allowing resolution of fields in your own Prototype, and on other named Prototypes and certain fields on certain other named nodes.

Field* ResolveField(Node* relativenode, char* name)
This call is used to turn a textual name for a node into a cookie for that node e.g.
ResolveField(NULL,"Windmill.Vane")
The relativenode field is used so that an applet can refer to a field in its own object without knowing its own name.

Changing Fields

It is desirable to be able to change arbritrary fields in the scene graph, as long as they are made public by declaring them with READWRITE. If we allow overloaded functions then this is easy. We have examples such as.
int Browser::SetField(Field* field, SFRotation(0,1,0,3.1415))
int Browser::SetField(Field* field, SFBool(TRUE));
In both these cases, field is obtained from the ResolveField call above.

If it is desired to avoid overloaded functions then we'll need to have seperate functions for each type e.g.

int Browser::SetFieldSFRotation(Field* field, SFRotation(0,1,0,3.14.15))
int Browser::SetFieldSFBool(Field* field, SFBool(TRUE))
Sometimes the Applet will need the value of a field, in which case we have for each SF* type.
SFRotation Browser::GetFieldSFRotation(Field* field)
SFBool Browser::GetFieldSFBool(Field* field)
MF* types are a little harder, I suggest.
int GetFieldMFNumber(Field* field)
To return the (possibly 0) number of values in the field

long GetFieldMFLong(Field* field, int n)
To get the n-th value from an MFLong.

int AppendFieldMFLong(Field* field, Long newvalue)
To add a new value to a MFLong - and return its index

DeleteFieldMFLong(Field* field, int n)
To remove the n-th value from an MFLong.

int findFieldMFString(Field* field, char* testvalue)
Enables some optimisation of string handling, by testing if testvalue is a member of the MFString.
Any approach that returns all the values with a single call may give us problems defining appropriate data structures that are cross-browser.

Although the need to optimise means that we need to seperate Resolution and the GetField call, for terseness of code, a set of calls will also be supported of the form:

char* GetNamedFieldSFString(char* name);
These will resolve the field, get the value, and free the pointer.

Adding and Deleting Nodes

With the new interface proposal, to be changed, nodes have to be pointed at by a field. Typically a node to be added will be compiled from the VRML first, and then a pointer to it will be added.
Node* Browser::CompileVRML(char* VRML)
Compiles some valid VRML into a snippet of Scene Graph (whose structure is browser dependant), returns NULL if VRML is invalid.

int Browser::AppendFieldMFNode(Field* parent, Node* child)
This requests the browser to append a node, created with CompileVRML or otherwise, as a child of parent.

int Browser::DeleteNode(Field* parent, int n)
Removes the pointer to the n-th child from the parent.

int Browser::InsertNode(Field* parent, int n, Node* child)
Insert new child at position "n", or if there are less than (n-1) children, insert at the end.
Open: It is an open issue as to whether, and if so how, an Applet should be able to read arbritrary geometry

Ref Counters

It is assumed that some browser dependant mechanism exists for keeping a count of References to a data structure, because of the asynchronous nature of the browser and Applet, it is essential that the browser not destroy a data structure which the Applet still has a pointer to. For this reason any time the Browser resolves a string into a pointer to a data structure, it increments the RefCount on that data structure, allowing the Applet to be sure that it will stay in existance. This means that the Applet must explicitly free any pointer it has, so that the Browser can delete the data structure if nothing else points to it.
void Browser::FreeNode(Node* old)
void Browser::FreeField(Field* old)

Render pausing and synchronization

Sometimes it is usefull to be able to group a set of events, so that they all appear in the same frame, for example deleting an object in one place, and adding somewhere else, you'd rather not have a frame where the object disappears. This is accomplished by sending the events between a pair of calls.
void Browser::RenderGroup(int start)
A typical example would be:
    Browser->RenderGroup(TRUE);
    Browser->DeleteNode(tablekids,1);
    Browser->AppendField(tablekids,newobj);
    Browser->RenderGroup(FALSE);

Event handling

Registering Events

The Applet needs to register to receive updates from the browser of certain activities. This could be done by compiling the VRML for an EventHandler, but this is a frequent enough requirement to be worth optimising. Its done through a set of calls which closely mirror the syntax of the EventHandlers.

    Node* Browser::AddEventHandler(Node* eventOn, int eventType, 
                    Node actionObject, char* actionMethod, 
                    Field** actionParameters);
    Node* Browser::AddTimerEvent(int type, time_t time, Node* actionObject, 
                    char* actionMethod, Field** actionParameters 
    Node* Browser::AddFieldEvent(Node* eventOn, Field* field, Node* actionObject, 
                    char* actionMethod, Field** actionParameters);     
Open: It is an open question as to what the actionParameters should look like - see the Behaviors document

Receiving Events

When one of these events, or any other event is triggered from the Browser, there are two possibilities, if the "actionMethod" is unspecified then a call will be returned which is specific to the event, for example.
    Applet::pick(Node* EventOn,Node* EventSentBy,SFTIME Time, 
                 SFVEC3F Coord, Field** actionParameters)
If the actionMethod is specified, then it will be called in the form:
    Applet::do(char* actionMethod, Node* eventSentBy, 
                Field** actionParameters)
This is typically converted by the interpreter to a call to the Applet
    <actionMethod>(Node* eventSentBy, Field** actionParameters)

On a non-object oriented language, the callbacks would need a first paramater to be the object the action is taking place on, i.e.

    do(actionMethod, Node* actionOn, Node* eventSentBy, 
                Field** actionParameters)

Removing Events

If the Applet is no longer interested in an event, then it can call.
    Browser::RemoveEvent(Node* event);
using the Node* event returned when the method was created.

Sending Events

The Applet can send a message directly to another Applet. With the call
    Browser::SendEvent(Node* destination, Applet* me, char* actionMethod, Field** actionParameters)
The browser will turn the "Applet* me" into a "Node*" pointer to the object this Applet is associated with, before passing on to the destination Applet.

Examples

These examples are written in C, obviously they would be written in Java or TCL or whatever language was being interpreted.

For example, to put a cube on two tables.

  {
    Node* table1 = ResolveField("Table1.top");
    Node* table2 = ResolveField("Table2.top");
    Node* newcube = CompileVRML("Cube { width 2 height 2 depth 2 }");
    AddFieldMFNode (table1, newcube);
    AddFieldMFNode (table2, newcube);

    // Now decrement Ref counts on pointers we have.
    FreeField(table1);
    FreeField(table2); 
    FreeNode(newcube);
  }  

Open issues / Further Work

Platform specific parts

This section needs filling out for each platform. Its probably going to be very similar between different applications on the same platform.

Apple

Apple Events are an obvious, but probably wrong, choice - an Apples Guru is needed here.

Windows (3.1, 95 and NT)

OLE or DLL are obvious choices, but a windows guru is needed here!

Unix

In Unix, shared dynamically linked libraries are probably the easiest way to go.

Sockets

SDSC has a way to extend the API directly over sockets, this needs writing up.

Language specific parts

Different languages will have specific implementation concerns, here are some comments, but this section needs replacing by people with expertise in those languages.

Java

Java requires that it run as a seperate thread, and wants to poll for events. The threading model is not specified in this API, the suggestion is that the API is implemented as a small library which just queues events for interpretation by the regular Java interpreter running in a seperate thread.

Extensability to new languages

One goal is to be able to add languages, with any of the above approaches, the interpreter is a seperate piece of code. This enables dynamic, or at least user-initiated, downloading. As a simple (but not neccessarily sufficient) solution, its suggested to create a known location (or one that can be set up in a config file) from which an interpreter can be downloaded. For example, if running on Windows, and needing Java, then the interpreter could be obtained from: http://vrml.org/windows/java.dll.

Obviously this brings up security concerns which is why the site should be specified in a config file, rather than in the VRML. Behind a firewall the MIS people can then specify a local site where only trusted interpreters are made available.

Open Issues

The following problems need sorting out before this can be finalised:

Acknowledgements

This paper condenses good ideas from a lot of other people's work. Thanks to the ideas from the papers from A document contrasting these papers is under construction - look at http://www.mitra.biz/vrml/vrml2/vrml-behavior-contrast.html

<Flame>: Please put URL's and email addresses on your papers - it makes it hard or impossible to cite them! </Flame>

Thanks also to