Wikitude開発入門(iOS編)[PR]

GrapeCity Garage Wikitude開発入門(iOS編)[PR]

iPhone向け拡張現実アプリの開発に挑戦してみた(Wikitude活用)[PR]

2016年6月8日

モバイルARアプリ開発に初挑戦! 位置情報を含むオープンデータの「バス停位置情報」と、ARライブラリの「Wikitude」を活用したら、拡張現実に対応した有用なiOSアプリが簡単に開発できた。

デジタルアドバンテージ 一色 政彦
  • このエントリーをはてなブックマークに追加

 前回は、AR(拡張現実)の概要と仕組み、主要なARライブラリ、Wikitudeの機能概要を紹介した。連載初回ということもあり、かなり力が入ってしまった……。通常記事2本分ほどの長さがあるので恐縮だが、まだ読んでいないという人は、ぜひざっと斜め読みしていただけるとうれしい。

 今回は前回の内容を踏まえ、ARライブラリの一つであるWikitudeを使って実際の開発に踏み出してみる。今回取り組むのは、Xcodeを使ったiOS(iPhone)アプリの開発だ。……でも開発を始めるのって、お高いんでしょ?!

 いえいえ大丈夫! 安心してほしい。Wikitudeには無料のトライアルライセンスが用意されており、WebサイトでWikitudeアカウントを作れば手軽にそのライセンスを取得できるからだ。

 まずはトライアルライセンスを使って、「どんなふうに開発できるか」を、手を動かしながら試すのがオススメだ。その後、「実際に作ってみたら良いアプリができたのでリリースに向けて開発を進めたい」という状況になったら、その段階で有償ライセンスを使った本格的な開発とアプリのリリースを行えばよい。

 今回は筆者もトライアルライセンスを使ってWikitude開発に挑戦してみた。トライアルライセンスといっても機能に制限はなく、開発においては何の不自由も感じなかった。白状してしまうと、記事用サンプルのモバイルARアプリを開発したところで、それほど実用的なものはできないだろうなと当初は思っていた。しかし最終的に出来上がったサンプルアプリを自分で使ってみると、意外にも役立ちそうなものに仕上がった。記事を担当したから胡麻をするわけではないが、「モバイルARアプリにはまだまだ可能性があるかも」と感じているところだ。

 さて、前置きはこのくらいにして、iOS向けWikitude SDKの概要と開発可能なAR機能を紹介し、筆者が作ったサンプルアプリの概要とロケーションベースのARアプリの開発方法を説明していこう。なお本稿の目的・趣旨として、

  • 前半: 「WikitudeではどのようなAR機能を開発できるのか」をできるだけ分かりやすく示す
  • 後半: モバイルAR開発に関心がある方に向けて「Wikitude開発にはどのような特徴があるのか」というポイント、開発内容のエッセンスをできるだけシンプルに伝える

といったことを目指している。よってチュートリアルのような開発手順の説明はしないのであらかじめご了承いただきたい。

1. Wikitude SDK(JavaScript API)のアーキテクチャと、開発可能な機能

 Wikitude SDKの概要と機能一覧については、前回も簡単に紹介している。今回はiOS向けのSDKを用いて、SDKの中身とそれにより実現できる機能について、より具体的に説明しよう。

1.1 Wikitude SDKの種類と選択指針

 Wikitude SDKのダウンロードページを開くと(アカウントの作成とサイトへのログインが必要)、以下のような3種類のSDKが提供されている。

  • Wikitude SDK(JavaScript API) 標準。モバイルAR用の「Android版」と「iOS版」、スマートグラスAR用の「Epson版」と「Vuzix版」
  • Wikitude SDK(ネイティブAPI) モバイルAR用の「AndroidネイティブAPI版」と「iOSネイティブAPI版」
  • 拡張機能 モバイルAR用の「Cordovaプラグイン版」「Xamarinコンポーネント版」「Titaniumモジュール版」「Unity3Dプラグイン版」

 前回も「JavaScript APIを推奨」と書いたが、まずはJavaScript APIを使って開発してみて、それだと実現できないことなどが見えてきた段階で、ネイティブAPIを使えばよいだろう。JavaScript APIなら、自由にデザインしやすく、使い慣れたHTML+CSS+JavaScriptAR体験を構築できるのがメリットである。

 ただし、もちろんネイティブAPIが必須のケースもある。例えば「3Dモデルの認識とトラッキング」(ベータ機能)は、ネイティブAPI用のSDKにしかサンプルが含まれていないので、これを利用したい場合はネイティブAPIを選択する必要があるといった具合だ。

1.2 Wikitude SDK(JavaScript API)のアーキテクチャ(iOS編)

 本稿では、Wikitude SDK(JavaScript API)に絞って説明する。その構成内容・アーキテクチャを図1に示す。

図1 iOS版Wikitude SDK(JavaScript API)のアーキテクチャ

 ポイントは、Wikitude SDK(JavaScript API)には下記の2つの構成要素が内包されているということだ。JavaScript APIオンリーではない。

  • JavaScript API: Wikitudeが提供するAR世界を表現するオブジェクトである「ARchitect World」(=ユーザー視点では「AR体験」を意味する)を構築するためのJavaScript用APIで、「SDKの本体部分」といえる。iOSとAndroidで共通。
  • iOS Architect SDK API: プラットフォーム固有のビューコンポーネントである「ARchitectView」(=ユーザー視点では「UIのビュー」を意味する)を構築するためのiOSネイティブAPIで、「SDKのエンジン部分」といえる。

 図1のそれ以外の項目は、次節で示す動画を視聴すると理解できるので、ここでは説明を割愛する。特に分かりにくいものだけ、ヒントを書いておこう。方向インジケーターは、対象地点を指示する▲マークの矢印のことである。また、「ADE.js」(ARchitect Desktop Environment)は、デスクトップブラウザーでARオブジェクトの内容を確認したり、手動で何らかの操作を実行したりできる、ARchitect World専用のデバッグツールである。ADEは次回詳しく説明する。

 この図には示していないが、SDK機能を拡張するためのプラグインをC++やJavaで開発するためのPlugins APIもある。これを使えば、OpenCVのようなサードパーティ製のライブラリの機能を取り入れ、例えば顔認識やバーコード認識など、より高度な機能を独自に追加実装できる。

