How to align the coordinate system of your tracking device with the physical coordinates of your space.
Unless you're more mathematically sophisticated than we are, this is the tricky bit. You've got your tracking device talking with a Syzygy device driver, but its native coordinate axes point in different directions from the Syzygy coordinate system (which, like OpenGL coordinates, is +X=right, +Y=up, +Z=back when you are facing in your VR apparatus' "forwards" direction. For example, in the Beckman Institute Cube the axes are +X=East, +Y=up, +Z=south). How to get the two sets of axes aligned?
First, if you don't already know it you need to determine what directions
the tracker axes are pointed. This is easiest to do with the DeviceClient
utility in 'position' mode. For example, if you're running your tracker
as input service
SZG_INPUT0 in the context of a
virtual computer named 'vc', then you would
start DeviceClient using e.g.:
DeviceClient 0 -position 0 -szg virtual=vc
...and it would print out a stream of positions computed from the incoming values of matrix event #0, which would correspond to the head tracking sensor (the first '0' above is the input service number, while the second is the matrix event index). Now you can move the sensor around and observe how the reported position values change from place to place.
You perform the axis transformation by bracketing each incoming matrix event with a pair of matrices, one of which is the inverse of the other:
M' = C * M * C-1
C matrices are constructed such that when multiplied by M they swap
two of its rows or columns (depending on whether it's left- or right-multiplication)
along with an optional change of sign (multiplication by -1). Each
row and column of each of these matrices has exactly one non-zero
element, which is equal to 1 or -1, and the lower-right element is 1.
Take the Ascension Flock of Birds™ tracker. It uses a right-handed coordinate system with the X-axis pointing away from the transmitter power cable and the Z-axis pointing down (i.e. towards the side containing the hole for the mounting screw). Let's say that we've got it mounted so that the power cable points forwards (away from the user), such that +X=back, +Y=left, +Z=down, and we've confirmed with DeviceClient that these are the tracker coordinate axes. We need to map these to the Syzygy coordinates +X=right, +Y=up, +Z=back. In other words we want:
Tracker X => Syzygy +Z Tracker Y => Syzygy -X Tracker Z => Syzygy -Y
We construct the transformation matrix using the table below. The tracker axes go along the top and the desired Syzygy axes along the side, with the sign of the non-zero element corresponding to the sign of the mapping:
|Tracker X||Tracker Y||Tracker Z|
and because these are orthonormal matrices, the inverse is the same as the transpose, i.e.:
<param> <name> fob_tracker </name> <value> <szg_device> <input_sources> arBirdWinDriver </input_sources> <input_sinks></input_sinks> <input_filters></input_filters> <pforth> /* Declare matrix variables (each 'matrix' call allocates 16 cells in the dataspace and defines a new word, e.g. 'Xin', that pushes the address of the first cell onto the stack. */ matrix Xin matrix Xout matrix C matrix Cinv /* Store transformation matrices. */ 0 -1 0 0 0 0 -1 0 1 0 0 0 0 0 0 1 C matrixStoreTranspose 0 -1 0 0 0 0 -1 0 1 0 0 0 0 0 0 1 Cinv matrixStore /* Define 'filter' word to be called when any matrix event passes through the filter. */ define filter_all_matrices Xin getCurrentEventMatrix /* Multiply C * Xin * Cinv, store result in Xout */ C Xin Cinv 3 Xout concatMatrices Xout setCurrentEventMatrix enddef </pforth> </szg_device> </value> </param>
This device definition loads one device driver plugin (shared library):
arBirdWinDriver, the Syzygy Flock-of-Birds™ driver that is based on
the Bird.dll supplied by Ascension (Windows only). It also defines a PForth
filter to be applied to the output of this device.
PForth (or PseudoForth) is a FORTH-like language. This is, it's
stack-based and uses RPN notation like an HP calculator. For example, to add
two numbers together you would type
3 2 + ("Place the numbers 3 and 2 on
the stack, then call the '+' word, which takes the top two numbers off the stack
and pushes their sum onto the stack"). PForth is compiled; when the PForth filter
is loaded, the source code gets converted into an STL vector<> of pointers to
objects, one for each PForth word. Running a filter word consists in iterating
through its vector<> of pointers and calling
i->action() for each one, so it's
The filter defines four matrix variables,
Xout are just temporary storage for the initial and final
values of the tracker matrices.
Cinv are the two axis-transformation
matrices we constructed above. It may be a bit confusing that we use
matrixStoreTranspose to store the values in
In Syzygy, as in OpenGL, matrix values are internally stored in a one-dimensional
array with the row subscript varying faster, i.e. going down the first column,
then the second, and so on. The
matrixStore word reads in the numbers and stores
them in the same order: In other words, the numbers in the top row of the matrix text
above end up being stored internally as the first column of the matrix, so you end
up with the transpose of the matrix as it appears in the PForth source code. The
matrixStoreTranspose word allows you to enter matrices in human-readable form.
Finally, the code defines a filter word,
filter_all_matrices, which will be
applied to any outgoing matrix event. The matrix value is stored in
Xin in the
first line, the transformation equation is applied in the second (
multiplies together a variable number of matrices, specified in this case by the '3'),
and the result is stuffed back into the matrix event in the third.
Still to do...
Suppose the aforementioned tracker transmitter is mounted two feet off the ground, so we need to add 2 to the Y position coordinate of each matrix event from the tracker. Modify the PForth program as follows:
/* Declare matrix variables (each 'matrix' call allocates 16 cells in the dataspace and defines a new word, e.g. 'Xin', that pushes the address of the first cell onto the stack. */ matrix Xin matrix Xout matrix C matrix Cinv matrix originOffset /* Store transformation matrices. */ 0 -1 0 0 0 0 -1 0 1 0 0 0 0 0 0 1 C matrixStoreTranspose 0 -1 0 0 0 0 -1 0 1 0 0 0 0 0 0 1 Cinv matrixStore /* create a matrix that translates by (x,y,z)=(0,2,0) */ 0 2 0 originOffset translationMatrix /* Define 'filter' word to be called when any matrix event passes through the filter. */ define filter_all_matrices Xin getCurrentEventMatrix /* Multiply originOffset * C * Xin * Cinv, store result in Xout */ originOffset C Xin Cinv 4 Xout concatMatrices Xout setCurrentEventMatrix enddef
The tracker origin offset transformation comes before the rest.
Now we have the problem of attaching tracking sensors to things that we want to track. The natural orientation for the Flock-of-Birds™ sensors is base-down with the cord pointing forwards. That will most likely not be a convenient way to mount them. Let's say that we have two of them and we want to mount one on the left side of a pair of glasses with the base facing right and the cord facing the back; this sensor will provide matrix event #0. The other one will be mounted on the bottom of a gamepad with the base facing up and cord facing back; this will provide matrix event #1.
The first mounting transformation can be composed of a 180-degree rotation around the Y axis (to get the cord pointing backwards) followed by a -90-degree rotation around the Z-axis (remember, after the first rotation the positive Z axis points forwards).
The second one is a 180-degree rotation around Y followed by another 180-degree rotation around Z.
We extend the PForth filter as follows:
/* Declare matrix variables (each 'matrix' call allocates 16 cells in the dataspace and defines a new word, e.g. 'Xin', that pushes the address of the first cell onto the stack. */ matrix Xin matrix Xout matrix C matrix Cinv matrix originOffset /* Store transformation matrices. */ 0 -1 0 0 0 0 -1 0 1 0 0 0 0 0 0 1 C matrixStoreTranspose 0 -1 0 0 0 0 -1 0 1 0 0 0 0 0 0 1 Cinv matrixStore /* create a matrix that translates by (x,y,z)=(0,2,0) */ 0 2 0 originOffset translationMatrix /* Define 'filter' word to be called when any matrix event passes through the filter. Applies transmitter coordinate transformations (common to all sensors) */ define filter_all_matrices Xin getCurrentEventMatrix /* Multiply originOffset * C * Xin * Cinv, store result in Xout */ originOffset C Xin Cinv 4 Xout concatMatrices Xout setCurrentEventMatrix enddef matrix ySensorRot matrix zHeadRot matrix zGPadRot matrix headRot matrix GPadRot /* Construct 3 rotation matrices, 2 containing separate Z-rotations for head and gamepad and a common Y-rotation */ 180 yaxis ySensorRot rotationMatrix -90 zaxis zHeadRot rotationMatrix 180 zaxis zGPadRot rotationMatrix /* Pre-construct the head and gamepad transformations */ ySensorRot zHeadRot headRot matrixMultiply ySensorRot zGPadRot GPadRot matrixMultiply /* Define filter words to be called when matrix events with particular indices (i.e. that originate from particular sensors) pass through. They apply mounting transformations for individual sensors */ define filter_matrix_0 Xin getCurrentEventMatrix Xin headRot Xout matrixMultiply Xout setCurrentEventMatrix enddef define filter_matrix_1 Xin getCurrentEventMatrix Xin GPadRot Xout matrixMultiply Xout setCurrentEventMatrix enddef
All of the new code is at the bottom. We need five additional matrices, one
for each of the three rotation components discussed above and two more
to hold the concatenation of the two components for the head and for the
gamepad. We could have left the components separate and multiplied them
enddef blocks using
concatMatrices, but that
would have been less efficient; better to perform the multiplication once
at compile time when the filter is initialized.
filter_matrix_# words are called for matrix events with the appropriate
index # after
filter_all_matrices is called.