How to align the coordinate system of your tracking device with the physical coordinates of your space.

Swapping Axes

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?

Mapping the Tracker Axes

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.

The Axis-Transformation Equation

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

The 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.

Determining the Axis-Transformation Matrices: An Example

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
Syzygy X 0 -1 0 0
Syzygy Y 0 0 -1 0
Syzygy Z 1 0 0 0
0 0 0 1

i.e.

and because these are orthonormal matrices, the inverse is the same as the transpose, i.e.:

Implementing the Axis Transformation in a PForth Filter

The best way to implement these transformations is in a PForth filter inside your global input device definition. Here's an example using the above transformations:

<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 quite fast.

The filter defines four matrix variables, Xin, Xout, C, and Cinv. Xin and Xout are just temporary storage for the initial and final values of the tracker matrices. C and 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 C and matrixStore for Cinv. 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 (concatMatrices 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.

Correcting Residual Coordinate-Axis Direction Errors

Still to do...

Specifying the Origin Offset

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.

Correcting Sensor Orientations

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 inside the define/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.

The filter_matrix_# words are called for matrix events with the appropriate index # after filter_all_matrices is called.