1.3 Wikitude SDKで開発可能な機能(SDKサンプルの実行)

 Wikitude SDKの機能を一通り網羅した多数のサンプルが、SDKパッケージに同梱されている(表1)。次の動画では、これらのサンプルを動かしながらWikitude SDKの機能を紹介している。24分ほどあるので少し長いが、自分で試すよりは時間節約になるのでぜひ視聴してみてほしい。

動画1 Wikitude SDKのサンプルをデモ(Examples Demo)[日本語]

 表1は、SDKが提供する全てのサンプルの一覧である。内容を理解しやすいように、日本語訳の内容と前回の「表1 Wikitude SDK(JavaScript API)の機能一覧表」で示した機能名も併記している。ちなみに、表中のPOI(Point of Interest)とは、「誰かが便利、あるいは興味のある所と思った特定の場所」のことで、略すと「対象地点」のような意味で使われている。

サンプル名 意味・内容 試せる機能
●CLIENT RECOGNITION クライアント認識
Image on Target ターゲットの認識 [ビジョン]オフラインでのマーカーレス2D画像の認識&トラッキング
Multiple Targets 複数ターゲットの認識
Extended Tracking 拡張追跡 [ビジョン]拡張トラッキング
Interactivity 拡張オブジェクトへのイベント追加
Html Drawable HTMLコンテンツの拡張 [ARオブジェクト]HTMLコンテンツの拡張
Bonus: Sparkles スプライトシートアニメーションの追加 [ARオブジェクト]スプライトシートアニメーション(=全てのキーフレーム画像を含むスプライトシートを用いたアニメーション)
Bonus: Distance to target ターゲットまでの距離測定と距離更新時の応答 [ビジョン]物理ターゲットへの距離
●CLOUD RECOGNITION クラウド認識
Basic Recognition On: Click 基本的な認識方法と随時認識モード [ビジョン]クラウドでのマーカーレス2D画像の認識&トラッキング
Continuous Recognition vs On: Click 連続認識モードと随時認識モードの比較
Using Response MetaInformation 応答に含まれるメタデータ(例:ワインに関する情報)の使用方法 [ARオブジェクト]基本的な拡張(テキスト、画像、ボタン)
●3D MODELS 3Dモデル
3D Model on Target 3Dモデルの拡張 [ARオブジェクト]3Dモデルの拡張
Appearing Animation アニメーションの追加 [ARオブジェクト]プロパティアニメーション(=開始値、終了値、持続時間の設定に基づき拡張ARオブジェクトのプロパティを連続的に変化させて実現するアニメーション)
Interactivity 3D拡張オブジェクトへのイベント追加
Snap to Screen 3D拡張オブジェクトの固定表示 [拡張機能]スクリーンへのスナップ(=ARchitect Worldから切り離してカメラビューに固定表示すること)
Animated Model Parts 3Dアニメーションの再生 [ARオブジェクト]3Dモデルアニメーション
3D Model at GeoLocation 特定ロケーションへの3Dモデル拡張
●POINT OF INTEREST (POI) POIデータ
POI at Location 特定のロケーションへのマーカー表示 [ロケーション]ジオロケーション
POI with Label 特定のロケーションへのラベル付きマーカー表示
Multiple POIs 複数のロケーションへのマーカー表示とマーカーへのイベント追加
Selecting POIs マーカーへのアニメーション追加と方向インジケーターの表示
●OBTAIN POI DATA POIデータの取得
From Application Model ネイティブコードから
From a Local Resource ローカルリソースから
From a Webservice Webサービスから
●BROWSING POIS POIデータの表示
Presenting Details POIデータの詳細情報の表示 [ロケーション]距離に基づくスケーリング
Adding Radar POIデータのレーダー表示 [ロケーション]レーダーUI要素
Limiting Range レーダーに表示するPOIデータの距離による表示制限
Reloading Content マーカーのリロード
Native Detail Screen POIデータの詳細情報をアクティビティに表示
Bonus: Capture Screen Bonus スクリーンショットの取得 [拡張機能]スクリーンショット
●VIDEO DRAWABLES 動画の描画
Simple Video 基本的な動画の拡張方法 [ARオブジェクト]動画の拡張
Playback States 動画再生の制御方法
Snapping Video 動画拡張オブジェクトの固定表示
Bonus: Transparent Video 透過動画の拡張方法 [ARオブジェクト]動画の透過処理
●HARDWARE CONTROL ハードウェア制御
Front Camera フロントカメラの使用方法 [拡張機能]外/内のカメラ制御
Camera Switching カメラの切り替え方法
Camera Control 高度なカメラ機能の使用方法 [拡張機能]カメラズーム、フラッシュライト制御
●PLUGINS API プラグインAPI
QR & Barcode QRコードおよびバーコード読み取りプラグイン [拡張機能]プラグインAPI
Face Detection 顔検出プラグイン
●DEMO デモ
2D Tracking And Geo ビジョンベース(画像認識型)ARとロケーションベース(位置情報型)ARの統合
Solar System (Geo) 太陽系(ロケーションベースAR) [ロケーション]相対位置
Solar System (2D Tracking) 太陽系(ビジョンベースAR)
表1 Wikitude SDK(JavaScript API)に同梱されているサンプルの一覧表

