2018.10.17

グーグルの最先端モバイルAR技術(ARCore)でAR空間共有を試してみた


はじめに

こにちは、次世代システム研究室のBMKです。

今年の6月頃にアップルの開発者向けイベント「WWDC2018」で発表された新ARキット「ARKit2」でAR空間共有機能が発表されました。その前に5月頃にグーグルの開発者イベントGoogle I/Oにて、同機能をグーグルのARプラットフォーム「ARCore」のアップデートとして発表しました。複数のデバイスから同じAR空間にアクセスする可能になり、より充実したARアプリの制作機能が追加されています。ARCore上AR空間共有機能のキーワードは「クラウドアンカー」です。クラウドアンカーを説明する前に、まず、アンカーの概念から見てみましょう。


アンカー(Anchor)とは

ARKit又はARCoreのAR技術は端末に搭載されているセンサーのデータ及び所持するカメラの画像を利用して、カメラのポーズ(位置や向き)を推定し、そのポーズに応じて適切なARコンテンツを重ね合わせ表示する技術を指します。カメラの画像による実環境の自然特徴を洗い出し、それらの特徴に基づきカメラの自己位置推定を行います。特徴を取り出せる環境内の物(床、テーブル、椅子等)はトラッカブル(追跡可能な物)と呼ばれます。時間軸でどの物を継続してトラッキングを行うのかARKit又はARCore内でその物を指する概念(インターフェース)はアンカーと言います。アンカーはワールド座標上継続して追跡される物のポーズの情報を持っています。


クラウドアンカー(Cloud Anchor)とは

クラウドアンカーのクラウドのところはGCP(グーグルクアウドプラットフォーム)を指します。GCPで提供されているAPIを介してアプリ上で作成したアンカーはクラウドを経由して共有されます。クラウド上に保存するアンカー毎に対してGCPのアンカーIDを発行し、これらのIDを複数端末に共有する時、UnityのネットワークAPI(通称UNET)を利用します。


共有の流れ

参加するデバイスの最低の数は2台が必要となり、デバイスAをホスト(サーバ)とし、デバイスAと同一ネットワーク上にあるデバイスBをクライアントとします。

AR空間の共有は以下の流れになります。
1. デバイスA(ホスト)がルーム(Room)を作成します
2. デバイスA(ホスト)で共有したい空間を検知して、アンカーを作成します
3. デバイスA(ホスト)がアンカーをGCPにアップロードして、クラウドアンカーIDを取得します
4. デバイスB(クライアント)はデバイスAのIPアドレス及びルーム番号をを用いてデバイスAに接続します
5. デバイスAはクラウドアンカーIDをデバイスBに送付します
6. デバイスBはGCPにアクセスしてクラウドアンカーIDで探し、IDに紐づくアンカーの情報を取得します
7. アンカーからポーズの情報を取り出し、リアルタイムにARコンテンツのポーズに反映します。
8. デバイスBのARCoreは自らカメラで撮った画像と内装センサーのデータも同時に利用して、アンカーの追跡を継続的に行います。

開発環境

  • Unity3D 2018.2.5f1 Personal
  • ARCore SDK for Unity 1.4.1
  • Google Pixel XL
  • Android OS 8.0

クラウドアンカーAPIの利用前の準備

APIを有効にする

GCPは沢山のAPIを提供しますがどのAPIを使うかそのAPIを有効にしなければなりません。任意のグーグルアカウントでGCPに登録して、GCP上任意のプロジェクトを作成して、プロジェクト内に使いたい APIを検索して有効にします。

 

クラウドアンカーAPIキーを取得

グーグルAPIを呼び出す時にAPIキーを渡す必要となります。
APIを有効にしたプロジェクトの内で”APIとサービス” → “認証情報”→”認証情報を作成”との順番で選択するとAPIキーが発行されます。


Unity上の設定

ARCore公式サイトからARCoreのUnity向けプラグインをダウンロードし、Unityにインポートした後、Unityのメインメニューから”Edit” → “Project Settings” → “ARCore”との順番でUnityエディタ上でAPIキーを設定できます。

今回、プラグイン内クラウドアンカーサンプルを利用しますのでそれに沿ってスクリプトの構成を説明します。


  1. CloudAnchorController.c : ルームとアンカーの作成、クラウドへの保存、共有等の処理を行う
  2. RoomSharingServer.cs: サーバー側としてルーム共有の対応を行う
  3. RoomSharingClient.cs: クライアント側としてルーム参加の対応を行う
  4. RoomSharingMsgType.cs: サーバー、クラインと間の送受信するメッセージの定義

サーバーの役割を持つデバイス側の処理

サーバーの起動とコールバック関数の登録

UNETのNetworkServerクラス(RoomSharingServer.cs)を利用して、サーバーの起動とコールバック関数の登録を行います。


