RACCOON TECH BLOG

株式会社ラクーンホールディングス 技術戦略部より、
tipsやノウハウなど技術的な話題を
発信いたします。

UnityでARアプリの作り方4/4 コンパスで3Dオブジェクトを操作

こんにちは、Paid開発ユニット werdnaの酒井です。

ARアプリの作り方、最後の記事です。
UnityでARアプリの作り方1/4 環境構築+androidビルドからカメラ表示
UnityでARアプリの作り方2/4 GPSの緯度経度から地図の表示
UnityでARアプリの作り方3/4 Google Direction APIの経路を地図に表示
UnityでARアプリの作り方4/4 コンパスで3Dオブジェクトを操作(本記事)

概要

本記事では「磁気センサーからの角度と次の経路への角度から進むべき方向を指すオブジェクト作成」を目標とします。
具体的には下記内容を順に説明していきます。

磁気センサーについて

今回の行き先表示のために、端末がどの方向を向いているのか?という情報が必要になります。
そこで利用するのが「磁気センサー」です。
名前の通りですが、地球からの磁気で端末のx, y, z方向での向きや傾きを取得できます
(「android 端末の方向」で調べるとジャイロセンサーや加速度センサーでも取得できそうですが、本記事では磁気センサーを利用してみました。)

データの内容と取得は下記の通りです。(Unityのリファレンスより確認)

項目 取得方法
度単位の方向読み取りの精度 Input.compass.headingAccuracy
磁北極に基づく度単位の進行方向 Input.compass.magneticHeading
地理的北極に基づく度単位の進行方向 Input.compass.trueHeading
マイクロテスラ単位の raw 地磁気データ Input.compass.rawVector
進行方向が最後に更新された時のタイムスタンプ Input.compass.timestamp

磁北極と地理的北極という知識がなく...
簡単に言えば、磁石が指す北と北極点は異なるもので、磁石が指す北を表すものが「磁北極」
そこに補正を加えて北極点を指すようにしたものが「地理的北極」のようです。

補正のために地理的北極を取得するためにはInput.location.start()が必要になります。
今回はせっかくなので地理的北極であるtrueHeadingを利用します。

また、rawVectorからx, y, z方向でのデータ取得もできるようですが、
今回必要な端末が今向いている方向はtrueHeadingで取得できるようでしたのでそのまま利用しています。
(後ほど作成する矢印オブジェクトを様々な方向に動かしたい場合など rawVector を使うといい感じに動きそうです。)

進む方向を表示するためのオブジェクト作成

行き先表示に良い感じのオブジェクトがデフォルトで存在していなかったのでBlenderを使って矢印を作りました。

本題とはそれてしまうので割愛させていただきますが、AssetStoreで販売されているものや、矢印の画像をオブジェクトとして配置する等で代用してください。
Blenderでやる場合には、fbkファイルにエクスポートし、Unity側でファイルを開けば自動的に取り込んで表示してくれます。

2点間での角度の取得

タイトルから感じる数学感で気持ち悪くなってきますが、2点間の角度を取得するときには逆三角関数のアークタンジェントを利用します。
こちらについても本筋とずれてしまうので具体的な説明は割愛しますが、UnityEngineに定義されているMathfのAtan2(x, y)にlatitude, longitudeの差を渡すことでradian形式の角度を返してくれます。
radian は 1rad = 180/π 度 という単位です。
そのままだと角度として利用できないため、同じくMathfに定義されているRad2Degをかけて角度に直して利用します。

後ほど記載するコードの一部ですが下記のように書くことで角度を取得できます。

// 逆三角関数で次の地点と現在地点から角度(radian)を取得
float rad =  Mathf.Atan2(nextLat - startLat, nextLng - startLng);
// radianのままだと使えないので360度の形式(degree)に変換
float deg = rad * Mathf.Rad2Deg;

行き先表示

