June 1, 2015
But first, why is a gyro better than an accelerometer?
While the accelerometer can tell you which way is down, and therefore the angle that your device is being held at, it can not tell you about ‘Yaw’ or how the device is spinning or oriented on a flat surface (what’s called z axis info). Without this piece of data, you can’t get a complete picture of how your device is moving in 3D space. You could couple accelerometer with compass data but the results won’t be smooth or update to an accurate state quick enough.
The gyroscope tells you how your device is moving or oriented in 3D space. You would think that converting gyro data into something useful would be a pretty straightforward programming task, but it turns out that it’s a PITA and requires a bit of fundamental physics and math knowledge. That’s OK because it’s definitely stuff worth knowing and the more that developers start to understand these concepts, the more futuristic cool and innovative apps will start to appear that use next-gen interfaces.
This deceptively simple human interface device turns out to have fairly complex technical concepts behind it. At this point in time, I don’t know of any magic bullet code library that handles all of this auto-magically for you. So a bit of time and effort in understanding the the core concepts behind it all is necessary to get anything really useful or flexible out of gyroscope functionality. My goal of this blog post is to provide all that knowledge and code to back it up so that you will have what you need to get useful results.
So here goes: I’m going to start very basic and get very technical. Feel free to skim ahead to the point wherever you find things getting confusing.
First off, imagine the gyroscope toy, it has a spinning wheel that stays upright (resists any kind of rotation) relative to however you try to twist its base. As you move it, you could describe its movement / orientation by describing how it rotates in the 3 planes of 3D space. On the gyroscope spinning top toy, each rotation in a plane could be handled by 1 of 3 gimbals (assuming they happened to be lined up all perpendicular to each other.) A gyroscope in a smart phone gives us a constantly updating stream of 3 numbers, each of which replresents the current state of rotation in each of the 3 diminsions. This 3 rotations are called Euler angles and can be used to describe any type of rotation. Seems simple enough but it turns out there is a huge catch, but we’ll get to that later.
Before we do anything, let’s draft out the actual standard for which we will describe rotations. I’m going to use the system I see most commonly used, but be warned, you may come across descriptions or systems that are different. X axis rotations are rotations about an axis going left / right just like you would think. The y axis is perpendicular to this going away from you but also flat as if it were on a table, not from the ground to the sky like you might think. The z axis comes out from the table and goes up into the sky. This might seem a little different than what you would have intuitively thought, but it seems to be the standard and is referenced this way in both the IOS documentation and the W3 gyroscope draft. To say it another way, spinning your phone face up on a table is spinning on the z axis.
Additionally, rotation in any positive amount results in a counter-clockwise (right hand rule) movement assuming you’re staring at the end of the axle. (x on the right side, y on the top end away from you and z looking straight down into the ground)
That enough theory, lets actually do something.
Alpha is rotation around the z axis with values from 0 to 360
Beta is rotation around the x axis with values from -180 to 180
Gamma is rotation around the y axis with values from -90 to 90
Let’s get this info and display it. First let’s start with a generic page to show our data. Hers’s the html:
The first part adds an event listener for the gyro data. The processGyro function actually does something with it, for now, it just displays it.
The final page looks like this. Take a look on your gyro-enabled mobile device (Note: It won’t do anything interesting on desktops)
Note, that IOS values don’t line up. An iSomething returns the range:
|parameter||returned by IOS||supposed to be|
|alpha (z)||0 to 360||0 to 360|
|beta (x)||-90 to 90||-180 to 180|
|gamma (y)||-180 to 180||-90 to 90|
WTF’s up with that? What originally looked to be an amazingly annoying flagrant IOS bug actually makes some sense when you dig into it. Too bad they never bothered to explain it to anyone, but here’s my best guess as to what’s going on:
Apple wanted the alpha value to always reflect a change in where the top of the device is pointing (z axis). This is useful for apps in which you point your phone in some direction and you could get the value easily, like a compass heading. However, they wanted the gamma to reverse when the device hit 90 degrees upright instead of the expected 0 or 180 (flat or upside down). Why? I’m not positive, but I think I read somewhere that they wanted the ‘direction’ the device is pointing to be dictated by the direction the top of the device is pointing (where the power button is), not the more obvious exact-opposite-side of the screen direction (which makes a lot more sense because it’s like you’re looking through the screen for pointing). Anyhooooooo, I expect Apple to come to their senses about this one some day, but until then we have some weirdness to deal with. In order to make this nonsense work, they had to tweak the beta and gamma values to differ from the w3 spec to compensate, so as to not be erroneous, even though the resulting numbers for beta and gamma make almost zero intuitive sense. But, after you pass it all through a decoding matrix, the math is actually in fact accurate, and you can get an accurate quaternion / AxisAngle that represents orientation without any gimble lock.
Now we know that the gyro works, let’s make it do something interesting.
First up, use html5 canvas element to draw-render some 3d objects: a cube and some axis lines. In reality it would probably be a bit quicker and easier to use something like three.js for rendering but since we’re trying to cover everything from the ground up we’re going to do it the hard way from scratch. Besides, this will make it much possible to make the jump directly into webgl later. First, add the bare bones canvas stuff to our code. First some CSS:
and just before the javscript in the body add:
This is the bare bones overhead code to get canvas ready to draw. the renderLoop() function will be called on every frame redraw. Therefore it currently contains code to continue the loop, and clear the screen.
Lets build some data stractures that represent our objects in 3d. Being canvas we could just use .rect to draw squares but once again we are going to do things unecesarrily difficult to make the code transplant to webgl later on much easier. That means creating a strip of triangles that represent the cube. Add the makeRect function and some global definitions just before the renderloop function:
Now we need to draw this on the canvas. Add this inside the renderloop function at the end:
and add the renderObj function:
IMPORTANT: Notice the y componant of each point is negated to render upside down. The reason for this is that the coordinate system of the screen has y increasing while going down but in a cartesian coordinate system, y values increase as they go up. This is a common thing to overlook in 3d rendering and whenever you run across code that inverts y it’s usually to deal with this scenario.
Now load up the page. You should see some colorful rectangles.
Now that we have a series of 3d points, how do we rotate them? Remember sin() and cos() from trigonometery? Of course not, nobody does. That was high school nap time. So here’s a quick refresher. Lets say you have a point on the x axis of a 2d cartesian coordinate plane. If you want to rotate it some number of degrees (assuming the 0,0 point is the center of rotation on the theoretical z axis) its new x position is x*cos(some number of degrees) It’s y position can be determined by x*sin(some number of degrees). Easy.
But what if we start with a point that’s not on the x axis, It already has a y value. Or to say it another way, it’s already 2 dimensional. The cleverly derived formula for that is:
z axis rotation:
newX = x * cos(angle) - y * sin(angle)
newY = y * cos(angle) + x * sin(angle)
You’ll notice it’s backwards compatible with the simpler cos(angle) sin(angle) formul as as if the y value was 0 just sitting right there on the x axis (no 2nd dimension data yet) 0*sin(anything) = 0 and 0*cos(anything) = 0 so the equations do simplify to what we were supposed to have learned in high school.
Sweet, now we’re up to 2 dimensions, still need to add one more (a ‘z’ dimension) and fascilitate rotating on the other 2 axes (x and y). For this we just twist this equation on it’s side for each of the remaining 2 dimensions and swap in the correct values. For rotation around the x axis we plug in y and z values in the x and y spots respectively.
x axis rotation:
newY = y * cos(angle) - z * sin(angle)
newZ = z * cos(angle) + y * sin(angle)
And finally for rotation around the y axis we plug in x and z values.
y axis rotation:
newZ = z * cos(angle) - x * sin(angle)
newX = x * cos(angle) + z * sin(angle)
At the very begining of the processGyro funtion add:
Then, add this to the very begining of the renderObj function
and change the lines:
and also change
Now add rotateObject function:
It simply loops through each vertex in the object rotating each point through the rotatePointViaGyroEulars function creating a new object to eventually return along the way. Speaking of, lets create that rotatePointViaGyroEulars funtion (plus one helper degrees to radians function) now:
This is the meat and potatos of the rotation. It simply takes a 3d point as in an array as the argument, runs the point through the 3 formulas described earlier, and returns the adjusted values as a new array. the degToRad function converts standard degrees into radians which sin and cos require. a full 360 circle in radians = 2 * PI.
IMPORTANT: Notice that each of the 6 degToRad functions returning the Euler rotation data are negated. This is because the gyro data represents how the device currently is currently oriented. What we actually want to show is exact opposite of that becuase we want the gyro to appear to counter act or stay in the same place no matter how we rotate the device. Forgetting this (or forgetting to invert the y rendering above) are 2 very common sources of massive headaches debugging 3d code.
One last optional step:
For those of you who want to see this render on a regular non-mobile screen. Lets fake some gyro data for regular stationary computers. This way, you’ll at least be able to see something happening if viewed on a regular desktop or laptop. In the renderloop function add this at the very begining of the function:
Go ahead and load this page up on your mobile device and move it around.
Sweet! Moving stuff! Don’t forget to disable device display rotation on your mobile device as that will get annoying quick.
You’ll notice It’s not quite right. It’s hard to tell what’s close and what’s far away. This is because in real life far away things look smaller to us than close things. Our eyes are really good at deciphering this relationship without us even thinking about it and therefore this orthagonal ( aka no parralax distance info rendered ) drawing looks slightly odd. Luckily we can fix this easy. By taking the so far unused z value and simply using it as a basis to scale the x and y values we can create the illusion of depth. Values at z=0 are rendered exactly to size, points with large z values are rendered bigger and points with smaller z values are rendered smaller
This is done with the focal length formula which is:
scale = focalLength/(zValue+focalLength)
where focallength is the distance between you and the drawing in pixels. In reality you can pic any number you find pleasing with smaller focal length numbers rendering a bigger effect. This brings us to the last frequenlty missunderstood concept. IMPORTANT. This formula assumes z gets larger as you go away which for our purposes is incorrect. For us, Z values are largest on the top side of the phone and smallest on the other side of the screen. Therefore our formula needs to be:
Let’s add this function to complete our 3d render engine:
To use this function, change the folowing lines in the renderObj funtion from:
noting that we need to keep negating that y value.
Now save all that and take a look:
Done! Now we have a fully functioning basic 3d rendering algorythm. (Here’s the final code) It’s interesting to note how we are using Euler angles and not running into gimbal lock problems. Now if we tried to add 2 rotations together they would most likely show up. For that reason and others it is time to graduate from Eulers to something a bit more elegant and IMO actually easier to comprehend.
OPTIONAL: Some technical notes: