VRML Behaviors - an API
Mitra mitra@mitra.biz
Yasuaki Honda <honda@csl.sony.co.jp>,
Sony Computer Science Laboratory
Kouichi Matsuda <matsuda@csl.sony.co.jp>,
Sony Computer Science Laboratory
This is a living document, please fetch the latest version from http://www.mitra.biz/vrml/vrml2/vrml-api.html.
This version was last updated by Mitra 9 Dec 95 at 17:42.
This proposal should be read in conjunction with the rest of the behaviors proposal,
that specifies the nodes inside VRML that support this. It is available from
http://www.mitra.biz/vrml/vrml2/behaviors/behaviors.html
Overview and Goals
Note: SGI agrees with the goals and overall architecture presented in
this document. However, due to time limitations, SGI has not yet commented
on the details of this proposal.
After an event has arrived at a ScriptNode it is forwarded to an Applet
via either an embedded or plug-in interpreter. The goal of this document
is to present a straightforward API for the interfaces between the Browser,
the Interpreter and the Applet. It must be:
-
Implementable - to ensure support across a variety of browsers
-
Efficient - so that complex behaviors can run fast.
-
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.
-
Allow Applets to be written for any particular language so that they will
run on any platform that supports the API.
This API presents a detailed API that Applet authors can work to.
This API also specifies an interface between the Browser and an Interpreter,
this is intended so that plug-ins can be built that will work with a variety
of different browsers. This API is not required where the Browser supports
the language internally, and support of the Interpreter API, and whether
the Applet is running in an embedded, or plug-in (e.g. DLL) interpreter
should not change the Applet code at all.
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.
We believe that this API should be treated as experimental, to give
us a basis for interoperability. It remains to be seen what other functionality
will be required.
API - Object Model
Overview - three 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
An interpreter for the language which can manage one or more running
Applets passed to it by the browser.It may be embedded in the browser,
or be available via a plug-in model, for example a DLL, or shared-library.
The interpreter is responsible for forwarding calls from the browser to
calls to specific Applets, and calls from the Applets to 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
SGI's Webspace will incorporate Java directly, and not need the interface
to the Interpreter, while Paper Inc's WebFX is likely to go through Netscape
to it's Java plug-in using Netscape's plug-in interface. How this is done
is irrelevant as long as the calls arrive at the Applet in a manor that
is identical across platforms.
-
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. More discussion
of assumptions that Authors can make is likely to follow.
-
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. Each language will have to define
it's own binding to these calls, but these should take a one-to-one form
so that the glue layers can be extremely thin. 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.
Explanation of Terms
-
Cookie
-
An opaque value that can be thought of semantically as a pointer, however
the receiver of a cookie cannot assume its syntax for example to dereference
it. All that can be done with a cookie is to pass it back to the place
it came from in another call.
Cookies are represented as Foo* meaning a cookie that represents an
object of type Foo.
-
aaa::bbbb(cccc)
-
This syntax means placing a call to the object represented by "cookie"
aaa, calling function "bbbb" with paramaters "cccc" and "dddd".
Starting the Applet
Starting the Applet is seperated into three stages, locating the interpreter,
loading the program, and running the program.
-
Internal call to locate the interpret
The browser has to first locate a suitable interpreter for running
the Applet. If the interpreter is internal to the browser, then the interface
is entirely outside the scope of this API. If the interpreter is external
to the browser then it may involve significant processing, for example
to locate a suitable DLL.
-
Initialize the Interpreter
If the interpreter is external to the browser, previous step will have
resulted in a cookie allowing calls to the interpeter. The first of which
will be InitializeInterpreter(). which can be used by the interpreter
to set up internal data structures etc.
-
Downloading the Applet
The browser is responsible for locating and downloading the Applet.
Browsers already have, or have access to, this functionality so there is
no need to put it in the Interpreter.
-
Loading the Applet
The next stage is to load the program and sanitize it (e.g. remove
calls to "eval"). Since this step may take a while in some languages (for
example Perl), it can be performed at any time, allowing Applets to be
pre-loaded before they will cause noticable delay. The call.
Applet* Interpreter::Load(Node* ScriptNode, char* Program)
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. The Node*
paramater can be used by the Interpreter layer in order to send messages
back to the specific ScriptNode in the browser.
Initializing the Applet
Once the Browser decides the Applet is active, for example because
its outputs are visible for the first time, then it will send an Init method
to the Applet (see below for how this is done).
Applet::Init()
The Applet is guarranteed to get this before any other events, and can
use it to initialize internal data structures. It is also possible for
the Applet to use this opportunity to call back to the Browser to, for
example add Sensors on objects it is interested on, set optimization parameters
etc.
Issue: Should Init be a call to the Applet, or the first event, these
are equivalent in the syncronous case, but if the Applet wants to run in
a seperate thread, then the init call may be usefull to create a sepeperate
thread. This may be a language-dependant issue.
Sending methods to the Applet
One the Applet is loaded, all calls to it are sent in the same way.
The browser creates an event data structure. Looking like.
Structure {
event* nextEvent; // Pointer to the next event
char* eventName; // The Ascii name of the function being called
double* eventTime; // Time event occured (seconds since 1 Jan 1970 GMT)
enum eventType; // Specifies the data following
int dataSize; // The number of bytes following
bytes // The data that follows depends on the EventType.
}
Note that the eventTime is the time the input was first detected by the
system, not neccessarily the time it was processed by a sender, or that
this data structure was created.
The browser calls to the Interpreter.
int Interpreter::ProcessEvents(Applet*, Node*, Event*)
If the interpreter is internal to the browser, it is free to skip the step
above, and call directly to the Applet.
The interpreter converts this data structure into a corresponding one
that makes sense for the language being handled, for example a list of
Java classes. And then turns the Applet* cookie into a real address for
the Applet, and calls the applet.
int Applet::ProcessEvents(Node*, Event*);
The Applet will typically define ProcessEvents as a dispatcher, for example
- in C this might be.
ProcessEvents(Node* node, Event* eventList) {
for (event=eventList; event; event=event->nextEvent) {
if (strcmp(event->eventName,"Init") {
init(event->eventType, event->eventData);
} else if (strcmp(event->eventName,"SetColor") {
SetColor(event->eventType, event->eventData);
} else {
return VRMLAPI_NO_SUCH_EVENT;
}
}
Of course, because of the tedious boredom of programming the above it is
likely that a trivial macro will be provided to automate the process.
Issue: Three other approaches were considerd and discarded.
-
Direct calls from the interpreter to the function:
Unfortunately this requires the interpreter to know a lot about
the internal calls of the Applet.
-
Direct calls from the browser to the function
Unfortunately this will not work with most libraries (e.g. DLL's)
where the calls into the library have to be known at compile-time.
-
Sending just one event at a time, however this leads to inefficiencies
in the case where several inputs change at the same time, and outputs need
only be computed once.
By the time the Applet::ProcessEvent call has returned it must have finished
with, or copied the data, and the Interpreter is free to destroy it. By
the time the call to Interpreter::ProcessEvent returns the Interpreter
must have copied or be finished with the data, so that the Browser can
now destroy it.
Note that there is no requirement that the events have actually been
processed by the time the calls return. In some languages and platforms
the event processing will be scheduled asynchronously.
Events from the Applet to the Browser
The browser communicates back to the Browser by sending events via the
Interpreter. It does this through a single call:
SendEvent(Node* destination, Applet* self, Event* eventList);
Normally the Node* is that passed to the Applet with the Init call. Self
is a pointer to itself, and event is a data structure of the same format
as those it receives in calls to ProcessEvents.
The interpreter will convert the Event* data structure into a C data
structure, convert the Applet* pointer into an Applet* cookie and place
an identical call to the Browser.
It should be noted that if the Applet is to send events to anything
other than the Node* passed at Init time, then it must have the SIDEEFFECTS
flag set in the ScriptNode, so that the browser knows not to optimise this
node.
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 as a single
EventList, in one call to SendEvent.
Stopping the Applet
Terminating the Applet
When the Browser determines that it is finished with the Applet it first
signals the Applet to gracefully end whatever it is doing, for example
closing sockets, freeing up memory etc. It does this by sending the Applet
the "End" method. After this point, the browser should not send any more
events to the Applet.
Applet terminated
When the Applet has successfully cleaned up, it calls the interpreter with
a call:
Ended(self);
The interpreter cleans up its data structures for this Applet, and calls
back to the browser
Ended(Applet*);
Which signals the Browser that the Applet has successfully exited, and
that to run the Applet again will require a repeat of the LoadApplet and
Init sequence.
Resolution
The resolution calls are used by the Applet to aquire a pointer to a node
in the Browser, in order to send it events.
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.
Field* ResolveNode(Node* relativenode, char* name);
This call is used to turn a textual name for a node into a cookie for that
node e.g. The relativenode field is used so that an applet can refer to
a node within a particular context. This is an area for further discussion
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 cookie for a data
structure, or otherwise passes an Cookie to the Applet. 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 cookie it has, so that the Browser can delete the data structure if
nothing else points to it. This is done via a call to FreeNode(Node*).
Note that this cannot be done by a garbage collection mechanism, because
the browser has no access to the data structures of the Applet to see what
pointers exist.
Events
Events communicated between the browser fall into the following groups:
Generic Events
The ScriptNode specifies a set of events that the Script can send to the
ScriptNode, which will then be sent on by the Browser depending on the
ROUTEs set up. The Applet can only send events to a Node whose pointer
it has obtained in some way, for example by using the Resolution call above.
Changing Fields
Changing fields is done identically to sending generic events, except that
the names of the events to do so are defined in the VRML spec, or by means
of a Prototype declaration, for example by building an event{ eventType=SFColor
SFName="setDiffusiveColor" SFData=1 0 0}.
In order to make this painless, a set of Event constructors are defined,
so that for example.
sendEvent(Node*, this, t1 = eventSFColor("setDiffusiveColor",1,0,0)); destroy(t1);
can be used to send an event. Issue Is it worth defining a slightly
higher level call of the form.
sendEventSFColor(Node*, this, "setDiffusiveColor", 1,0,0);
to create an event, send the event, and destroy the event.
Sending MF events is only a little harder, with similar constructors
being called, e.g.
sendEvent(Node*, this, t1=eventMFFloat("doIt",1.0,3.1415,2E10));
Sometimes the Applet will need the value of a field, in which case we have
for each SF* type.
Open issue Getting fields needs a little more thought, since
the Applet may be running asynchronously, and so be unable to wait for
a return from the browser.
Needs have currently been identified for functions of the form
-
xxx GetFieldSFxxx(fieldname). To read a field
-
Node* FindFieldMFxxx(fieldname, xxx): To determine if xxx is a member of
a MFField.
-
int GetSizeMFxxx(fieldname): To determine how many elements in a MFField
-
xxx GetFieldMFxxx(fieldname, element)
Adding and Deleting Nodes
To edit the scene graph we have to be able to obtain pointers to parts
of it, turn VRML into a browser dependant data structure, and add and delete
those structures from the scene. Passing entire chunks of the Scene Hierarchy
across the API should be avoided wherever possible.
Typically the first thng that will be done is to compile the VRML. This
done by a call via the interpreter.
Node* Browser::CompileVRML(char* VRML)
This compiles some valid VRML into a Browser dependant snippet of Scene
Graph. It returns NULL if VRML is invalid. At this point the VRML data
structure is owned by the Browser, but is not rendered. It can be manipulated
using any of teh calss described here before being placed in the scene,
typically by sending a AppendNode(Node*) event to a Frame.
This is not done by sending an event because it needs to return before
the Applet can proceed.
We are assuming that the Frames proposal
is accepted. But this approach would also work , with minor changes, with the
existing Separators.
The Frame node will be changed to support the events needed to allow
editing of the scene graph.
Frame {
.... transform properties and events
# eventOut SFLong GetNoChildren # Find the number of children.
# eventOut SFNode GetChildN. # Return the n-th child.
issue: How to specify the parameter N
# eventIn SFNode setChildren # Replaces all children
# eventIn SFNode appendChild # Appends child
# eventIn SFNode DeleteChild # Removes a child from Frame
}
The events above allow for the arbritrary changes to a node once the Behavior
has a cookie for it. Note that the order of children in Frames is not important,
so there is no need for an event to insert a child in a particular order.
To discover exactly what VRML to pass to a browser, the Applet can call:
bool Interpreter::NodeSupported("NodeType");
Which will return true if the node is supported, and false otherwise.
Issue: It is an open issue as to whether a Behavior should be able
to create arbitrary geometry, however if access is restricted to things
we have cookies to, then adding a behavior that is similarly restricted,
to a node we have access to should not violate security.
Culling events
Gavin: Does not believe these are neccesary, and would use VisibilitySensors
instead In order for the script to optimise itself, for example to
reduce Geometry updates when not visible, it needs to be able to receive
events related to the system. This set of predefined events are sent by
the browser in response to various culling, and administration situations.
They are controlled by flags on the scriptNode.
-
SFBool "Cull"
Triggered when rendering is being culled, a behavior may suspend, or
scale back, its activities. The flag is set to TRUE when the object becomes
visible, FALSE when it disappears.
-
SFBool "World"
This is called with the parameter FALSE when the world is unloaded
and TRUE when loaded again. Note these are distinct from the Init and End
methods which are called when the applet is loaded and unloaded. The typical
semantics are that the Applet should save its state when called with FALSE,
and restore when called with TRUE.
-
FRAME frame(...)
Sent once per frame, this is usefull to enable optimal calculation
of animations etc. It is usefull in situations where calculations are irrelvant
if done more than once per frame, and may lead to jerkiness if done less
than once per frame.
Dynamic Routing
The Applet needs to be able to respond to things that happen in the browser.
In a static world, this is accomplished by the path:
-
User clicks on object
-
Browser looks up the scene hierarchy to find a ClickSensor
-
Event is sent from ClickSensor node to Script node. (established by the
ROUTE call).
-
Script passes event to the script (via the interpreter).
In a dynamic world, the behavior needs to be able to set up this path.
To do this it needs to be able to do 2 things.
-
Create a Sensor
-
Add a new Sensor
-
Route an existing, or new Sensor to an existing ScriptSensor.
and the opposites
-
Delete a Sensor
-
Remove a route
Creating a Sensor could be done by compiling the VRML for it, but this
could be required sufficiently frequently that calls to the Browser (via
the Interpreter) should be provided to do this efficiently i.e.:
Node* CreateClickSensor();
Node* CreateLineSensor(...);
and so on for all the predefined Sensor nodes.
Adding a new Sensor can be done by a new event on the Frame or a Leaf
node.
#eventIn SFNode AddEvent
which will add a Sensor (Click or Drag) to an existing node, and a corresponding
#eventIn SFNode DeleteEvent
to remove the sensor.
Adding and deleting routes is a little more complicated, because it
requires multiple paramaters (not allowed in the architecture proposed).
It should probably be done by a call to the Browser (via the Interpreter),
which has the required parameters, and can be converted by the Interpreter
into a number of browser specific events needed to set up the required
data structures, dependency graphs etc.
AddRoute(Node* sourceNode, SFString sourceEvent,
Node* destinationNode, SFString destinationEvent,
SFString userData);
To remove an event we have the complementary call
DeleteRoute(Node* sourceNode, SFString sourceEvent,
Node* destinationNode, SFString destinationEvent,
SFString userData);
Bindings
SFEnum
In order for the Applet to be able to know what values etc to return it
is essential that the Applet have values bound to some things which have
been symbolic in VRML1.0. In particular this applies to Enums.
There are three ways the binding could be done - ranging from most flexible
at the top, to most efficient at the bottom.
-
All calls are passed as strings - e.g. SendEventSFBool(Node*, this,
"setOn", "TRUE");
-
A call to determine the value of bindings for that platform e.g. QueryEnum("ShapeHints.vertexOrdering")
resulting in some data structure that can be reused.
-
Loading a platform dependant header file from with the application.
-
Defining the binding across platforms, at the time the Enum is defined.
We believe that the fourth option is the best, since there is little to
be gained by leaving enum assignement to the browsers. The process of defining
these values is a fairly minor administrative task. In the meantime it
should be assumed that enum's are allocated in the order they are mentioned
in the VRML1.1 specification, starting with 0. e.g. For Vertex ordering
in the ShapeHints node. UNKNOWN_ORDERING=0 CLOCKWISE=1 COUNTERCLOCKWISE=2.
Note, FALSE=0 TRUE=1.
SFBitMask
The same logic applies to SFBitMask. Allocate 1 to the first mentioned,
2 to the second, 4 to the third. With a maximum of a 32 bit word being
allowed. An extension will be needed to allow definitions of Enum values
when declaring new node types with the PROTO keyword.
Error codes
The following error codes are defined.
-
VRMLAPI_NOSUCHEVENT: When an unknown eventName is passed
-
More to be filled in as a need is determined.
Event Types
The EventType field of an Event is defined as follows:
-
0 means no data following,
-
If the top bit is set, then its an MF* field, and the next field gives
the number of values. Each of which conform to the rules below. Clear the
top bit and look in the SF* table below.
-
If the top bit is NOT set, then it contains the following values.
-
SFBitMask
The next field is a 32 bit word containing a field-dependant bitmask.
-
SFBool
The next 32 bit field contains 1 for TRUE and 0 for false.
-
SFColor
The next 3 32 bit fields contain IEEE floating point values for the
colors
-
SFEnum
The next 32 bit word contains a field-dependant enum
-
SFFloat
The next 32 bit word contains a IEEE floating point value
-
SFImage
The next 32 bit fields contain an SFImage. The number of bytes following
can be determined by multiplying the first 3 fields together.
-
SFLong
The next 32 bit field is an integer.
-
SFMatrix
The next 16 32 bit fields represent a matrix
-
SFNode
The next 32 bit field is a cookie for the SFNode.
-
SFRotation
The next four 32 bit fields contain IEEE floating point values.
-
SFString
The next 32 bit field contains a length, followed by that number of
bytes, followed by a NULL.
-
SFVec2f
The next 2 32 bit fields contain IEEE floating point values
-
SFVec3f
The next 3 32 bit fields contain IEEE floating point values
-
SFTime
The next 64 bit field is a double precision floating point number -
the number of seconds since Jan 1, 1970 GMT.
-
SFPick
The next 10 32 bit fields are SFBOOL isover SFBOOL isActive SFVEC3F
hitPoint SFVEC3F hitNormal SFVEC2F hitTexture.
Gavin: Would rather see these as seperate events
Issue: We might prefer to send a cookie and allow callbacks, especially
for calculating the hitNormal and hitTexture.
-
SFProximity
The next 11 32 bit fields are SFTIME enter, SFTIME exit, SFVEC3F position
SFROTATION orientation
-
SFCollision
The next 9 32 bit fields are SFTIME collisionTime, SFVEC3F position,
SFROTATION orientation
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.
Requirements
For each language we need to define:
-
The correspondance of basic data types - SFLONG SFIMAGE etc to language
specific types, and the constructor, destructor and access methods for
those basic data types.
-
The exact call from the Interpreter to the Applet and back to the Interpreter
for each of the calls specified above. This should be a simple one-to-one
mapping once the data types are defined.
Java
Java requires that the interpreter runs 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. In many cases Java is going to be included inside
a browser in which case the Interpreter calls mentioned above are all internal,
and outside the scope of the API.
Extensability to new languages
One goal is to be able to add languages, with any of the above approaches,
the interpreter can be a seperate piece of code. This enables dynamic,
or at least user-initiated, downloading. As a simple (but not neccessarily
sufficient) solution, it is 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 / 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.
-
Allowing multiple instantiations of the same script.
-
GetField calls
-
ILU - for binding, why rejected
-
Environment events - cull, lod, world, frame
-
Tough examples - given a pointer to a Frame, send an event to all of its
children.