「試せる機能」列には、前回の「表1 Wikitude SDK(JavaScript API)の機能一覧表」で示した機能名を記載している(前述の「3Dモデルの認識とトラッキング」機能に加えて、ActionRangeクラスで実現できる「[ロケーション]ジオフェンス」機能が含まれていないので注意されたい)。上から順にサンプル実行をしていくことを想定し、各機能は初出の1回のみ記載した。[ ]で記載されているのはカテゴリで、前回の機能一覧表では以下のように表記していたものを指す。
[ビジョン]: ビジョンベース(画像認識型)AR
[ロケーション]: ロケーションベースAR
[ARオブジェクト]: ARオブジェクト
[拡張機能]: 拡張機能
[プラグイン]: モバイル開発プラットフォーム

1.4 Wikitude開発に役立つドキュメント類(iOS編)

 Wikitude SDKの構成や機能について簡単に紹介した。より詳しい情報を知りたい方のために、SDK開発用の各種ドキュメントについて箇条書きで示しておく(ヘルプドキュメントはSDKにも同梱されており、下記のフォルダー内に存在する)。

○ Web上でのみ提供されている公式チュートリアル
  • Wikitude SDK iOS ドキュメント iOSでWikitude開発を始めるときに参考になるチュートリアルがまとめられている。必修のコンテンツ。
○「/Reference/iOS Architect SDK API」フォルダー

 iOSネイティブ部分の開発には基本的にObjective-C言語を使う必要がある。よって公式のObjective-C開発用のAPIリファレンスも示しておこう。

○「/Reference/JavaScript API」フォルダー

 ちなみにJavaScript APIではなくiOSネイティブAPIを使う場合は、

を参照する必要がある。

 以上、iOS版Wikitude SDK(JavaScript API)を使うための基礎知識を示した。それでは実際の開発に入ろう。

2. iOS向けWikitude SDK(JavaScript API)開発のポイント

 まずは今回どのようなサンプルアプリを作ったのかを紹介する。

2.1 サンプルアプリの内容

 今回のサンプルアプリの内容は、

周囲500m以内に存在する最寄りのバス停の情報とそのバス停までの距離情報が、その地点の方向・場所にポップアップで表示される

というものだ。

 身の回りの360度をぐるりとカメラでかざしながら適切なバス停を直観的に探せる。一番近い距離のバス停に向かって歩き出せば距離の値がどんどん小さくなっていくので、近づいているか離れているかがARで見える化されるというわけだ(今回のサンプルはあくまで開発方法を示すことが目的なので必要以上の機能は盛り込んでいない。例えばさらにバス停の時刻表データと組み合わせるなどすれば、さらに実用的なものにできるだろう)。動画2は、そのアプリを実際に使っているときの画面例である。

動画2 サンプルアプリの実行例

 このサンプルを開発するための技術選択ポイントは下記の2点となる。

  • iOS向けWikitude SDK(JavaScript API)を使用し、ロケーションベースのARを採用する
  • オープンデータの「バス停の位置情報」を活用する(データはJSON形式のローカルリソースとして配置する)

 実際の開発内容を説明する前に、その開発環境を準備しておこう。

2.2 開発環境の準備

 iOS開発なので、Xcode(つまりMac)を使用する。また、開発言語はObjective-Cを用いる(現時点ではまだSwiftのサンプルやヘルプが提供されていないためだ)。繰り返しになるが本サンプルでのWikitudeライセンスは、誰もが手軽にまねられるように無償のトライアル版を使う。

 手順を示すと長くなってしまうので、以下ではWikitude+Xcode環境を構築するためのポイントのみを示す。なお、Xcodeは先にインストールしておいてほしい(Xcode環境の構築方法についての説明は割愛する)。

Wikitude SDKのダウンロード

 下記のリンク先にアクセスして[iOS]用のWikitude SDKをダウンロードする。なお初回アクセス時は、アカウント作成(無料)が促されるので、流れに沿って作成する。

無償トライアルライセンスキーの入手

 下記のリンク先の説明に従って無償のライセンスキーを取得する。

図2 無償のトライアルライセンスキーをダウンロードできるページ

2.3 Wikitudeアプリ開発のひな型コードの作成

 次に、XcodeでiOSアプリのプロジェクトを作成する。

 今回の[Project template]は「Single View Application」を選択した。プロジェクト作成時に表示されるウィザードでは、図3のように指定した。

図3 Xcodeでプロジェクトを新規作成する際のオプション指定例

 あとは、このプロジェクトにWikitude SDKを追加する。この手順は公式サイト(下記のリンク先)で分かりやすく説明されているので、そちらを参考にしてほしい。注意点として、説明の中で「.dylib」ファイルが登場するが、最新のXcodeでは「.dylib」ファイルの代わりに「.tbd」というテキストベースのスタブライブラリをプロジェクトに組み込むように変更されており、これを使う必要がある。

 この手順に従うと、図4のようなフォルダー&ファイル構成になったはずだ。

図4 ひな型コードのフォルダー&ファイル構成
図4 ひな型コードのフォルダー&ファイル構成

 下記の2つのフォルダーが存在するが、これらがWikitude SDK関連のものになる。前掲の図1で示したアーキテクチャを思いだしてほしい。それがこのフォルダー構成とマッチしていることが分かるだろう。

  • ArchitectWorldフォルダー: JavaScript APIを用いたARchitect World。最初、中身は空になっている。JavaScript APIを使用した部分は、このフォルダー内で作り込んでいく
  • Frameworksフォルダー: iOS Architect SDK API

 ARchitect WorldをロードするビューコンポーネントであるARchitectViewは、ARビュー画面に対応するViewController内で使われている。参考までに、ARchitect Worldをロードしている箇所のコードを抜き出して掲載しておく。

