OpenGLによるARアプリ 〜いつでも札幌駅の方向を優しく教えてくれる、めそ子さんを作ってみた〜

ios

1 はじめに

お酒を飲んでお店を出た時、どっちに向かって歩けばいいのか全く分からない、超方向音痴な私は、いつも聞いてしまいます。「札幌駅はどっち?」 そんな私のために、札幌駅の方向を優しく教えてくれる、めそ子さん作りました。

札幌駅の方向さえ分かれば、だいたい何とかなる「札幌あるある」案件です。

004 005

最近、Wikitudeを使用させて頂いて、ARなアプリを幾つか作成していたのですが、「座標に基づくAR表示ぐらいなら、自前でも簡単に作れるよ」とのアドバイスを頂きましたので、今回は、OpenGLで書いてみましたので、その覚書です。

なお、誤解を生じないように、記載しておきますが、Wikitudeは、「座標に基づくAR表示」だけでなく、画像認識、3Dモデル、動画の表示など、多数の機能を持ったライブラリであり、本記事は、決してこれを置き換えるようなものではありません。

2 AR画面の仕組み

最初に、AR画面の作成要領です。

まずは、OpenGLで左上 (-0.5,-0.5)、右下 (0.5,0.5)で、四角を書いてみました。(Z軸 奥行きは仮に-3としました)

// オブジェクトの位置を決定する
glRotatef(0, 0, 1, 0); // 正面を向いたまま
glTranslatef(0, 0, -3); // Z軸は-3 

// 頂点座標を登録
GLfloat left = -0.5f;
GLfloat right = 0.5f;
GLfloat top = -0.5f;
GLfloat bottom = 0.5f;
GLfloat squareVertices[] = {
    left, bottom,
    right, bottom,
    left, top,
    right, top,
};

そして、次に、覗いているカメラを右に20度動かしたことを模擬して、次のようにプログラムします。

glRotatef(20, 0, 1, 0); // 20度、カメラを右にふる

表示された画面は次のとおりです。表面に置いた四角形が、カメラをふることで左にずれている事が確認できます。

001

次に、先ほどプログラムで動かした右へ20度の変化を、iPhoneの方位(コンパス)の値で変化させます。また、同じように、上下及び傾きもジャイロからのデータで変化させてみます。

// gravity 加速度センサーのデータ
glRotatef(gravity.z * -90, 1, 0, 0);
glRotatef(gravity.x * 90, 0, 0, 1);

// heading コンパスのデータ
glRotatef(heading , 0, 1, 0);

すると、ちょっと安定して表示させるのは難しいですが、北を向いてiPhoneをまっすぐに立てて、正面を見ると次の様に表示されます。 そして、2枚目は、少し右を向いて見た様子です。

002

この状態から、OpenGL画面の背景を透明にして、カメラの画像を表示すると次のようになります。

// レイヤー設定
CAEAGLLayer *layer = (CAEAGLLayer*)self.layer;
// カメラの表示が見えるようにするため透明にする
layer.opaque = NO;

先ほどと同じように、北を向いてiPhoneをまっすぐに立てて、正面を見たものと、少し右に向けて見たものです。

003

もう、説明は不要かも知れませんが、このようにiPhoneのコンパスと加速度センサーの値をOpenGLの画面に連動させて、背景にカメラの画像を表示すれば、AR画面となります。

表示するARの位置(経緯度)と自分の位置(経緯度)から、方向を計算して、OpenGLのレイヤに書き込めば終わりということになります。 なお、仰角については、標高差を計算すれば同じように表現することが可能ですが、距離については、OpenGLのZ軸に適当な定数を掛けて表現するしかないでしょう。

以降は、カメラの画像表示、センター値の取得、そして、自分の位置(経緯度、標高)の処理方法について、それぞれ纏めてみます。

3 カメラの画像表示

カメラの映像は、UIImagePickerControllersourceTypeUIImagePickerControllerSourceTypeCameraを指定して表示できます。

考慮する事項としては、シャッターなどのコントロールを非表示にすることと、画面サイズに合わせて、サイズを調整するぐらいです。 そして、cameraOverlayViewプロパティに、OpenGLを表示したビューを載せれば、AR画面の完成です。

※OpenGLのビューをを生成して、すぐにcameraOverlayViewにセットすると認識されない場合があった。サンプルでは、ビューを生成してから少しSleepを置くことで対処しました。

