じゅころぐAR

ARのブログ

AROWオープンテストバージョンでARアプリを作る

引き続き、AROWを使ったアプリ開発です。
今回は、主に"サーバからのマップ取得""AR表示"について。

前回:AROWオープンテストバージョンでUnityに3Dマップを表示するまで - じゅころぐAR

サーバからマップを配信する

マップデータダウンロードにて地方毎(北海道地方、東北地方・・・)のarowmapが配布されています。
関東地方のzipファイルを解凍するとこんな感じです。

f:id:jyuko49:20190501171524p:plain

osm_[経度(度)]_[緯度(度)]のディレクトリにblock_[緯度]_[経度].arowmapのファイルが格納されています。osmはOpenStreetMapのデータであることを示すプレフィックス、経度・緯度はそのままマップの範囲を表していると思われます。

ファイル名の経緯度ですが、4-5桁目が00-60ではなく00-99になっているため、度分秒ではなく度(degree)形式の経緯度を文字列にした値のようです。また、末尾5桁は全て00000になっているので、1/100度単位でファイルが区切られています。

例:
139.13度 = 1391300000
35.74度 = 0357400000

とりあえずのテストとしてローカルPC(Mac)にファイルを置いて、http-serverで配信できるようにしました。

$ cd ./Kanto
$ http-server
Starting up http-server, serving ./
Available on:
  http://127.0.0.1:8080
  http://192.168.0.2:8080

上記例ではhttp://192.168.0.2:8080でローカルサーバが起動したので、http://192.168.0.2:8080/osm_139_035/block_1390000000_0350000000.arowmapのようにリクエストすればファイルが取得できます。

ライセンスを読むと、"本コンテンツ(含むマップデータ)""単独""バイナリ形式での頒布"が許可されているため、publicなWebサーバからの配信もOKな気がします。

マップをサーバから取得する

マップデータが緯度経度に基いて分割されていることがわかったので、緯度経度指定で周辺のマップを取得する形に書き換えます。
元の処理はAssets/ArowSample/Scripts/Demo/ArowDemoMain.csが参考になります。

private string mapSerrver = "http://192.168.0.2:8080"; //ローカルサーバ

//マップの初期化(緯度経度指定)
public void Initialize (float lon, float lat) {
    //経緯度(lon, lat)から周辺マップデータのパスを作成
    string directory = "/osm_" + Math.Truncate(lon).ToString() + "_" + Math.Truncate(lat).ToString().PadLeft(3, '0');
    string filePath = "/block_" + Math.Truncate(lon * 100).ToString() + "00000_" + Math.Truncate(lat * 100).ToString().PadLeft(5, '0') + "00000.arowmap";

    //サーバにリクエストを投げる
    StartCoroutine(GetMap(mapSerrver + directory + filePath));
}

UnityWebRequestでGETリクエストを投げ、レスポンスのバイナリデータを使ってArowMapObjectModelを生成します。
ローカルファイルから読みこむ場合との違いは、mapDataBytesFile.ReadAllBytes()で読み込んでいるか、webRequest.downloadHandler.dataで取得しているかだけです。

IEnumerator GetMap(string uri) {
    using (UnityWebRequest webRequest = UnityWebRequest.Get(uri)) {
        yield return webRequest.SendWebRequest();

        if (!webRequest.isNetworkError) {
            //webRequestで取得したdataからArowMapObjectModelを生成
            byte[] mapDataBytes = webRequest.downloadHandler.data;
            arowMapObjectModel = ArowMapObjectModel.LoadByData(mapDataBytes);

            var mapRect = MapRectInt.FromDictionary(arowMapObjectModel.InfoList.FullInfoList);
            worldCenter = MapUtility.CalculateWorldCenter(mapRect.East, mapRect.North, mapRect.West, mapRect.South);

            CreateConfig config = ScriptableObject.CreateInstance<CreateConfig>();
            CreateRuntimeBuilder.SetupConfigForSampleSlice(config);
            config.SetupSharedMaterials = CreateRuntimeBuilder.GetFixTextureSuiteCallbackForSampleSlice(config);

            //Mesh Colliderの設定
            config.BuildingColliderElement = CreateConfig.BuildingCollider.Mesh;

            CreateBuildings(config); //建物の生成
            CreateRoads(); //道路の生成
        }
    }
}

Tips

CreateConfigクラス

建物にMesh Colliderを設定するため、CreateConfigクラスのプロパティを変更しています。
リファレンスを読むと、Colliderの設定だけでなく、オブジェクトに設定するマテリアル生成するメッシュのパターンなども変更できるので、本クラスのリファレンスは要チェックです。

ArowMain.Runtime.CreateConfig クラス

ARで表示する

AROWをARKitと組み合わせてみます。

ただARで表示させるだけなら難しくはなく、Unity-ARKit-Pluginをセットアップして、マップを表示させればOKです。

建物にMesh Colliderを付けているので、画面タップでHitTestを行って対象のオブジェクトをスクリプトで取得することもできます。
取得したオブジェクトをDestroy()すれば消える。