実際に行き先表示を実施します。
まずは行き先を指すオブジェクトが必要になるので、矢印オブジェクトを配置します。
この時、初期状態で進行方向が前になるように矢印オブジェクトの向きを調整してください。
(元々作りたかったものがカメラ画像上に地図や行き先表示があるものだったので、前回のレイアウトから少し変更しています。
 変更した内容は下記3点です。
 1. 背景のカメラ画像を表示するcanvasを画面いっぱいに広げる
 2. 目的地入力を一番上に持ってきた
 3. 前回確認用に作成していたGPS情報のTextを無効化)

次に適当なGameObjectを作成し、前を向いている矢印オブジェクトが子になるように設定します。(GameObjectの名前をarrowRootに変更しています)
Hierarchyウィンドウの矢印オブジェクトを作成したGameObjectにドラッグアンドドロップすることで簡単に親子関係にすることが出来ます。

赤枠内のようになっていれば親子関係の設定完了です。

※もし矢印オブジェクトを回転させることなく前向きになっていた場合には親オブジェクトを作成しなくても問題ありません。
親オブジェクトを作成する意味としては、対象のオブジェクトにデフォルトで回転がかかっていた場合、次の進行方向の角度にデフォルトの角度を加えて計算する必要が出てきてしまうためです。
(Unityでは親オブジェクトを移動・回転したときに子オブジェクトも同じく移動・回転します。それを利用して親オブジェクトを回転させます。)

オブジェクトの配置は完了ですが回転させるscriptを作成する前に、次の経路情報を受け取れるように前回作成したDirectionControllerを修正します。
丸ごと張り付けてはいますが、L:18-20 定義の追加、L:67-69 変数に格納 のみ追加していただければ大丈夫です。

using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
using System.Runtime.Serialization.Json;
using System.Text;
using System.IO;
using UnityEngine.UI;

public class DirectionController : MonoBehaviour
{
    private const string GOOGLE_DIRECTIONS_API_URL = "https://maps.googleapis.com/maps/api/directions/json?key=${APIKey}&mode=walking";

    // 目的地を入力させるInputField
    public InputField destination;
    // APIより取得した経路をパイプで繋いだもの(staticMapControllerに渡すためのパラメータ)
    public string destinationRoute = "";

    // 行き先表示用に次の経路を定義
    public string nextLat = "";
    public string nextLng = "";

    private int frame = 0;

    void Start()
    {

    }

    void Update()
    {
        // 更新は5秒に一度、目的地が設定されていない場合は取得しない。
        if (frame >= 300 && destination.text != "")
        {
            Debug.Log(UnityWebRequest.UnEscapeURL(destination.text));
            StartCoroutine(GetDirection());
            frame = 0;
        }
        frame++;
    }

    private IEnumerator GetDirection()
    {
        // origin=開始地点。現在地からの経路を出したいので現在地を渡す。
        var query = "&origin=" + UnityWebRequest.UnEscapeURL(string.Format("{0},{1}", Input.location.lastData.latitude, Input.location.lastData.longitude));

        // destination=目的地。InputFieldに入力した文字列をエスケープして渡す。
        query += "&destination=" + UnityWebRequest.UnEscapeURL(destination.text);
        UnityWebRequest req = UnityWebRequest.Get(GOOGLE_DIRECTIONS_API_URL + query);
        yield return req.SendWebRequest();
        if (req.error == null)
        {
            // 返ってきたjsonをByte[]形式を処理できるMemoryStreamで受け取る
            MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(req.downloadHandler.text));

            // google DirectionのJSONをオブジェクトとして読み込めるようにシリアライザを作成
            var serializer = new DataContractJsonSerializer(typeof(GoogleDirectionData));

            // 作成したJSON用のクラスにデシリアライズ
            GoogleDirectionData googleDirectionData = (GoogleDirectionData)serializer.ReadObject(ms);

            // データの形式としてroutes[0].legs[0]は固定のようなので一旦変数に格納
            var leg = googleDirectionData.routes[0].legs[0];

            // 書き込み前に初期化
            destinationRoute = "";

            // 行き先表示用に次の経路情報を保存
            nextLat = leg.steps[0].end_location.lat;
            nextLng = leg.steps[0].end_location.lng;

            for (int i = 0; i < leg.steps.Count; i++)
            {
                // 経路は|緯度,経度|という書き方になるので、受け取ったlatitude, longitudeをパイプとカンマを付けて追加していく
                destinationRoute += "|" + leg.steps[i].end_location.lat + "," + leg.steps[i].end_location.lng;
                // 経路が多すぎるとUriFormatExceptionで落ちるため上限を設定しておく。
                if (i > 20) break;
            }
        }
    }
}

