iPhoneアプリの開発を実践的に解説する本連載だが、前回の記事からかなり時間が空いてしまった。筆者の都合により執筆のためのまとまった時間が足りなくなったためで、素直に申し訳なく思う。どうにか再開できることとなったので、今度はできるだけ間を空けないようにがんばりたい。

連載が中断されている間、iPhoneとそのOSには、大きい動きが次々とあった。iPadとiPhone 4といった新しいハードが登場し、それにあわせてOSの名称もiOSと変更された。バージョンもiOS 4となり、マルチタスクを始めとする多くの新機能が追加された。

連載は、前回の続きであるARアプリの作り方から再開しよう。不幸中の幸いというか、新たに登場したiPhone 4とiOS 4でもARに使える機能追加が行われたので、それを早速使うことにしよう。

デバイスの位置と向きを特定する

ARアプリではカメラ画像の上に、まるで現実かのようにオブジェクトなどを表示する。それには、いまデバイスがどの位置にあり、どのような向きにあるかを知る必要があるだろう。

デバイスの位置や向きを知るには、ハードウェアとしてのセンサが必要である。iPhoneでは、登場当初はGPSと加速度センサを備えており、iPhone 3GSでは電子コンパスが追加された。さらに、iPhone 4ではジャイロセンサも搭載することになった。

それぞれのセンサで何ができるか、説明していこう。まずGPSを使うと、デバイスが地球上のどこに存在するか、その位置を知ることができる。位置情報は、緯度経度の形で与えられる。GPSには、Core Locationフレームワークを使ってアクセスする。このプログラミングについては、本連載の第15回から始まる「ロギングアプリの作り方」で詳しく解説しているので、そちらを参照してほしい。

電子コンパスは、デバイスが東西南北どちらの方位を向いているのか、を検出するために使う。GPSと電子コンパスを組み合わせることで、地球上のどの位置に立ち、カメラをどちらに向けているか、が分かるようになった。これにより、カメラ画像の上にその位置に関する情報を重ね合わせて表示する、AR的な機能が実現できるようになった。

加速度センサはデバイスのx、y、z軸方向にかから加速度の値を知るためのものだ。これを使うと、重力加速度による加速度値を得ることにより、デバイスの向きを知ることができる。iPhoneはデバイスを縦横に回転させると画面の表示内容が変わるが、これは加速度センサによるものだ。また、デバイスを細かく振る、シェイクモーションの検出にも使われる。

加速度センサによるデバイスの向きの取得には、欠点がある。デバイスが静止した状態であれば、かかる加速度は重力加速度のみとなるので、デバイスの向きが分かる。だがデバイスを動かすと、その動きにより加速度がかかるので、重力加速度以外の要因が加わることになる。こうなってしまうと、正しい向きは分からないことになる。実際にアプリに組み込むことを考えると、AR的なアプリではデバイスをかかげて周りを見回すことが多いので、結構頻繁にデバイスは動く。したがって、見回しているときに位置がずれてしまうという事態が発生する。

この問題は、ジャイロセンサを使うことで解決することができる。ジャイロセンサはx、y、z軸の角加速度を調べる事ができるものだ。この値を使うことで、オイラー角(roll、pitch、yawといった姿勢角)を計算することができる。

これらのセンサを使うことで、現実空間にあるデバイスの位置および向き情報を取得し、その上に仮想現実を重ねることができるようになるのだ。

Core Locationで方位を取得する

では、プログラム上でそれらにどうやってアクセスするかを説明しよう。

まずは電子コンパスだ。電子コンパスにアクセスするには、GPSのときにも使用した、Core Locationフレームワークを使う。まず、現在のデバイスで電子コンパスが使用可能かどうか調べる。これには、CLLocationManagerクラスのheadingAvailableというクラスメソッドを使う。使用可能であれば、CLLocationManagerをインスタンス化してデリゲートを設定し、startUpdatingHeadingメソッドを呼び出す。このメソッドを呼び出すことで電子コンパスによる測定が始まる。

