4. Replacing Buttons with Orientation Sensors
The next step is carefully integrating
support for the compass and accelerometer APIs. I say “carefully”
because we’d like to provide a graceful runtime fallback if the device
(or simulator) does not have a magnetometer or accelerometer.
We’ll be using the accelerometer to obtain
the gravity vector, which in turn enables us to compute the phi angle
(that’s “altitude” for you astronomers) but not the theta angle
(azimuth). Conversely, the compass API can be used to compute theta but
not phi. You’ll see how this works in the following sections.
4.1. Adding accelerometer support
Using the low-level accelerometer API
directly is ill advised; the signal includes quite a bit of noise, and
unless your app is somehow related to The Blair Witch
Project, you probably don’t want your camera shaking
around like a shivering chihuahua.
Discussing a robust and adaptive low-pass
filter implementation is beyond the scope of this book, but thankfully
Apple includes some example code for this. Search for the
AccelerometerGraph sample on the iPhone developer
site (http://developer.apple.com/iphone) and
download it. Look inside for two key files, and copy them to your
project folder: AccelerometerFilter.h and
AccelerometerFilter.m.
After adding the filter code to your Xcode
project, open up GLView.h, and add the three code
snippets that are highlighted in bold in Example 10.Example 10. Adding accelerometer support to GLView.h
#import "Interfaces.hpp" #import "AccelerometerFilter.h" #import <UIKit/UIKit.h> #import <QuartzCore/QuartzCore.h>
@interface GLView : UIView <UIAccelerometerDelegate> { @private IRenderingEngine* m_renderingEngine; IResourceManager* m_resourceManager; EAGLContext* m_context; AccelerometerFilter* m_filter; ... }
- (void) drawView: (CADisplayLink*) displayLink;
@end
|
Next, open GLView.mm,
and add the lines shown in bold in Example 11. You might grimace at the sight of
the #if block, but it’s a necessary evil because
the iPhone Simulator pretends to support the accelerometer APIs by
sending the application fictitious values (without giving the user
much control over those values). Since the fake accelerometer won’t do
us much good, we turn it off when building for the
simulator.
Note:
An Egyptian software company called
vimov produces a compelling tool called
iSimulate that can simulate the
accelerometer and other device sensors. Check it out at http://www.vimov.com/isimulate.
Example 11. Adding accelerometer support to initWithFrame
- (id) initWithFrame: (CGRect) frame { m_paused = false; m_theta = 0; m_phi = 0; m_velocity = vec2(0, 0); m_visibleButtons = 0;
if (self = [super initWithFrame:frame]) { CAEAGLLayer* eaglLayer = (CAEAGLLayer*) self.layer; eaglLayer.opaque = YES;
EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES1; m_context = [[EAGLContext alloc] initWithAPI:api]; if (!m_context || ![EAGLContext setCurrentContext:m_context]) { [self release]; return nil; } m_resourceManager = CreateResourceManager();
NSLog(@"Using OpenGL ES 1.1"); m_renderingEngine = CreateRenderingEngine(m_resourceManager);
#if TARGET_IPHONE_SIMULATOR BOOL compassSupported = NO; BOOL accelSupported = NO; #else BOOL compassSupported = NO; // (We'll add compass support shortly.) BOOL accelSupported = YES; #endif if (compassSupported) { NSLog(@"Compass is supported."); } else { NSLog(@"Compass is NOT supported."); m_visibleButtons |= ButtonFlagsShowHorizontal; } if (accelSupported) { NSLog(@"Accelerometer is supported."); float updateFrequency = 60.0f; m_filter = [[LowpassFilter alloc] initWithSampleRate:updateFrequency cutoffFrequency:5.0]; m_filter.adaptive = YES;
[[UIAccelerometer sharedAccelerometer] setUpdateInterval:1.0 / updateFrequency]; [[UIAccelerometer sharedAccelerometer] setDelegate:self]; } else { NSLog(@"Accelerometer is NOT supported."); m_visibleButtons |= ButtonFlagsShowVertical; }
[m_context renderbufferStorage:GL_RENDERBUFFER fromDrawable: eaglLayer]; m_timestamp = CACurrentMediaTime();
m_renderingEngine->Initialize(); [self drawView:nil]; CADisplayLink* displayLink; displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawView:)]; [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; } return self; }
|
Since GLView sets itself
as the accelerometer delegate, it needs to implement a response
handler. See Example 12.
Example 12. Accelerometer response handler
- (void) accelerometer: (UIAccelerometer*) accelerometer didAccelerate: (UIAcceleration*) acceleration { [m_filter addAcceleration:acceleration]; float x = m_filter.x; float z = m_filter.z; m_phi = atan2(z, -x) * 180.0f / Pi; }
|
You might not be familiar with the
atan2 function, which takes the arctangent of the
its first argument divided by the its second argument (see Phi as a function of acceleration). Why not use the plain old single-argument
atan function and do the division yourself? You
don’t because atan2 is smarter; it uses the signs
of its arguments to determine which quadrant the angle is in. Plus, it
allows the second argument to be zero without throwing a
divide-by-zero exception.
Note:
An even more rarely encountered math
function is hypot. When used together,
atan2 and hypot can convert
any 2D Cartesian coordinate into a polar coordinate.
Phi as a function of acceleration
Phi as a function of acceleration shows how
we compute phi from the accelerometer’s input values. To understand
it, you first need to realize that we’re using the accelerometer as a
way of measuring the direction of gravity. It’s a common misconception
that the accelerometer measures speed, but you know better by now! The
accelerometer API returns a 3D acceleration vector according to the
axes depicted in Figure 4.
When you hold the device in landscape mode,
there’s no gravity along the y-axis (assuming you’re not slothfully
laying on the sofa and turned to one side). So, the gravity vector is
composed of X and Z only—see Figure 5.
4.2. Adding compass support
The direction of gravity can’t tell you
which direction you’re facing; that’s where the compass support in
third-generation devices comes in. To begin, open
GLView.h, and add the bold lines in Example 13.
Example 13. Adding compass support to GLView.h
#import "Interfaces.hpp" #import "AccelerometerFilter.h" #import <UIKit/UIKit.h> #import <QuartzCore/QuartzCore.h> #import <CoreLocation/CoreLocation.h>
@interface GLView : UIView <CLLocationManagerDelegate, UIAccelerometerDelegate> { @private IRenderingEngine* m_renderingEngine; IResourceManager* m_resourceManager; EAGLContext* m_context; CLLocationManager* m_locationManager; AccelerometerFilter* m_filter; ... }
- (void) drawView: (CADisplayLink*) displayLink;
@end
|
The Core Location API is an umbrella for
both GPS and compass functionality, but we’ll be using only the
compass functionality in our demo. Next we need to create an instance
of CLLocationManger somewhere in
GLview.mm; see Example 14.
Example 14. Adding compass support to initWithFrame
- (id) initWithFrame: (CGRect) frame { ...
if (self = [super initWithFrame:frame]) {
...
m_locationManager = [[CLLocationManager alloc] init];
#if TARGET_IPHONE_SIMULATOR BOOL compassSupported = NO; BOOL accelSupported = NO; #else BOOL compassSupported = m_locationManager.headingAvailable; BOOL accelSupported = YES; #endif if (compassSupported) { NSLog(@"Compass is supported."); m_locationManager.headingFilter = kCLHeadingFilterNone; m_locationManager.delegate = self; [m_locationManager startUpdatingHeading]; } else { NSLog(@"Compass is NOT supported."); m_visibleButtons |= ButtonFlagsShowHorizontal; }
... } return self; }
|
Similar to how it handles the accelerometer
feedback, GLView sets itself as the compass
delegate, so it needs to implement a response handler. See Example 6-31. Unlike the accelerometer, any noise
in the compass reading is already eliminated, so there’s no need for
handling the low-pass filter yourself. The compass API is
embarrassingly simple; it simply returns an angle in degrees, where 0
is north, 90 is east, and so on. See Example 15 for the compass response
handler.
Example 15. Compass response handler
- (void) locationManager: (CLLocationManager*) manager didUpdateHeading: (CLHeading*) heading { // Use magneticHeading rather than trueHeading to avoid usage of GPS: CLLocationDirection degrees = heading.magneticHeading; m_theta = (float) -degrees; }
|
The only decision you have to make when
writing a compass handler is whether to use
magneticHeading or trueHeading.
The former returns magnetic north, which isn’t quite the same as
geographic north. To determine the true direction of the geographic
north pole, the device needs to know where it’s located on the planet,
which requires usage of the GPS. Since our app is looking around a
virtual world, it doesn’t matter which heading to use. I chose to use
magneticHeading because it allows us to avoid
enabling GPS updates in the location manager object. This simplifies
the code and may even improve power consumption.