// 使用するタイプはカメラ
UIImagePickerControllerSourceType sourceType = UIImagePickerControllerSourceTypeCamera;
// 利用可能かどうかの確認
if ([UIImagePickerController isSourceTypeAvailable:sourceType]) {
    UIImagePickerController *cameraPicker = [[UIImagePickerController alloc] init];
    cameraPicker.sourceType = sourceType;
    cameraPicker.showsCameraControls = NO; // シャッターボタンなどの非表示

    // カメラサイズの調整
    CGSize screenSize = [[UIScreen mainScreen] bounds].size;
    float heightRatio = 4.0f / 3.0f;
    float cameraHeight = screenSize.width * heightRatio;
    float scale = screenSize.height / cameraHeight;
    cameraPicker.cameraViewTransform = CGAffineTransformMakeTranslation(0, (screenSize.height - cameraHeight) / 2.0);
    cameraPicker.cameraViewTransform = CGAffineTransformScale(cameraPicker.cameraViewTransform, scale, scale);

    // 重ね書きするオブジェクト
    cameraPicker.cameraOverlayView = arView; // arViewは、OpenGLを表示するビューです。

    // カメラ表示
    [self presentViewController:cameraPicker animated:NO completion:nil];
}

4 各種センサー

(1) コンパス(Core Location)

方位については、CLLocationManagerを使用します。 startUpdatingHeadingで、コンパスの使用を開始し、デリゲートメソッドでデータを受け取ります。

#import <CoreLocation/CoreLocation.h>

@interface ViewController ()<CLLocationManagerDelegate>
@property (nonatomic)CLLocationManager* locationManager;
@end

使用可能かどうかを確認し、デリゲートをセットして使用を開始する

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

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

データは、デリゲートで受け取ります。

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

コンパスのデータには、trueHeading(真北)とmagneticHeading(磁北)があり、とりあえず、真北を使用しています。

コンパスの値は、デバイスを回転すると変わるため、デバイスの状態も合わせて取得して、考慮する必要があります。

// コンパスによる回転
float heading = _heading;
switch(self.orientation) {
    case UIDeviceOrientationPortrait:            // Device oriented vertically, home button on the bottom
        break;
    case UIDeviceOrientationPortraitUpsideDown:  // Device oriented vertically, home button on the top
        heading -= 180;
        break;
    case UIDeviceOrientationLandscapeLeft:       // Device oriented horizontally, home button on the right
        heading -= 270;
        break;
    case UIDeviceOrientationLandscapeRight:      // Device oriented horizontally, home button on the left
        heading -= 90;
        break;
}
glRotatef(heading , 0, 1, 0);

(2) 加速度センサー (Core Motion)

デバイスの向きと傾きは、Core Motionで取得します。

データはCMDeviceMotionのオブジェクトで与えられますが、そのメンバーであるCMAccelerationからxyzといった姿勢角を取得することができます。

iPhoneを立てに持ってカメラを覗いた姿勢では、x,y,zは、OpenGLの空間では、それぞれ、x(回転)、y(左右)、z(上下)方向へのカメラの移動に関連しており、y(左右)については、コンパスの値を使用するため、ここでは利用しませんでした。

#import <CoreMotion/CoreMotion.h>

@interface ViewController ()

@property(nonatomic, strong) CMMotionManager *motionManager;

@end

CMMotionManagerのデータは、ブロック構文で受け取ることが出来ます。

_motionManager = [[CMMotionManager alloc] init];
if (_motionManager.deviceMotionAvailable) {
    // 更新の間隔を設定する
    _motionManager.deviceMotionUpdateInterval = 0.5f;
    [_motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue]
                                        withHandler: ^ (CMDeviceMotion* motion, NSError* error) {
                                            NSLog(@"motion { x:%f, y:%f, z:%f }",motion.gravity.x,motion.gravity.y,motion.gravity.z);
                                        }
        ];

}

5 位置情報の取得

AR表示で対象物の位置(カメラから覗く方向)を得るためには、まずは、自分の位置が何処であるかの情報が必要です。 位置の情報は、CLLocationManagerで取得できます。

#import "CoreLocation/CoreLocation.h"

@interface ViewController ()<CLLocationManagerDelegate>
@property (nonatomic)CLLocationManager* locationManager;
@end

位置利用の許可を得た後、startUpdatingLocationで情報の取得を開始します。

self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
// iOS8以上では、位置情報を取得の許可を得る(このコードは、iOS7では実行できません)
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0) {
    [ self.locationManager requestAlwaysAuthorization];
}
[self.locationManager startUpdatingLocation]; 

データは、デリゲートで受け取ります。

