Power Wall Pseudo-VR Environment
We recently installed a 10' x 8' tiled display wall (2 x 2 projectors with 1280 x 1024 pixels each), and during our recent furniture update I found two neglected Ascension Flock of Birds tracking devices with attached FakeSpace Pinch Gloves. I decided to give trackers and gloves a new home and some TLC, and enable the Power Wall for pseudo-VR applications (it won't do stereographic rendering, and currently there is no head-tracking).
Tracker and Glove Daemon
To get started, I implemented a simple tracker and pinch daemon to talk to the devices. This daemon runs on one of our new cluster PCs, and talks to VR applications via a TCP/IP protocol. (The daemon's host address and listening port are available upon request.) This allows us to run VR applications on any of the four cluster machines under Linux, and also on the fifth machine under Windows 2000. The so-called "Pinch Glove Daemon" can support up to eight clients simultaneously, and turns the VR devices on/off on demand. This should allow us to run it as a background daemon process for extended periods of time without user interaction.
A gzipped tar file containing source code specifying the daemon's TCP protocol and implementing a simple client can be downloaded from here. The client is mostly OS-independent; the only caveat is that it uses POSIX pthreads to implement server-side pushing of pinch glove data. To port it to other OSes, one would either have to disable this useful feature, or re-implement it using another lightweight process API.
Pinch Glove Client API Manual
The following sections describe the client's programming interface in more detail. The client is implemented as a single object per connected daemon, with methods to communicate to the daemon and extract tracker and pinch glove state.
Initialization and Shutdown
- PinchGloveClient::PinchGloveClient(const char* serverName,int serverPort)
- Tries connecting to the given server host on the given port address. If problems arise, the client will dump messages to stderr and return. After creating a PinchGloveClient object, its state can be checked using the isConnected() method.
Initially, the client is in polling mode; that is, packets are sent from the server only on request, and the initial tracker and pinch glove state is undefined.
- PinchGloveClient::~PinchGloveClient(void)
- Disconnects from the server (nicely) and releases all allocated resources. Another way to disconnect from the server is to just crash the client application; the server will detect crashed clients relatively reliably and close their connections automatically.
- bool PinchGloveClient::isConnected(void) const
- After a PinchGloveClient object has been created, its connection state should be checked using this method. If it returns false, the connection to the server could not be established, and calling any other methods - except for the destructor - results in undefined behaviour.
- int PinchGloveClient::getNumGloves(void) const
- This method returns the number of gloves connected to the Pinch Glove Daemon. This number is currently, and for the foreseeable future, two.
Server Communication
- bool PinchGloveClient::requestUpdate(void)
- If the client is in polling mode, this methods requests another pinch glove update packet from the server. The method will block until the server delivers a packet, or until it times out after one second. In the former case, the method returns true and pinch glove state can subsequently be queried using methods from the next section; in the latter case, it will return false and not change the current pinch glove state.
Note: In polling mode, the client application has to request a pinch glove update packet at least every sixty seconds; otherwise, the server will assume the client has crashed and will close its connection.
- void PinchGloveClient::startStreaming(int updateRate)
- Calling this method will switch the client from polling to streaming mode, i.e., afterwards the server will send pinch glove update packets at regular intervals, and the client application can use the query messages from the next section at any time to retrieve the most recent data. The updateRate parameter tells the server how often to send update packets. Using Ascension Flock of Birds tracking devices, the full update rate is about 103 packets per second; this rate will be divided by updateRate to yield the effective update rate. To change the update rate, a client application has to stop streaming mode, and then re-establish it with the desired updateRate.
-
- void PinchGloveClient::stopStreaming(void)
- This method will switch the client back from streaming mode to polling mode, i.e., update packets will only be sent on request, and the query methods will report the state after the most recent request method call.
Tracker and Pinch Glove State Retrieval
The following methods retrieve tracker information from the client. For each connected tracking device (currently two), the client reports tracker position and orientation. The used physical coordinate system is a right-handed XYZ frame, with X going on the floor from left to right parallel to the Power Wall screen, Y going on the floor towards the Power Wall, and Z going straight up. The unit of measurement is inches, and the coordinate system's origin is the center of the Power Wall screen projected onto the floor, see Figure 1.
 |
| Figure 1: Physical tracker coordinate system. The coordinate system unit is inches. |
Tracker orientation is reported using Euler angles, unit quaternions and homogenuous transformation matrices. The Euler angles (xangle, yangle, zangle) define an orientation by a rotation of zangle radians around the Z axis, followed by a rotation of yangle radians around the Y axis and finally a rotation of xangle radians around the X axis. Due to the problems inherent in Euler angle notation (gimbal lock, increased noise when approaching the north/south poles, general nastiness) these should never be used by applications. The quaternion or matrix representation is much more stable. Additionally, Euler angles will not be calibrated in future versions of the Pinch Glove Daemon; quaternions and transformation matrices will be.
The unit quaternion representation uniquely defines a tracker's orientation using four components (x, y, z, w), such that the Euclidean norm of the 4D vector is one. A singe rotation to transform the physical coordinate frame into a tracker's frame can be obtained from the quaternion as such: angle = 2 acos(w), axis = (x/sin(angle/2), y/sin(angle/2), z/sin(angle/2)). In the special case of angle = 0, any axis can be used for the identity transformation represented by the identity quaternion (0,0,0,1).
A tracker's matrix directly describes a homogenuous transformation from physical coordinates to tracker coordinates. All matrices returned by the client are in fact rigid body transformations, i.e., affine transformations with all three axis vectors normalized and pairwise orthogonal to each other. The pair of (position, quaternion) is a more compact representation of the same transformation.
Glove Position Locking
Since the client is multithreaded, and the represented pinch glove state can change asynchronously when the client is in streaming mode, any accesses to tracker and pinch data must be protected by mutex semaphores. The client provides two methods to lock glove state that are used when querying tracker position and orientation data. The methods to access pinch state and events use implicit locking; to ensure correct function, a glove position lock must be released before pinch state or events are queried. A typical source code fragment to access pinch glove state would look like the following:
/* Extract position/orientation data: */
client.lockGlovePosition();
/* Any combination of calls to getPosition(int), getOrientation(int) etc. */
/* ... */
client.unlockGlovePosition();
/* Extract pinch events: */
while(client.isEventPending()) // Are there events waiting?
{
/* Get the next event: */
PinchGloveClient::PinchEvent event=client.getNextEvent();
/* Process the event: */
/* ... */
}
- void PinchGloveClient::lockGlovePosition(void)
- This method locks the current glove position. To ensure correct retrieval, this function must be called before tracker position state is queried, and the accompanying unlockGlovePosition() method must be called right afterwards.
Note: If pinch state/events are queried, the glove position must not be locked; otherwise, the client will deadlock.
- void PinchGloveClient::unlockGlovePosition(void)
- Releases the lock on the glove position. This function must be called between a call to lockGlovePosition() and querying pinch state/events. Also, locks should be released as quickly as possible to not interfere with server-side pushing of pinch glove state.
Position/Orientation Retrieval
The following methods return position and orientation data as constant pointers. These pointers are only guaranteed to be valid inside a lockGlovePosition()/unlockGlovePosition() bracket. Especially, it should not be assumed that these pointers remain valid after the client's pinch glove state has been updated, either in streaming mode or as a result of a requestUpdate() call.
- const float* PinchGloveClient::getPosition(int gloveIndex) const
- This method returns a pointer to the given glove's position, stored as three floats (x, y, z).
- const float* PinchGloveClient::getEulerAngles(int gloveIndex) const
- This method returns a pointer to the given glove's Euler angles, stored as three floats (xangle, yangle, zangle).
Note: Euler angles are not calibrated inside the Pinch Glove Daemon. In other words, they depend on the tracker emitter's physical position, which can and will change without notice. Currently, as of 04/10/2002, the coordinate frame obtained from Euler angles has to be pre-multiplied with a rotation of -90 degrees around the Z axis to obtain compatible orientations. The only reason I report them at all is that they are the native tracker orientation representation. You have been warned.
- const float* PinchGloveClient::getOrientation(int gloveIndex) const
- This method returns a pointer to the given glove's orientation quaternion, stored as four floats (x, y, z, w).
- const float* PinchGloveClient::getMatrix(int gloveIndex) const
- This method returns a pointer to the given glove's transformation matrix, stored as a row-major 4 x 4 matrix of floats.
Pinch State/Event Retrieval
- PinchGloveClient::FingerIndex
- Enumerated type for fingers. The provided identifiers are PinchGloveClient::INDEX, PinchGloveClient::MIDDLE, PinchGloveClient::RING, and PinchGloveClient::PINKY. Names should be self-explanatory. These constants should be used when querying pinch state and processing pinch events.
- PinchGloveClient::PinchEvent
- This structure is used to report pinch events to the client application. It contains the following fields and methods:
- PinchGloveClient::PinchEvent::EventType
- Enumerated type for types of events. There are two types: PinchGloveClient::PinchEvent::PINCH and PinchGloveClient::PinchEvent::RELEASE.
- PinchGloveClient::PinchEvent PinchGloveClient::PinchEvent::eventType
- Defines the type of a given event.
- int PinchGloveClient::PinchEvent::gloveIndex
- Index of the glove that generated the event.
- PinchGloveClient::FingerIndex PinchGloveClient::PinchEvent::fingerIndex
- Index of the finger that generated the event.
- int PinchGloveClient::PinchEvent::pinchMasks[numGloves]
- Bit masks of pinched fingers for each glove. This field should not be accessed directly, since its internal format might change. Pinch states should be queried using the next method.
- bool PinchGloveClient::PinchEvent::isPinched(int gloveIndex,int fingerIndex) const
- Returns the pinch state of a given finger. The returned value reflects state right before the reported event. In other words, if a pinch event for glove 0, finger 2 is reported, the result of isPinched(0,2) will be false in this event, and true in the next reported event.
- int PinchGloveClient::getPinchMask(int gloveIndex) const
- Returns the bit mask of pinched fingers after all pinch events have been processed. This method should rarely be useful. It is recommended to always process pinch events in order to disambiguate multiple pinched/released fingers between update packets. The daemon and client take quite some pain to report an uninterrupted stream of pinch events, even when the update rate is low.
- bool PinchGloveClient::isEventPending(void) const
- This method checks if there are unprocessed pinch events in the client's event queue. If the message returns true, at least one event can be extracted without blocking the caller.
- PinchGloveClient::PinchEvent PinchGloveClient::getNextEvent(void)
- Returns the next event from the client's event queue. If no event is pending, the caller will block until another event arrives. This, of course, can only happen if the client is in streaming mode. If the client is in polling mode, this method should only be used after checking the return value of isEventPending().
Virtual Reality User Interface (Vrui) Toolkit
Over the last couple of months, I developed a VR framework that simplifies porting of existing OpenGL-based graphics applications to a Virtual Reality environment. I have been using this toolkit to great success to develop applications for our lab's Virtual Workbench, for FakeSpace ImmersaDesks, for CAVE Virtual Environments, and, most recently, for Power Wall Pseudo-VR. I will make this toolkit available in the near future.