無音でDestroyするだけでは面白くないので、クエリちゃんに消していただきました🍊  自分で作っておいてなんですが、笑い声がリアルで怖いです...
query-chan.com

ARでマップを空間と合わせたい

3DマップをARで使うにあたって現実の空間とできるだけ位置を合わせた方が実用的です。
そのためには、マップのスケール、位置(緯度経度)、方位、高さを現実と合わせる必要があります。

スケール

1/100度で分割されたarowmapファイルをUnityで表示させてみます。
worldScaleはMapUtility.WorldScaleを指定しており、値はVector2.one * 0.01fになっています。

f:id:jyuko49:20190502034241p:plain

スケールの確認のため、x方向とz方向を1,000倍に引き伸ばしたCube(白い四角)を配置してみました。
マップはCubeと同じサイズで、Unity単位で1,000 x 1,000の範囲内に表示されています。一部Cubeからはみ出て見えますが、範囲内に少しでもかかっているオブジェクト(建物、道路)はマップに含まれる仕様と思われます。

1Unity単位は1メートルなので、1/100度 x 1/100度のマップを1,000m x 1,000mで表示していることになります。
じゃあ1/100度は何メートルかと言うと、緯度経度の1度あたりの距離は、経度が約90km(緯度35度付近)、緯度が約111kmなので、1/100度は900m x 1,110mになります。

緯度経度1度の距離 - モノノフ日記

デフォルトでも大体合っているのですが、より正確なスケールで表示したいので、worldScaleにnew Vector2(0.009f, 0.0111f)を設定してみます。

f:id:jyuko49:20190502172130p:plain

マップの左側に表示されている楕円形の建物は等々力陸上競技場なのですが、OpenStreetMapと比べると、スケール変更前は緯度方向に若干潰れたような表示になっており、スケール変更後の方が正しい比率に見えます。

位置(緯度経度)

マップの表示位置を合わせるには、worldCenterを変更する方法が利用できます。

サンプルのスクリプトでは以下のコードで、読み込んだマップデータの中心をworldCenterに設定しています。

var mapRect = MapRectInt.FromDictionary(arowMapObjectModel.InfoList.FullInfoList);
worldCenter = MapUtility.CalculateWorldCenter(mapRect.East, mapRect.North, mapRect.West, mapRect.South);

設定値の確認のため、"マップデータ(arowmap)のファイルパス"、"mapRect"、"worldCenter"をDebug.Log()で表示させてみます。

f:id:jyuko49:20190502181439p:plain

mapRect.WestmapRect.Southがファイルパスの経度・緯度と一致し、mapRect.EastmapRect.Northは1/100度にあたる100000をそれぞれ足した値、worldCenterはmapRectの中心という関係性になっています。

worldCenterの変更でマップの中心が変わることを確認したいので、試しにworldCenter.xをmapRect.Westに変更してみましょう。

worldCenter.x = mapRect.West;
worldCenter.y = (mapRect.South + mapRect.North) / 2;

f:id:jyuko49:20190502183545p:plain

Unity上でのマップの中心がworldCenterで指定した緯度経度=マップデータの左端(西端)に移動していることがわかります。
よって、現在地の経度(lon)と緯度(lat)からworldCenterを計算することで、現在地を中心にしたマップが表示できます。

worldCenter.x = (int)Math.Truncate(lon * 10000000);
worldCenter.y = (int)Math.Truncate(lat * 10000000);

現在地の緯度経度はInput.location.lastdataから取得できます。

Unity - Scripting API: LocationService.Start

方位

ARでマップを使う場合、方位=マップの向きはかなり重要です。

UnityではInput.location.Start()で位置情報の取得を開始し、Input.compass.enabledtrueにすることで、デバイスに内蔵されたコンパスに基づく方位が取得できます。

Input.location.Start();
Input.compass.enabled = true;

現在デバイスが向いている方位はInput.compass.headingAccuracyで取得できます。
ただし、ARセッションを既に開始している場合、コンパスから値を取得した時点でデバイスが回転している可能性があります。その場合、ARカメラ(例として、Camera.main)の回転角Camera.main.transform.eulerAngles.yを引くことでARセッション開始時の方位角=z軸に合わせたい方位が得られます。

//ARセッション開始時の方位角を得る
azimuth = Input.compass.headingAccuracy - Camera.main.transform.eulerAngles.y;

Compass-trueHeading - Unity スクリプトリファレンス

この方位がz方向になるようマップの向きを合わせたいのですが、建物のメッシュ生成を行うCreateBuildingMeshScripts クラスに方位角を指定する引数はなく、デフォルトでは北が正面(Unity座標系でのz軸方向)で表示されます。

これを調整する方法として、メッシュを生成し終わった後に、親オブジェクトのtransformを回転させています。
あえて"メッシュを生成し終わった後に"と書いたのは、メッシュ生成前に親オブジェクトを回転させてもメッシュ生成時に北がz方向になるよう相対的な回転が入ってしまい、期待した結果にならなかったためです。