上の修正が終わったら、回転させるオブジェクトにscriptを反映させていきます。
前回・前々回と実施している通りですが、Add Componentより新規Scriptを作成します。
行き先を指すので少し意味合いが違いますが、CompassControllerという名前で作成しています。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class CompassController : MonoBehaviour
{
    // 経路取得コントローラ内で次の地点をセットしているのでコントローラをセットさせる。
    public DirectionController direction = null;

    void Start()
    {
        // 端末の磁気センサー有効化
        Input.compass.enabled = true;

        // trueHeadingを利用するためlocation.serviceの有効化
        Input.location.Start();

        // angleを取得
        float angle = getAngle();

        // y軸の回転をangleで制御(
        this.transform.rotation = Quaternion.Euler(0, angle, 0);
    }

    void Update()
    {
        float angle = getAngle();
        this.transform.rotation = Quaternion.Euler(0, angle, 0);
    }

    private float getAngle()
    {
        float angle = 0;
        if (direction.nextLat != null && direction.nextLng != null)
        {
            // 現在地点を端末から取得
            float startLat = Input.location.lastData.latitude;
            float startLng = Input.location.lastData.longitude;

            // 目的地への次の地点をDirectionControllerから取得
            float nextLat = float.Parse(direction.nextLat);
            float nextLng = float.Parse(direction.nextLng);

            // 逆三角関数で次の地点と現在地点から角度(radian)を取得
            float rad =  Mathf.Atan2(nextLat - startLat, nextLng - startLng);
            // radianのままだと使えないので360度の形式(degree)に変換
            float deg = rad * Mathf.Rad2Deg;

            // 端末の向きによってを磁気センサーより取得(degree)
            float phone_deg = Input.compass.trueHeading;

            // 目的地の方向から端末の向きを引き、向いている方向から見た次の地点を指す角度を算出
            angle = deg - phone_deg;
        }
        return angle;
    }
}


scriptを作成してDirectionControllerを紐づけました。

早速動かしてみると下記のように矢印オブジェクトが動くことを確認できるかと思います。

上手く動かない場合には、前回GPSの緯度・経度を表示していたのと同様に次の目的地への角度 / 磁気センサーから取得した向き / 目的地と現在の向きを計算した結果を表示しつつ確認してみると良いかもしれません。

これにてカメラ表示・地図表示・経路表示・経路案内の実装完了です!

まとめ

入門者向け記事!として書いたせいかかなり文字数が多くなってしまいました。
冒頭でも書いていましたが、個人的にやりたかったこととしては、年内に発売予定と発表があったHololens2に向けて、
・UnityでAPIを叩きWeb上のデータへの接続
・利用する端末のセンサーからパラメータの取得やカメラ映像の取得
あたりを試してみたく今回のアプリ制作を実施していました。
(Hololens1が販売された当初、TwitterでGoogleMapの経路を矢印で表示している動画を見て感動した覚えがあり、それをイメージして作っていました。)

参考にした「UnityによるARゲーム開発」ですが、どういった手順を踏んだのか?という部分がしっかりと記載されているので、読みながら追っていくのがとてもやりやすくまとまっていました。
発売が2017年のためサンプルコードをそのまま利用した場合、最新のUnityだと部分的に動かないところや非推奨なメソッドになっているコードなども存在していましたが、そこはVisual Studio側で推奨メソッドを返してくれたのでサクサクと動く形にしつつ進められました。

この調子でHololens2発売までにもうすこし色々触ってみようと思います。
もしこの記事を読んで、アプリ作成に触れて頂き、Unityを触ってみようかな?と思って頂ければとても嬉しいです。

関連記事

運営会社:株式会社ラクーンホールディングス(c)2000 RACCOON HOLDINGS, Inc