Objective-C
/**
* [iOSビューイベント]ViewControllerのビューが(主にNibファイルから)ロードされた後に呼び出されます(初回に一度)。ここに「追加のセットアップ処理」などを実装してください。
*/
- (void)viewDidLoad {
  [super viewDidLoad];
  ……中略……

    // ARchitect Worldは、WTArchitectViewのレンダリングから独立してロードする必要があります。ロードしたARchitect WorldはarchitectWorldNavigationプロパティに指定します。
    // 注意: architectWorldNavigationプロパティはこの時点で割り当てられます。ナビゲーション(=Architect World URL)オブジェクトは、他のARchitect Worldがロードされるまで有効です。
    // ここでは-loadArchitectWorldFromURL:withRequiredFeatures:メソッドに、
    // リソースファイル名「index」+拡張子「html」+フォルダーへの相対パス「ArchitectWorld」から取得したファイルURLを指定し、
    // さらに必要な機能(WTFeature_Geo | WTFeature_2DTracking | WTFeature_3DTracking)を指定することで、ARchitect Worldをロードしています。
    self.architectWorldNavigation = [self.architectView loadArchitectWorldFromURL:[[NSBundle mainBundle] URLForResource:@"index" withExtension:@"html" subdirectory:@"ArchitectWorld"] withRequiredFeatures:WTFeature_Geo | WTFeature_2DTracking];

  ……中略……
}
リスト2 ARchitect Worldをロードしている箇所のコード(抜粋)

 iOS Architect SDK APIを用いたViewControllerの開発は、ちょっとしたパラメーター調整などはあるものの、ほとんどの場合では定型のコードとなるので、初学者はざっと流れを理解しておくだけで十分だ。本稿では解説を割愛するが、流れを理解する助けになるように、本稿のサンプルアプリのコードには、しつこいくらい詳しい説明(日本語)をコメントとして入れているので、そちらを参照してほしい。

 筆者がサンプル開発を試したかぎりでは、Wikitude公式のチュートリアルやサンプルに含まれるコードには、非推奨(deprecated)になったAPIが使われていたり、iOS 8以降で位置情報を使うにはInfo.plistファイルに<key>NSLocationWhenInUseUsageDescription</key> <string>Accessing GPS information is needed to display POIs around your current location</string>というコードの追記が必要であったり、いくつか微調整する必要があった。

 deprecatedなAPIは、上記リンク先のサンプルコードではiOSバージョンを見て条件分岐する処理を追加している。また、iOSアプリに対する位置情報をユーザーが許可するようになっていないと、ロケーションベースのARが実現できないので注意してほしい。筆者はこの問題に気付かず半日ほどムダな時間を過ごしたので、ARchitect Worldが動かないときはこれが原因かもと疑ってみてほしい。

2.4 ARchitect Worldのひな型コードをSDKサンプルからコピー

 ここまで来たら、あとはARchitect WorldをHTML+CSS+JavaScriptで自由に実装していくだけだ。「腕の見せ所」というわけだが、開発に慣れないうちは、前半で紹介したSDKサンプルのさまざまなARchitect Worldの中から選んでひな型コードとして使ってみると、開発を始めるのが楽になる。

 本稿のサンプルの開発では最初、複数のバス停情報をポップアップ表示するので、Wikitude SDKに含まれる「/Examples/SDKExamples/ARchitectExamples/4_PointOfInterest_3_MultiplePois」のフォルダーとファイルを先述の「ARchitectWorld」フォルダーの中に丸々コピーして、それをカスタマイズする形で開発を始めた。

2.5 ARchitect Worldの実装内容

 以下では、ARchitect Worldの実装内容がどうなっているか、コードを掲載しよう。なお、これらのコードにも詳細な説明をコメントとして入れているので、それを理解のヒントにしてほしい。

UIの実装内容

 まずはARchitect WorldのUI部分である/ArchitectWorld/index.htmlファイルの内容だ。

HTML
<!DOCTYPE html>
<html>

<head>

  <!--  基本的なメタ情報 -->
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <meta http-equiv="Content-Type" content="application/json; charset=utf-8">
  <meta content="width=device-width,initial-scale=1,maximum-scale=5,user-scalable=yes" name="viewport">

  <title>My ARchitect World</title>

  <!-- 「architect://」プロトコルは「Wikitude World Browser」内でWikitude標準ライブラリのインポートに使われます。それ以外では内部的にJavaScriptエラーになって無視されています。 -->
  <script src="architect://architect.js"></script>
  <!-- ADE(ARchitect Desktop Environment)ライブラリをインポートしてデスクトップブラウザーで実行すれば、緯度・経度などの挙動をシミュレーションした上でARchitectオブジェクトの内容を確認できます。 -->
  <script src="js/ade.js"></script>

  <!-- jQuery MobileのCSS ファイルをインポートします。 -->
  <link rel="stylesheet" href="jquery/jquery.mobile-1.3.2.min.css" />
  <!-- 背景を透明にするのと、「クリックスルー」を有効にするのに必要です。 -->
  <link rel="stylesheet" href="jquery/jquery-mobile-transparent-ui-overlay.css" />

  <!-- jQuery JS ファイル -->
  <script src="jquery/jquery-1.9.1.min.js"></script>
  <script src="jquery/jquery.mobile-1.3.2.min.js"></script>

  <!-- JSON形式のPOIデータをインポートします。「myJsonData」オブジェクトからアクセスできます。 -->
  <script src="js/myjsondata.js"></script>

  <!-- マーカー(=ポップアップするバルーンUI)機能をインポートします。 -->
  <script src="js/marker.js"></script>

  <!-- ARchitect Worldのメインロジックをインポートします。 -->
  <script src="js/mainlogic.js"></script>

</head>

