The four matrices that lie at the heart of Aviator's 3D model
There are four matrices in Aviator that are absolutely core to the 3D geometry used to generate the view from the cockpit. These rotation matrices can be interpreted in a number of ways, but perhaps the easiest way to understand them is via different frames of reference.
Imagine we are sitting in our Spitfire, hurtling through the air in search of aliens to attack. Attached to our right wing is one of our guns. As we fly around, looping the loop and shooting under the suspension bridge, then as far as we're concerned, that gun stays exactly where it is: on our right wing. From our point of view as the pilot, the gun is stationary and doesn't move; it's always just to our right.
Now look at the same scene from the point of view of an observer on the ground. From their perspective, the gun on our right wing is flying all over the sky - it certainly isn't stationary, it's flying under bridges and attacking aliens and not sitting still for one moment.
The difference is one of perspective. From our perspective - with respect to our frame of reference - the gun is always just to our right, but from the observer's perspective - with respect to the outside world's frame of reference - the gun is flying all over the place. Rotation matrices give us a way of taking 3D coordinates and vectors, and mathematically moving them from one frame of reference to another.
Rotating and translating
------------------------
Generally, the full process for moving 3D point coordinates between frames of reference consists of both a rotation and a translation. In the case of the ground-based observer, the translation is between the observer's origin and the plane's origin, while the rotation is between the observer's coordinate axes and the plane's coordinate axes. See the deep dive on rotating and translating points in 3D space for more about how this combination is calculated.
When moving vectors between frames of reference, such as the vectors that represent aerodynamic forces acting on the plane, or the velocity vectors for the bullets, the transformation only requires us to do the rotation part. See the deep dive on the flight model for examples of moving forces between frames of reference.
This deep dive concentrates on the maths behind the rotation aspect of these transformations. This, then, is what the four rotation matrices do:
- Matrix 1 transforms a coordinate from the outside world's frame of reference to the plane's frame of reference.
- Matrix 2 does the reverse, and transforms a coordinate from the plane's frame of reference to the outside world's frame of reference.
- Matrix 3 transforms a coordinate from the plane's frame of reference to the outside world's frame of reference, but only in the roll axis, not the pitch and yaw axes. This calculation is used in the flight model calculations.
- Matrix 4 transforms a coordinate from the outside world's frame of reference to the plane's frame of reference, but only in the pitch and roll axes, not the yaw axis. We can use this matrix to transform the horizon line and gravity vector, as they aren't affected by yaw rotation, so using this matrix is a bit quicker than doing a full matrix 1 transformation.
Let's look at how the four matrices are constructed.
Matrix 1
--------
Matrix 1 is the main rotation matrix that implements the core frame of reference transformation in Aviator. Here are the details:
- Rotates a point or vector through the plane's pitch, roll and yaw
- Rotates a point or vector from the outside world's frame of reference to the plane's frame of reference
- Used in ApplyFlightModel (Part 2 of 7), ProcessLine (Part 6 of 7) and UpdateRadarBlip
When applied to a point, matrix 1 will rotate it around the origin by the orientation of the plane (i.e. by the plane's pitch, roll and yaw). If you imagine seeing a plane overhead and trying to visualise what the pilot might be seeing out of their cockpit, you might subconsciously turn your head and incline your neck to face in the same direction as the plane. That's essentially what this matrix does, by rotating the three axes from their orientation on the ground to their orientation on the plane.
The standard rotation matrices for rotating around the three individual axes are as follows:
- Pitch through angle a (rotate about x-axis):
[ 1 0 0 ] [ 0 cos(a) -sin(a) ] [ 0 sin(a) cos(a) ]
- Yaw through angle b (rotate about y-axis):
[ cos(b) 0 sin(b) ] [ 0 1 0 ] [ -sin(b) 0 cos(b) ]
- Roll through angle c (rotate about z-axis):
[ cos(c) sin(c) 0 ] [ -sin(c) cos(c) 0 ] [ 0 0 1 ]
These are the matrices for a right-handed set of axes, but Aviator uses a left-handed set of axes, so we reflect the angles by subtracting them from 180 degrees. As sin(180 - X) = sin(X) and cos(180 - X) = -cos(X), we need to use the following matrices, which are the same except all the cos values are negated:
- Pitch through angle a (rotate about x-axis):
[ 1 0 0 ] [ 0 -cos(a) -sin(a) ] [ 0 sin(a) -cos(a) ]
- Yaw through angle b (rotate about y-axis):
[ -cos(b) 0 sin(b) ] [ 0 1 0 ] [ -sin(b) 0 -cos(b) ]
- Roll through angle c (rotate about z-axis):
[ -cos(c) sin(c) 0 ] [ -sin(c) -cos(c) 0 ] [ 0 0 1 ]
(Incidentally, this latter matrix is transposed to create matrix 3 - see below for details.)
We can combine these three rotations into one transformation by multiplying the three matrices together. The order in which they are multiplied affects the final result, and Aviator calculates its transformation matrix as "roll then pitch then yaw", so that's what we calculate below.
In the following, sa is shorthand for sin(a), cb is shorthand for cos(b), and so on.
First, we multiply the roll and pitch matrices:
[ -cc sc 0 ] [ 1 0 0 ] [ -cc -ca * sc -sa * sc ] [ -sc -cc 0 ] x [ 0 -ca -sa ] = [ -sc ca * cc sa * cc ] [ 0 0 1 ] [ 0 sa -ca ] [ 0 sa -ca ]
(Incidentally, we copy this interim result into matrix 4 - see below for details.)
We then multiply the result by the yaw matrix to get the final rotation matrix for rotating a point through the three angles:
[ -cc -ca * sc -sa * sc ] [ -cb 0 sb ] [ -sc ca * cc sa * cc ] x [ 0 1 0 ] [ 0 sa -ca ] [ -sb 0 -cb ] [ (cb * cc) + (sa * sb * sc) -ca * sc (sa * cb * sc) - (sb * cc) ] = [ (cb * sc) - (sa * sb * cc) ca * cc -(sa * cb * cc) - (sb * sc) ] [ sb * ca sa ca * cb ]
This is what we want our matrix to look like, so now let's look at how this is implemented in Aviator. As a first step, the ProjectAxisAngle routine converts the rotation angles into the constituent parts by calculating the following variables, using the Sine16Bit function for the trigonometry:
mx1 = -cos(a) mx2 = sin(a) my1 = -cos(b) my2 = sin(b) mz1 = -cos(c) mz2 = sin(c)
If we substitute these values into the above matrix, we end up with the following:
[ (mx2 * my2 * mz2) -mx1 * mz2 (mx2 * my1 * mz2) ] [ + (my1 * mz1) - (my2 * mz1) ] [ ] [ -(mx2 * my2 * mz1) mx1 * mz1 -(mx2 * my1 * mz1) ] [ + (my1 * mz2) - (my2 * mz2) ] [ ] [ mx1 * my2 mx2 mx1 * my1 ]
So this is how the matrix is calculated, piece by piece, until it is stored in matrix 1. See the SetMatrices routine for all the gory details.
Matrix 2
--------
Matrix 2 is the opposite transformation to matrix 1. Here are the details:
- Reverses the rotation in matrix 1
- Rotates a point or vector from the plane's frame of reference to the outside world's frame of reference
- Used in ApplyFlightModel (Part 6 of 7) and FireGuns
Matrix 2 is the transpose of matrix 1. In other words, if matrix 1 looks like this:
[ m0 m1 m2 ] [ m3 m4 m5 ] [ m6 m7 m8 ]
then matrix 2 looks like this:
[ m0 m3 m6 ] [ m1 m4 m7 ] [ m2 m5 m8 ]
The transpose of a rotation matrix performs the reverse transformation, so when matrix 2 is applied to a point after matrix 1 has been applied, it will rotate it back to the original point. In practice, the transpose is used to take a point that's relative to the plane's orientation, and translate it so that it's relative to the outside world.
Matrix 3
--------
Matrix 3 just rotates through the inverse of the plane's roll, i.e. around the z-axis. Here are the details:
- Rotates a point or vector through the inverse of the plane's roll angle
- Used in ApplyFlightModel (Part 6 of 7)
Matrix 3 is as follows:
[ -cos(c) -sin(c) 0 ] [ sin(c) -cos(c) 0 ] [ 0 0 1 ]
which is calculated like this:
[ mz1 -mz2 0 ] [ mz2 mz1 0 ] [ 0 0 1 ]
This matrix is the transpose of one of the three rotation matrices from the matrix 1 calculation above - the matrix that rolls through angle c (rotate about z-axis). This matrix therefore undoes that rotation, so it effectively rotates through an angle of -c.
The 0s and 1s in matrix 3 are hardcoded in memory, with the latter being stored as &FFFE = %11111111 11111110, with the sign in bit 0.
Matrix 4
--------
Matrix 4 is the same as matrix 1, except it doesn't rotate through the yaw angle. Here are the details:
- Rotates a point or vector through the plane's pitch and roll only (not the yaw)
- For the horizon and gravity vectors only, this matrix rotates a point from the outside world's frame of reference to the plane's frame of reference (as rotating either of these vectors by yaw has no effect)
- Used in ApplyFlightModel (Part 1 of 7), ProcessHorizonLine and ArtificialHorizon
Matrix 4 is a copy of the interim result when calculating matrix 1, as follows:
[ -cos(c) cos(a) * sin(c) sin(a) * sin(c) ] [ sin(c) cos(a) * cos(c) sin(a) * cos(c) ] [ 0 sin(a) -cos(a) ]
which is calculated like this:
[ mz1 -mx1 * mz2 mx2 * mz2 ] [ mz2 mx1 * mz1 -mx2 * mz1 ] [ 0 mx2 mx1 ]
This is the rotation matrix for a "roll then pitch" rotation (i.e. without any yaw). This can be used in place of a matrix 1 rotation for vectors that are not affected by the yaw rotation - specifically, the gravity vector (which only has a value in the y-axis) and the horizon (which doesn't change if we yaw the outside world's frame of reference). In other words, this does the same job as matrix 1, but is a bit more efficient for these two special cases.
Incidentally, this last matrix is what Elite uses to rotate its points, because the Cobra Mk III doesn't have a yaw function - it only has pitch and roll. See the deep dive on pitching and rolling in Elite for details.