ろうでんのブログ

ただのブログ

【Unity】PLATEAUの都市モデルとGPSログの軌跡を重ねてみる

この記事はAkatsuki Games Advent Calendar 2023の17日目の記事です。

昨日の記事はHajime Sannoさんの「Shader の GUI を実装する」でした。 普通のInspector拡張を触ることが多い自分にとっても、ShaderGUIの話はかなり新鮮でした。 今後Shaderを実装した際にShaderGUIも書いてみたいなと思える記事でした!

他にもクライアントサイド、サーバサイドだけでなくゲーム以外の技術等々、たくさんの記事があります。よければ見ていってください〜

はじめに

去年あたりからドライブにドハマりしていて、その過程で溜まってきたGPSログの軌跡データを使って何かをしたかったので、今回はUnity空間上に描画してみました。 都合よく日本の都市の3DモデルがUnity向けに開発・提供されているので、今回はそれも使ってみました。 軽めの記事のつもりです。

本記事の内容を3行にまとめると

です。

目次

Unity空間上に日本の都市を召喚

GPSログの軌跡を見るだけなら、それこそ (エクスポートせずとも) Googleマップのタイムライン機能で見たり、(エクスポートするにしても) Google Earthで見ればいい話です。

Googleマップのタイムライン機能
が、Unityでの開発を生業にしている身なのでUnityの勉強がてらにUnityを選択しました。

Unityのシーン上に何も配置せずにGPSログの軌跡を描画しても味気ないので、PLATEAUの3Dモデルを配置して、そこに表示を重ねることにしました。

① Unityインストール

PLATEAU SDK for Unityのマニュアルによると、執筆時点で最新リリースのv2.2.1-alphaの推奨環境はUnity 2021.3.30f1以上とのことなので、執筆時点で最新バージョンの2021.3.33f1をインストールします。 今回はEditor上で動けば十分なので、追加のモジュールは入れません。

Unity 2021.3.33f1インストール後のUnity Hubの様子

② 空のUnityプロジェクト作成

特にひねりを入れずに、通常の「3D」テンプレートから作成します。

空のプロジェクトを作成した様子

③ PLATEAU SDK導入

入手方法は3つありますが、今回は手間が少なそうな2を選びます。

  1. GitHubのReleasesから入手 (マニュアル)
  2. UnityのPackage Managerでgit URLを指定 (マニュアル)
  3. Asset Storeから入手

今回はv2.2.1-alphaを使うので、UnityのPackage ManagerのAdd package from git URL...にて

https://github.com/Project-PLATEAU/PLATEAU-SDK-for-Unity.git#v2.2.1-alpha

を指定します。

PLATEAU SDK導入後のPackage ManagerとProjectの様子

④ 都市3Dモデルのインポート & 配置

SDKにはモデルデータそのもののアセットは同梱されてないので、配置に先立ってモデルデータをインポートする必要があります。 まずメニュー/PLATEAU/PLATEAU SDKからPLATEAU SDKビューを開きます。

モデルデータをインポートする際の参照元としてローカル(ダウンロード済みのデータを使う) とサーバー(新たにサーバーからダウンロードする) を選べますが、今回は何もダウンロードしてない状態なのでサーバーを選びます。

データセットの選択では「どの都市のモデルデータをインポートするか」を指定できますが、今回はアカツキゲームスがある東京都23区を選びます。

基準座標系では浮動小数点数由来の誤差を1小さくするために選択した都市に近い基準地を指定します。 今回は東京都23区を選んだので、基準地には09: 東京(本州), 福島, 栃木, 茨城, 埼玉, 千葉, 群馬, 神奈川を選びます。

マップ範囲選択では、Sceneビュー上2で「データセットに含まれる範囲のうち、どの範囲をインポートするか」を選択します。 今回はアカツキゲームスのある目黒駅周辺の9マス分3を選びます。 地物別設定では、モデルデータの他に都市計画決定情報等のデータをインポートするかを選べます。 今回はモデルデータ以外を使わないため、インポートするの☑を外します。

ここまでやったらいざインポートを実行します。

PLATEAU SDKビューでのモデルインポート直前の様子

今回は3分足らずでダウンロードからインポートまで完了しました。 インポート完了時に現在開いているシーンの原点とインポートしたモデルデータの中心が重なるよう自動的に配置されるので注意しましょう (KMLからインポートした点群をどれだけオフセットさせればよいかに関わる) 。 なおモデルデータは原寸大なのでスケールは調整不要です。

