凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Tango のアプリ開発はじめました

はじめに

Mogura VR さん経由で ASUS 社の ZenFone AR のエンジニアリングサンプル品をお借りすることが出来たので色々調べてみました。Tango についての一般的な事柄や ZenFone AR そのものについては Mogura VR さんの方に寄稿いたしましたので、併せてお読みいただけると嬉しいです。

www.moguravr.com

本エントリでは寄稿した記事の補足や向こうには書かなかったコードと紐付けた紹介、推測や妄想も交えてまとめていきたいと思います。こうしたらアプリが作れるよ、という解説ではなく、あくまで概念や世界観の理解の方がメインです。なお「はじめました」というタイトルをつけたのですが、だいぶ前に返却してしまったので、続きはまた発売後に購入出来たら書きます...。

追記(2017/06/23)

本日より発売されました!

Tango とは

https://get.google.com/tango/get.google.com

Tango は Google による Andorid デバイス上で動く AR プラットフォームです。デプスセンサや広角カメラなど幾つか特殊なデバイスが必要なため、現状は Lenovo 社による Phab 2 Pro と、この夏発売予定の ASUS 社による ZenFone AR のみが我々が手に入れられるデバイスになります。また、先日 Google I/O にて発表された WorldSense の基盤技術にもなっています。

blog.google

Tango のコアとなるコンセプトは以下の 3 つです。

Motion Tracking

バイスの位置・姿勢を 3 次元空間でトラッキングできます。いろんな方向にデバイスを動かしても、それがどこに居るのか、どこを向いているのかを把握できます。これは IMU と広角カメラによる VIO(Visual-inertial odometry)によって行われています。

Area Learning

空間の特徴量を覚えさせ ADF(Area Description File)というファイルにこの特徴量を保存・ロードすることで、デバイスがその ADF で記述された空間内のどこに居るかを把握(localization)できるようになります。また Motion Tracking で生じるドリフトの補正もできるようになります。

Depth Perception

周辺のデプスが取れます。これによって床の上にあるかのようにオブジェクトを出したり、そのオブジェクトが椅子で隠れたり、物理エンジンで動かして周囲と衝突するように出来たりします。デプスの取得方法はデバイス依存です(ZenFone AR や Phab 2 Pro は ToF、他にもパターン照射型やステレオカメラでも○)。

Google I/O で発表された GPS ならぬ VPS(Visual Positioning Service)ですが、明確な記述はないのですが恐らく Area Learning(およびそこを抽象化した機能)を利用していると思われます。

www.moguravr.com

これらをどのように実装に落とし込んでいるか、というのを見ていくと、より理解が深まったり開発の勘所につながると思いますので、本記事ではそのあたりに触れられればと思います。

Unity SDK

以下からダウンロードできます。Android SDKGoogle USB Driver をインストールして、Tango Unity SDK(.unitypackage)を Unity にインポートすれば動作します。

developers.google.com

また、ここに乗っていない Cardboard との連携やマルチプレイのサンプルなどは GitHub に上がっています。

github.com

サンプルをビルドして見ると挙動が把握できると思いますが、まずは公式の How-to Guides をやるのをオススメします。

SDK 俯瞰

SDK は以下のようなスタックになっています。

f:id:hecomi:20170507042533p:plain

下の Tango Service はスタンドアロンで動くサービスになっており、ここで Motion Tracking や Area Learning、Depth Perception を処理しています(Tango Core でしょうか?)。各アプリケーションはこのサービスに通信を行い指示したり情報を得たりしています。

たまにこのサービスが止まってしまう状態が現状ではあるようで、一度そうなったらアプリケーションをまたがって Tango の機能が使えなくなるので泣く泣く端末を再起動することが何度か有りました。

細かい所を見ていく前に

Vision Summit 2017 のキーノートにて、Google のプロダクトマネージャの Nathan Martz 氏によって、Tango の API が Unity 2017 に組み込まれる旨を発表していました。

https://visionsummit2017.com/visionsummit2017.com