<body>

   <div data-role="page" id="page1" style="background: none;" >
      
      <!-- メインページのコンテンツ -->

      <!-- 下部中央の透明なフッター -->
      <div data-role="footer" class="ui-bar" data-theme="f" data-position="fixed" style="text-align:center;">

        <!-- 小さなアイコンボタンを表示します。 -->
        <a style="text-align:right;" id="popupInfoButton" href="#popupInfo" data-rel="popup" data-role="button" class="ui-icon-alt" data-inline="true" data-transition="pop" data-icon="alert" data-theme="e" data-iconpos="notext">Log</a> </p>

        <!-- ボタンがクリックされるとポップアップが表示されます。 -->
        <div data-role="popup" id="popupInfo" class="ui-content" data-theme="e" style="max-width:350px;">
          <p style="text-align:right;" id="status-message">現在地を探っています...</p>
        </div>
        
      </div>
      

    </div>

  </body>

</html>
リスト2 ARchitect WorldのUIの実装内容(/ArchitectWorld/index.html)

 特に難しいところもないだろう。<script src="js/mainlogic.js"></script>というコードはメインロジック、<script src="js/myjsondata.js"></script>はデータ、<script src="js/marker.js"></script>はポップアップ表示用のUI部品(=ARオブジェクト)*1で、それぞれARchitect World内で使えるようにここでインポートされている。

  • *1 このサンプルでは、ポップアップ表示されるバルーン型UI部品を「マーカー(Marker)」と表現している。ちなみに前回、ターゲット認識の手法として「マーカー」と「マーカーレス」という用語を説明したが、これを意味するわけではないので注意してほしい(紛らわしいが、WikitudeのサンプルではこのタイプのUI部品を「マーカー」と呼んでいるケースが多々ある……)。

 jQueryやjQuery Mobileなどの一般的なJavaScriptライブラリもインポートして使用できる。CSSのスタイルシートでUIデザインを柔軟にカスタマイズすることもできる。本稿では使っていないが、レーダーUI要素などを独自のデザインに変更するのも簡単だ。このような柔軟さが、Wikitudeの売りの一つとなっている。

 <script src="architect://architect.js"></script><script src="js/ade.js"></script>などのコードは、記載しなくても今回のアプリを実行する上で影響はない。これらの機能内容については、もう少し詳しい説明が必要になるので次回あらためて説明する。

メインロジックの実装内容

 次に、ARchitect Worldのロジック部分である/ArchitectWorld/js/mainlogic.jsファイルの内容である。

