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 25 October 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.

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 implementation of the API is split into two distinct parts, a Browser specific part supports initialising the language interpreter, passing it new behaviors to run, handling call backs from the interpreter to manipulate the scene graph or send messages, and passing messages to running applets.

The language specific part has to be able to load the interpreter, handle new behaviors - e.g. by spawning a thread or adding to a queue or whatever. A typical interpreter writer would also provide a library of callbacks that the applets could call to manipulate the scene graph. The interpreter also has (where supported by the language) to be able to receive messages, or method calls to running applets.

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.

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.

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.
Note how the parameters are passed as Field**, the browser should convert whatever parameters are given into temporary fields, so that they look the same to the Applet as a pointer to a field.

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 Pointers by the browser allowing them to be cached by the Applet.
Field* ResolveNode(char* name)
This call is used to turn a textual name for a node into a pointer to that node e.g.
ResolveField(NULL,"Windmill.Vane")
Note that because of the way interfaces are designed, the only fields a behavior can access are those that are declared with READ or READWRITE in a Separator, or otherwise determined by the spec to be accessable to behaviors.

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.

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);     

Receiving Events

When one of these events, or any other event is triggered from the Browser, the actionMethod is called as
    Applet::do(char* actionMethod, Node* eventOn, int eventType, Field** actionParameters)
This is typically converted by the interpreter to a call to the Applet
    <actionMethod>(Node* eventOn, int eventType, Field** actionParameters)
Note that special values will be required for the eventType field of Timer, and Field callbacks, so that they can be distinguished form PICK, GRAB etc.

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.

    <dogt;(actionMethod, Node* actionOn, Node* eventOn, int eventType, 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 an event directly to another Applet. With the call
    Browser::SendEvent(Node* destination, Applet* me, int eventType, 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);
  }  

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.

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.

TCL

BESoft

Physics

Open Inventor

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