blogs.unity3d.com

そのため、現状の SDK に特化したコードや Unity 上での Prefab の設定そのものは恐らくあまり役に立たなくなるのではないかと考えられます(ちなみに現状の Tango の SDK でもたくさんの deprecated なコンポーネントやそれらを使用したサンプルが含まれています)。しかしながら、ドキュメントによる説明やコードを通じて得られる知見のうちのいくつかは今後も残ると思いますので、そのあたりを汲んで読んで頂ければ...と思います。

Frames of Reference

各実装を確認する前にいくつか概念を覚えておくと理解の助けになると思います。

その一つが、Tango の座標系を理解する上で重要な Frames of Reference です。モーショントラッキングをした結果、自身がどの位置にいてどこを向いているのか、その位置と姿勢(Pose と呼ぶ)を把握するためには、どこからの位置なのか、どこからの回転なのか、そういった基準となる座標系(Coordinate Frame)が必要になります。例えばゲーム開始時の Pose を原点としたり、事前に学習させた空間の基準を原点としたり、あるいは地球上で共通のただひとつのある基準点を原点とするかもしれません。

そのため、ある瞬間の Pose を求めたいとした場合、その基準となる原点(e.g. ゲーム開始時、エリア学習の基準)と、求めたい対象(e.g. デバイス本体、各センサ)のペアが必要になります。このペアが Frames of Reference になります。そしてこのペアを指定することで Pose が求められる API が用意されています。例えば、ベースをゲーム開始時、ターゲットをデバイスとしたペアを与えて問い合わせると、ゲーム開始時を基準とした現在のデバイスの Pose を取得することが出来ます。また、タイムスタンプを渡すことで、過去の時点での Pose も求めることが出来ます。

込み入ったアプリケーションを作るのでなければ(SDK でよしなに吸収されるので)コードから触れることは余り無いかもしれませんが、具体的に Pose を取得する Unity でのコードをちら見すると、こんな感じです。

TangoCoordinateFramePair pair;
var pose = new TangoPoseData();
pair.baseFrame = TangoEnums.TangoCoordinateFrameType.TANGO_COORDINATE_FRAME_AREA_DESCRIPTION;
pair.targetFrame = TangoEnums.TangoCoordinateFrameType.TANGO_COORDINATE_FRAME_DEVICE;
PoseProvider.GetPoseAtTime(pose, timestamp, pair);

PoseProvider というクラスに求めたい Pose の基準と対象のペアを与え、タイムスタンプを指定して Pose を取得しています。

この座標系は他にも色々と用意されていて、ミクロなものだと、基準を IMU、ターゲットをデプスカメラにすることで、デバイス位置からのデプスカメラ位置を取得したりも出来ます。これは Point Cloud の描画などに使われていて、デバイスごとに異なるデプスカメラの位置を吸収するのに役立っていると思われます。具体的なコードはこんな感じです:

// TangoPointCloud.cs

double timestamp = 0.0;
TangoCoordinateFramePair pair;
TangoPoseData poseData = new TangoPoseData();

// Query the extrinsics between IMU and device frame.
pair.baseFrame = TangoEnums.TangoCoordinateFrameType.TANGO_COORDINATE_FRAME_IMU;
pair.targetFrame = TangoEnums.TangoCoordinateFrameType.TANGO_COORDINATE_FRAME_DEVICE;
PoseProvider.GetPoseAtTime(poseData, timestamp, pair);
m_imuTDevice = DMatrix4x4.FromMatrix4x4(poseData.ToMatrix4x4());

// Query the extrinsics between IMU and depth camera frame.
pair.baseFrame = TangoEnums.TangoCoordinateFrameType.TANGO_COORDINATE_FRAME_IMU;
pair.targetFrame = TangoEnums.TangoCoordinateFrameType.TANGO_COORDINATE_FRAME_CAMERA_DEPTH;
PoseProvider.GetPoseAtTime(poseData, timestamp, pair);
m_imuTDepthCamera = DMatrix4x4.FromMatrix4x4(poseData.ToMatrix4x4());