JavaScript
// ARchitect World(=AR体験)の実装
var World = {
  // データロードを1回のみにするためのフラグ。
  initiallyLoadedData: false,
  
  // ロケーション情報を変更中かを判定するフラグ。
  changingLocationDisplay: false,
 
  // さまざまなPOIのMarkerのアセット(assets)。
  markerDrawable_idle: null,
  markerDrawable_selected: null,
 
  // ARchitect World内に表示されているAR.GeoObjectリスト。
  markerList: [],
 
  // 最後に選択されたマーカー。
  currentMarker: null,
 
  // 新しいPOIデータを注入するために呼ばれる関数。
  loadPoisFromJsonData: function (poiData) {
 
    AR.context.destroyAll();
    
    // マーカーのアセットをイメージリソースとしてロードします。
    World.markerDrawable_idle = new AR.ImageResource("assets/marker_idle.png");
    World.markerDrawable_selected = new AR.ImageResource("assets/marker_selected.png");
    
    // 見えるマーカーのリストを空にします。
    World.markerList = [];
 
    // 全てのPOI情報をループしながら、1つのPOIごとにAR.GeoObject(=マーカー)を作成します。
    for (var currentPlaceNr = 0; currentPlaceNr < poiData.length; currentPlaceNr++) {
      var singlePoi = {
        "id":          poiData[currentPlaceNr].id,
        "latitude":    poiData[currentPlaceNr].latitude,
        "longitude":   poiData[currentPlaceNr].longitude,
        "altitude":    poiData[currentPlaceNr].altitude,
        "distance":    poiData[currentPlaceNr].distance,
        "busstopinfo": poiData[currentPlaceNr].busstopinfo
      };
 
      // ユーザーが何もないスクリーン上をタップした際に選択中のマーカーを選択解除できるようにするため、
      // 1つ1つのマーカーが含まれる配列をWorldオブジェクトに保持させます。
      World.markerList.push(new Marker(singlePoi));
    }
  },
 
  // 下部中央付近にあるフッターに、警告時=[△]/通常時[i]の小さなボタンを表示し、そのボタンクリック時にポップアップする状態メッセージを更新します。
  updateStatusMessage: function (message, isWarning) {
 
    var themeToUse = isWarning ? "e" : "c";
    var iconToUse = isWarning ? "alert" : "info";
 
    $("#status-message").html(message);
    $("#popupInfoButton").buttonMarkup({
      theme: themeToUse
    });
    $("#popupInfoButton").buttonMarkup({
      icon: iconToUse
    });
  },
 
  // ロケーションを更新します。Androidネイティブ環境でarchitectView.setLocationメソッドが呼び出されるたびに、この関数は呼び出されます。iOSではネイティブサービスのstartUpdatingLocationメソッドを呼び出すとここが呼び出されるようです(※ドキュメントなし)。
  locationChanged: function (lat, lon, alt, acc) {
 
    if (World.changingLocationDisplay) return;
    World.changingLocationDisplay = true;
    
    // World.initiallyLoadedDataフラグを確認して、初回起動時にのみPOIデータをロードする処理を実行します。
    if (!World.initiallyLoadedData) {
      // 渡された緯度(=latitude)と経度(=longitude)を指定して、requestDataFromLocal関数を呼び出し、現在地周辺のPOIデータを取得します。
      // 最後にフラグを「読み込み済み」(=true)に設定します。
      World.requestDataFromLocal(lat, lon, alt, acc);
      World.initiallyLoadedData = true;
 
    } else {
      // 対象地点からの距離情報を頻繁に更新します。
      World.updateDistanceValues(lat, lon, alt, acc);
    }
    
    // テスト表示用([i]ボタンをタップすると表示される状態メッセージをセットします)。
    World.updateStatusMessage(World.markerList.length + "カ所、緯度・経度:" + lat + ", " + lon);
    //alert("緯度・経度:" + lat + ", " + lon);
 
    World.changingLocationDisplay = false;
  },
 
  // カメラビュー内でユーザーがマーカーを押した時に呼び出されます。
  onMarkerSelected: function (marker) {
 
    // 前回選択されていたマーカーの選択を解除します。
    if (World.currentMarker) {
      if (World.currentMarker.poiData.id == marker.poiData.id) {
        return;
      }
      World.currentMarker.setDeselected(World.currentMarker);
    }
 
    // 今回のマーカーを選択してハイライトします。
    marker.setSelected(marker);
    World.currentMarker = marker;
  },
 
  // ロケーションARオブジェクト以外のスクリーンがクリッックされた時に呼び出されます。
  onScreenClick: function () {
    if (World.currentMarker) {
      World.currentMarker.setDeselected(World.currentMarker);
    }
  },
 
  // 指定された地点における全てのPOIデータをロードします。
  requestDataFromLocal: function (centerPointLatitude, centerPointLongitude, centerPointAltitude, centerPointAccuracy) {
 
    // 念のためValidate(値検証)しています。
    if ("number" !== typeof centerPointLatitude || centerPointLatitude < -90) centerPointLatitude = -90;
    if ("number" !== typeof centerPointLatitude || centerPointLatitude > 90) centerPointLatitude = 90;
    if ("number" !== typeof centerPointLongitude || centerPointLongitude < -180) centerPointLongitude = -180;
    if ("number" !== typeof centerPointLongitude || centerPointLongitude > 180) centerPointLongitude = 180;
    //if ("number" !== typeof centerPointAltitude || centerPointAccuracy <= 0) centerPointAccuracy = 0;
    
    var poiData = [];
 
    // myJsonDataオブジェクトは、myjsondata.jsに定義しておいたバス停のPOIデータです。
    for (var i = 0, length = myJsonData.length; i < length; i++) {
      var distance = World.getDistance(myJsonData[i].latitude, centerPointLatitude, myJsonData[i].longitude, centerPointLongitude);
      if (distance > 500.0) continue;  // 0.5km(=500m)以上先のPOIデータは破棄します。
      var distanceString = (distance > 999) ? ((distance / 1000).toFixed(2) + " km") : (Math.round(distance) + " m");
      
      poiData.push({
        "id":          (myJsonData[i].id),
        "latitude":    (myJsonData[i].latitude),        // 緯度
        "longitude":   (myJsonData[i].longitude),       // 経度
        "altitude":    parseFloat(centerPointAltitude), // 現在地の高度に合わせています。ちなみに標高の平均といえる「日本水準原点」の値は「24.3900」です。
        "distance":    (distanceString),                // 現在の地点からの距離(単位は「km」もしくは「m」)
        "busstopinfo": ("[" + myJsonData[i].busstopname + "]" + myJsonData[i].buslinename) // 「[バス停名]バス路線名」の形式で文字列化しています。
      });
    }
 
    World.loadPoisFromJsonData(poiData);
  },
  
  // 表示されているARオブジェクトの距離表示を更新します。
  updateDistanceValues: function (centerPointLatitude, centerPointLongitude, centerPointAltitude, centerPointAccuracy) {
    
    for (var i = 0; i < World.markerList.length; i++) {
      
      var distance = World.getDistance(World.markerList[i].poiData.latitude, centerPointLatitude, World.markerList[i].poiData.longitude, centerPointLongitude);
      if (distance > 500.0) {
        // 既存のマーカーの中から0.5km(=500m)以上離れたPOIデータが出てきた場合は、全部をリロードし直します。
        World.requestDataFromLocal(centerPointLatitude, centerPointLongitude, centerPointAltitude, centerPointAccuracy);
        return;
      }
      var distanceString = (distance > 999) ? ((distance / 1000).toFixed(2) + " km") : (Math.round(distance) + " m");
      World.markerList[i].distanceLabel.text = distanceString;
    }
  },
 
  getDistance: function (targetLatitude, centerPointLatitude, targetLongtitude, centerPointLongitude) {
    // 参考:http://www.movable-type.co.uk/scripts/latlong.html
    var Δφ = (centerPointLatitude - targetLatitude) * Math.PI / 180;
    var Δλ = (centerPointLongitude - targetLongtitude) * Math.PI / 180;
    var a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + Math.cos(targetLatitude * Math.PI / 180) * Math.cos(centerPointLatitude * Math.PI / 180) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
    var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    return 6371e3 * c
  },
 
};
 
/*
  ロケーションが変更された時の処理を実行する関数をセットします。
*/
AR.context.onLocationChanged = World.locationChanged;
 
/*
  (描画物をよけて)スクリーンがクリックされた時の処理を実行する関数をセットします。選択中のマーカーを非選択状態にするなどの処理が考えられます。
*/
AR.context.onScreenClick = World.onScreenClick;
リスト3 ARchitect Worldのメインロジックの実装内容(/ArchitectWorld/js/mainlogic.js)