public void Start()
{
    NetworkServer.Listen(8888);
    NetworkServer.RegisterHandler(RoomSharingMsgType.AnchorIdFromRoomRequest, OnGetAnchorIdFromRoomRequest);
}

ルームの作成

次にサーバー側はルームを作成します。RoomIDは1〜9999間の乱数で生成されます。


public void OnEnterHostingModeClick()
{
    if (m_CurrentMode == ApplicationMode.Hosting)
    {
        m_CurrentMode = ApplicationMode.Ready;
       _ResetStatus();
       return;
    }
    m_CurrentMode = ApplicationMode.Hosting;
    m_CurrentRoom = Random.Range(1, 9999);
    UIController.SetRoomTextValue(m_CurrentRoom);
    UIController.ShowHostingModeBegin();
}

アンカーの作成及びクラウドへの保存

カメラで撮った画像フレーム内に追跡可能な物を選択するとユーザがタッチした画面上の位置からレイキャスティングを実施し、ワールド座標上その物との交差点にアンカーを作り、アンカーの三次元の座標の情報を返します。


           Touch touch;
            if (Input.touchCount < 1 || (touch = Input.GetTouch(0)).phase != TouchPhase.Began)
            {
                return;
            }

            // Raycast against the location the player touched to search for planes.
            if (Application.platform != RuntimePlatform.IPhonePlayer)
            {
                TrackableHit hit;
                if (Frame.Raycast(touch.position.x, touch.position.y,
                        TrackableHitFlags.PlaneWithinPolygon, out hit))
                {
                    m_LastPlacedAnchor = hit.Trackable.CreateAnchor(hit.Pose);
                }
            }
            else
            {
                Pose hitPose;
                if (m_ARKit.RaycastPlane(ARKitFirstPersonCamera, touch.position.x, touch.position.y, out hitPose))
                {
                    m_LastPlacedAnchor = m_ARKit.CreateAnchor(hitPose);
                }
            }

アンカーの作成を成功した後、XPSession.CreateCloudAnchor(anchor)でGCPに登録します。


        private void _HostLastPlacedAnchor()
        {
#if !UNITY_IOS || ARCORE_IOS_SUPPORT

#if !UNITY_IOS
            var anchor = (Anchor)m_LastPlacedAnchor;
#else
            var anchor = (UnityEngine.XR.iOS.UnityARUserAnchorComponent)m_LastPlacedAnchor;
#endif
            UIController.ShowHostingModeAttemptingHost();
            XPSession.CreateCloudAnchor(anchor).ThenAction(result =>
            {
                if (result.Response != CloudServiceResponse.Success)
                {
                    UIController.ShowHostingModeBegin(
                        string.Format("Failed to host cloud anchor: {0}", result.Response));
                    return;
                }

                RoomSharingServer.SaveCloudAnchorToRoom(m_CurrentRoom, result.Anchor);
                UIController.ShowHostingModeBegin("Cloud anchor was created and saved.");
            });
#endif
        }

XPSessionとはARCoreが提供しているクロスプラットフォームARCoreセッションAPIを表します。XPSessionCreateCloudAnchor関数は新しいアンカーを非同期的にクラウドにホストしまます。XPSession.CreateCloudAnchorの実行は成功すると、CloudAnchorResultにクラウドアンカーIDが返されます。クラウドアンカーIDはルームIDと合わせてRoomSharingServer.SaveCloudAnchorToRoom(m_CurrentRoom, result.Anchor)でDictionaryに登録して保持しています。


public void SaveCloudAnchorToRoom(int room, XPAnchor anchor)
{
    m_RoomAnchorsDict.Add(room, anchor);
}

クライアントの役割を持っている端末側の処理

クライアントの役割を持っている端末はホストの役割を持っている端末のIPアドレスとルームIDを用いてホストの端末に接続します。


        public void OnResolveRoomClick()
        {
            var roomToResolve = UIController.GetRoomInputValue();
            if (roomToResolve == 0)
            {
                UIController.ShowResolvingModeBegin("Anchor resolve failed due to invalid room code.");
                return;
            }

            UIController.SetRoomTextValue(roomToResolve);
            string ipAddress =
                UIController.GetResolveOnDeviceValue() ? k_LoopbackIpAddress : UIController.GetIpAddressInputValue();

            UIController.ShowResolvingModeAttemptingResolve();
            RoomSharingClient roomSharingClient = new RoomSharingClient();
            roomSharingClient.GetAnchorIdFromRoom(roomToResolve, ipAddress, (bool found, string cloudAnchorId) =>
            {
                if (!found)
                {
                    UIController.ShowResolvingModeBegin("Anchor resolve failed due to invalid room code, " +
                                                        "ip address or network error.");
                }
                else
                {
                    _ResolveAnchorFromId(cloudAnchorId);
                }
            });
        }

