214. Flying in 3D with quaternions (revised)
NOTE – This post has been substantially revised to show a better solution. You don’t need any special knowledge to read it or to use the code.
About 18 months ago, I posted about the difficulties in flying in 3D, and provided a fairly complex solution arrived at with the help of LoopSpace, which involved quaternions (two separate quaternions).
I’ve had another go at this, and found a simpler way to do it with one quaternion. It took me a month and many hours, but I learned a lot doing it, and I think the solution is very neat, so I’m sharing it in this post.
Rotating in 3D
When you rotate in 2D, you can only turn left or right, and you can add rotations together quite happily. So if you rotate 30 degrees, and then -10 degrees, you are at 20 degrees, which makes programming easy.
When you rotate in 3D, you are rotating in 3 dimensions at once, and the problem is that they affect each other. For example, suppose you are flying straight. If you steer left, the plane will turn left. But if instead, you are flying upside down, and steer left, the plane will turn right in the sky, because everything is reversed.
So in real life, the way the plane behaves in the sky depends not only on how the pilot uses the controls, but on the current rotation of the plane.
To make it worse, Codea doesn’t have a useful way to store the total existing rotation, and update it as we fly around. This is not a criticism of Codea – the problem is common to all graphics using x,y,z angles. But the result is that we literally have no way of managing all the rotations happening as we fly around the sky, in a way that lets us draw the plane at the correct angle, and there are other problems I’ve written about before.
Quaternions
And that is where quaternions come in. They are mysterious and wonderful – mysterious because the math behind them is so difficult that few people understand it, and wonderful because a set of just four numbers can hold the total of all previous rotations. Fortunately, you don’t need to understand them to use them.
A quaternion can efficiently and magically
- store the current rotation of the plane as an axis-angle
- add new rotations, correctly adjusting them for the current plane rotation
- produce a rotation matrix which allows us to correctly rotate and draw the plane
Note – an axis-angle is a point in 3D space, which when you connect it to (0,0,0), gives a line showing the direction of the rotation. But while the aeroplane is pointing in that direction, it could be rotated at any angle around it, so you need an extra number giving the angle. That’s why it’s called axis-angle.
There are special formulae for all of this, and I have included them in a quaternion library so you don’t need to worry about them.
Levelling
It’s difficult to fly exactly straight, so it’s useful to have a feature that “levels” the plane when you take your finger off the controls. My preference is to reduce pitch (up/down) and roll (tipping left/right) to nil, and leave yaw (turning left/right) as it was, so the plane keeps pointing in the same direction, but the wings level off and it flies at a constant height (ie not up or down).
This is not easy when you are using quaternions, because quaternions don’t have numbers for pitch, roll and yaw (an axis-angle is a direction and an angle), so how we know the current yaw? It is possible to extract yaw angles from a quaternion, but this comes with limitations and creates problems I won’t detail here, except to say I spent dozens of hours trying to solve them.
Finally, I think I found the answer (skip if this makes no sense to you). Applying the quaternion to the forward facing vector (0,0-1) gives me the direction of the quaternion. I can use the x and z values of the result, and the atan function, to work out the yaw angle. This can be used to make a target quaternion with the existing yaw, but nil pitch and roll. Then we can use a special quaternion technique to interpolate smoothly towards that target.
This seems to produce fairly realistic results.
Plane
The plane in the demo below is a very simple set of boxes (produced by the Utility tab) to avoid the need to import a 3D model. I have written before about importing these models, and provided code for doing it, so you can use your own models if you want.
You can also use multiple planes if you like.
Sky
It’s important to have a sky image when flying, otherwise it’s hard to see what’s happening. The Utility tab contains code for creating a sphere centred on the plane, covered with a sky image.
The code below uses this this background picture, saved in your dropbox folder as SkyGlobe. You can use your own picture instead, but first read the notes in the Utility tab.
Controls
The Controls tab provides a joystick class. A 2D joystick only has two directions, and I need three controls, so I decided to have two joysticks.
For the left hand joystick,
- up and down movements “pitch” the plane up and down, and
- left and right movements “roll” the plane left and right.
and for the other, left and right movements turn (“yaw”) the plane turn left or right. I would prefer not to have two controls, but even in real planes, the yaw is done separately with foot pedals.
What controls you use, how many you have, and how you use them, is entirely up to you. The quaternion class doesn’t care – it only needs to know what changes you want to make to the x,y,z rotations.
Demo
Using the libraries provided, it doesn’t require much code from you, to do basic flying. Here is the entire demo app in 30 lines of code.
function setup() --plane setup local p=MakePlane() --create the mesh plane=QC(p) --create a quaternion, pass it the mesh --joysticks joyPR=JoyStick() --pitch/roll joyY=JoyStick({centre=vec2(310,110)}) --yaw --plane position and sky pos=vec3(0,0,0) sky=Sky(5000,readImage("Dropbox:SkyGlobe")) speed=10 --camera settings --1 = directly behind, 2 = behind and above, 3 is cockpit view parameter.integer("Camera",1,3,1) camSettings={{vec3(0,0,30),false},{vec3(2,5,25),false}, {vec3(0,1.5,-6.75),true}} end function draw() background(120) perspective() local jPR=joyPR:update() --get joystick movement local jY=joyY:update() --if joystick used, rotate plane, else level it if joyPR:isTouched() or joyY:isTouched() then --convert joystick values to x,y,z rotations --how you do this is up to you plane:AdjustOrientation(jPR.y,-jY.x,-jPR.x/3) else plane:RollLevel() end local p=plane:Move(vec3(0,0,-speed)) --velocity pos=pos+p*DeltaTime --calculate new position plane:SetCamera(pos,camSettings[Camera][1], camSettings[Camera][2]) sky:draw(pos) plane:draw(pos) joyPR:draw() joyY:draw() end function touched(t) joyPR:touched(t) --get joystick touches joyY:touched(t) end
Code is here, and don’t forget to also put this image in Dropbox, named SkyGlobe.
this looks cool
how hard is this codea