※2016/07/02修正:コードの一部に不具合がありました。お詫びして訂正させていただきます。

 AR.ImageResourceAR.contextというコードを見ると気付くように、WikitudeのJavaScript APIでは、ARモジュールが全てのクラスにアクセスするための窓口になっている。また、Worldオブジェクトの中に全ての処理ロジックを実装していくのが定石となっている。

 「ロケーション変更のイベント通知」/「何もないスクリーン部分がクリックされた時のイベント通知」を受けて何らかの処理をしたいという場合は、AR.context.onLocationChangedAR.context.onScreenClickを使えばよい。今回は、現在地の情報に基づきポップアップ表示を行うロケーションベースARなので、このイベント通知に指定されているWorld.locationChanged関数が特に重要だ。これを起点にコードを追いかければ、処理ロジックの流れが見えてくるだろう。

 ポップアップするバス停データは、myJsonDataオブジェクトを参照しているだけだ。これはローカルリソースとして、次に説明する「位置情報データ」のファイル内で定義されている。

位置情報データの内容

 位置情報データである/ArchitectWorld/js/myjsondata.jsファイルの内容を示す。

JavaScript
// JSONデータの見だし: id, latitude(緯度), longitude(経度), busstopname(バス停名), buslinename(バス路線名)
// なお、altitude(高度)の情報はありません。
// 以下は、JSON文字列ではなく厳密にはJavaScriptオブジェクトリテラルで記述されているが、
// myJsonDataオブジェクトを使う側はデータオブジェクト化手法の違いは意識せずに「JSONデータ」として扱えるようにしている。

// 日本における1km離れた地点間の緯度・経度の差は約0.01度程度になるので、便宜上、ある地点(東京駅、池袋駅)の周囲3kmとして、その前後の0.03度の範囲のみに絞ってデータを軽くしています。
// 参考1:東京駅の緯度「35.681368」、経度「139.766076」
// 参考2:池袋駅の緯度「35.72888」、経度「139.7081588」
// このファイルには東京の全てのバス停データが含まれています。