- (void)locationManager:(CLLocationManager *)manager
     didUpdateLocations:(NSArray *)locations
{
    // 位置情報を取り出す
    CLLocation *newLocation = [locations lastObject];
    NSLog(@"緯度:%.2f 軽度:%.2f 標高:%.2f",newLocation.coordinate.latitude,newLocation.coordinate.longitude,newLocation.altitude);
}

info.plistに利用目的の記載が必要です。 詳しくは、下記をご残照ください。
参考:[iOS] 位置情報の取得

6 OpenGL

OpenGLには、ES1とES2がありますが、今回のような要件であれば、ES1で十分だと思います。

  • kEAGLRenderingAPIOpenGLES1(固定機能を使用したOpenGL ES 1.1)
  • kEAGLRenderingAPIOpenGLES2(自分で機能を全て記述するタイプの OpenGL ES 2.0)

OpenGLの基本的な使用方法は、専門に譲るとして、ここでは、ARオブジェクトを置く方法について紹介します。

ARオブジェクトを指定の位置に置く際は、glPushMatrix()glPopMatrix()で座標系のスタックを行い、中心からの変化分angle(角度)、distance(距離)などを指定して記述すると簡単でしょう。

- (void) rectangle:(float)angle :(float)distance
{
    // 現在の行列を保存する
    glPushMatrix();

    // オブジェクトの位置を決定する
    glRotatef(-angle, 0, 1, 0);
    glTranslatef(0, 0, -distance);

    // 頂点座標を登録
    GLfloat left = -0.5f;
    GLfloat right = 0.5f;
    GLfloat top = -0.5f;
    GLfloat bottom = 0.5f;
    GLfloat squareVertices[] = {
        left, bottom,
        right, bottom,
        left, top,
        right, top,
    };
    glVertexPointer(2, GL_FLOAT, 0, squareVertices);

    // 頂点色を設定(半透明の白色)
    const GLubyte squareColors[] = {
        255, 255, 255, 155,
        255, 255, 255, 155,
        255, 255, 255, 155,
        255, 255, 255, 155,
    };
    glColorPointer(4, GL_UNSIGNED_BYTE, 0, squareColors);
    glEnableClientState(GL_COLOR_ARRAY);
    // 描画する
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    // 以前の行列に戻す
    glPopMatrix();
}

7 描画の更新

CADisplayLinkを使用すると描画更新のタイミングをトリガーにしたイベント実行が可能になります。 drawView:の中で、OpenGLの描画メソッドを記述することで、リアルタイムな更新が可能になります。

- (void)startAnimation
{
    displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawView:)];
    [displayLink setFrameInterval:1];
    [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}

- (void)stopAnimation
{
    [displayLink invalidate], displayLink = nil;
}

8 方位角

自分と対象物の経緯度から、表示する方位角を計算することが出来ます。

- (float) angle :(CLLocationCoordinate2D) coordinate1: (CLLocationCoordinate2D) coordinate2
{
    float longitudinalDiff = coordinate2.longitude - coordinate1.longitude;
    float latitudinalDiff  = coordinate2.latitude - coordinate1.latitude;
    float azimuth = (M_PI * .5f) - atan(latitudinalDiff / longitudinalDiff);
    azimuth *= 360 / (M_PI*2);
    if (longitudinalDiff > 0) {
        return( azimuth );
    }
    else if (longitudinalDiff < 0) {
        return azimuth + 180;
    }
    else if (latitudinalDiff < 0) {
        return 180;
    }
    return( 0.0f );
}

9 最後に

今回は、ARで位置表示するアプリの作成方法を纏めて見ました。まだ、間違っている部分も有るように思っています。何かお気づきの箇所がありましたら、ぜひ教えてやってください。

コードは下記にあります。
github [GitHub] https://github.com/furuya02/ARSample

10 参考資料


Open GL ES入門 – シリーズ –
[iOS][AR]七夕にちなんで天の川の位置を探すiOSのソースコードを公開しました!〜HAPPY BIRTHDAY!Classmethod〜 ios
iOSのカメラ機能を使う方法まとめ【13日目】
iPhoneSDKでOpenGL ESのテクスチャ画像の呼び方
OpenGL+Objective-C編
iPhone用ARアプリで使える緯度・経度を元に、方位角の求め方!!
参考 [iOS] AR(拡張現実)アプリ開発用SDK「Wikitude」のセットアップ手順
参考 [Xamarin.iOS] WikitudeでモバイルAR(拡張現実)アプリを作ってみた