m_deviceTDepthCamera = m_imuTDevice.Inverse * m_imuTDepthCamera;

IMU 基準のデバイスのトランスフォームと、IMU 基準のデプスカメラのトランスフォームを求めておき、前者の逆数を手前から掛けて、デバイス上でのデプスカメラのトランスフォームを求めています。デプスカメラ以外にも RGB カメラやモーショントラッキングカメラのフレームもあります。これらは異なるデバイス毎のハードウェアの配置の差異を吸収するために利用されていると思われます。

逆にマクロなものだと、GPS測地系である WGS84 が基準として提供されています。実装箇所を見てみると experimental な機能として提供されている Cloud ADF(ネットワーク越しに ADF を共有?)で使われています。この他にも、WorldSense の記事にもあったように、GPS で店舗まで案内し、VPSVision Positioning Service)で店舗の中を商品の場所まで案内するという連続性のある UX を提供するために準備しているものではないかと考えられます。具体的には、店舗の近くまでは WGS84 系で案内してもらい、近くまで来たら WGS84 -> Device、事前に店舗ごとに用意した WGS84 -> Area Learning、そして保存してある商品の Area Learning -> 商品といった座標系から各 Pose を取得し、組み合わせることで、現在のデバイスの位置から(誤差はちょっと大きくなりますが)外からでも商品の位置を見通すことが出来るかもしれません。そこからシームレスに商品まで誤差を修正しながら案内もできそうです。

他にも、WGS84 -> Device の座標系を友人とネットワーク越しに共有することで、相手のいる方向や距離を把握したり、更に Area Learning された結果を持つお店の中などにいれば、事前に正確に求めておいた不変の WGS84 -> Area Learning、および誤差の少ない Area Learning -> Device という変換で、自分のより正確な位置(WGS84 -> Device)を相手と伝えあうことも出来ると考えられます。単に、同じエリアにいる他の端末と情報を共有するのであれば、同じ ADF を共有することで、簡単に座標系を合わせることも出来ます。拡張性も有り、他にも例えば後でカメラなどによる外部センサを追加しても対応できますし、そこでトラッキングした追加コントローラの座標をマージしたり、といったことも簡単に出来そうです。

Area Learning

前節でも見ましたが、Frames of Reference で Area Learning のフレームをベースのフレームに指定すると、現在ロードされている ADF の基準を原点とする Pose が取得できます。つまり、事前に学習させておいたエリアの基準から現在の位置や姿勢が求められるため、バーチャルなオブジェクトを配置した際にそのエリアの基準点からの Pose を保存しておけば、次回ロード時に全く同じ場所に復元したり出来るわけです。

次の動画は(今はダウンロードできない)Tango Explorer という公式アプリです。はじめは Area Learning 無しで動作させた時で、激しく動かしてトラッキングロストすると、Relocalization(現在の Pose を再計算)した時に明後日の方向を向いてしまっています。しかしながら、Area Learning をして、その ADF をロードした後は、Relocalization した後も正しい位置に戻ってきていることがわかります。

また、Area Learning を使うと以下のように動き回ってもドリフトせずにその場にとどまってくれます。

ちょっと複雑なのですが、Area Learning には以下の 4 つの動作モードが存在します。

Learning
Mode
ADF
Loaded
Start →
Device
AD →
Device
AD →
Start
Save
ADF
No No 出来ない
Yes No 出来る
No Yes Localization 後 Localization 後 出来ない
Yes Yes Localization 後 Localization 後 Localization 後
新ファイルになる