ArowMain.Runtime.BuildingCreator.Builder クラスに、メッシュ生成完了時のcallbackを指定するSetOnCompletedCreateMeshCallBack()があるので、callbackの処理内でマップを回転させます。

buildingMeshCreator.SetOnCompletedCreateMeshCallBack((List<BuildingDataModelWithMesh> list) =>
{
    //メッシュ生成後に方位角(azimuth)に合わせて回転
    buildingParent.transform.Rotate(new Vector3(0, -azimuth, 0));
});

azimuthを負の値にしているのは、方位角に対して、その方位をUnity座標系での正面に向けたいからです。

デバイスの方位角が90°(東を向いている場合)を例にすると、Input.compass.trueHeadingから取得できる値は90fとなり、マップをy軸で-90f回転させると東(方位角90°)がUnity座標系でのz方向(y軸での回転が0°)に向きます。

実際に表示させてみると、わかりやすいです。

f:id:jyuko49:20190503064534p:plain

上記でマップの向きを方位に合わせたものの、コンパスから取得できる方位はかなり精度が低いので、初期表示ではコンパスを使い、精度の高い情報が得られたらマップの方位を補正する実装にしておくと良さそうです。
デバイスがある程度移動する前提でGPSが移動した軌跡から現在のデバイスの向きを推定した方が精度が良さそうですし、予め決められた場所での利用ならマーカー等で位置合わせするのも手です。

高さ

最後に、高さの調整です。

デバイスからはInput.location.altitudeでGPSの高度が取れるのですが、これは地面からの高さではないため使い物になりません(なお、精度も良くない)

せっかくのARなので水平面の検知を行い、平面の高さで補正する方法が良さそうです。
ARKitで試した際の実装例として、以下のようなコードになります。

using UnityEngine.XR.iOS;

public class ArowTest : MonoBehaviour
{
    private UnityARAnchorManager unityARAnchorManager;

    [SerializeField]
    private GameObject ground;  //地面として表示するPlaneObject

    void Start()
    {
        unityARAnchorManager = new UnityARAnchorManager();
    }

    void Update()
    {
        if (unityARAnchorManager.GetCurrentPlaneAnchors().Count != 0) {
            //最後に検知した平面を取得
            ARPlaneAnchorGameObject plane = unityARAnchorManager.GetCurrentPlaneAnchors().Last.Value;

            //建物と道路の親オブジェクトを取得
            GameObject buildingParent =  arowTestMain.buildingParent;
            GameObject roadParent =  arowTestMain.roadParent;

            //補正前のposition
            Vector3 currentPosition = buildingParent.transform.position;

            //建物のposition補正
            buildingParent.transform.position = new Vector3(
                currentPosition.x,
                plane.gameObject.transform.position.y,
                currentPosition.z
            );

            //道路のposition補正
            roadParent.transform.position = new Vector3(
                currentPosition.x,
                plane.gameObject.transform.position.y - 1.5f,
                currentPosition.z
            );

            //地面の高さを合わせる
            ground.transform.position = new Vector3(0, plane.gameObject.transform.position.y, 0);
        }
    }
}

道路メッシュのみ-1.5fしているのは、デフォルトが地面から若干浮いているためです。-1.5fすると道路メッシュの上面が地面の高さと一致します。

f:id:jyuko49:20190505125839p:plain
デフォルトは道路が地面から浮いた状態

f:id:jyuko49:20190505124944p:plain
地面にぴったり合わせた状態

上記例では簡単のために最後に検知した平面の高さを使ってますが、このままだと地面以外の水平面を検知してしまったときに表示がずれます。再度地面を検知すれば直るので、良しとするスタンス。
より正確性を求めるなら、"取得した平面をforeachで回して、高さが最も低い平面を使う"、"カメラからの距離が1m未満の平面は無視する"などの工夫が必要です。もしくは、UIとしてユーザがボタンを押したときだけ補正するとか。

また、ユーザが地面より少し高い場所でアプリ起動するケースも悩みどころです。ただ、マップ自体も起伏や傾斜、段差が考慮されている訳ではないので、ある程度割り切るしかないかなと思います。

まとめ

AROWのマップをAR表示させてみました。多分、大体合ってると思う。
ARKitのみでARCoreは試していませんが、特別な実装はしていないので同じ感じになるはず。

また、AROWの使用感として、若干仕様に癖があるかなとは思いつつ、慣れればスクリプトで色々できるので割と使いやすいです。

今後の予定は未定ですけど、SDKに付属していたサンプル(↓)で指定した二地点の経路を求める処理があるので、簡単なARナビゲーションなら作れそう(作るとは言ってない)

クレジット

本記事内のコンテンツでは、"AROW"、"OpenStreetMap"が使用されております。

© 2019 「AROW」Drecom Co.,Ltd. All rights reserved.
© OpenStreetMap contributors