まず、new RoomSharingClient()でNetworkClientを作成し、roomSharingClient.GetAnchorIdFromRoom()でホストの端末に接続します。


  public void GetAnchorIdFromRoom(Int32 roomId, string ipAddress, GetAnchorIdFromRoomDelegate GetAnchorIdFromRoomCallback)
        {
            m_GetAnchorIdFromRoomCallback = GetAnchorIdFromRoomCallback;
            m_RoomId = roomId;
            RegisterHandler(MsgType.Connect, OnConnected);
            RegisterHandler(RoomSharingMsgType.AnchorIdFromRoomResponse, OnGetAnchorIdFromRoomResponse);
            RegisterHandler(MsgType.Disconnect, OnDisconnected);
            RegisterHandler(MsgType.Error, OnError);
            Connect(ipAddress, 8888);
        }

RegisterHandler()でコールバック関数群を設定しおきます。サーバーへの接続が成功したら、OnConnectedが呼ばれます。


        private void OnConnected(NetworkMessage networkMessage)
        {
            AnchorIdFromRoomRequestMessage anchorIdRequestMessage = new AnchorIdFromRoomRequestMessage
            {
                RoomId = m_RoomId
            };

            Send(RoomSharingMsgType.AnchorIdFromRoomRequest, anchorIdRequestMessage);
        }

メッセージにルームIDを渡して、ルームIDに紐づくクラウドアンカーIDを要求します。Send()でサーバーに送ります。サーバー側はクライアントから送ったメッセージからルームIDを取り出し、ルームIDに紐付くクラウドアンカーIDがDictionaryにあるかを探し、見つかったらクラウドアンカーIDをAnchorIdFromRoomResponseメッセージに乗せて、リクエストを送ってきたクライアントに送り返します。


        private void OnGetAnchorIdFromRoomRequest(NetworkMessage netMsg)
        {
            var roomMessage = netMsg.ReadMessage<AnchorIdFromRoomRequestMessage>();
            XPAnchor anchor;
            bool found = m_RoomAnchorsDict.TryGetValue(roomMessage.RoomId, out anchor);
            AnchorIdFromRoomResponseMessage response = new AnchorIdFromRoomResponseMessage
            {
                Found = found,
            };

            if (found)
            {
                response.AnchorId = anchor.CloudId;
            }

            NetworkServer.SendToClient(netMsg.conn.connectionId, RoomSharingMsgType.AnchorIdFromRoomResponse, response);
        }
    }

クライアント側はサーバーからのメッセージを受信できたら、RegisterHandler()で登録されておいたOnGetAnchorIdFromRoomResponseを実行します。


       private void OnGetAnchorIdFromRoomResponse(NetworkMessage networkMessage)
        {
            var response = networkMessage.ReadMessage<AnchorIdFromRoomResponseMessage>();
            if (m_GetAnchorIdFromRoomCallback != null)
            {
                m_GetAnchorIdFromRoomCallback(response.Found, response.AnchorId);
            }

            m_GetAnchorIdFromRoomCallback = null;
        }
    }

メッセージからクラウドアンカーIDを取り出してクールバック関数m_GetAnchorIdFromRoomCallbackを呼び出します。コールバック関数が登録されたところを見ると実際に_ResolveAnchorFromId関数を実行となっています。


       private void _ResolveAnchorFromId(string cloudAnchorId)
        {
            XPSession.ResolveCloudAnchor(cloudAnchorId).ThenAction((System.Action<CloudAnchorResult>)(result =>
            {
                if (result.Response != CloudServiceResponse.Success)
                {
                    UIController.ShowResolvingModeBegin(string.Format("Resolving Error: {0}.", result.Response));
                    return;
                }

                m_LastResolvedAnchor = result.Anchor;
                Instantiate(_GetAndyPrefab(), result.Anchor.transform);
                UIController.ShowResolvingModeSuccess();
            }));
        }

ここでXPSessionを経由してクラウドアンカーIDでクラウド上クラウドアンカーIDに紐づくアンカー情報を検索して収得します。取得したアンカー情報からアンカーのポーズ(result.Anchor.transform)を取り出し、空間上の同じ場所にARコンテンツ(_GetAndyPrefab)を合わせて表示します。また、内部処理としてクライアント側のARCoreは自分の端末から撮った画像とセンサーデータを用いて端末とアンカーの相対位置を継続的にトラッキングを行います。


まとめ

今回、ARCoreが提供している新機能空間共有の仕組みを調べて纏めました。グーグルはクラウド技術を活かしてAR空間を複数のデバイスに共有することができ、さらに共同操作もできるようになりより充実したAR体験ができるようになりました。ARゲームを始め、AR共同作業「例:共同デザイン、共同設計等」までの応用を期待できます。


最後に

次世代システム研究室では、アプリケーション開発や設計を行うアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。