Learning Mode は現在 Area Learning を実行しているかどうか、ADF Loaded は既存の ADF をロードしたかです。この 2 x 2 の組み合わせで 4 つの動作モードになります。このモードに応じて、AD(Area Description)基準の座標系が利用できるか、ADF がセーブできるか、といった点が変わってきます。「Area Description → Start」という Frames of Reference がありますが、これは Area Learning の基準位置からゲーム開始時の姿勢になります。これは Tango SDK からイベントとして Relocalization(自己位置推定のやり直し)や Loop Closure と呼ばれるキャリブレーションが走った際にイベントとして通知される座標系になります。Relocalization はデバイスを速く動かしたりカメラの絵から判断できないような場所から戻ってきた際に起こり、Loop Closure は Learning Mode という Area Learning を実行しつつ動かすモードにおいて、誤差を修正した際に起こります。自身の位置に誤差がある状態で配置したオブジェクトは、この Loop Closure が走った時に結構ズレてしまうと考えられるので、それに対応するために、デバイス -> オブジェクトの Pose をタイムスタンプ付きで保存しておき、Loop Closure が走った際に、その補正をよしなに処理してやらないとならない形です(HoloLens では WorldAnchor とかでこれをやっているのかな、と推測しているのですがどうなんでしょうか...)。

ただ現状、色々と難点があり、一番ツライのがこの Area Learning をするための手順が面倒なことです。今のところ Unity で SDK をインポートしてビルドして Area Learning のサンプルを動かし、学習データを作るしか方法が無いように思われます。。同様の UI を作ったとして、ユーザにキャリブレーションをしてもらうのは大分体験としてツライ感じがします。この辺りはあるしおうね先生の記事に詳しく書かれています。

panora.tokyo

もっと簡単にエリア学習できるようになったり、更には無数に誰かが学習してくれたデータが転がっている未来になってくれると良いですね。

Depth Perception

HoloLens では生成されたメッシュのみが利用可能でしたが、Tango では直接 Point Cloud も利用できます。

また、メッシュ化するコンポーネントも提供されています。簡単に頂点カラーもつけることが出来ます。

こうして生成された Point Cloud やメッシュを使って遮蔽表現をしたり、影を実際の空間に重ねて落としたり、コリジョンに利用したり、ナビゲーションに使用したり出来ます。後の節でタッチ点の座標と法線を取得するサンプルを紹介します。

www.moguravr.com

一方で、Phab 2 Pro や ZenFone AR のような ToF 方式では太陽光に弱いため、日向での利用はなかなか厳しいといった欠点もあります(冒頭の記事参照)。

この辺りは今後の技術の進歩に期待しましょう。

Tango の Unity コンポーネント

長くなりましたが、Unity の中でどうなっているか見ていきましょう。メインとなる登場人物はそれほど多くありません。TangoPrefabs 以下にある Prefab 達を押させておけば通常の開発では問題ないでしょう。

Tango Manager

  • TangoApplication は Tango のアプリの起点となるコンポーネントになります。Area Learning を使うか、デプスを使うか、カメラ画を背景に使うか、メッシュ生成をするかといった設定がポチポチと行なえるようになっています。

f:id:hecomi:20170507024017p:plain

各サンプルのシーンの設定をコピペで大体うまく動くと思います。

Tango Camera

Main カメラの代わりにこのカメラの Prefab を使います。

  • TangoPoseController は Area Learning の有無を参照したりしながらモーショントラッキングを行うデバイスの Pose を自動で反映してくれる役割を果たします。
  • TangoEnviromentalLighting を利用すると専用のシェーダ(TangoEnvironmentalDiffuse など)に対して周囲の光の影響を反映させるようになります。
  • TangoARScreen がアタッチされていると CommandBuffer を経由して画面にカメラ画を書き込みます。

Tango Point Cloud

コンポーネントを自作したい時

現在の SDKITangHogeHoge インターフェースを継承してメソッドをオーバーライドし、TangoApplication.Regsiter() で登録することで対象のメソッドが呼び出されるようになる、という仕組みになっています(登録し忘れると呼ばれない)。例えばタップした画面上の点が実際の世界でどこに当たるのか調べるために、タップした時だけデプスを更新、更新後にその座標を計算、計算後にそこにオブジェクトを生成してピッタリ重なるよう配置する、みたいなユースケースに対応するコードは以下になります。