var myJsonData = [
  { "id": "n24", "latitude": 35.67323691, "longitude": 139.76207717, "busstopname": "数寄屋橋", "buslinename": "都03|都04|都05|銀座・汐留エリア~成田空港線" },
  { "id": "n35", "latitude": 35.67918695, "longitude": 139.76834472, "busstopname": "東京駅八重洲口", "buslinename": "東京~松江・出雲線" },
  { "id": "n44", "latitude": 35.67865750, "longitude": 139.77312083, "busstopname": "東京駅八重洲通り", "buslinename": "ままかりライナー 倉敷・岡山~東京駅八重洲通り線" },
  { "id": "n45", "latitude": 35.67942639, "longitude": 139.76567278, "busstopname": "東京営業所", "buslinename": "ままかりライナー 倉敷・岡山~東京駅八重洲通り線" },
  ……中略……
];
リスト4 位置情報データの内容(/ArchitectWorld/js/myjsondata.js)

 見ての通り、JSON形式のデータとしてmyJsonDataオブジェクトが作成されている。データはこのようなローカルリソース以外にも、クラウドなどにホストしたWeb APIを使う方法なども考えられる。次回はWeb API経由でデータを扱ってみる。

 このデータは、前回の「3.1 位置情報が含まれる主要なデータ」で示したオープンデータの「国土数値情報 バス停留所」を使った。JSONデータへの加工は、独自のプログラムを作って行った(C#のプログラムになるが、関心がある方はGitHubのページを参照してほしい)。

ARオブジェクト(UI部品)の実装内容

 最後に、ARオブジェクト(UI部品)が定義・実装されている/ArchitectWorld/js/marker.jsファイルの内容を参考までに掲載する。ここで実装されているMarker関数は、mainlogic.jsファイル内のnew Marker(singlePoi)というコードで使われている。

JavaScript
function Marker(poiData) {

  /*
    マーカー(=ポップアップするバルーンUI)を作成するには、
    ジオロケーション(=地球上の三次元空間座標)に結び付けられた新しいAR.GeoObjectオブジェクト(=ロケーションベースのARオブジェクト)を作成します。
    このAR.GeoObjectは、必ず1つ以上のAR.GeoLocation(=ロケーション)と、複数の関連付けられたAR.Drawable(=描画物)が必要です。
    AR.Drawablesは、カメラ(cam)や、レーダー(radar)、方向インジケーター(indicator)といったターゲットに対して定義できます。
  */

  this.poiData = poiData;

  // POIデータ(緯度=latitude、経度=longitude、高度=altitude)からマーカーロケーション(=AR.GeoLocationオブジェクト)を作成します。
  var markerLocation = new AR.GeoLocation(poiData.latitude, poiData.longitude, poiData.altitude);

  // アイドル状態時のイメージリソースと、高さ(2.5)、各種オプションを指定して、マーカー用のAR.ImageDrawable(=画像の描画物)を作成します。
  this.markerDrawable_idle = new AR.ImageDrawable(World.markerDrawable_idle, 2.5, {
    zOrder: 0,
    opacity: 1.0,
    /*
      ユーザーの操作を受け付けるには、それぞれのAR.DrawableでonClickプロパティに関数をセットしてください。
      この関数は、ユーザーが描画物をタップするたびに呼ばれます。この例では、本ファイル内に定義された下記のヘルパー関数を指定しています。
      クリックされたマーカーは、引数としてこの関数に渡されています。
    */
    onClick: Marker.prototype.getOnClickTrigger(this)
    // オプションは数が多いので割愛。こちらを参照: http://docs.grapecity.com/help/wikitude/wikitude-sdk-js-api-reference/classes/ImageDrawable.html
  });

  // 選択状態のマーカー用のAR.ImageDrawableを作成します。
  this.markerDrawable_selected = new AR.ImageDrawable(World.markerDrawable_selected, 2.5, {
    zOrder: 0,
    opacity: 0.0,
    onClick: null
  });

  // マーカーの距離表示用のAR.Label(=ラベルの描画物)を作成します。
  this.distanceLabel = new AR.Label(poiData.distance.trunc(10), 1, {
    zOrder: 1,
    offsetY: 0.55,
    style: {
      textColor: '#FFFFFF',
      fontStyle: AR.CONST.FONT_STYLE.BOLD
    }
    // オプションは数が多いので割愛。こちらを参照: http://docs.grapecity.com/help/wikitude/wikitude-sdk-js-api-reference/classes/Label.html
  });

  // マーカーのバス停情報表示用のAR.Labelを作成します。
  this.busstopinfoLabel = new AR.Label(poiData.busstopinfo.trunc(15), 0.8, {
    zOrder: 1,
    offsetY: -0.55,
    style: {
      textColor: '#FFFFFF'
    }
  });

  // 1つ以上のマーカーロケーションと、複数の描画物を指定して、AR.GeoObjectを作成します。
  this.markerObject = new AR.GeoObject(markerLocation, {
    drawables: {
      cam: [this.markerDrawable_idle, this.markerDrawable_selected, this.distanceLabel, this.busstopinfoLabel]
    }
    /*
      ここではdrawablesオプションを指定していますが、指定可能なオプションは以下のとおりです。
       ・enabled: Boolean型(デフォルト値: true)。有効/無効を指定します。
       ・renderingOrder: Number型(デフォルト値: 0) 。描画順序を指定します。
       ・onEnterFieldOfVision:AR.GeoObjectが表示開始された時の処理を実施する関数を指定します。
       ・onExitFieldOfVision: AR.GeoObjectが表示終了する時の処理を実施する関数を指定します。
       ・onClick: ユーザークリックを処理する関数を指定します。
       ・drawables.cam: Drawable[]型。カメラビュー内の描画物を指定します。
       ・drawables.radar: Drawable2D[]型。レーダー内の描画物を指定します。
       ・drawables.indicator: Drawable2D[]型。方向インジケーター内の描画物を指定します。
    */
  });

  return this;
}

Marker.prototype.getOnClickTrigger = function(marker) {

  /*
    この関数内では、描画物の選択状態を判定して、選択状態を設定し直し、適切な処理が実行します。
  */

  return function() {

    if (marker.isSelected) {

      Marker.prototype.setDeselected(marker);

    } else {
      Marker.prototype.setSelected(marker);
      try {
        World.onMarkerSelected(marker);
      } catch (err) {
        alert(err);
      }

    }

    return true;
  };
};

Marker.prototype.setSelected = function(marker) {

  marker.isSelected = true;

  marker.markerDrawable_idle.opacity = 0.0;
  marker.markerDrawable_selected.opacity = 1.0;

  marker.markerDrawable_idle.onClick = null;
  marker.markerDrawable_selected.onClick = Marker.prototype.getOnClickTrigger(marker);
};

Marker.prototype.setDeselected = function(marker) {

  marker.isSelected = false;

  marker.markerDrawable_idle.opacity = 1.0;
  marker.markerDrawable_selected.opacity = 0.0;

  marker.markerDrawable_idle.onClick = Marker.prototype.getOnClickTrigger(marker);
  marker.markerDrawable_selected.onClick = null;
};

// Stringクラスに長すぎる文字列を短く省略するための関数を追加定義します。
String.prototype.trunc = function(n) {
  return this.substr(0, n - 1) + (this.length > n ? '...' : '');
};
リスト4 ARオブジェクトであるマーカーの実装内容(/ArchitectWorld/js/marker.js)

3. まとめ

 以上、Wikitude SDKのアーキテクチャ&提供機能と、ロケーションベースARに対応したiOSアプリの開発方法をできるだけ手短に説明した。特にSDKサンプルはかなり参考になるので、本稿のサンプルを試した後はSDKサンプルを動かし、コード内容を眺めてみてほしい。

 次回は、デスクトップブラウザーの利用(ADE)や実機デバッグといった、より実践的な開発方法について取り上げる。また、Android版のWikitude SDKのポイントや、Web APIの利用方法について解説する。

1. 拡張現実(AR)とは? モバイルARを実現するテクノロジーと開発ライブラリ[PR]

ARの概要と特徴、利用モデルを図と動画で初歩から解説。主な開発ライブラリと、その一つであるWikitudeを紹介し、AR開発で使える「位置情報データ&API」も紹介する。

2. 【現在、表示中】≫ iPhone向け拡張現実アプリの開発に挑戦してみた(Wikitude活用)[PR]

モバイルARアプリ開発に初挑戦! 位置情報を含むオープンデータの「バス停位置情報」と、ARライブラリの「Wikitude」を活用したら、拡張現実に対応した有用なiOSアプリが簡単に開発できた。

3. WikitudeでカンタンAndroid向けAR開発。ブラウザーでデバッグ可能[PR]

位置情報を含むレストラン検索の「ホットペッパーAPI」を活用したAndroid向けモバイルARアプリを作成。Wikitudeを使えば、使い慣れたテキストエディターで開発し、ブラウザーで手軽にデバッグできる。

4. AR(拡張現実)開発を始めよう! 複数ターゲットの認識と、距離・角度の把握(Wikitude活用、iOS編)[PR]

2人の相性を写真から診断するアプリを、画像認識型ARで開発。ターゲット写真を認識してARオブジェクトを表示し、2枚のターゲット間の距離と角度が一定の条件内になれば診断を開始する仕組みだ。

5. 身の回りの3D物体を認識して、そこにAR(拡張現実)オブジェクトを表示する方法(Wikitude活用、Android編)[PR]

市販のドレッシングボトルを映せばレシピが提案されるアプリを、物体認識型ARで開発。物体ターゲットを認識すると、その物体空間に合わせてARオブジェクトを表示し、それをタップするとレシピページにジャンプする。

サイトからのお知らせ

Twitterでつぶやこう!