インポート完了後の様子

GPSログの準備 & パーサ実装

KMLファイルへのエクスポート

まずはGPSログのデータを準備します。 前述の通り、Googleマップのタイムライン機能からKMLファイルをエクスポートします4。 今回は目黒駅周辺のモデルデータのみをインポートしたので、目黒駅周辺を通った2023-04-09のデータを用います。 タイムライン画面上で⚙ボタンをクリックすると出てくるこの日のデータを KML 形式でエクスポートをすると、KMLファイルがダウンロードされます (今までの全期間の一括ダウンロードはできないように見える) 。

2023-04-09の移動軌跡をタイムライン機能で表示した様子

KMLファイルのインポート

次にUnityプロジェクトにKMLファイルをインポートします。 拡張子が*.kmlのままだとUnityにTextAssetとして認識されずにDefaultAssetとして扱われてしまって不都合が多いため、*.xmlにリネームします。 そしてAssets/Resources/フォルダを作り、リネームしたファイルをその配下にインポートします5

KMLファイルのパーサ実装

次にKMLファイルから緯度・経度のタプル列を抽出するパーサを実装します。 KMLファイルはXMLのフォーマットに従っていて、スキーマ公式リファレンスに記載されている通りです。 今回は移動軌跡を取れれば十分なので、KML全体のパースは試みず、移動軌跡にあたる部分のみをパースできれば十分です。 KML上のkml/Documentタグで囲われた部分は配列になっていて、様々な種類の子要素が入っています。 それらの子要素のうち、今回必要な座標の点群データはPlacemarkタグで囲われた部分にあります。 Placemarkには以下の2パターンがあるので、両方に対応します。

  • 1点のデータのみを含むもの (訪問先を表すデータ): Placemark/Point/coordinates内に記述されている
  • 複数点のデータを含むもの (移動中の座標の履歴を表すデータ): Placemark/LineString/coordinates内にスペース区切りで記述されている

手抜き気味ですが、以下のコードで最低限動きます。

public static IEnumerable<(double latitude, double longitude)> ExtractGeodeticCoordinates(string kml)
{
    // エラーハンドリング等は手抜き
    var xDoc = XDocument.Parse(kml);
    XNamespace xmlns = "http://www.opengis.net/kml/2.2";
    var kmlElement = xDoc.Element(xmlns + "kml");
    var documentElement = kmlElement!.Element(xmlns + "Document");

    foreach (var placemarkElement in documentElement!.Elements(xmlns + "Placemark"))
    {
        var coordinatesElement = placemarkElement.Element(xmlns + "Point")?.Element(xmlns + "coordinates") // 1点のみ
            ?? placemarkElement.Element(xmlns + "LineString")?.Element(xmlns + "coordinates"); // 複数点

        foreach (var coordinate in coordinatesElement!.Value.Split(' '))
        {
            // 例: 139.7161781,35.633174499999996,0
            if (coordinate.Split(',') is { Length: 3 } coordinates)
            {
                // KMLファイル中は経度,緯度,高度の順番になっているので逆にする。高度には0しか入ってないので省略する。
                yield return (double.Parse(coordinates[1]), double.Parse(coordinates[0]));
            }
        }
    }
}

軌跡を描画

最後に、緯度・経度のタプル列をワールド座標に変換してLineRendererで描画します。

緯度・経度 (測地座標系) から平面直角座標系に変換するコードは以下の記事を参考にしました (長いので省略) 。 yubeshicat.hatenablog.com

変換の際に必要となる基準地点は、インポートしたモデルの中心座標35.634375, 139.7171875とします。 この値は、モデルの親GameObjectについているPLATEAUInstancedCityModelコンポーネントのLatitudeプロパティ・Longitudeプロパティで簡単に得られます。

また、モデルをシーン上で動かしたい場合は、変換により得られたワールド座標をさらにオフセットさせる必要があります。

LatLon2Coordinate()で得られる平面直角座標はZ-upの右手座標系なので、最後にUnity標準のY-upの左手座標系に変換します。

