Richard C. Waters and David B. Anderson
This document is available as "http://www.merl.com/opencom/opencom-c-api.html".
This version of the document was created on February 19, 1997.
Copyright 1997, MERL - A Mitsubishi Electric Research Lab. All rights reserved.
This is a machine generated document created from a database of information. It exists in both paper and HTML form. Other documents describe the API exported to other programming language environments.
The API also includes the following ordinary data structures:
- - spWM - A world model.- - spFn - Operation that can be mapped.- - spMask - World model view mask.- - spTransform - Position, axis, angle, scale specification.- - spVector - 3-element vector.- - spRotation - Rotation specified using axis and angle.- - spQuaternion - A quaternion.- - spMatrix - 4x4 transformation matrix.- - spFormat - Sound data format.Finally, the API includes the following shared object classes:
- - sp - Top of shared object hierarchy.- - - spRegion - Basis of location-addressable communication.- - - spLink - Link to large slowly-changing data.- - - - spVisualDefinition - Link to graphic model.- - - - spSound - Link to stored sound.- - - - spBoundary - Link to bounding box.- - - - spClass - Link to description of a shared class.- - - spThing - Thing in the virtual world.- - - - spRoot - Root of recognized whole.- - - - - spAvatar - Whole representing user or agent.- - - - spAudioSource - Source of sound data.- - - - spBeacon - Basis of content-addressable communication.- - - - - spSpeaking - Connects to user's speech.- - - - - spHearing - Connects to user's ears.- - - - - spSeeing - Connects to user's eyes.- - - - spPOV - Point of view.- - - - spAction - Program triggered by system core.- - - - - spCallback - General callback.- - - - - - spIntervalCallback - Interval callback.- - - - - - spBeaconExamine - Inspect beacons.- - - - - spRemoteAction - Remotely triggered programs.- - - - - - spOwnershipRequest - Requests getting ownership.The document is organized around the various classes enumerated above. At the top level, there is a section for each class. Within these sections, there are subsections corresponding to each instance variable and method.
There are two ways to look up instance variables and functions in this document. By using the table of contents, you can look them up by the name of the class. By using the quick reference index, you can look them up alphabetically by the name of the instance variable or method.
There are several quite different kinds of classes listed above: , passive data structures, and shared objects that are the basis of communication between processes.
The passive data structures (e.g., spTransform) are pieces of data that are stored in shared objects or used in intermediate computations. In the C interface, these items are not objects, but rather just simple vectors and structures.
The shared objects consist of the class sp and its subclasses. These classes receive specialized treatment by the system. In particular, instances of the shared classes are communicated between processes. Applications can define new shared classes using a Java interface supported by a special Java preprocessor.
Most of this document consists of descriptions of the shared classes. The first line of each section for a shared class shows the declaration used in the Java interface to define the class. This is followed by a brief discussion of the class and a listing of the externally available shared instance variables and functions in the class. Subsections of the section for a class describe each of its instance variables and functions in detail.
The first line of a subsection discussing an instance variable of a shared object shows the declaration used in the Java interface to define the variable. This line includes both the type of the variable in Java (in the middle) and the type in C (in a comment at the end of the line). From the perspective of this document, only the C type is relevant. The variable declaration line is followed by a general discussion of the instance variable and the various access functions available for the variable.
The first line of a subsection discussing a function is the ANSI C signature of the function. This is followed by a summary of what the function does.
The names of C functions in the API are derived using the following naming scheme. If a class spK contains a method M, then the corresponding C function has the name spKM. Since every shared class name begins with the letters ``sp'', every C function in the API begins with the letters ``sp''.
The sections on non-shared classes have much the same format as the sections on shared classes.
At various places in this document, topics of interest are discussed at length in subsections. One reason for gathering information into a separate section is so that it can be referred to from many places in the document.
Applications do not communicate directly with each other, but rather
only with the world model. This allows applications to be written
without thinking about how communication is achieved. An application
does exactly the same things when it is interacting with an
application running in shared memory on the same machine as it does
when interacting with an application connected via the Internet.
Figure 1: The programming model.
The world model is not a scene graph, but rather an object-oriented
database that does not consider one kind of content to be any more
important than another. In particular, we believe that audio
information and autonomous behavior are at least as important as
visual information and should not be limited by constraints inherited
from visual rendering.
The world model specifies what objects exist in the virtual world,
where they are, what they look like, and what sounds they are making.
The world model does not contain historical information, but rather
just a snapshot of what the virtual world is like at the current
moment. As the virtual world changes second by second, the world
model changes.
The emphasis in the design of the world model is on the term
database, not object oriented. The objects have methods
associated with them, but by far the dominant operations consist of
reading and writing data stored in the instance variables of world
model objects.
Applications observe the virtual world by retrieving
data from the world model. Applications affect the virtual world by adding, removing, and
modifying objects in the world model. To avoid readers/writers
conflicts, each object in the world model has one process as its owner
and only the owning process can modify it. However, the ownership of
an object can be transferred from one process to another.
By itself, the system does not cause objects to persist over time. An
object exists only so long as the process that owns it runs. To have
persistent objects, an application must provide persistent processes
that accept ownership of these objects. These processes could make
use of a persistent file format for objects to provide efficient long
term support for infrequently visited parts of a virtual world.
Figure 2: The communication model.
A central feature of the system is that it is designed to be scalable
to a large number of users (e.g., thousands) interacting in real time.
Two key features support this: providing only approximate equality of
local world model copies and dividing the world model into chunks each
of which is communicated only to the small group of users that are
actually interested in it, rather than to all the users of the world
model.
Distributed databases typically require that all local copies of the
database must agree exactly on the information in the database.
However, this requires object locking and handshaking that is
incompatible with real-time interaction if there are more than a very
small number of users. In contrast, the focus here is on real-time interaction at all costs, providing only approximate equality between world model copies.
The primary way in which world model copies are only approximately
equal is that different users observe things as occurring at slightly
different times. We call this a relativity model of
communication and is actually not unlike the real world. When you
hear sounds from distant sources, you do not hear the sound that is
being made now, but rather the sound that was made seconds ago. As a
result, people in different locations do not hear the same things at
the same time. How great the differences are depends on how far apart
the sound sources and people are.
Similarly, when an application process finds out about a world model
change, it is not finding out about a change that is happening now,
but rather about one that happened some time ago. How long ago
depends on the network distance between the two processes. In
general, this distance is not more than a couple hundred milliseconds
and does not lead to world model differences that are unduly large.
Having only approximate equality of world model copies allows
real-time interaction, but does not of itself prevent the computation
required to maintain each local world model copy from growing in
proportion to the total number of simultaneous users of a virtual
world. To prevent this, the world model is broken up into many small
chunks called regions and information about a given region is
communicated only to the small number of users that are near enough to
that region to be interested in it. Each region is associated with a
separate communication channel so that processes that are
not interested in a region do not have to expend any processing
ignoring it. This allows the system to scale based solely on the
maximum number of users that are gathered in any one region, rather
than on the total number of users in the virtual world.
Figure 3: Servers.
In the lower right of the figure is the session manager. It
handles the connection of new users to an on-going session and their
eventual disconnection. Its workload is proportional only to the
number of users that enter or leave the session in a given minute, not
the total number of connected users.
One or more servers are included in a Spline session to act as:
user servers, region servers, beacon servers, and contact points. For flexibility, there is only one kind of server process, which can act in any of the above roles, or several at once.
The purpose of a user server is to support users with
slow network connections (e.g., modems) that do not allow them to
operate as first class peers. When acting in this role, a server intercepts all
communication to and from the user. The message traffic to the user
is compressed to take maximum advantage of the bandwidth available.
As part of this, audio streams are combined and localized before
sending them to the user. Servers are replicated as needed so that no
one server has to support more users than it can handle. Two servers acting in this capacity are
shown in Figure 3.
A region server maintains a record of everything in a given region.
When the locus of attention of a user process enters a new
region, the appropriate region server is queried in order to obtain initial information about the state of objects in the region. After this initial download, the user process obtains further incremental information by peer-to-peer communication. Responsibility for regions is parceled out among a set of servers,
which is made large enough that no one server is responsible for a
larger piece of the virtual world than it can easily handle.
Because the world model copy in an application process only contains
information about the objects in the regions the process is attending
to, there has to be an explicit mechanism for locating far away
objects in the virtual world. This is done by having the servers provide a name service that makes it possible to locate specialized objects called
beacons by name no matter where they are in the virtual world. When a user process wants to locate a particular beacon object, it consults the appropriate beacon server in order to find the beacon. As with regions, responsibility for the beacon name space is parceled out among a set of servers, which is made large enough that no one server is responsible for more beacons than it can easily
handle.
To simplify the communication between user processes and the various servers it has to interact with, each user process has a server assigned to it that acts as a sole contact point for the process. Every message from a user process (or user server acting on its behalf) that requests a service, is sent to the contact point. This allows the user process to always operate as if there was only one server. The various complexities that arise when servers are replicated are handled by the contact point, which decides where to route the messages it receives from user processes.
The session manager monitors the servers in a session and if it detects that one has crashed, restarts it. This is facilitate by the fact that the user processes and the session manager are the only sources of information. The various servers merely act as repositories of information obtained from elsewhere and can straightforwardly be reinitialized if they need to be restarted.
As shown in Figure 3, the system utilizes a hybrid communication model
in which client/server communication is primarily point-to-point but
server/server communication is peer-to-peer. In addition, users with
sufficiently fast network connections (e.g., users on corporate
intranets) can interact directly with each other and the servers using
peer-to-peer communication.
Figure 4: An application process.
The foundation of the system is the inter-process communication
module shown at the bottom of Figure 4. It provides all the processing necessary to maintain
approximate consistency between the world model copies associated with
a group of communicating processes, sending messages describing
changes in the world model caused by the local application and
receiving messages from other processes about changes made remotely.
The network interface specifies the format of these messages. Any
process that obeys this interface can interoperate.
The messages sent are of three kinds, corresponding to three kinds of
data in the world model: small rapidly changing objects, large slowly
changing objects, and continuous streams of data. An important
feature of the system is that it includes an efficient scheme for
synchronizing these different kinds of data.
The most prevalent kind of object in the world model is small things
that can change rapidly. For example, an object representing
something in the virtual world (e.g., a chair) requires only a small
description---i.e., to specify its position and orientation, whether
it is contained in some other object, and which appearance should be
used when displaying it. The features of small objects can be changed
very rapidly.
Messages describing changes in small objects are sent using
User Datagram Protocol (UDP) messages. This allows them to be
communicated very rapidly. The objects must be small enough so that
a message describing one will fit in one UDP message.
Graphic models, recorded sounds, and behaviors are represented using
large objects. These objects are identified by Universal Resource
Locators (URLs) and communicated using standard World Wide Web
protocols. Standard formats are used (e.g., VRML for graphic models, WAVE for sounds, and Java for behaviors)
so that standard tools can be
used. There is no limitation on the size of the
large objects, but several seconds can be
required to communicate one. Fortunately, since these objects change
infrequently, this latency can generally be masked by preloading the
objects before they need to be used.
The final kind of object in the world model corresponds to continuous
streams of data such as sound captured by a microphone. These streams
are communicated in small chunks using UDP messages. At the moment,
video streams are not supported. When they are, they will be
communicated in a similar fashion, but of necessity using larger
messages.
A central feature of the system is that using the various messaging
approaches above, every kind of data in the world model can be
communicated between processes. Therefore, applications can modify
and extend every aspect of a virtual world. Furthermore, while it is
often advantageous to prestore data for an application to use (i.e.,
by delivering it on a CD-ROM) this is not necessary. Once started, an
application will fetch everything it needs that has not been
prestored.
Figure 5: Typical configuration supporting a user.
Visual and audio rendering modules are provided; however rather than
being tightly integrated with the system, they are separate
applications interfaced to the system. They use the same API as other
applications and do not have to be tightly coupled with the main
application.
One advantage of the loose coupling of renderers is that the renderers
interacting with a person can run in separate processes from the main
application. This allows them to operate on separate machines in
situations where maximum performance is required. However, the
primary mode of operation is for them to run in the same process with
the main application, sharing a single copy of the world model as
shown in Figure 5.
The greatest advantage of the loose coupling between rendering and the
system core is that the system is not tied to any one renderer.
Rather, the API is designed to be easily interfaced to almost any
renderer. Default renderers are supplied with the system, but it is
expected that demanding applications will switch to renderers that are
tuned to the task at hand.
Consider visual rendering as an example. In the world model,
objects can have positions, orientations, and appearances. The visual
renderer creates a scene graph by combining the appearances associated
with the objects that are near enough to be seen and renders the scene
graph from the vantage point specified by the application. The system
itself does nothing with the graphic models that describe the
appearances of objects. The only thing that matters is that the
visual renderer being used can load them.
Audio rendering is supported in a similar fashion. The world
model contains objects representing sources of sounds. These objects
specify the places where the sounds are located. They can either be
point sources or diffuse ambient sources. Sound to be played through
these objects can be captured live from a microphone or prestored in
recorded sound objects. The audio renderer creates an audio
scene graph by combining the sounds associated with sound source
objects that are near enough to be heard and renderers this from the
vantage point specified by the application. The system itself does
nothing with sound encodings. The only thing that matters is that the
audio renderer being used can decode them.
Figure 6: Typical configuration
supporting a simulation.
A simulation operates just like any other application using the
same API to interact with the world model. However, no visual or
audio rendering is needed, because there is no person to see or hear
it. Complex simulations,
intelligent agents, and large persistent databases can all be directly
connected to a virtual world. Large powerful computers without support for graphics or sound
can be used to manage shared content.
It is important to note that it is a great deal easier to say that a
simulation interacts with the world model just like any other
application than it is to make it possible for a simulation to
effectively interact with the world model. The reason for this is
that applications supporting a user have a human being in the loop and
simulations do not.
For example, it is typically easy for a person to look at an
avatar and determine which way the avatar is facing, based on a
rendered image. However, it verges on impossible for a program to
tell where the face of an avatar is by looking at a list of polygons.
To deal with this kind of problem, the system has been designed to
make information such as which way an avatar is facing easily
accessible to programs. Specifically, Spline's world model contains
an object class spAvatar that is used to identify avatars as opposed
to other kinds of objects and by convention the center of the coordinate system for an object is at the center of the object, the Y axis is up, and
objects face done the negative Z axis
The following paragraphs briefly describe each kind of atomic
data used in the API. Some of these types of data are described at
greater length in other sections.
Boolean values represented using the type
spBoolean. These are used to indicate True and False
values.
16-bit integers represented using the type short.
These are used when low range integers are adequate.
32-bit integers represented using the type long.
These are used for representing most integer values.
32-bit floating point numbers represented using the type
float. In keeping with standard graphics practice, these are
used to represent most non-integer values. They have limited
precision, but are compact.
32-bit unique names represented using the type
spName. The system dynamically generates names that are
unique across all the processes participating in a session.
These names are used both to identify shared objects and to identify
the processes that own the objects.
IP addresses/port pairs represented using the type spAddress.
These are used to represent network addresses. They contain two pieces of information, a 32-bit IP host address and a 16-bit port number. In C an spAddress is a struct containing the host address, followed by the port number. followed by 16 unused bits to pad the total field out to two full words.
32-bit time durations in milliseconds represented using
the type spDuration. Whenever an API function takes a
time duration as an argument, this argument is in terms of
milliseconds. For compactness, 32-bit integers are used. This allows
a range of approximately plus or minus two weeks. (This does not impose any limitation on the lifetime of a session in the virtual world, but rather only limits the maximum difference between two times that can be represented.)
World model view masks represented using the type spMask.
These bit masks are used to control the visibility of shared objects in the world model.
Audio formats represented using the type spFormat.
These values are used to represent
audio encodings and sample rates.
A key feature of the API is that pointer data is always passed
into and returned from functions by reference rather than copying.
This promotes efficiency, but means you must pay close attention to
the following.
1- The system never alters data that is passed to it via a pointer
unless this document explicitly states otherwise.
This means you can depend on the value remaining the same
unless you change it yourself.
2- The system never retains a pointer passed to it beyond the time
the function that was given the pointer returns. (If the system
needs to keep any of the data, it copies it.)
3- When a pointer is passed to you, you must never alter the data
pointed to unless it is explicitly stated that you should, because
the system depends on the data not being altered. In particular, you must
never free something referred to by a pointer unless you yourself
allocated the storage.
4- It is allowed that you retain pointers returned by an API function, but
only until the next time spWMUpdate is called. The reason
for this is that the system assumes that during calls on
spWMUpdate, it can free any storage it has allocated. The
only exception to this is certain shared objects.
If you want to save data across a call on spWMUpdate, you
must copy it.
The following paragraphs briefly describe each kind of pointer
data used by the system. Many of these types of data are described at
greater length in other sections.
A UTF8 ASCII string represented using the type char *.
This is a null-terminated string. If the characters are all 7-bit ASCII characters, then this is an entirely ordinary string. If extended characters are used, then special escape sequences are present.
An immutable spFixedAscii string containing no more than 500 characters
represented using the type spFixedAscii. This is the same as
the above, but is limited in length so that it can fit into a single
UDP message. It is used for most shared character data in shared objects.
This data must be specified when the object is initially
created and cannot be altered afterward.
A 17-element position, orientation, and scaling vector represented
using the type spTransform. This structure is used as the primary representation of the position and orientation of objects.
A 3-element vector represented using the type spVector.
Vectors containing three values are
used for several different purposes.
A 4-element quaternion represented using the type spQuaternion.
Quaternions are one way of
representing rotations.
A 4x4 transformation matrix represented using the type spMatrix.
Following standard graphics
practice, the position, orientation, and scaling of objects can be
represented using a 4x4 matrix.
A locally defined operation represented using the class
spFn. The API makes heavy use of callback functions and the mapping
of functions over objects. For convenience, a
single type is used to represent functional arguments in all these
situations.
An interaction window represented using the type
spWindow. Exactly what type this is depends on
the execution environment.
An opaque pointer represented using the type
void *. In a number of places, the system records pointers that
are not part of the external interface and are not intended to be
manipulated by applications.
All interaction with instance variables in shared objects is via access functions, rather than via direct access to the instance variables or structure fields. In the external API, the number of access functions available depends on the visibility of the instance variable in the API. The following discusses the accessors in detail for an instance variable V in a shared class spK containing data of type T.
The following accessor obtains the value of the instance variable V for an object. It reports an error if the object from which the data is being obtained is not an instance of (a subclass of) the class spK.
The following accessor is available for setting the value of V. A set accessor is available in the external API only if the external API allows the variable to be set. The Set accessor reports an error if the object to be modified is not an instance of (a subclass of) the class spK, if the object has been removed from the world model, or if the value being stored is a shared object that has been removed. If V is an instance variable that is shared between the processes in a session, then the Set accessor reports an error if the owner of the object is not equal to the current value of spWMGetMe.
A few instance variables in shared objects are constant in nature in that they cannot be altered after an object is initially created. For example, this is true for variables whose values have the C type spFixedAscii. If a shared instant variable is not constant in nature, then the following accessor is available for obtaining the value of V prior to a change. (Specifically, the accessor obtains the value V had at the end of the last call on spWMUpdate.)
The primary intended use of these accessors is in callback tests.
The accessor reports an error if the object from which the data is being obtained is not an instance of (a subclass of) the class spK.
Obtaining an old value can take substantially more time than obtaining the corresponding current value.
In the interest of saving space in this document, the discussions of individual instance variables in shared objects merely list which accessors are available with reference to this section for greater details.
It should be noted that for a given class spK, the accessors above are available not just for the variables directly defined in the class, but also for the variables that are inherited. For example, if spK inherits a variable V from a superclass spJ and the accessor spJSetV is available, then the accessor spKSetV is also available. In the interest of brevity, these inherited accessors are not explicitly listed in this documentation.
A very important feature of the API is that the local world model copy
in a given process does not contain everything in the entire virtual
world, but rather only a subset of the objects that are of local
interest. As discussed elsewhere, the determination of subsets is
based primarily on regions.
Because only some of the objects
in the world model are in the local world model copy at a given
moment, it is entirely possible that the local world model might
contain an object A that refers to an object B (e.g.,
A's Parent might be B) and yet the local world
model might not contain the object B.
Communication is arranged so that if two objects
refer to each other, then in general they are both communicated in the
same region. As a result, the kind of inconsistency described above
seldom exists for long. However, it is very common for this kind of
inconsistency to exist briefly for a variety of reasons.
(1) When the focus of interest moves into a new region, a process
inevitably hears about some objects before others. (Because there are places where circularity of references is required, nothing can avoid the fact that this can cause temporary dangling references.)
(2) When new objects are created, a process inevitably hears about some
objects before others.
(3) When objects are removed, a process may hear that an object is
gone before hearing that other objects have stopped referring to it.
The above situations are handled by essentially putting
the burden of a sanitized version of the problem on the application.
When an instance variable contains a shared object, the access
function that obtains the value (e.g.,
spGetParent) returns the object referred to only if
the object is in the local world model copy. Otherwise it returns
Null.
Suppose again that there is an object A in the local
world model copy. If spGetParent returns
non-Null when applied to A, then the return value is
the Parent of A. However, if
spGetParent returns Null, then this could
mean either that A has no Parent or merely that the Parent of
A does not yet exist in the local world model copy. (There is
no way to tell the difference between these two things without
inspecting details of A that are below the level
of the even the internal API.)
Suppose that A does indeed have a Parent B and
suppose further that A appears in the local world model before
B. When B later appears, this registers in all respects
just the same as if the Parent of A changed from Null
to B. In particular, if there is a callback watching for such a change, it will be triggered.
The above approach works well because it does not matter
to the typical application whether the Parent of A has changed
or has merely just become known. An application has to be able to
deal with arbitrary changes made by other processes, and doing this
properly typically leads to a solution that operates properly in the
presence of evolving partial knowledge as well. However, it is important to keep clearly in mind that a
process's knowledge of the world model is always partial and
therefore, it is never justified to draw more than merely provisional
conclusions from the absence of something in the world model, since anything
can appear during a call on spWMUpdate.
A particularly good example of the problems related to asynchronous changes is what must be done to properly handle the asynchronous removal of objects. To start with, it is important to understand what happens when an object is removed.
First, the IsRemoved bit is set on in the object R being removed and all connections between R and other objects are broken. Specifically, any instance variable in any other object that refers to R is set to Null. In addition, any instance variable of R that refers to any other shared object is set to Null. A result of this is that having an object's Parent removed is treated very much the same as if the object's Parent was simply changed to Null.
After the first stage of removal above, one can still consult all the instance variables of R, with the proviso that the variables that point to other objects have all become Null. (For variables that are shared with other processes, you can consult the old values of the instance variables to find out what they used to point to.)
Second, some time later (exactly how much time later is discussed in detail below) the storage corresponding to object R is freed. After that time, one can no longer access any of the instance variables of R, and it can be disastrous to try. A key purpose of the first step of removal is to ensure that no other shared object can retain a pointer to a freed object. Applications must be sure that they don't either. There is a separation in time between the first and second stages of removal so that applications have time to notice when objects have been removed.
To deal with asynchronous object removal, an application must check on a regular basis whether any objects it is interested in have been removed and respond appropriately before the objects are freed.
The system has carefully designed rules about when objects can be removed and freed in order to reduce the number of times an application has to check for an object's continued existence.
Between calls on spWMUpdate, objects are never removed unless they are explicitly removed by the local process. The basic result of this is that it is sufficient for a process to check for the continued existence of an object it is keeping track of once each time after spWMUpdate returns. This checking is done using the accessor spGetIsRemoved. Once an object has been removed, the local process should drop all pointers to it immediately, because they will very soon be invalid. (A particularly good way to do the checking above is to use an spJustRemoved callback on the object in question.)
Objects are never freed except during calls on spWMUpdate. In addition, if an object is removed after the beginning of a call on spWMUpdate it is not freed until the very end of the next call on spWMUpdate. This means that for every object, there will be at least one period between spWMUpdate calls where the object has been removed and not yet freed. This gives the application an opportunity to detect this fact and do something about it. (It also guarantees that the object stays around long enough for any spJustRemoved callbacks to get triggered.)
As a result of the above, C applications need not worry much about objects being asynchronously freed as long as they check that the objects are still in the world model once after each call to spWMUpdate (e.g., with callbacks).
In the Java interface, garbage collection and finalize methods ensure that shared objects are not freed as long as any pointers to them remain. In the C interface the function spWMRegister is used to obtain a somewhat similar level of protection. This can be used to make sure that the object pointed to from a given variable will never be freed and therefore it will always be valid to check whether the object has been removed.
An important special case is worthy of note. If the local process owns an object, then it can rely on the fact that the object will not get removed unless the local process removes it. This obviates the need for a significant amount of spGetIsRemoved checking in typical applications.
If the local process has multiple threads, then several problems arise. First, the other threads in the process can remove anything and so no object access can be considered entirely safe. Second, it is likely that some of the threads will not be synchronized with calls on spWMUpdate. It is difficult for these threads to know when they should check that objects still exist.
The most robust way to avoid problems with threads is to avoid having asynchronous threads manipulate shared objects directly, but rather have them communicate in some other way with the main thread in a process. Barring this, spJustRemoved callbacks are the best way to ensure safety.
The extended Java syntax is supported by a preprocessor called
SPOT. In general, SPOT takes in a Java file containing a shared class
definition and produces:
In C-only mode, SPOT is used purely to extend the C API. In C-only
mode, everything in the definition of a class must be native. (What
this means for instance variables is discussed below.) In C-only mode
outputs (2) and (3) are unnecessary.
In Java-only mode, SPOT operates purely to extend the Java API. In
Java-only mode, classes can not contain any native elements. Things are
arranged so that the shared class can be used from Java without having
to link anything additional into the system core. In particular,
outputs (1) and (3) are unnecessary.
In mixed mode, the class can contain native and non-native elements and all
four outputs are produced so that the class can be used to maximum
effect in both C and Java. The shared classes described in this
document are defined solely using native elements and are processed
using the mixed mode of SPOT so that they are available fully in both the
Java and C APIs.
An appendix at the end of this documentation contains the SPOT input corresponding to the entire API described in this document.
The following example is used throughout the explanation
below. It is contrived to illustrate many different features in a
small space.
For the most part, the class definition above is standard Java.
The exceptions to this are the keyword `shared', the use of the
keyword `native' in an instance variable declaration, the use of the
syntax keyword/keyword, and the uses of the //* comments. (spThing
and spAction are shared object classes. spTransform and spDuration
are types in the C API.)
A class is a shared class if and only if it is the root
shared class sp or extends another shared class. (In the case above,
spExample extends the shared class spThing.) The key feature of a shared class is that the objects that are
created as instances of the class are shared between the processes
participating in a session.
The definition of a shared class must use the keyword `extends' and
can use the keywords `public' and/or `abstract'. However, it cannot
use any other keywords. In particular, it cannot use the keyword `implements'.
The open brace that begins the body of a shared class definition
can be followed by a comment beginning with `//*'. The text in the
comment can begin with [+/-SendToRegion +/-SendToContact]. This
specifies two key facts about the class: whether it is communicated in
the normal region-based way and whether it is communicated directly to
the contact point for a process, with `+' signifying yes and `-'
signifying no. If either the SendToRegion or the SendToContact
specification is omitted, it defaults to the value inherited from the
class that is being extended.
The SendToRegion and
SendToContact specifications are
used in a few crucial places in the definition of the standard shared
classes, but it is very unlikely that an application programmer would
ever need to use these flags. The reason for this is that for any
class that an application programmer is likely to define, the
inherited values of these flags will be correct.
The keyword `shared' specifies that an instance variable is shared
between processes. When a shared variable is set in one
process, the change is automatically reflected in all other processes.
In contrast, variables that are not marked as shared exist in
each process, but the values of non-shared variables are set
separately by each process, with no effect on their values in any
other processes.
The keyword `native' specifies that an instance variable is
represented in C so that efficient methods exist for accessing it
directly from C. (Shared implies native.)
The keyword native by itself is used often when defining the
standard classes in the system, and would be used by someone extending
the C API. It would never be used by someone who was solely
extending the Java API.
Shared and native instance variables cannot be
directly referred to as instance variables. (For instance, when using
an instance X of the class spExample, one cannot write X.Agent
anywhere.) Rather, a shared instance variable Var in the class spK is
accessed solely through the use of automatically generated
access methods.
Shared and native instance variables cannot redefine any variable in
the class being extended. (This is because, for efficiency, the
system assumes that none of the instance variables it uses can have
their definitions changed.)
If the keyword shared or native is used, one can also use the
access control keywords private, protected, and public. If an access
control keyword appears, then the Get and Set methods have the
specified access control. It is permissible to use the form
Keyword/Keyword, in which case the Get method has the access
limitations specified by the keyword before the / and the Set method
has the access limitations specified by the keyword after the /. (The
keyword before or after the / can be omitted with the meaning that the
corresponding accessor can be used anywhere in the package.) For
example, the Agent instance variable of spExample can be read by
everyone, but only written by the methods of the spExample class and
its subclasses.
If the keyword shared is used, it is not permissible to use the
keywords static or final. However, it is permissible for a native
variable to be static or static final.
If a native variable is specified to be static, it is processed
by SPOT in the same way as a variable that is not static except that
the methods created for accessing the variable are static. The only
class in the API that uses native static variables is the class
spWM.
If a native variable is specified to be static final, it acts as
a constant that is known both to Java and C.
It is referred to directly as a variable in both APIs, rather than
being accessed via methods. In C native static final
variables are If the keyword shared or native is used, then it is not
permissible to have an initialization expression, except for a
variable that is native static final. Rather, one uses an
Initialization method.
If the keyword shared or native is used, then the data stored in the
variable must be one of only a few permitted types. These types fall
into two groups: scalar types and pointer types.
A shared or native instance variable can have as its type any of
Java's eight primitive scalar data types (byte, short, int, long,
float, double, char, and boolean).
Since each shared and native instance variable is represented in C, it
must have a C type as well as a Java type. The following table shows
the default C type associated with each of the scalar Java types.
The semicolon ending a shared or native instance variable declaration
can be followed by a comment beginning with `//*' that specifies a
non-default type to use for the variable in the C API. This
specification has the form [type] where the type is a C type (defined
separately in a C file). The only restriction on the C type is that
it occupy the same amount of storage as the Java type. For instance,
an int in Java is represented using 32 bits. Any 32-bit C type can be
used in conjunction with it. Whenever they are passed as arguments to
functions or passed between C and Java, scalar values are copied.
In general, SPOT does nothing special with individual types. It
merely needs to know what types to use in Java and in C and what their
sizes are. However, one scalar type gets special treatment.
If the C type of a shared or native instance variable is spBoolean,
then the value is stored in a single bit using special internal variables of the class sp. However, once the bits
reserved in these variables have been exhausted, then a whole byte is
used for an spBoolean value.
In addition too the eight Java scalar types, a shared or native
instance variable can have one of the following pointer types.
As with scalar types, each of the pointer types above must be mapped
to a C type. The correspondence is shown in the table below.
In C, all shared objects are referred to by pointers of type sp and no
other C type can be specified. All functional arguments are referred
to by pointers of type spFn and no other C type can be specified.
Note that it is not possible to have a shared or native variable whose
Java type is anything other than the types specified above.
For strings and arrays, there is no default C type. Rather, the C
type must be specified using a `//*' comment. When specifying a pointer
type, this comment begins with [type:size] where type must be a C type
that is a pointer (e.g., float*) and where size specifies how many
elements there are in the string or array. For instance, in the
spExample class, the C type spTransform points to a vector of 17
floats.
When pointer types are passed as arguments to functions, they
are passed whenever possible by reference via a pointer. However,
when pointer types are passed between C and Java, the data is
typically copied so that appropriate data structures can be maintained
separately in C and Java.
As with scalar types, SPOT typically does nothing special with
individual pointer types. It merely needs to know what types to use
in Java and in C and what the memory size in C is. (For ease of
allocation and communication, strings and arrays stored in shared or
native variables are always stored in-line in the C representation
for an object.) However, several pointer types get special treatment.
The shared object types and the type spFn get special treatment so
that appropriate objects will be available efficiently in both Java
and C. In addition, Strings get special treatment.
A difference between Java and C is that Strings in Java use
Unicode characters while strings in C use ASCII characters. To
accommodate this, Java strings are encoded as null terminated UTF8
strings when communicated to C. (If a string only contains 7-bit
ASCII characters, this encoding leaves the bits unchanged.)
The C type spFixedAscii specifies a string of variable length
that can only be set at the moment when a shared object is being
created and cannot be changed later. To minimize the memory used for
spFixedAscii values, they are placed in a special variable-sized part
of an object. (A consequence of this is that it is not possible to
obtain the old value of an spFixedAscii variable.)
The type spFixedAscii can only be used for shared variables as
opposed to ones that are merely native. There can be at most one
spFixedAscii variable in a shared object. Unlike other array-like
types, no explicit size can be specified when using the type
spFixedAscii.
The total size (in C) of all the shared instance variables must fit in
a single UDP message (i.e., be less than 600 bytes or so). A warning
is issued if this restriction is violated.
The methods in the definition of a shared class are specified using
entirely standard Java code. However, for a method to be available in
C, it must be native, or redefine one of a few special methods that
the system core uses.
The specification of a native method in a shared class is
obligatorily followed by a //* comment containing the signature of the
C function that implements the method. The signatures are used by
SPOT both to create appropriate C .h files and to produce appropriate
stub files linking the Java and C API's.
In order to make it possible to create stubs correctly, types in
the C function signature that correspond to arrays
must obligatorily contain a specification of their
sizes as illustrated below. This is the same convention that is used
when specifying native or shared variables that contain arrays. In general, lengths must also be included for string types. However, it can be omitted if the string is null terminated.
For the generation of stubs, the names of the arguments are used
to determine the correspondence between the arguments of the Java and
C signatures. The Java and C type of an argument of a native method
must be one of the types that are valid for native variables. The
corresponding C type of the argument must be one of the C types that
are permitted to correspond to the Java type. Automatic conversions
are performed when passing values between Java and C.
If a method is not a static method, then the object the method is
applied to is passed to the C function via the first argument whose
name is not the same as the name of any argument in the Java method.
Otherwise, if an argument appears only in the C signature, it is
passed the value 0. If an argument appears only in the Java signature
it is not be passed to the C function.
In the standard API classes, the convention is followed that if a
class spK has a method M, then the name of the corresponding C
function is spKM, the arguments are in the same order, and the
argument to the C function that receives the object the method is
applied to is the first argument.
The following methods are interpreted in special ways. (Note that for
all these methods, there can be at most one method with the indicated
name in a given shared class definition.)
Initialization - As noted above, you cannot have initialization
expressions for native variables. However, you can define a method
called Initialization that initializes variables to any values you
want. (Note that unlike merely using initialization expressions, this
approach allows you to change the initialization of variables that are
inherited from an ancestor class.) If no Initialization method is
provided, SPOT defines one that does nothing.
New - You can define a static method called New that can be
called from C to create an instance of the class. This method can
take various arguments and initialize various values. If no New
method is provided, then SPOT creates one with no arguments that does
nothing other than call spClassNewObj, which in turn calls the
Initialization methods for the class and every ancestor class (calling
the ancestor methods first). (Operating in Mixed or Java-only mode
SPOT generates a creation method for each shared class which calls the
New method.)
C - A static method called C is automatically generated by SPOT. This
method returns the class object corresponding to the class being
defined. You cannot define a method with this name.
ReadData,
Function,
Predicate,
Inside - These four methods
are recorded in the class descriptor object (along with the
Initialization method) so that they can be called by the system core.
Accessor methods - In general, there is no
explicit mention in a shared class definition of the access methods
corresponding to shared and native variables because these methods are
generated by SPOT. However, If you include an explicit definition of
an accessor method, it will override what SPOT would have created. In
the definition of spExample above, this is the case for the method
SetTimeout.
Returning to the example above.
SPOT expects the following C functions to be defined separately:
Operating in C-only or mixed mode, SPOT generates the following functions:
SPOT would have generated spExampleNew, spExampleInitialization,
and spExampleSetTimeout if they had not been provided by the
programmer. The generated spExampleNew would just call
spClassNewObj with the class spExample as
its argument. The generated spExampleInitialization would do nothing.
The generated spExampleSetTimeout would merely set the value of the field.
A Java file input to SPOT can define non-shared classes
that are meaningful to Java and will be available in Java. These
classes cannot use the keyword shared. However, they can have native
static variables, native static final variables, and methods. These
three constructs are handled in exactly the same way as in shared
classes. In particular, the native variables are accessible in both C
and Java, and SPOT automatically creates stubs so that the native
methods are available in Java and C.
A Java file input to SPOT can define a non-shared class that does
not contain anything that is either native or shared. When that is
the case, the class has no relationship whatever to the system.
However, anything that is acceptable to Java can be used in the
definition of such a class.
A shared class can contain instance variables that are neither shared
nor native. Such a variable can be specified in any way that is
acceptable to Java. However, the variable will not be accessible from C.
In addition, a shared class can define non-native methods.
However, except for the methods Initialization, ReadData, Function,
Predicate, and Inside, non-native methods have nothing to do
with the system and will not be called by the system or available from C.
The central data structure in the API is the world model. There can be only one world model object at a time. It contains the various shared objects that are communicated among a group of processes. The spWM type does not extend the class sp and does not correspond to an object in the world model.
To start up a process, the first thing one does is create the world model to operate on. When the process wishes to terminate, it should remove the world model. In between, the world model is repetitively updated to reflect changes in the objects in the world model caused by other processes.
The class spWM defines the following instance variables:
The class spWM defines the following functions:
The following access functions are available:
Processes are identified by 32-bit session-wide unique owner ids of type spName that are assigned by the session manager. The Me variable of the spWM object contains
the owner id of the activity currently in control.
When the world model is created, the Me value is set to a main owner id assigned by the session manager. It can be changed at will later. Additional owner ids can be obtained using spWMGenerateOwner.
The following access functions are available:
The MainOwner variable of the spWM object contains the owner id assigned to the local process by the session manager. It is set when the world model is initially created and cannot be altered later.
The following access functions are available:
The Error instance variable of an spWM object records the most recent error to have occurred during the evaluation of any API function. When the world model is created the Error value is set to Null. It is changed whenever an error occurs. You can set the Error value back to Null if you want to. However, it cannot be directly set to any other value, but rather only indirectly via spWMReportError. Error strings are limited to being no more than 500 characters long.
The following access functions are available:
The LastError instance variable of an spWM object is identical to the Error instance variable except that it cannot be directly modified by an application, but rather only indirectly via spWMReportError.
The LastError value can always be consulted to determine what the most recent error, if any, was.
The following access functions are available:
The Interval instance variable of a world model records the time in milliseconds between the ends of the last two calls on spWMUpdate. When the world model is created, the interval is set to zero. After the second and subsequent calls on spWMUpdate, the interval value is updated to reflect the timing of events. It must not be modified by an application.
To be precise, the interval value is updated just before action processing begins and reflects the interval in time between corresponding points in spWMUpdate calls. Note particularly, that the interval is calculated after any waiting that spWMUpdate does in order to achieve the desired interval.
The following access functions are available:
The DesiredInterval instance variable of a world model controls the interval between calls on spWMUpdate as follows. When spWMUpdate is just about to begin processing actions, it determines the elapsed time since the last time actions were processed. If this time is less than the desired interval, then spWMUpdate waits until the interval is reached. If the elapsed time is greater than or equal to the desired interval, spWMUpdate proceeds without waiting. There is nothing that spWMUpdate can do to make processing take less time than it is taking. However, spWMUpdate ensures that the actual interval will not be less than the desired interval. Since there is a good deal of imprecision in the system's timing mechanisms, the system cannot guarantee that the actual interval will be equal to the desired interval; however, as long as the desired interval is long enough to be achievable, the system guarantees that the average error over time will be low.
When the world model is initially created, the desired interval is set to zero. This causes spWMUpdate to run as fast as possible without ever waiting. You can change the desired interval at any time.
It is important to think carefully about how often spWMUpdate
gets executed. If it executes too often, time is wasted looking at data
that has not changed in any useful way. Alternatively, If
spWMUpdate executes infrequently, you will be operating on
very stale data much of the time. Further, if spWMUpdate runs very infrequently (e.g., less than once
a second or so) some of the information about world model changes will
probably be lost as queues and buffers overflow. (Things are
arranged, however, so that even if you never call
spWMUpdate, the system will not crash.)
The following access functions are available:
The Window instance variable contains the current user interaction window. The exact type of the window object and the operations that can be applied to it depend on the operating environment---In Java they are one thing and in C under Windows95 they are another.
When the world model is first created, the Window is set to Null. The surrounding environment may force a particular value to be used subsequently. For instance, this is the case when using the system as a Netscape plugin. Otherwise, the application is free to create or choose a window to use.
Creates the world model. As part of this, establishes a connection to a session via a session server.
A process can only create one world model at a time.
The first argument is a DNS string that identifies the session server to connect to (e.g.,
A process must create the world model before using any shared object operation.
After initialization, the world model contains nothing but definitions of the built-in shared classes and a few internal objects that are not observable by applications.
The transfer vector is used to specify key internal operations (e.g., allocating memory) that may have special definitions that are required by the surrounding environment. In simple applications, Null is an acceptable value for this argument.
Eliminates the world model and disconnects the
process from the session it is in. Among other things, this causes all of the objects owned by the process to be removed from the world model and ensures that all the other processes in the session are informed of this fact. A process should always call spWMRemove before terminating. Once spWMRemove has been called, a process cannot use any shared object operation unless it first creates a new world model.
Causes the local world model copy to be updated to reflect changes made by other processes. The fundamental operation of an application process is based
on a cycle of: updating the world model (based on information
received from other processes); running the
application, waiting until the desired update interval has been reached, updating the world model again, and so on.
A number of important things happen each time
spWMUpdate is called.
(1) Other processes are notified of changes made in the local world
model copy only when spWMUpdate is called. This
gives the application control over when other processes see
changes. (For instance, one can ensure that several instance variables of an
object will change simultaneously).
(2) The world model is brought up to date by
processing messages from other processes. It is guaranteed
that the world model will not change in any way between calls to
spWMUpdate, unless the application makes the
changes itself.
(3) Any actions, including Callbacks, in the world model are run.
(This is the only time they are run.)
(4) As part of updating the world model, some objects may be removed
from the world model.
Activities in processes are identified by 32-bit session-wide unique ids of type spName. These owner ids are used to tag the objects created by different activities. The main owner id of a process is assigned by the session manager. Additional ids can be created by using the function spWMGenerateOwner. Each time this function is called it returns an additional owner id.
There are two kinds of owner ids: system and ordinary. spWMGenerateOwner returns ordinary owner ids. System owner ids are only used by the system core; there is no function in the API for creating them.
The purpose for having multiple owner ids in a single process is as the basis for controlling the visibility of objects via world model view masks.
The API utilizes a uniform approach to error reporting. This centers around the error reporting strings created by spWMReportError. Whenever an error occurs, spWMReportError is called and an error reporting string is stored so that it can be easily retrieved.
An error is created based on the data passed to spWMReportError as follows. The description string becomes the heart of the error report. It should be human readable, containing as much contextual information as possible. The system does not modify the descriptive string and does not retain a pointer to it.
The code is converted to ASCII and appended to the front of the description followed by a blank.
The code should be a unique identifier of the error.
(The codes for the errors reported by the system are all negative and every different error has its own unique error code. Applications are advised to use positive error codes.)
Programs that want to handle errors can tell which error occurred by looking at the error code portion of the report string.
The error string created is stored so that it can be retrieved as the value of spWMGetError and spWMGetLastError.
As an example of the way error reporting is used, consider that a careful program that was not completely sure that the variable X contained an spThing, might retrieve the Transform from X as follows:
After a shared object has been removed, the storage associated with it is eventually freed. However, if a pointer is registered using the function spWMRegister, then the system will never free an object that is pointed to by the pointer. This means that it will be safe to save an object in this pointer indefinitely. To stop protection, thereby allowing eventual freeing of the storage, call spWMDeregister.
The system essentially supports a restricted form of garbage collection where the function spWMRegister is used to specify exactly which pointers are taken into account for garbage collection purposes. Note that several threads can each register the same pointer and the pointer will be protected until after all the threads have deregistered it. Note also that it is pointers that are being protected, not objects per se.
Cancels the protection of a pointer started by spWMRegister. It is important to cancel registration when a pointer no longer needs to be protected. This is particularly true if the pointer is stack allocated and the function it is declared in is about to return, rendering the address of the pointer meaningless. Calling spWMRemove cancels all pointer registration.
An important part of the API is functions that map functional
arguments over objects. There are three basic kinds of mapping functions.
In C, spFn is the following functional type:
The following spFn could be used to count.
For instance, the following code counts the number of children of an object X.
Note that the state value that is passed to spExamineChildren and then on to the spFn spCount, is passed directly without copying. This is essential so that it can communicate information by side-effect. However, it means that the state must be in existance for the full period of time that spExamineChildren runs. This is not a problem for spExamineChildren, but requires more thought for operations like spExamineBeacons where the operate continues asynchronously for a potentially long period of time.
When an spFn is used as a callback predicate, the return value is interpreted as specifying whether an event has occurred. Specifically, the predicate should return True or
False depending on whether it observes that the event it
tests for has occurred.
Callback predicates define events in terms of changes to shared
instance variables. In particular, they compare the current state of
these variables with the state of these variables at the end of the
last call on spWMUpdate. (Since callback
predicates are evaluated during each call on
spWMUpdate, this ensures that any state change will
be detected.)
The following shows an example of a simple callback predicate.
There are several key things to note about callback predicates.
First, since callbacks are only applied to objects whose shared variables have changed, callback predicates are not called in situations where there
has been no change in the shared instance variables of an
object. Therefore, events must involve some change in these variables.
Second, the event generally must focus on changes in the shared instance
variables of an object. The API does not provide any way to look at
old values of local instance variables.
Third, callback predicates should not modify the object passed to them.
The API includes the following predefined callback predicates. Users can define any other predicates they want.
A fundamental feature of the API is the notion of a view mask for the
world model. The purpose of these masks is to control the visibility
of objects when using functions like spClassExamine.
The spMask type does not extend the class sp and does not correspond to objects in the shared world model. Rather, spMask data is stored in shared objects and used in intermediate computation.
The class spMask defines the following instance variables:
There are several kinds of objects that an application
typically should not see. First, they should not see objects that
exist purely for the use of the system core and are not part of the
external API. Second, they should not see objects that are created by
another activity and exist in the local world model copy only because
this activity happens to be running as part of the same process,
rather than in a separate process. (For instance, an application does
not want to see objects owned by other activities that are not
communicated to other processes listening in the regions the objects
are in.) Note that an application also typically does not want to see objects that have been removed. However, this is no problem because objects that have been removed are not in the local world model copy and therefore are not encountered by an application.
A world model view mask (spMask) is an integer bit mask that
controls what objects are viewable. A view mask is created by adding
(or or'ing together) the constants above.
Before discussing the exact meaning of these constants, it is
useful to consider a simple example. The following expression applies
F to every spThing object that is not the
private object of some other activity.
In contrast, the following expression applies F to every spThing
object that is owned by the current activity. It does not apply F to any objects owned by
other activities.
The following pseudo-code shows the reasoning applied in
functions like spExamineChildren to determine whether an
object is compatible with a view mask.
For a view mask to make any objects viewable, at least one of the
spMaskMINE and spMaskOTHERS bits must be on.
The three view masks constructible using spMaskMINE and
spMaskOTHERS that view any
objects are all of potential use to applications. The most common
single case is spMaskNORMAL which combines
spMaskMINE and spMaskOTHERS and views all
ordinary objects. This is the default
value for spMasks in most situations.
The functions spExamineChildren,
spExamineDescendants, spClassExamine, and
spClassMonitor all have view mask arguments that limit the
objects that are viewed. Actions and
callbacks also make use of view masks.
View mask restrictions do not apply to the access functions
(such as spGetParent) for obtaining the value of instance
variables. This is not a problem because, in general, you cannot get
from objects you should see to ones you should not see. Objects with ordinary
owners should never point to objects with system owners. Objects that
are communicated via regions never point to ones that are not. Note that no object
ever points to an object that has been removed.
As illustrated above, view masks can be created by adding or or'ing these constants together.
The data type spTransform is used as the fundamental representation of the position, orientation, and scaling of objects. A key advantage of an spTransform is that the various components are easy to understand. The components are also well suited to interpolation. The spTransform type does not extend the class sp and does not correspond to objects in the shared world model. Rather, spTransform data is stored in shared objects and used in intermediate computation.
The class spTransform defines the following instance variables:
The class spTransform defines the following functions:
An spTransform contains the same information as in a VRML transform node. Specifically, an spTransform is a vector of 17 floats representing 5 logical values as follows.
The first three elements are a vector representing the translation. The next four elements are an spRotation vector representing a rotation. The next three elements specify the amount of scaling along the X, Y, and Z axes with the value 1.0 indicating no scaling. The next four elements are an spRotation that is applied before the scaling is performed. The final three elements specify a center point that specifies the center point for both rotations.
As in VRML, the parts of an spTransform act together as follows.
Note that because of the ScaleOrientation, the scale value can specify
shear as well as scaling. It can be shown that the composition of any two spTransforms can be represented as an spTransform and the inverse of any spTransform can be represented as an spTransform.
By convention, Spline uses a right-hand coordinate system with the
Y axis up and objects facing down the negative Z axis. (No assumptions are made about the relationship of the X and Y axes to compass directions.) It should be noted that
different graphics modeling languages disagree with each other
about these conventions. For instance, some have the Z axis up. The way spVisualDefinition links are
specified makes it easy to use any kind of graphic model, without
having to modify it.
The origin of the coordinate system for an object should be in
the middle of the object unless there is a compelling reason
otherwise. This is needed so that the InRadius and OutRadius of
spThings will be meaningful. (An example of a compelling reason why
the origin of an object would not be in the middle is that the origin
of a subpart of an articulated form should be at the pivot point.
This makes the mathematics of moving the subpart much easier.)
Any object that has a recognizable up direction should be
oriented so that this up direction is parallel to the Y axis and
points toward positive Y. Similarly, any object that has a
recognizable front should have the front facing toward the negative Z
axis. For instance, an object corresponding to the torso of an avatar
would have the origin of its coordinate system in the middle of the
chest, with the Y axis pointing up toward the head and the negative Z axis
pointing out through the front of the torso. These axis conventions
are needed both so that it is easy to combine different objects into a
single scene and so that simulations that want to interact with
objects can find where the tops and fronts of the objects are.
All units are in terms of meters and radians. Radians are used because they are more natural for numerical calculations. However, most people think more easily in degrees. In recognition of this, the following constant is provided.
Using this constant you can specify PI/2 radians (90 degrees) as follows.
In C, an spTransform is the following type.
In addition, the following type is available for allocating
memory for an spTransform. When calling a function that operates on
spTransforms, one can pass in a variable that is either of the type
spTransform or spTransformData.
Second, you can use the functions described in the following subsections
to operate on spTransform objects. These make it easier to do
complex operations. However, they
require a clear understanding of transform interactions in
order to figure out how to obtain a desired effect. In
addition, these operations are static in the sense they call for the
computation of particular spTransforms, rather than sequences of transform
values over time.
Several conventions are worthy of note about the functions on
spTransform objects.
None of the functions ever allocates memory.
Rather, the control of memory is left entirely up to the application.
All modifications are by side-effect. However, to make it easier to create nested expressions the modified value is used as the return value.
A key complexity is that four different representations for
rotations are supported. This is necessary because different
communities of people are accustomed to different representations and
justified because each representation has a situation where it is
particularly convenient. The primary rotation representation is spRotation vectors such as those included in an spTransform. The second rotation representation is a
vector of 3 Euler angles (in an spVector). The Third rotation
representation is a vector of 4 floats representing a quaternion (spQuaternion). The fourth
rotation representation is the
rotation part of an spMatrix.
For example, you might write.
Copies the data from a Source spTransform to another, which is returned.
Initializes the values in an spTransform so that they specify no translation, rotation, or scaling. That is to say, the spTransform is set to (0,0,0, 0,0,1,0, 1,1,1, 0,0,1,0, 0,0,0).
It is important to initialize an spTransform before using it as a source of data, because the operations on spTransforms do not test that transforms are well formed before beginning their operations. In particular, they assume that the rotations in it are well formed.
Returns the Translation portion of an spTransform. In C, the spVector returned shares memory with the spTransform.
Sets the Translation portion of an spTransform. The other parts of the spTransform are not altered.
Returns the Rotation portion of an spTransform represented as an spRotation.
In C, the spRotation returned shares memory with the spTransform.
Sets the Rotation portion of an spTransform. The other parts of the spTransform are not altered.
Returns the Scale portion of an spTransform. In C, the spVector returned shares memory with the spTransform.
The three elements of the spVector contain the amount of scale along the X, Y, and Z axes respectively. The value 1.0 indicates no change in scale.
Sets the Scale portion of an spTransform. The other parts of the spTransform are not altered.
Returns the ScaleOrientation portion of an spTransform represented as an spRotation.
In C, the spRotation returned shares memory with the spTransform.
Sets the ScaleOrientation portion of an spTransform. The other parts of the spTransform are not altered.
Returns the Center portion of an spTransform.
In C, the spVector returned shares memory with the spTransform.
Sets the Center portion of an spTransform. The other parts of the spTransform are not altered.
The type spVector is a 3-element vector of floats. It is used to
represent several distinct things. The spVector type does not extend the class sp and does not correspond to objects in the shared world model. Rather, spVector data is stored in shared objects and used in intermediate computation.
The class spVector defines the following instance variables:
The class spVector defines the following functions:
There are three major uses of vectors. The first is as an
ordinary vector in Cartesian coordinates, with the three elements
being the X, Y, and Z coordinates respectively. These vectors are
used to specify translations, rotation axes, and center points.
The second use of vectors is as a set of Euler angles. In this
use, the first element is a rotation around the X axis in radians;
the second element is a rotation around the Y axis in radians; and
the third element is a rotation around the Z axis in radians. Since
rotations do not commute, it is important to realize that these
correspond to first rotating around Z, and then rotating around Y,
and lastly rotating around X.
The third use of vectors is as a representation for scaling. In
this use, the three elements represent the amount of scaling in the X,
Y, and Z axes respectively. A value of 1.0 in an element specifies no
scaling.
In C, an spVector is the following type.
In addition, the following type is available for stack allocating
memory for an spVector. When calling a function that operates on
spVectors, one can pass in a variable that is either of the type
spVector or spVectorData.
Several conventions are worthy of note about the functions on
spVectors.
None of the functions ever allocates memory.
Rather, the control of memory is left entirely up to the application.
All modifications are by side-effect. However, to make it easier to create nested expressions the modified vector is used as the return value.
For example, you might write.
The following four constants contain particular vectors that are useful as arguments to various API functions. (In C, these constants are external variables initialized to appropriate values.)
For example, you might write.
Copies a vector into another.
Sets all three elements of an spVector to be equal to a given Scalar.
Returns True if two vectors are component-by-component equal within a small system-defined tolerance.
Returns True if two vectors are component-by-component equal within a tolerance specified by the user.
Adds two vectors together and stores the result in the first vector.
Subtracts a second vector from a first vector and stores the result in the first vector.
Modifies a vector by multiplying each element by a scalar.
Modifies a vector by dividing each element by a scalar.
Computes the cross product of two vectors and stores it in the first vector.
Computes the dot product of two vectors and stores the result in the first vector.
Modifies an spVector by multiplying each element by the corresponding element of another spVector.
Computes the length of a vector. That is to say, the square root of the sum of the squares of the elements.
Converts a vector into a unit length vector pointing in the same direction. That is to say, divides each element by the length of the vector.
The principle way that rotations are specified is in terms of a rotation axis passing through the origin and a rotation angle about this axis in radians. Typically, the rotation angle is between plus or minus PI. A key advantage of an spRotation is that the various components are easy to understand. In addition, because the components are relatively independent, they are well suited to interpolation. The spRotation type does not extend the class sp and does not correspond to objects in the shared world model. Rather, spRotation data is stored in shared objects and used in intermediate computation.
The class spRotation defines the following instance variables:
The class spRotation defines the following functions:
An spRotation is a vector of 4 floats consisting of two parts as follows.
The first three elements are a vector representing an axis through the origin to rotate about. The last element is an amount of rotation in radians.
In C, an spRotation is the following type.
In addition, the following type is available for allocating
memory for an spRotation. When calling a function that operates on
spRotations, one can pass in a variable that is either of the type
spRotation or spRotationData.
Several conventions are worthy of note about the functions on
spRotations.
None of the functions ever allocates memory.
Rather, the control of memory is left entirely up to the application.
All modifications are by side-effect. However, to make it easier to create nested expressions the modified value is used as the return value.
For example, you might write.
Copies the data from a Source spRotation to another, which is returned.
Initializes the values in an spRotation so that they specify no rotation about the Z axis. That is to say, the spRotation is set to (0,0,1,0).
It is important to initialize an spRotation before using it as a source of data because the operations on spRotations do not test that rotations are well formed before beginning their operations. In particular, they assume that the axis does not have zero length.
Returns the Axis portion of an spRotation. In C, the spVector returned shares memory with the spRotation.
Sets the Axis portion of an spTransform. The angle is not altered.
Returns the Angle in an spRotation.
Sets the Angle in an spTransform. The axis is not altered.
Computes the spQuaternion corresponding to an spRotation.
Computes the spRotation corresponding to an spQuaternion.
Computes the Euler angles corresponding to an spRotation.
Computes the spRotation corresponding to a vector of Euler angles.
Computes an spRotation that represents the composition of two spRotations. Specifically, an spRotation A is modified to represent the effect of applying a second rotation B after A. Note that this means that B is effectively multiplied on the left, not the right.
Computes the absolute spRotation that should be used to view one position from another. In particular, an spRotation R is computed so that if the rotation is used for an object A at the From position, then the negative Z axis of A's coordinate system points to the specified To position, and A's Y axis and the specified Up vector are co-planar. Typically, the Up vector is chosen to be parallel to the Y axis of the main coordinate system. This causes objects that are upright to appear upright if the rotation is used to position an spSeeing beacon.
The rotation that is the target of spRotationLookAt is modified by filling it in with the viewing rotation and then returned.
Several different representations for rotations are supported. One
of these is spQuaternion, which is a 4-element vector of floats representing a quaternion.
There are very few functions in the class spQuaternion, because spRotation is much more central to the API.
The spQuaternion type does not extend the class sp and does not correspond to objects in the shared world model. Rather, spQuaternion data is stored in shared objects and used in intermediate computation.
The class spQuaternion defines the following functions:
The four elements of an spQuaternion
represent a quaternion number.
(The vector is required to represent a unit quaternion, I.e.,
the sum of the squares of the elements of an spQuaternion
is 1.0.)
In C, an spQuaternion is the following type.
In addition, the following type is available for stack allocating
memory for an spQuaternion. When calling a function that operates on
spQuaternions, one can pass in a variable that is either of the type
spQuaternion or spQuaternionData.
Copies the data from a Source spQuaternion to another, which is returned.
Initializes the values in an spQuaternion so that they specify no rotation. That is to say, the spQuaternion is set to (0,0,0,1).
It is important to initialize an spQuaternion before using it as a source of data because the operations on spQuaternions do not test that the spQuaternions are well formed before beginning their operations.
Computes an spQuaternion that represents the composition of two spQuaternions. Specifically, an spQuaternion A is modified to represent the effect of applying a second rotation B after A. Note that B is effectively multiplied on the left, not the right.
Following standard graphics practice, the positions, orientations, scaling, and shear
of objects can be represented using 4x4 transformation matrices
called spMatrix objects. These matrices are also capable of
representing other effects such as taper, however, because spTransforms are used as the fundamental representation, these additional effects cannot be used. The various operations on spMatrix objects assume that the matrix corresponds purely to translation, rotation, scaling, and shear. The spMatrix type does not extend the class sp and does not correspond to objects in the shared world model. Rather, spMatrix data is stored in shared objects and used in intermediate computation.
The class spMatrix defines the following functions:
An spMatrix is a matrix of the following form.
The upper right hand quadrant is a 3x3 rotation matrix specifying
orientation, scaling, and other effects. The last column specifies translation (i.e., position).
An spMatrix is stored in column major order as a
vector of 16 floats. In C, an spMatrix is the following type.
In addition, the following type is available for stack allocating
memory for an spMatrix. When calling a function that operates on
spMatrix objects, one can pass in a variable that is either of the type
spMatrix or spMatrixData.
Modifies one matrix to equal another.
Modifies an spMatrix to be the identity matrix. The identity matrix is one where the rotation matrix is the identity matrix specifying no changes and there is no translation. That is to say, the spMatrix is set to (1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1). It is important to initialize an spMatrix before using it as a source of data because the operations on spMatrix objects do not test that the matrices are well formed before beginning their operations.
Returns the translation component of an spMatrix. In C, the spVector returned shares memory with the spMatrix.
Sets the translation component of an spMatrix, leaving the rest of the spMatrix unmodified.
Modifies a matrix so that it has the translation, rotation, scaling, and shear specified by an spTransform.
Computes the spTransform that corresponds to an spMatrix and stores the result in the indicated spTransform. (It should be noted that this operation is computationally quite expensive and should be avoided if possible.)
Not every spMatrix can be converted into an spTransform. For example, an spTransform that specifies taper cannot be. However, any spMatrix that can be computed by using the functions provided here can be converted into an spTransform. The only exception to this is if the individual elements of an spMatrix are set directly. This is not recommended.
Modifies a matrix so that it becomes the inverse of its previous value. That is to say so that it specifies an opposite rotation about the same axis, inverse scaling, and the negation of the translation. The product of a matrix and its inverse is the identity matrix.
Computes the product of two matrix objects. This is the sum of the translations, the product of the scaling and the result of performing one rotation after the other. A matrix A is multiplied by another matrix B (with A on the left and B on the right) and A is modified to contain the result and returned.
Multiply an spMatrix times an spVector to determine what the transformed vector is. This determines the result of the transformation specified by an spMatrix on the given vector. The result is stored in the argument vector and returned.
As an example, the following code shows how to calculate a vector Up in the local coordinate system of an spThing A that corresponds to the global up direction spVectorAXISY. The code multiplies the spMatrix relating the topmost coordinates to A's coordinates by a copy of spVectorAXISY to determine a vector in A's coordinates that corresponds to up.
The spFormat type does not extend the class sp and does not correspond to objects in the shared world model. Rather, spFormat data is stored in shared objects and used in intermediate computation. spFormats specify various sound data formats including sampling rate and encoding.
The class spFormat defines the following instance variables:
The class spFormat defines the following functions:
In C, An spFormat is the following structure. The various fields can be independently manipulated; however, it is primarily intended that applications use a small set of constant values and the small number of methods described below.
The rate specifies how many times per second the sound data is sampled. Popular rates include 8000, 16000, 32000, and 44100.
The encoding (an integer code) specifies what kind of data encoding is used.
The bit samples specifies how many bits (e.g., 8, 16) are in each sample.
The samples per frame specifies how many channels of data are represented (e.g., 1 (mono), 2 (stereo), 4 (quad)).
All sound data accessible to applications is linearly encoded with 16 bits per sample.
(More compact encodings are used when communicating between processes.)
Typically, sound is output through spAudioSources in mono and rendered through a user's headphones in stereo.
Computes the duration in milliseconds (rounded up to the nearest millisecond) of the specified number of bytes of sound data stored using the indicated format.
Computes the number of bytes required to represent the specified number of milliseconds of sound data using the indicated format.
This is the root of the shared object hierarchy. The class is abstract in the sense that applications do not create objects of the class sp. However, the functions associated with this class are applicable to every shared object.
The class sp defines the following instance variables:
The class sp defines the following functions:
It is not possible to create instances of the class sp. Rather, one can only create instances of particular subclasses of the class sp.
Using this constant you can specify PI/2 radians (90 degrees) as follows.
The following access functions are available:
Each shared object has a shared instance variable called its Class that contains an spClass object describing the local and shared instance variables associated with the object.
The Class of an object is set when it is initially created and cannot change after that time. Information about the Class is shared between the processes in a session.
The following access functions are available:
An important feature of the API is that every shared object has a single owner and the owning activity is the only process that can modify any of the shared data associated with the object. This avoids readers/writers conflicts when using shared objects. The only exception to the above is that a process can create an spAction object that can run in other processes and remotely modify objects that are owned by the same process as the spAction.
Each shared object has a shared instance variable called its Owner that contains the unique id of the activity that owns it. The name space of owner ids is the same as the name space of object ids. Owner ids are created using the function spWMGenerateOwner. To determine whether you are the owner of an object X perform the test:
The Owner of a shared object is set to the value of spWMGetMe when the object is created.
After that time, the Owner of an object is free to change the Owner to a different owner id, thereby giving up ownership of the object to another process. A process must be careful to set the Owner to a valid owner id. If not, the object will be irrecoverably lost in limbo, until it eventually times out. Information about the Owner is shared between the processes in a session.
A process can request that ownership of an object be transferred to it by creating an spOwnershipRequest object. The owner may or may not satisfy the request.
The following access functions are available:
A key feature of shared objects is that they can be hierarchically arranged. For example, an articulated humanoid figure might be constructed so that the top level object corresponds to the torso. The torso might have a head, two upper arms, and two thighs attached to it. Each upper arm might have a lower arm attached to it and so on.
Hierarchical relationships between shared objects are represented by using a shared instance variable called the Parent. In the example above, the lower arms of the humanoid figure would have as Parents the corresponding upper arms which would have as their Parents the torso.
An equally important feature of the Parent of an object is that it determines what region the object is in. This in turn determines how the object is communicated, because information about an object is sent only to the region it is in. Therefore, if the Parent of an object is Null, information about it will not be sent anywhere.
There are no explicit access functions for the inverse of the Parent relationship. However, this information can be indirectly obtained using the functions spExamineChildren and
spExamineDescendants.
The Parent of a shared object must be another shared object, or Null, meaning that there is no Parent. When an object is created, its Parent is initialized to Null. Information about the Parent is shared between the processes in a session.
The following access functions are available:
The local instance variable IsRemoved has the value True if and only if the object has been removed from the local world model copy.
It is still permissible to look at the values of the instance variables of an object after the object has been removed.
(This is important in callbacks.) However, one has to be very careful about retaining pointers to objects that have been removed, because the system frees the storage corresponding to an object soon after it is removed.
The IsRemoved bit is set to False when an object first appears in the world model.
It is set to True when the object is removed. The IsRemoved bit must never be altered in any other way.
Information about the IsRemoved bit is shared between the processes in a session.
By comparing the current and old values of the IsRemoved bit, one can determine whether an object was recently removed. In particular, if the current value is True while the old value is still False, then the object was removed since the end of the most recent call on
spWMUpdate to have been completed.
The following access functions are available:
The local instance variable IsNew has the value True if and only if the object appeared in the local world model copy since the end of the last call on spWMUpdate.
The IsNew bit is set to True when an object first appears in the world model.
It is set to False at the end of the next call on spWMUpdate.
Information about the IsNew bit is maintained separately in each process.
The following access functions are available:
Every shared object has a local instance variable called AppData, which applications can use to store any 32-bit quantity they want to. This can be very convenient in many situations. However, note that if you store pointers to dynamically allocated structures, you must carefully monitor the removal of objects (e.g., with a callback) or risk having a memory leak due to the allocated structures not getting freed when objects are removed. (Since the system does not understand anything about what is in this variable, it cannot take any action to prevent memory leaks.) In addition, if two applications running on the same machine and sharing the same world model copy both try to use this variable, severe problems will arise unless they cooperate closely.
When an object is created, the AppData value is set to Null. Applications can do whatever they want with it after that time. Information about the AppData is maintained separately in each process.
Given an object, one can obtain the descriptor of the object's class by using the accessor spGetClass. This is all that is needed in many situations. However, it is also often convenient to be able to obtain the description of a particular class even though you do not have any particular instance of the class. For example, you might want to use spClassExamine to find any instances that exist.
For each class spK, the system automatically generates a function spKC with no arguments that returns the class descriptor for the class. For example the class sp includes the function spC, which can be used as follows:
With regard to constructing objects, shared classes fall into three categories. A couple of classes (such as sp and spLink) are abstract in the sense that they merely group together some useful operations or specify a pattern that must be specialized before it can be used. It is not possible to construct instances of these classes.
(As a result, the function spNew does not actually exist. It is described here in order to present what New functions must look like for shared classes that are not abstract.)
For most classes spK, instances are created using an automatically generated zero-argument constructor named spKNew. When this is the case, no special comment is made in this documentation beyond noting the existence of the constructor. The way spKNew is implemented is shown below.
Some classes (for example spRegion) have constructors that take arguments. In this case, the constructor is not automatically generated. Nevertheless, it must begin by calling spClassNewObj as in the automatically generated constructor shown above.
Whenever a special New function exists, this document contains a subsection describing it. A key reason for having constructors with arguments is that a number of classes have instance variables that must be set at the moment an object is created and cannot be changed later.
Initializes the values of instance variables. Whenever an instance of a shared class is created, by a New function,
The values of the instance variables are all set to all bits zero. After this, the Initialization functions are called for the class and all its ancestors in order to properly initialize any variables that need to have non-zero values. The Initialization functions are called starting with spInitialization and then working down to the most specific class so that the Initialization function for a class can override the Initialization function(s) for its superclass(es).
In order to reduce the number of Initialization functions that have to be written, the API is designed so that as much as possible, all bits zero is an appropriate initial value. The only major exception to this is that spTransforms are initialized to the identity transform, which is not all zero. Wherever a non-zero initialization is needed, this fact is pointed out in this documentation.
Whenever the a new shared class is defined that requires a variable to be initialized to something other than zero, an Initialization function must be defined for the class. If no Initialization function is defined, then the system automatically generates one that does nothing.
Removes an object from the world model, setting the IsRemoved bit to True. If an object is removed by a process that does not own it, then the object is removed from the local world model copy of that process with no other effect. However, if an object is removed by the process that owns it, then the fact that the IsRemoved bit has been set is communicated to all the other process that know about the object, causing the object to be removed in these processes as well. One must be very careful when accessing objects that may have been removed.
Removal is different from freeing the storage for an object. Freeing happens separately and not necessarily immediately. Removal of an object by its owner signals the explicit desire to remove an object from the world model and therefore from the view of other processes. It is essential that it occur immediately, not at some later time.
Applies an operation to all the direct children of an object that are compatible with the Mask. (An object C is a child of an object P if and only if P is the Parent of C.) There are no guarantees as to the order in which the children will be accessed.
There is no explicit representation of the children of an object, and no direct way to access the children of an object. However, the indirect approach provided by spExamineChildren is typically at least as convenient as some more direct approach would be.
Applies an operation to all the children of an object that are compatible with the Mask and then to all their children that are compatible with the Mask and so on. The order of application is undefined except that the operation will be applied to a given descendent D before it is applied to any of the descendants of D.
Computes the highest level ancestor of an object that defines the coordinate system the object is in. This value is often useful when using operations like spThingRelativeMatrix. If the object is not in any region, then Null is returned.
Prints out the contents of all the instance variables of a shared object. This is very useful for debugging.
Regions break the world model up into pieces that are handled separately. They are the basis for the scalability of the world model. They make it possible for both the size of the local world model copy and the number of incoming messages that have to be handled per second to be dependent only on the part of the virtual world that is `near' to the local process, rather than dependent on the total size of the virtual world as a whole.
The shared class spRegion inherits all the instance variables and functions of the
class sp.
The class spRegion defines the following instance variables:
The class spRegion defines the following functions:
The concepts underlying spRegions are a simplification of the concepts presented in
Barrus J.W., Waters R.C., and Anderson D.B., ``Locales: Supporting Large Multiuser Virtual
Environments'', IEEE Computer Graphics and Applications, 16(6):50--57, November 1996.
Like any other object, a region has an owner that is the only process that can change it.
The region will go away, turning all the objects in it into orphans, if the owner of a region leaves the session.
Each region is associated with a particular set of communication addresses. Information about objects in a region is communicated only via those addresses. An orphaned object that is not in any region because it has no Parent is not communicated to anybody.
An important feature of a region is its boundary. The boundary specifies the three dimensional extent of the region. The boundaries of an spRegion must be specified when the region is first created and cannot be changed later.
It must be stressed that what region an object is in for communication purposes is determined by its Parent as discussed above, not any geometric considerations. However, in general, an object should be placed in a region only if its position is within the boundary of the region. (A point is within a boundary only if there is both a point below it and a point above it that are on the boundary.)
In general, transferring an object form one region to another involves nothing more complicated than changing its Parent. However, it can be somewhat complicated to figure out what region an object should be in. Determining this is facilitated by the function spThingLocalize. If an object whose Parent is an spRegion is not within the boundary of the region, and there is a nearby region whose boundary does contain the object, then spThingLocalize transfers the object to the appropriate nearby region.
A key concept above is what regions are nearby a given region. This is an induced relation based on the boundaries of regions. Specifically, a region R is near another region S if and only if the axis aligned bounding
boxes of the boundaries of R and S touch---i.e. if there is any 3D
point that is in (or on) both boundaries. (Note, no matter how complex the boundary object is,
only the bounding box is being compared. This is sloppy but allows
rapid determination of nearness. It is not crucial that nearness
be exactly computed. What is typically essential is that a process
listens to enough nearby regions. It usually matters little if a
couple too many regions are attended to.)
A set of regions that neighbor each other pairwise is called a scene. (One can move incrementally step by step from one region to another in a scene using spThingLocalize; however, the only way to get from one scene to another is to teleport to the destination by explicitly setting the Parent of the object to be moved.)
The following access functions are available:
The Boundary shared instance variable of an spRegion object specifies the object describing the boundary of the region. The Boundary must be an spBoundary object. It is used to determine whether a object should be placed in a region and what regions are near other regions. The Boundary must be specified when a region is created and cannot be modified later. Information about the Boundary is shared between the processes in a session.
Creates a new object of the class spRegion, with the local process as its owner. In addition, sets the boundary of the spRegion as specified. To insure proper communication of the Boundary, the Parent of the Boundary is set to be the region created.
To create a region, you might write the following:
Shared objects are divided into two major categories: small objects that can be communicated very rapidly between processes (on the order of a hundred milliseconds) and large slowly changing objects that are communicated more slowly between processes (on the order of a few seconds). The various subclasses of the class spLink comprise the large slowly changing objects.
The key feature of spLink objects is that they refer to an arbitrarily large piece of data via a Uniform Resource Locator (URL). The link object itself, including the URL, is communicated to other processes in the same way, and just as fast, as any other object. However, once the link itself has been received, additional work has to be done to retrieve the data specified by the URL. This can take several seconds.
The data pointed to by the URL is retrieved over the World Wide Web using standard web protocols.
Like any object, the Parent of a link controls how the link is communicated. In particular, the Parent of a link must ensure that the link is known in the region where it is used. Typically, the Parent of a link is set to the object that uses it. To support prefetching of data, one can place links to the data in regions adjacent to the region where it is used. Note that having multiple copies of a link does not cause the data referred to be loaded into memory multiple times.
An application may create links that it is not using at the moment but wishes to be able to use rapidly. For example, suppose that there is an avatar that wishes to be able to switch rapidly between several visual appearances.
This can be supported by creating a link for each appearance and making the avatar be the Parent of these links. This will cause the links to be communicated to the region the avatar is in and therefore preloaded in this region. The avatar can then change its appearance instantaneously by changing which link its VisualDefinition variable points to.
The shared class spLink inherits all the instance variables and functions of the
class sp.
The class spLink defines the following instance variables:
The class spLink defines the following functions:
It is not possible to create instances of the class spLink. Rather, one can only create instances of particular subclasses of the class spLink.
The following access functions are available:
The central piece of information in an spLink object is a shared instance variable called the URL, which contains a Uniform Resource Locator (URL) identifying the large piece of data being linked to. For example, an spVisualDefinition link must contain the URL of a 3D graphical model. When a process is informed of the existence of an spVisualDefinition link, it fetches the data referred to by the link's URL for later use.
The URL of an spLink must be a string. It must be less than 500 characters in length, so that the containing object can fit into a single UDP message. It must be specified at the time the link is initially created and cannot be modified after that time. Information about the URL is shared between the processes in a session.
It is inadvisable to have an object you own refer to a link created by another process because the link may get removed at any time. Rather, you should create your own link with the same URL and refer to that. (The system takes care of making sure that there are not two copies of the same URL data in memory at once, even if there are multiple links containing the URL.)
You really should not refer to someone else's URL either, unless you can trust them not to change or delete either the URL or the data it points to. To be completely safe, you should make a copy of the URL data and then refer to your own copy.
The following access functions are available:
The data fetched from the URL of an spLink is stored in a local instance variable called the Data. If several links have the same URL, then the data is only read into memory once and each link points to the same block of data.
The kind of data pointed to by the Data pointer of a link differs from one kind of link to another. The Data pointer is automatically filled in by the system when a new link appears in the world model and is automatically updated when the underlying data changes. The Data pointer and the data it points to should never be altered in any way. Information about the Data is maintained separately in each process.
A key feature of the Data variable is that it is guaranteed that the Data value will be filled in before the Link becomes visible in the world model. Specifically, whenever a new link is created, the Data is read in by the New function. In addition, whenever a message is received describing a new link, the act of actually entering the link into the local world model is delayed until after the Data has been obtained. This delay makes things more convenient for processes that want to do something with the data and is irrelevant to ones that don't.
This function does not exist for the class spLink itself. It is described here because it is an abstract prototype of the creation function that must be written when a new subclass of spLink is defined. In particular, link creation functions must take a URL string as their first argument and create a link object containing the URL. This is the only way the URL in a link can be set.
The code below shows the general form that the New function for a subclass spL of spLink must have. In particular, it must call spClassNewLink to create the link itself. This insures that the ReadData function will be called when the link is created.
This function does not exist for the class spLink itself. It is described here because it is an abstract prototype of the URL data reading function that must be written when a new subclass of spLink is defined.
The purpose of the data reading functions for link classes is to fetch the information corresponding to a link's URL and read it into memory and store a pointer to the result in the data variable of the link object. It may initialize other local fields of the object as well.
An instance of the class spVisualDefinition is a link to a 3D graphical model specifying a visual appearance. The URL of an spVisualDefinition link identifies a graphical model in VRML. Ordinary applications do not read in these 3D models. Rather, they are only read in by the visual renderer. When they are read in, they are converted into scene graphs in the internal format used by the renderer. The chunk of scene graph created is stored in the data variable of the spVisualDefinition link.
The shared class spVisualDefinition inherits all the instance variables and functions of the
classses spLink and sp.
The class spVisualDefinition does not define any additional instance variables.
The class spVisualDefinition defines the following functions:
The basic approach for creating images of a virtual world centers around spThings and their
visual definitions. The appearances of all the spThings in view at a given moment are combined together into a scene graph and rendered. The positions
and orientations of the various appearances are taken from the Transforms of the corresponding spThings.
Getting an appearance entered into the world model is a three step process.
(1) One first creates (or locates) a 3D model in VRML, or whatever other format the renderer you are using supports. This is done using standard modeling tools.
(2) Next one has to decide how the model should be positioned on an object in the world model. As discussed in detail below, this positioning is specified using an intermediate description file.
(3) Finally, to get the appearance into a particular virtual world, you create an spVisualDefinition link whose URL refers to the intermediate description file and make it be the appearance of one or more spThing objects. The following code shows an example of this.
The intermediate description files have a two level structure. First, they can have one or more entries corresponding to alternate 3D models. These models might be in different formats or at different levels of detail. The process loading a link is free to choose whichever model is most appropriate for it. Second, each individual entry contains several key pieces of information, each on a separate line. (Note that this information is utilized only by the visual renderer.)
The central element above is the positioning transform. It allows you to take an arbitrary model and use it without having to alter it in any way. For example, suppose that you are creating a virtual world that contains a virtual car that can move around. To conform with the API's object orientation standards, the spThing for the car must be oriented with the Y axis up and with the front of the car facing down the negative Z axis. Further, to make it easy to move the car around, it is convenient to place the origin of the car on the ground under the center of the car's appearance. In addition, the car in the virtual world has some intended size. (These latter two needs would remain even if the API had no object orientation standards.) It is very unlikely that a given model for the car's appearance will meet these conditions. However, using a positioning transform it is trivial to adapt the model to its use in the virtual world without having to modify the model itself.
Continuing the example above, The programming API considers graphical models to be completely opaque. All it cares about is that the visual renderer being used can understand the models created by the modeling tools being used. The system itself never looks at what is in a graphical model. In particular, it is not intended that an application dynamically modify models, but rather only effect the overall scene graph by affecting the aspects of spThings (such as their Transforms) that are visible in the world model.
Creates a new object of the class spVisualDefinition, with the local process as its owner. In addition, sets the URL of the spVisualDefinition as specified.
An important feature of the API is a facility for prerecorded sound. This is intended to be
used for (usually short) things that get used over and over (like the
sound of a door closing). Their advantage is that the data can be
prestored at the destination processes instead of having to be
transmitted every time it is used.
An instance of the class spSound is a link to a recorded block of sound data such as ambient background sound or a sound effect.
The data can be created using a number of standard tools such as
sound editors and written in files in a number of standard formats.
They are then referred to using spSound links.
The shared class spSound inherits all the instance variables and functions of the
classses spLink and sp.
The class spSound defines the following instance variables:
The class spSound defines the following functions:
The basic approach presented here for creating sound effects in a virtual world centers around spSound and spSoundSource objects. The spSound objects specify what the various sound effects sound like. The positions of the spSoundSources specify where sounds emanate.
Getting a sound effect to happen in a virtual world is a three step process.
(1) One first creates (or locates) sound files in one of several common formats. This is done using standard sound capture and editing tools.
(2) As discussed in detail below, one then creates an intermediate description file that summarizes key features of one or more versions of the sound.
(3) Finally, to get the sound into a particular virtual world, you create an spSound link whose URL refers to the intermediate description file. You then create one or more spSoundSource objects and use spSoundPlay to play the sound at the right places and the right times. The following code shows an example of this.
The intermediate description files have a two level structure. The first line of the file specifies the duration of the sound rounded to the nearest millisecond. This value is needed so that the Duration variable of an spSound can be set correctly even if the sound data itself is not read into memory.
Subsequent groups of lines specify one or more sound files that correspond to the sound.
Each sound file entry contains several key pieces of information:
It is expected that the various sound files referred to will have different encodings or different sample rates. For example, you might have 8 KHz file for the use of low powered machines and a 32 KHz high fidelity version for more powerful machines. The process loading a sound is free to choose whichever sound file is most appropriate for it.
Continuing the example above, The programming API considers stored sounds to be completely opaque. All it cares about is that the audio renderer being used can understand the sound files created by the modeling tools being used. The system itself never looks at what is in a sound file.
The following access functions are available:
spSound objects have a local instance variable call the Duration that records the period of time
corresponding to the sound data referred to. This is recorded in a variable so that processes that processes other than an audio renderer can reason about how much time is required to play back the data without having to load the entire sound file into memory.
The Duration is a 32-bit integer of the type spDuration specifying the time in milliseconds. It is set by spSound ReadData function based on the URL data even when the full sound data is not read into memory.
If the number of samples does not correspond to an exact integer
number of milliseconds, then the time is calculated by rounding it to the nearest millisecond.
The Duration should not be altered after that time. Information about the Duration is maintained separately in each process.
Creates a new object of the class spSound, with the local process as its owner. In addition, sets the URL of the spSound as specified.
Causes a stored sound to be played through an spAudioSource. The key feature of spSoundPlay is that it does not actually output the sound through the source, but rather creates an action that simulates the receipt of the sound by each process that can hear it, without having to send the sound data over the network.
The playing of sound continues until: the data runs out, the action object returned by spSoundPlay is removed, the source object is removed, the spSound object is removed, or some other sound is played through the same spAudioSource. Note you can only be playing one sound at a time through a sound source.
Boundaries encapsulate the basic concept of a machine manipulable description of a 3D volume. Their primary use is in conjunction with spRegion objects. However, they can also be used in other situations as well.
The shared class spBoundary inherits all the instance variables and functions of the
classses spLink and sp.
The class spBoundary defines the following instance variables:
The class spBoundary defines the following functions:
The URL in an spBoundary link identifies a file containing a detailed description of the boundary. In the case of spBoundary objects themselves, the description is an axis aligned bounding box. Specifically, the file identified by the URL must contain one line of text consisting of 6 floats separated by spaces. The six floats specify the minimum X value, minimum Y, minimum Z, maximum X, maximum Y, and maximum Z values of the spBoundaries bounding box respectively. For instance, the file might contain:
The boundary information provided by an spBoundary object is very simple, and sufficient for some applications. Subclasses of spBoundary are free to use more detailed representations such as BSP trees as long as they also specify the bounding box.
spBoundary objects are separate objects from spRegions because they
support a completely separate concept. spRegions need spBoundaries to
work, but spBoundaries can be useful without spRegions. In
particular, an spBoundary smaller than (or larger than) an spRegion
can be used by itself as part of controlling a motion, without wanting
to have any effect on communication.
The following access functions are available:
The Min local instance variable of an spBoundary object contains the coordinates of the corner of the bounding box that has the smallest X, Y, and Z values. This is set when the boundary information is loaded into memory and cannot be changed later. Note that specializations of spBoundary must be sure to set these values. Information about the Min is maintained separately in each process.
The following access functions are available:
The Max local instance variable of an spBoundary object contains the coordinates of the corner of the bounding box that has the greatest X, Y, and Z values. This is set when the boundary information is loaded into memory and cannot be changed later. Note that specializations of spBoundary must be sure to set these values. Information about the Max is maintained separately in each process.
Creates a new object of the class spBoundary, with the local process as its owner. In addition, sets the URL of the spBoundary as specified.
Given an (X,Y,Z) position P, this function determines whether a point P is above a boundary, and if it is, the position on the floor that is immediately below P. Specifically, spBoundaryBelow casts a ray downward from P and determines whether and where this ray intersects the boundary.
Since `down' is defined to be along the negative Y axis, if there is a point below P on the boundary, then this point has the same X and Z values as P, but an equal or a lessor Y value.
If there is not a point on the boundary below P, then False is returned. Otherwise, True is returned. In addition, if the Q argument is not Null, then the X, Y, and Z components of Q are altered so that they are equal to the coordinates of the boundary point below P.
(It is permissible for the two vector arguments of spBoundaryBelow to be identical.)
For the class spBoundary itself, the definition of Below is trivial. To determine whether the ray case downward from P intersects the boundary, It merely has to determine if the X and Z coordinates of P are within the X-Z bounds of the region. If so the intersection point has the X and Z coordinates of P and has a Y value that is largest of the Min or Max Y value of the boundary that is less than the Y value of P. However, it is expected that the definitions of Below for subclasses of spBoundary will do more detailed and complex calculations.
The Above function for a subclass of spBoundary is identical to the Below function except that it determines whether there is a point on the boundary that is above the test point, rather than below.
Given an (X,Y,Z) position P, this function determines whether a point P is on or inside the 3D volume specified by an spBoundary. If P is on or within the boundary, then True is returned. Otherwise, False is returned.
For the class spBoundary itself, the definition of Inside is trivial. However, it is expected that the definitions of Below for subclasses of spBoundary will do more detailed and complex calculations.
An spClass object describes the layout of the data in the memory representation of a shared object class. The URL of an spClass object identifies a file that contains this information. This file is created by the SPOT Java preprocessor supporting the definition of shared classes.
spClass objects are used extensively in the internal operation of the system. When writing applications they have important uses as well, but primarily only as opaque identifiers that are passed as arguments to functions like spClassExamine.
In addition, knowing the identity of the class descriptor of an object is very useful for runtime dispatching in an application. For example, an application might perform the following test
to determine whether an object X is a direct instance of the class spThing. (This expression returns False if X is an instance of a subclass of spThing.)
Alternatively, an application might perform the following test to determine whether X is an instance of spThing or any subclass of spThing.
The shared class spClass inherits all the instance variables and functions of the
classses spLink and sp.
The class spClass defines the following instance variables:
The class spClass defines the following functions:
Note that the various instance variables above primarily specify information only about the instance variables of a shared class as opposed to about methods. The reason for this is that shared classes are primarily passive data structures. It is therefore better in many ways to view the world model as a database rather than a collection of objects. The key focus is on communicating this data, not on the complex interaction of methods. Users can define new classes with as many new data variables to be communicated as they like.
It is possible to define methods when defining a new shared class; however, in general nothing will be done internally with these methods. (An application may take good advantage of them, however.) The only exceptions to this are the methods stored in the spClass variables InitializationFn, LinkReadDataFn, FunctionFn, PredicateFn, and InsideFn. These methods are each called internally in particular situations. They are recorded in spClass objects for ready accessibility.
This document describes the various built-in shared classes. These built-in classes are specified in a special way and are automatically loaded when the world model is initialized. Except for a few internal objects that are not visible to an application, the spClass objects corresponding to these classes are the only objects that exist in the world model immediately after a world model is created using spWMNew.
New classes defined by an application are handled in exactly the same way as any other kind of link object. They must be created by some process and are then communicated to other processes.
The following access functions are available:
Except for the root class sp, every shared class has a superclass that is another shared class. The superclass is stored in a shared instance variable called the Superclass.
The Superclass of an spClass object must be an spClass. The only exception to this is that the Superclass of the class sp is Null. The Superclass of a class must be specified when the class is initially created and cannot be changed after that time. Information about the Superclass is shared between the processes in a session.
The following access functions are available:
For debugging convenience, spClass objects have a local instance variable called the ClassName that contains an ASCII representation of the name of the class.
The ClassName is a string no more than 31 characters long. It is set by spClassReadData when a class is loaded from a class descriptor file and cannot be changed. Information about the ClassName is maintained separately in each process.
1.2 System Overview
Above all else, the system provides a convenient architecture for
implementing multi-user interactive environments. This architecture
is centered on a world model that mediates all interaction.
Figure 1 illustrates the application programming model. It shows five
applications interacting through the world model.
1.2.1 Scalability
Application programmers are encouraged to think in terms of Figure 1.
However, it would not work well to use a centralized architecture when
actually implementing the system. Rather, the system operates as
shown in Figure 2. To provide low latency interaction with the world
model, the world model is replicated so that a copy resides in each
application process. Messages sent over a computer network linking
the processes are used to propagate changes from one world model copy
to another.
1.2.2 Servers
A significant feature of Figure 2 is that it does not contain a
central process. To minimize latency, and prevent bottlenecks, the
primary communication used by the system is peer-to-peer rather than passing through
centralized processes. However, centralized server processes are used for
five key services. The way these servers interact with applications
is illustrated in Figure 3.
1.2.3 Application Processes
The structure of a single application process is shown in Figure 4.
The dashed box at the top of the figure shows how the application
itself fits into the picture.
1.2.4 The API
The Application Program Interface (API) described in this document
consists primarily of operations for creating/deleting objects in the
world model and reading/writing instance variables in these objects.
The application support module (see Figure 4) contains various facilities that make
application writing easier. For the convenience of application
writers, multiple APIs are provided. The principle APIs being in ANSI
C and Java.
1.2.5 Rendering
Figure 5 shows the system being used to support an application that
interacts with a human user. The primary feature of the figure is
that three applications are used in this situation. The main
application (in the dashed box) presents an interface to the user.
1.2.6 Supporting Simulations
From the earliest days of work on the system, we paid close
attention to supporting interaction with computer simulations as well
as people. The way this is done is shown in Figure 6.
1.3 Atomic Data
Much of the data used in the API is simple atomic data of 64
bits or less in length such as boolean values, integers, and floating
point numbers. This data is copied whenever it is passed to or
returned from a function.
1.4 Pointer Data
In addition to atomic data, the API makes use of several different
kinds of pointer data. As discussed at length in the next section,
the most important kind of pointer data is pointers to shared objects.
However, several other kinds of pointer data are used as well.
1.5 Shared Objects
The central kind of data in the API consists of shared objects. These objects are stored in a world model and shared between the participants in a session. (Other data is shared only if it is stored in a shared variable of a shared object.) The key feature of shared objects is instance variables. They can have methods associated with them as well; however, the system makes relatively little use of these methods. Rather, shared objects are essentially passive, principally just representing a database of information.
1.5.1 Accessors
T spKGetV(sp Object)
void spKSetV(sp Object, T Value)
T spKGetOldV(sp Object)
1.5.2 Referring To
Instance variables that point to shared objects are handled specially in a number of ways. This is necessary due to the partial and asynchronous way that the world model is copied between processes.
1.5.3 Removal
A key issue is asynchronous changes in the world model. In particular, objects owned by other processes can appear, change, and disappear from the world model any time spWMUpdate is called. In addition, if there are multiple threads in the local process, then from the perspective of a given thread, other threads can asynchronously create, modify, and remove locally owned objects.
1.6 Defining Shared Classes
No matter what API language is being used, shared objects are defined
using Java with a few small extensions. (The following discussion
assumes a basic understanding of Java.)
SPOT operates in 3 basic modes: C-only, Java-only, and mixed.
1.6.1 Example
public class spExample extends spThing { //* [+SendToContact]
shared public float[] Orientation; //* [spTransform:17]
shared public/protected spAction Agent;
native int Timeout; //* [spDuration]
native public static int New(float [] Orientation);
//* [sp spExampleNew(spTransform:17 Orientation)]
native public void Initialization();
//* [void spExampleInitialization(sp Object)]
native void SetTimeout(int Timeout);
//* [void spExampleSetTimeout(sp Object, spDuration Timeout)]
native public final void Setup(int Timeout, spAction Action);
//* [void spExampleSetup(sp ExampleObj, spDuration Timeout, sp Action)]
}
1.6.2 Shared Classes
1.6.3 Native Instance Variables
#defines. An example of a native static final
variable in the API is spDEGREES.
1.6.4 Scalar Types
1.6.5 Pointer Types
1.6.6 Native Static Final Variables
A shared or native variable has an initializer expression if and only
if it is a native static final variable. If the initialization of the
value in C needs to be different from the initialization in Java
(e.g., because it is not just a numeric constant) then the C
initialization can be specified after an `=' sign in the //* comment
that specifies the C type of the variable. For example the definition
of spDEGREES could be:
native static final public float DEGREES = Math.PI/180.0; //* [float=M_PI/180.0]
1.6.7 Methods
native public static int New(float [] Orientation);
//* [sp spExampleNew(spTransform:17 Orientation)]
1.6.8 Example Revisited
public class spExample extends spThing { //* [+SendToContact]
shared public float[] Orientation; //* [spTransform:17]
shared public/protected spAction Agent;
native int Timeout; //* [spDuration]
native public static int New(float [] Orientation);
//* [sp spExampleNew(spTransform Orientation)]
native public void Initialization();
//* [void spExampleInitialization(sp Object)]
native void SetTimeout(int Timeout);
//* [void spExampleSetTimeout(sp Object, spDuration Timeout)]
native public final void Setup(int Timeout, spAction Action);
//* [void spExampleSetup(sp ExampleObj, spDuration Timeout, sp Action)]
}
extern sp spExampleNew(spTransform Orientation);
extern void spExampleInitialization(sp Object);
extern void spExampleSetTimeout(sp Object, spDuration Timeout);
extern void spExampleSetup(sp ExampleObj, spDuration Timeout, sp Action);
extern sp spExampleC();
extern spTransform spExampleGetOrientation(sp Object);
extern spTransform spExampleGetOldOrientation(sp Object);
extern void spExampleSetOrientation(sp Object, spTransform Orientation);
extern sp spExampleGetAgent(sp Object);
extern sp spExampleGetOldAgent(sp Object);
extern void spExampleSetAgent(sp Object, sp Agent);
extern spDuration spExampleGetTimeout(sp Object);
1.6.8 Non-Shared classes
2 spWM
2.1 Me
native public static int Me; //* [spName]
spName spWMGetMe()
void spWMSetMe(spName X)
2.2 MainOwner
native public/ static int MainOwner; //* [spName]
spName spWMGetMainOwner()
2.3 Error
native public static String Error; //* [char *:500]
char * spWMGetError()
void spWMSetError(char * X)
2.4 LastError
native public/ static String LastError; //* [char *:500]
char * spWMGetLastError()
2.5 Interval
native public/ static spDuration Interval; //* [spDuration]
spDuration spWMGetInterval()
2.6 DesiredInterval
native public static spDuration DesiredInterval; //* [spDuration]
spDuration spWMGetDesiredInterval()
void spWMSetDesiredInterval(spDuration X)
2.7 Window
native public static int Window; //* [spWindow]
spWindow spWMGetWindow()
void spWMSetWindow(spWindow X)
2.8 spWMNew
spWM spWMNew(char * Server, spTransferVector V)
"node.myUniv.edu").
If the server address string is Null, then all phases of initialization are performed except contacting the session server. This is useful for some kinds of single-user testing.
2.9 spWMRemove
void spWMRemove()
2.10 spWMUpdate
void spWMUpdate()
2.11 spWMGenerateOwner
spName spWMGenerateOwner()
2.12 spWMReportError
void spWMReportError(sp Object, long Code, char * Description)
spWMSetError(NULL);
transform = spThingGetTransform(X);
if (spWMGetError()) ...
2.13 spWMRegister
void spWMRegister(sp * Pointer)
2.14 spWMDeregister
void spWMDeregister(sp * Pointer)
3 spFn
The type spFn is used for the functional arguments to
all the above functions. (A single type is used in all these situations
because this is considerably more convenient than having to
define a separate type for each purpose.)
The spFn type does not extend the class sp and does not correspond to objects in the shared world model. Rather, spFn data is stored in shared objects and used in intermediate computation.
typedef spBoolean spFn(sp Object, void * State)
The State argument is used to communicate state
information between calls to an spFn. It can be used to accumulate a
result. The return value is used for search-like operations. If an
spFn ever returns True, then the mapping activity
immediately halts. It is essential that an spFn be light weight in the sense that it runs quickly,
and must not call spWMUpdate.
spBoolean spCount(sp ignore, void * state) {
int * count = (int *)state;
*count = *count+1;
return FALSE;
}
int Counter = 0;
spExamineChildren(X, spMaskNORMAL, spCount, &Counter);
Result = Counter;
The value of the count is obtained by observing the value of the
state variable Counter after the examination is over.
3.1 spFn Predicates
Another key part of the API is the ability to install callback
functions that will automatically be called when certain events occur.
Events are defined by predicates that test changes in the shared
variables of objects. These predicates are defined using the same
type spFn as functions that are mapped over objects.
spBoolean spChangedParent(sp object, void * ignore) {
return spThingGetOldParent(object) != spThingGetParent(object);
}
4 spMask
spClassExamine(spThingC(), spMaskNORMAL, F, NULL)
spClassExamine(spThingC(), spMaskMINE, F, NULL)
boolean CompatibleWithMask(sp object, int mask) {
if (spGetOwner(object) == spWMGetMe()) {
return (spMaskMINE & mask)
}
else {
return (spMaskOTHERS & mask) &&
(~ SystemOwner(spGetOwner(object))) &&
( spClassGetSendToRegion(spGetClass(object)))
}
}
4.1 Constants
View masks are created by combining the following
constants. (In C these constants are
#defines.)
5 spTransform
(translate by Translation
(translate by Center
(rotate by Rotation
(rotate by ScaleOrientation
(scale by Scale
(rotate by -ScaleOrientation
(translate by -Center
...)))))))
90.0f * spDEGREES
typedef float * spTransform;
typedef float spTransformData[17];
There are several levels at which you can interact with an
spTransform. First, you can alter the underlying vector directly,
e.g., using the index constants above. This allows you to do
everything that is possible, but nothing easily.
5.1 Constants
The following constants are available for directly accessing the
various elements of an spTransform. (In C, these constants are
#defines.)
spTransform P;
P[spTransformX] = P[spTransformY] + 2.0;
5.2 spTransformCopy
spTransform spTransformCopy(spTransform Destination, spTransform Source)
5.3 spTransformFromIdent
spTransform spTransformFromIdent(spTransform Transform)
5.4 spTransformGetTranslation
spVector spTransformGetTranslation(spTransform Transform)
5.5 spTransformSetTranslation
spTransform spTransformSetTranslation(spTransform Transform, spVector Translation)
5.6 spTransformGetRotation
spRotation spTransformGetRotation(spTransform Transform)
5.7 spTransformSetRotation
spTransform spTransformSetRotation(spTransform Transform, spRotation Rotation)
5.8 spTransformGetScale
spVector spTransformGetScale(spTransform Transform)
5.9 spTransformSetScale
spTransform spTransformSetScale(spTransform Transform, spVector Vector)
5.10 spTransformGetScaleOrientation
spRotation spTransformGetScaleOrientation(spTransform Transform)
5.11 spTransformSetScaleOrientation
spTransform spTransformSetScaleOrientation(spTransform Transform, spRotation R)
5.12 spTransformGetCenter
spVector spTransformGetCenter(spTransform Transform)
5.13 spTransformSetCenter
spTransform spTransformSetCenter(spTransform Transform, spVector Center)
6 spVector
typedef float * spVector;
typedef float spVectorData[3];
6.1 Constants
The following three constants are provided for accessing the components of an spVector. (In C, these constants are external variables initialized to appropriate values.)
spVector V;
V[spVectorX] = P[spVectorY] + 2.0;
spRotation R;
spRotationLookAt(R, spVectorZERO, spVectorAXISX, spVectorAXISY);
6.2 spVectorCopy
spVector spVectorCopy(spVector Destination, spVector Source)
6.3 spVectorSetFromScalar
spVector spVectorSetFromScalar(spVector Vector, float Scalar)
6.4 spVectorEquals
spBoolean spVectorEquals(spVector A, spVector B)
6.5 spVectorEqualsDelta
spBoolean spVectorEqualsDelta(spVector A, spVector B, float Tolerance)
6.6 spVectorAdd
spVector spVectorAdd(spVector A, spVector B)
6.7 spVectorSubtract
spVector spVectorSubtract(spVector A, spVector B)
6.8 spVectorMultiplyByScalar
spVector spVectorMultiplyByScalar(spVector Vector, float Scalar)
6.9 spVectorDivideByScalar
spVector spVectorDivideByScalar(spVector Vector, float Scalar)
6.10 spVectorCrossProduct
spVector spVectorCrossProduct(spVector A, spVector B)
6.11 spVectorDotProduct
float spVectorDotProduct(spVector A, spVector B)
6.12 spVectorComposeScales
spVector spVectorComposeScales(spVector A, spVector B)
6.13 spVectorLength
float spVectorLength(spVector Vector)
6.14 spVectorNormalize
spVector spVectorNormalize(spVector Vector)
7 spRotation
typedef float * spRotation;
typedef float spRotationData[4];
7.1 Constants
The class spRotation includes the following constants for accessing the X, Y, Z, and Angle
components of an spRotation. (In C, these constants are
#defines.)
spRotation R;
R[spRotationX] = A[spRotationY] + 2.0;
7.2 spRotationCopy
spRotation spRotationCopy(spRotation Destination, spRotation Source)
7.3 spRotationFromIdent
spRotation spRotationFromIdent(spRotation Rotation)
7.4 spRotationGetAxis
spVector spRotationGetAxis(spRotation Rotation)
7.5 spRotationSetAxis
spRotation spRotationSetAxis(spRotation Rotation, spVector Axis)
7.6 spRotationGetAngle
float spRotationGetAngle(spRotation Rotation)
7.7 spRotationSetAngle
spRotation spRotationSetAngle(spRotation Rotation, float Angle)
7.8 spRotationToQuat
spQuaternion spRotationToQuat(spRotation Rotation, spQuaternion Quat)
7.9 spRotationFromQuat
spRotation spRotationFromQuat(spRotation Rotation, spQuaternion Quat)
7.10 spRotationToAngles
spVector spRotationToAngles(spRotation Rotation, spVector Vector)
7.11 spRotationFromAngles
spRotation spRotationFromAngles(spRotation Rotation, spVector Angles)
7.12 spRotationMult
spRotation spRotationMult(spRotation A, spRotation B)
7.13 spRotationLookAt
spRotation spRotationLookAt(spRotation R, spVector From, spVector To, spVector Up)
8 spQuaternion
q[0]*i + q[1]*j + q[2]*k + q[3]
typedef float * spQuaternion;
typedef float spQuaternionData[4];
8.1 spQuaternionCopy
spQuaternion spQuaternionCopy(spQuaternion Destination, spQuaternion Source)
8.2 spQuaternionFromIdent
spQuaternion spQuaternionFromIdent(spQuaternion Quaternion)
8.3 spQuaternionMult
spQuaternion spQuaternionMult(spQuaternion A, spQuaternion B)
9 spMatrix
R R R X
R R R Y
R R R Z
0 0 0 1
typedef float * spMatrix;
typedef float spMatrixData[16];
Several conventions are worthy of note about the functions on
spMatrix objects. All of the functions assume that a spMatrix represents only rotation, translation, scalling, and shear with no other effects like taper or reflection.
None of the functions ever allocates memory.
Rather, the control of memory is left entirely up to the application.
All modifications are by side-effect. However, to make it easier to create nested expressions the modified value is used as the return value.
9.1 spMatrixCopy
spMatrix spMatrixCopy(spMatrix Destination, spMatrix Source)
9.2 spMatrixFromIdent
spMatrix spMatrixFromIdent(spMatrix Matrix)
9.3 spMatrixGetTranslation
spVector spMatrixGetTranslation(spMatrix Matrix)
9.4 spMatrixSetTranslation
spMatrix spMatrixSetTranslation(spMatrix Matrix, spVector Translation)
9.5 spMatrixFromTransform
spMatrix spMatrixFromTransform(spMatrix Matrix, spTransform Transform)
9.6 spMatrixToTransform
spTransform spMatrixToTransform(spMatrix Matrix, spTransform Transform)
9.7 spMatrixInverse
spMatrix spMatrixInverse(spMatrix spMatrix)
9.8 spMatrixMult
spMatrix spMatrixMult(spMatrix A, spMatrix B)
9.9 spMatrixMultVector
spVector spMatrixMultVector(spMatrix Matrix, spVector Vector)
spMatrix M;
spVector Up;
Up = spVectorCopy(Up, spVectorAXISY);
spMatrixMultVector(spThingRelativeMatrix(A, spTopmost(A), M), Up);
10 spFormat
typedef struct _spFormat {
unsigned short rate;
short encoding;
unsigned short bitsPerSample;
unsigned short samplesPerFrame;
} spFormat;
10.1 Constants
The following constants contain useful spFormat values. (In C these constants are
#defines.)
10.2 spFormatDurationFromLength
spDuration spFormatDurationFromLength(spFormat Format, long Bytes)
10.3 spFormatLengthFromDuration
long spFormatLengthFromDuration(spFormat Format, spDuration Duration)
11 sp
public abstract class sp //* [+SendToRegion -SendToContact]
11.1 Constants
All angles are represented in radians. Radians are used because they are more natural for numerical calculations. However, most people think more easily in degrees. In recognition of this, the following constant is provided. (In C, this constant is a
#define.)
90.0f * spDEGREES
11.2 Class
shared public/ spClass Class; //* [sp]
sp spGetClass(sp Object)
11.3 Owner
shared public int Owner; //* [spName]
spName spGetOwner(sp Object)
void spSetOwner(sp Object, spName X)
spGetOwner(X) == spWMGetMe()
11.4 Parent
shared public sp Parent; //* [sp]
sp spGetParent(sp Object)
void spSetParent(sp Object, sp X)
11.5 IsRemoved
shared public/ boolean IsRemoved; //* [spBoolean]
spBoolean spGetIsRemoved(sp Object)
11.6 IsNew
native public/ boolean IsNew; //* [spBoolean]
spBoolean spGetIsNew(sp Object)
11.7 AppData
native public int AppData; //* [void *]
void * spGetAppData(sp Object)
void spSetAppData(sp Object, void * X)
11.8 spC
sp spC()
spClassExamine(spC(), spMaskNORMAL, F, NULL)
11.9 spNew
sp spNew()
sp spKNew() {
return spClassNewObj(spKC(), 0);
}
11.10 spInitialization
void spInitialization(sp Object)
11.11 spRemove
void spRemove(sp Object)
11.12 spExamineChildren
void spExamineChildren(sp Object, spMask Mask, spFn F, void * Data)
11.13 spExamineDescendants
void spExamineDescendants(sp Object, spMask Mask, spFn F, void * Data)
11.14 spTopmost
sp spTopmost(sp Object)
11.15 spPrint
void spPrint(sp Object)
12 spRegion
public class spRegion extends sp //* [-SendToRegion +SendToContact]
12.1 How Regions Work
The key feature of regions is that every ordinary shared object is in
exactly one region. The region is determined by following the object's Parent link. In particular, every ordinary object other than an spRegion needs to have a Parent. If the Parent of an object is a region, then the object is in that region. Otherwise, the object is in whatever region its Parent is in. The Parent of a region must be Null.
12.2 Boundary
shared public/ spBoundary Boundary; //* [sp]
sp spRegionGetBoundary(sp Object)
12.3 spRegionNew
sp spRegionNew(sp Boundary)
spRegion R;
R = spRegionNew(spBoundaryNew("http://www.BigRetailer.com/boundaries/BackRoom"));
13 spLink
public abstract class spLink extends sp
13.1 URL
shared public/ String URL; //* [spFixedAscii]
spFixedAscii spLinkGetURL(sp Object)
13.2 Data
native public/ int Data; //* [void *]
void * spLinkGetData(sp Object)
13.3 spLinkNew
sp spLinkNew(spFixedAscii URL)
sp spLNew(spFixedAscii URL) {
return spClassNewLink(spLC(), URL);
}
13.4 spLinkReadData
void spLinkReadData(sp Link)
14 spVisualDefinition
public class spVisualDefinition extends spLink
look = spVisualDefinitionNew("http://www.models.com/demo/models/table");
object = spThingNew();
spThingSetVisualDefinition(object, look);
spSetParent(look, object);
"http://www.models.com/demo/models/table" might contain:
model/vrml
http://www.models.com/demo/models/table.vrml
88347136
-1.0 8.0 0.2 0.0 0.0 1.0 1.523 1.0 1.0 1.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0
14.1 spVisualDefinitionNew
sp spVisualDefinitionNew(spFixedAscii URL)
15 spSound
public class spSound extends spLink
bang = spSoundNew("http://www.models.com/sounds/bang23.wav");
source = spAudioSourceNew();
spSetParent(bang, source);
spSoundPlay(bang, source, FALSE, 1.0);
"http://www.models.com/sounds/bang23" might contain:
123
audio/x-wav
6638759684996246
http://www.models.com/sounds/bang23.wav
4500
88347136
audio/x-aiff
5754345865388749
http://www.models.com/sounds/bang23.aiff
9000
72545754
15.1 Duration
native public/ int Duration; //* [spDuration]
spDuration spSoundGetDuration(sp Object)
15.2 spSoundNew
sp spSoundNew(spFixedAscii URL)
15.3 spSoundPlay
sp spSoundPlay(sp Sound, sp Source, spBoolean Loop, float Gain)
16 spBoundary
public class spBoundary extends spLink //* [+SendToContact]
-1.1 2.333 0.0 10.9 5.2 1.0
16.1 Min
native public/ float[] Min; //* [spVector:3]
spVector spBoundaryGetMin(sp Object)
16.2 Max
native public/ float[] Max; //* [spVector:3]
spVector spBoundaryGetMax(sp Object)
16.3 spBoundaryNew
sp spBoundaryNew(spFixedAscii URL)
16.4 spBoundaryBelow
spBoolean spBoundaryBelow(sp Boundary, spVector P, spVector Q)
16.5 spBoundaryAbove
spBoolean spBoundaryAbove(sp Boundary, spVector P, spVector Q)
16.6 spBoundaryInside
spBoolean spBoundaryInside(sp Boundary, spVector P)
17 spClass
public class spClass extends spLink
spGetClass(X) == spThingC()
spClassLeq(spGetClass(X),spThingC())
17.1 Superclass
shared public/ spClass Superclass; //* [sp]
sp spClassGetSuperclass(sp Object)
17.2 ClassName
native public/ String ClassName; //* [spAscii32:32]
spAscii32 spClassGetClassName(sp Object)