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.
- Implementable - to ensure support across a variety of browsers
- Work across multiple platforms
- Support a number of languages
- Avoid a M platforms by N languages implementation burden.
- Work with new nodes, without changing the interpreters or browsers.
- 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.
- 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.
- 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.
- 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:
- Threading models - i.e. whether each applet is in the browser thread, or its own thread, or a common thread for the interpreter.
- How applets running in the SAME interpreter call each other - they are free to take back doors for efficiency reasons.
- How clever browsers integrate languages they are good at - for example an SGI browser will probably implement Open Inventor Engines directly, while Sun might implement Java directly.
- Execution Model - several different execution models are possible, this doesn't specify how behaviors should be implemented. Documentation of one way of doing it will probably get added as an appendix.
- Networking Model - this API supports many different networking models, in particular it supports the three existing approaches.
- Server based approaches such as World's VRML+ or Sony's.
- Multicasting or DIS based approaches.
- Direct extension of the API over sockets, as in SDSC's VRBS.
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
- More work is needed on control, authorisation and security issues. At the very least the appropriate hooks are needed to allow these to be added later.
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:
- Address Behavior node and inheritance
- Define data structures for all basic types
- Define structure of Field**
- Define a structure to pass MFFields - and add calls to return the entire MFVec3f etc.
- Are any new calls required to deal with newly defined nodes, or will the new PROTYPE and EXTERNPROTO keywords be sufficient along with Behaviors?
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
- Rob Meyers, Gavin Bell of SGI. informal discussions about the activation nodes,
- to Chee Yu of SGI for comments on efficiency
- the rest of the Worlds VRML+ team (Alan Steremberg, Kyle Hayes, Jeff Close) for tearing apart the earlier versions :-)