これまた手抜き気味ですが、以下のコンポーネントをシーン上に配置し、PLATEAUInstancedCityModelコンポーネントとLineRendererコンポーネントをアタッチすることで最低限動きます。

 public class KmlRenderer : MonoBehaviour
 {
     [SerializeField] private PLATEAUInstancedCityModel cityModel;
     [SerializeField] private LineRenderer lineRenderer;
 
    private void Start()
     {
         var kml = Resources.Load<TextAsset>("history-2023-04-09");
         var geodeticCoordinates = KmlParser.ExtractGeodeticCoordinates(kml.text);
         var worldPositions = geodeticCoordinates.Select(LatLon2WorldPosition).ToArray();
         lineRenderer.positionCount = worldPositions.Length;
         lineRenderer.SetPositions(worldPositions);
     }
 
    private Vector3 LatLon2WorldPosition((double latitude, double longitude) geodeticCoordinates)
     {
         var cityModelPosition = cityModel.transform.position;
         var coordinate = GeoCoordinateConverter.LatLon2Coordinate(geodeticCoordinates.latitude, geodeticCoordinates.longitude, cityModel.Latitude, cityModel.Longitude);
         return new Vector3((float)(coordinate.X - cityModelPosition.x), (float)(50.0 - cityModelPosition.y), (float)(coordinate.Z - cityModelPosition.z));
     }
}

先程エクスポートした2023-04-09の移動経路を描画した結果はこちらです。 一応予想通り(?)の結果が得られました。

目黒駅周辺。ちょっとだけおかしいような……?
アカツキゲームスが入居しているoak meguro周辺
引きの視点

今後の発展

ひとまず技術検証はできたので、自分にとっての理想形を得るために続きをやるとしたらここらへんかな〜と思ってます。

  • 目黒駅周辺の9マスだけでなく、23区全体、東京全体、日本全体をインポートして表示してみる
    • Unityがどこまで耐えられるか次第
  • 手作業で1日ずつエクスポートするのは大変なので、Googleマップのタイムライン機能から何らかの方法で過去のすべてのデータをKMLファイルにエクスポートしてみる
  • KMLファイルには経度・緯度情報しか無いので代わりに決め打ちで標高50mとしていたが、固定値ではないそれっぽい値をでっち上げてPLATEAU空間上に表示してみる
    • 地表のメッシュにRaycastしたときの交点のY座標を使う?
  • 「どこで渋滞によくハマるかの可視化」等、面白そうなことに繋げられそうなので、軌跡上の通過速度を何らかの手法で取ってみる
    • KMLファイル中にはPlacemark/TimeSpanに出発時刻begin・到着時刻endが含まれているが、これでは表定速度しか得られない
    • Googleマップのタイムライン機能の「元データ」には座標ごとの計測時刻が記録されてるので、それを何らかの方法で取得する?
    • 他のGPSロガーのデータを使う?

おわりに

ひとまずできることは分かったので、ぼちぼちいじってみて何か面白いことが分かったらまた記事にしようと思います! (エクスポート面倒問題がネックですね……)

明日のAkatsuki Games Advent Calendar 2023の記事はぐんそうさんの何かだそうです! お楽しみに〜


  1. 日本のサイズは1000kmオーダーなので、Unityの単精度floatを用いた座標系ではcmオーダーの誤差が出る。今回は気にするほどのレベルではない (GPSの計測誤差の方がよっぽど大きい) が、わざわざ誤差が大きくなる基準地を選ぶ理由も無いので素直に09を選択。
  2. Sceneビュー上に地理院地図っぽい地図が出てきて、GUI操作で範囲選択できる。リアルタイムに地理院地図のデータをダウンロードしてきているのか、拡大縮小したりすると重たい。
    マップ範囲選択
  3. 1km弱四方。今回は動くようになるところまでを試したかったので、時間とストレージ容量の節約のためにかなり狭めの範囲とした。
  4. ロケーション履歴を有効にしたAndroid端末を普段から使っていて、運転時にGoogleマップのナビを使っているため、運転時のGPSログが (明示的に計測開始/終了せずとも) 自動的にここに集約されており、データソースとしてちょうどよかったためこれを選んだ。ちなみに、Android Autoでのナビ中はGPSを受信できないトンネル中でも最後の地点から速度・加速度・地磁気等のセンシングデータを基に推定した地点を記録してくれる (車側とスマホ側のどっちが推定しているかは分からない) 。
    首都高4号新宿線からC2中央環状線に入ったときの軌跡。西新宿JCTで山手トンネルに入ってから徐々に北東方向にズレが拡大している
  5. Unityのベストプラクティス的にはResourceフォルダを使った実装方法は推奨されていないが、今回は「動けばいい」のでResourcesフォルダを使った