List 1.

// コンパスが使用可能かどうかチェックする
if ([CLLocationManager headingAvailable]) {
    // CLLocationManagerを作る
    _locationManager = [[CLLocationManager alloc] init];
    _locationManager.delegate = self;

    // コンパスの使用を開始する
    [_locationManager startUpdatingHeading];
}

測定によりデバイスの方位が変化すると、デリゲートメソッドであるlocationManager:didUpdateHeading:が呼び出される。この引き数として渡ってくる、CLHeadingオブジェクトから、方位を知ることができる。

List 2.

- (void)locationManager:(CLLocationManager*)manager didUpdateHeading:(CLHeading*)newHeading
{
    // 方位を表示する
    NSLog(@"trueHeading %f, magneticHeading %f",
            newHeading.trueHeading, newHeading.magneticHeading);
}

方位を得るためのプロパティとしては、真北を表すtrueHeadingと、磁北を表すmagneticHeadingがある。真北は北極星を基準とした北を表し、磁北は方位磁石を基準とした北となる。一般に、真北と磁北の間にはずれがある。

では、真北と磁北のどちらを使うべきかというと、iPhoneで利用できるGoogleマップを含む多くの地図は真北が上になっているので、地図と重ね合わせる用途などでは真北になるだろう。磁北は、たとえば風水で使われる羅盤のように、方位磁石としての北が必要なときに使えるだろう。

Core Motionで向きを取得する

次に、加速度およびジャイロセンサにアクセスする方法を説明しよう。加速度は、iOS 4以前はUIAccelerometerクラスを使って取得することになっていた。iOS 4からは、Core Motionというフレームワークが追加された。このフレームワークを使うと、加速度およびジャイロセンサによる値、およびそれらから計算されたプログラミングする上で便利な値を取得することができる。今回は、こちらを使うことにしよう。

ARアプリでは、センサの生の値を取得するというよりは、デバイスの向きを知りたい。これはCore Motionでは、CMDeviceMotionクラスに含まれる、CMAttitudeというオブジェクトの形で提供される。CMAttitudeからは、roll、pitch、yawといった姿勢角を取得することができる。また、その回転を表す行列などが提供される。これらの数学的な定義は、オイラー角の定義などを参考にしてほしい。

プログラミングの仕方を説明しよう。まず、CMMotionManagerのインスタンスを作成する。deviceMotionAvailableというプロパティがあるので、向きを検出可能かどうかチェックする。可能であればCMDeviceMotionを取得するための設定を行うのだが、それにはこちらでタイマを回して取りにいく方法と、Core Motionから通知してもらう方法の、2通りがある。ここでは、後者の方法を説明しよう。

Core MotionからCMDeviceMotionを通知してもらうには、ハンドラを設定する。このハンドラは、ブロックとなる。ブロックは、iOS 4から追加された、Objective-Cの構文だ。ハンドラの引き数としてCMDeviceMotionが渡されるので、ここからCMAttiudeを取得し、姿勢角を得ることができる。

List 3.

// CMMotionManagerを作る
_motionManager = [[CMMotionManager alloc] init];

// 向きを検知可能かどうかチェックする
if (_motionManager.deviceMotionAvailable) {
    // 更新の間隔を設定する
    _motionManager.deviceMotionUpdateInterval = 1.0f / 60.0f;

    // ハンドラを設定する
    CMDeviceMotionHandler   deviceMotionHandler;
    deviceMotionHandler = ^ (CMDeviceMotion* motion, NSError* error) {
        // デバイスの向きを表示する
        NSLog(@"motion { %f, %f, %f }",
                motion.attitude.roll, motion.attitude.pitch, motion.attitude.yaw);
    };

    // 向きの更新通知を開始する
    [_motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue]
            withHandler:deviceMotionHandler];
}

次回は、これらのセンサの値を使って、カメラ画像の上にオブジェクトを表示することについて議論しよう。

ここまでのソースコード: AR-2.zip