この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
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(拡張現実)アプリを作ってみた