OpenGLによるARアプリ 〜いつでも札幌駅の方向を優しく教えてくれる、めそ子さんを作ってみた〜
1 はじめに
お酒を飲んでお店を出た時、どっちに向かって歩けばいいのか全く分からない、超方向音痴な私は、いつも聞いてしまいます。「札幌駅はどっち?」 そんな私のために、札幌駅の方向を優しく教えてくれる、めそ子さん作りました。
札幌駅の方向さえ分かれば、だいたい何とかなる「札幌あるある」案件です。
最近、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度、カメラを右にふる
表示された画面は次のとおりです。表面に置いた四角形が、カメラをふることで左にずれている事が確認できます。
次に、先ほどプログラムで動かした右へ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枚目は、少し右を向いて見た様子です。
この状態から、OpenGL画面の背景を透明にして、カメラの画像を表示すると次のようになります。
// レイヤー設定 CAEAGLLayer *layer = (CAEAGLLayer*)self.layer; // カメラの表示が見えるようにするため透明にする layer.opaque = NO;
先ほどと同じように、北を向いてiPhoneをまっすぐに立てて、正面を見たものと、少し右に向けて見たものです。
もう、説明は不要かも知れませんが、このようにiPhoneのコンパスと加速度センサーの値をOpenGLの画面に連動させて、背景にカメラの画像を表示すれば、AR画面となります。
表示するARの位置(経緯度)と自分の位置(経緯度)から、方向を計算して、OpenGLのレイヤに書き込めば終わりということになります。 なお、仰角については、標高差を計算すれば同じように表現することが可能ですが、距離については、OpenGLのZ軸に適当な定数を掛けて表現するしかないでしょう。
以降は、カメラの画像表示、センター値の取得、そして、自分の位置(経緯度、標高)の処理方法について、それぞれ纏めてみます。
3 カメラの画像表示
カメラの映像は、UIImagePickerControllerでsourceTypeにUIImagePickerControllerSourceTypeCameraを指定して表示できます。
考慮する事項としては、シャッターなどのコントロールを非表示にすることと、画面サイズに合わせて、サイズを調整するぐらいです。 そして、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からx、y、zといった姿勢角を取得することができます。
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] 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(拡張現実)アプリを作ってみた