using System.Collections;
using UnityEngine;
using Tango;

public class TangoHueControlManager : MonoBehaviour, ITangoDepth
{
    [SerializeField]
    TangoApplication tangoApplication;

    [SerializeField]
    TangoPointCloud pointCloud;

    [SerializeField]
    GameObject focusPrefab;

    bool isWaitingForDepth = false;

    void Start()
    {
        tangoApplication.Register(this);
    }

    void Update()
    {
        if (Input.touchCount == 1) {
            var t = Input.GetTouch(0);
            if (t.phase == TouchPhase.Began && !isWaitingForDepth) {
                StartCoroutine(_WaitForDepthAndFindPlane(t.position));
            }
        }
    }

    public void OnTangoDepthAvailable(TangoUnityDepth tangoDepth)
    {
        isWaitingForDepth = false;
    }

    private IEnumerator _WaitForDepthAndFindPlane(Vector2 touchPosition)
    {
        isWaitingForDepth = true;

        tangoApplication.SetDepthCameraRate(TangoEnums.TangoDepthCameraRate.MAXIMUM);
        while (isWaitingForDepth) {
            yield return null;
        }
        tangoApplication.SetDepthCameraRate(TangoEnums.TangoDepthCameraRate.DISABLED);

        var camera = Camera.main;
        Vector3 planeCenter;
        Plane plane;
        if (!pointCloud.FindPlane(camera, touchPosition, out planeCenter, out plane)) {
            yield break;
        }

        Vector3 up = plane.normal;
        Vector3 right = Vector3.Cross(up, camera.transform.forward);
        Vector3 forward = Vector3.Cross(right, up);
        Instantiate(focusPrefab, planeCenter, Quaternion.LookRotation(forward, up));
    }
}

ITangoDepth を継承して TangoApplicationRegister() することで、デプスが更新された際に OnTangoDepthAvailable() が呼ばれるようになります。ここではフラグ管理だけに用いて次のような処理が走ります。

  • タップしたら TangoApplication.SetDepthCameraRate() でデプスカメラでデータを取得するモードにして待ち受け(while())する
  • 新しいデータが来た(OnTangoDepthAvailable() が呼ばれた)ら待つのを終了し、タッチ点に対応する面および点を TangoPointCloud.FindPlane() で問い合わせる
  • 得られた座標および面の法線を使ってオブジェクトを生成する

こういった ITangoDepth のようなインターフェースは色々あるので、チュートリアルやリファレンスを見て欲しい機能を自分のゲームに組み込んでいきましょう。

アプリをつくってみた

取り敢えず家電操作してみた的なアレを今回もやってみました。タッチデバイスの良さは NUI とかと比べてタッチのインターフェースを作りやすく且つミスなく操作しやすいので、将来的な日常使いできる可能性があるかな...、ということでテストしてみました。

本当は操作してる様子を取りたかったんですが、残念ながら手が2つしかないので画面キャプチャしか出来ませんでした。。感動はないものの、まぁまぁ便利、という感じでした。理想的には Area Learning しながら対応する家電のリモコンインターフェースをダウンロードしてポチポチと事前にその位置に配置しておき、利用時には対象のデバイスの方にスマホを掲げて選択すると、セットしておいたリモコンインターフェースが全画面で表示される、みたいなものです。間接参照で操作するリモコンを直接参照にする、みたいなヤツです。同じ感じで HoloLens で試したヤツの方がインパクトはデカイのですが...、こちらの方が低カロリーだし良いかもしれない、という印象でした(個人的には HoloLens のデモの方が好き)。

ちなみに地道なリモコン配置作業はこちらです:

また、サンプルプロジェクトは以下になります:

github.com

おわりに

もう少し色々と検証したさもありましたが、ES 品なので今はここまでです。楽しみは発売後に取っておくことにします(買える力があるかは不明)。ちょっとゴテゴテしてしまいましたが、Tango アプリを開発する際に理解の一助になると嬉しいです。