PLATEAU SDKを用いた都市モデルのインポートと現実との位置合わせの方法

# はじめに
先日、ハッカソンでGeospatialAPIとPLATEAUの都市モデルを使用する機会があり、都市モデルを現実と位置合わせする過程で色々詰まった点があったので備忘録も兼ねてここに書き残そうと思います。

:::note warn
2023/01/21時点でのSDKの最新バージョンv0.6.0ではWindowsのみの対応です。Macでの開発を行っている方はより最新のバージョンで対応されていればそちらを、対応してなければWindowsでインポート→Export機能でMacにデータを移動させるというような対処をしてください。
:::

#### 開発環境
| 名前 | バージョン |
|:-:|:-:|
| Unity  | 2020.3.40f1  |
| PLATEAU SDK for Unity  | v0.6.0  |

# GeospatialAPIの導入
現実との位置合わせをするには、前提としてGeospatialAPIが使える状況でなければならないので、現時点でその準備がまだという方は下記記事を参考にして、環境を整えてください。

https://zenn.dev/tkada/articles/04b44474149130

# PLATEAU SDKの導入
これまでPLATEAUの都市モデルをUnityで利用しようと思うと色々と複雑な手順を踏まなければなりませんでしたが、PLATEAU公式によってまだβ版(2023/01/21時点)ですが、比較的容易に都市モデルをUnity内にインポートできるようになりました。

:::note
PLATEAU SDKの導入方法は公式でも[こちら](https://synesthesias.github.io/PLATEAU-SDK-for-Unity/index.html)で説明がされています。不明瞭な点があれば公式サイトを参照してください。
:::

PLATEAU SDKは下記URLのGitHubにて公開されています。Releasesから最新バージョンを選択し、tgzファイルをダウンロードした後、自身の使用するUnityプロジェクト内のPackageフォルダ直下に置いてください。

https://github.com/Synesthesias/PLATEAU-SDK-for-Unity

メニューバーからWindow→Package Managerを選択し、左上の「+」マークから「Add package from tarball」を使用し、先ほどダウンロードしたtgzファイルを開きます。インポートが完了すると、メニューバーにPLATEAUが追加されます。


![PLATEAUMenuBar.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111921/5dc557c7-3d98-fe8f-3de6-fed383d981c3.png)

# CityGMLファイルのダウンロード
早速PLATEAU SDKを使用したいところですが、これを使用するにはローカルでCityGMLファイルをダウンロードしておく必要があります。(2023/01/21時点ではデータサーバーからのダウンロードは未実装であるためです。将来的にデータサーバーから都市モデルのインポートが可能になればこの作業は必要なくなると思われます。)

各都市のCityGMLファイルは下記のリンクからダウンロードします。

https://www.mlit.go.jp/plateau/open-data/

![PLATEAU_CityGML.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111921/c6650b8b-5560-828f-7825-f1f61740c412.png)
※各都市で提供されているデータ形式には差がありますが、CityGML形式はどの都市でもあります。

# PLATEAU SDKを使用してインポート

これでPLATEAU SDKを利用する準備は整ったので早速インポートしてみましょう！

メニューバーのPLATEAU→PLATEAU SDKを選択すると下のようなウィンドウが出てきます。

![PLATEAU_SDK_Import.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111921/52652f33-3868-e923-4f99-01d21918b539.png)

ここで入力フォルダやインポートする範囲の設定などを設定していきます。各項目について簡単に説明すると・・・

- 入力フォルダ : 先ほどダウンロードしたCityGMLファイルで、直下にudxというフォルダがあるものを選択してください。
- 基準座標系 : 各都道府県に合ったものを選択してください。
- 範囲選択 : 押すとシーンビューの表示が切り替わり下のような範囲選択画面になります。
![PLATEAU_SDK_Decision.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111921/0ac2c673-b4a7-7bc3-98c9-725185c380c8.png)
範囲を示すオレンジ色の四角形を自由な大きさと位置に調整して左上の「決定」を押してください。
- 地物別設定 : 建築物や土地起伏などをインポートする/しないを決めます。建築物のみでも問題ありません。各々の活用方法に沿って決めてください。

これらの設定を全て行ったら、一番下の「モデルをインポート」を押します。よっぽど大きい範囲でなければ、比較的すぐにシーン上に配置されると思います。

:::note alert
インポートされたモデルはPrefab化して再配置を試みるとメッシュファイルやマテリアルの参照が切れてしまい、利用できなくなってしまいます。もし、間違ってPrefab化してしまった場合はもう一度インポートしてください。(どうしてもPrefab化したい場合はPLATEAU SDKで実装されているExport機能を利用してください)
:::

参考 : [【Unity】PLATEAU SDK for Unityの基本的な利用方法～利用時の注意点](https://qiita.com/OKsaiyowa/items/8dd405a76c5edfb5a64e)

# 現実との位置合わせ

シーン上に都市モデルをインポートできたので、これを現実と位置合わせしていきます。インポートされた都市モデルには下のようなスクリプトがアタッチされており、「位置」の項目にこの都市モデルの原点に対応する緯度、経度、高度が示されているためこれらを利用して位置合わせをします。ただし、高度は実際には0ではないので緯度経度から調べる必要があり、標高とジオイド高の2つが必要となります。
![PLATEAU_Script.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111921/437507d6-d132-50a5-e9aa-c72a904681b9.png)
下記の2つのサイトで標高とジオイド高をそれぞれ調べます。ジオイド高の取得の際、入力単位を十進法度単位とすることに注意してください。

https://vldb.gsi.go.jp/sokuchi/surveycalc/geoid/calcgh/calcframe.html

https://maps.gsi.go.jp/development/elevation.html

GeospatialAPIでは、任意の緯度、経度、高度、回転を指定することで、その位置にオブジェクトを設置することができます。以下がそのスクリプトと参考にした記事です。

```c#:BuildingsGeospatialController
using Google.XR.ARCoreExtensions;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

/// <summary>
/// アンカーの位置にオブジェクトを表示する
/// </summary>
public class BuildingsGeospatialController : MonoBehaviour
{
    [SerializeField] private ARAnchorManager arAnchorManager;
    [SerializeField] private AREarthManager arEarthManager;
    [SerializeField] private Transform arObject;

    [SerializeField] private double latitude;
    [SerializeField] private double longitude;
    [SerializeField] private double altitude;

    private const double VERTICAL_THRESHOLD = 25;
    private const double HOLIZONTAL_THRESHOLD = 25;
    
    private ARGeospatialAnchor anchor;

    private void Update()
    {
        //UnityEditorではAREarthManagerが動作しないのでスキップ
        if (Application.isEditor)
        {
            return;
        }
        
        //ARFoundationのトラッキング準備が完了するまで何もしない
        if (ARSession.state != ARSessionState.SessionTracking)
        {
            return;
        }
        
        if (!IsSupportedDevice())
        {
            return;
        }

        if (!IsHighAccuracyDeviceEarthPosition())
        {
            return;
        }
        
        if (IsExistGeoSpatialAnchor(latitude, longitude, altitude))
        {
            Adjust();
        }
    }

    /// <summary>
    /// 対応端末かチェック
    /// </summary>
    /// <returns>対応端末であればTrueを返す</returns>
    private bool IsSupportedDevice()
    {
        return arEarthManager.IsGeospatialModeSupported(GeospatialMode.Enabled) != FeatureSupported.Unsupported;
    }

    /// <summary>
    /// デバイスの位置精度をチェック
    /// </summary>
    /// <returns>閾値以上の位置精度であればTrueを返す</returns>
    private bool IsHighAccuracyDeviceEarthPosition()
    {
        //EarthTrackingStateが準備できていない場合
        if (arEarthManager.EarthTrackingState != TrackingState.Tracking) return false;

        //自身の端末の位置を取得し、精度が高い位置情報が取得できているか確認する
        var pose = arEarthManager.CameraGeospatialPose;
        var verticalAccuracy = pose.VerticalAccuracy;
        var horizontalAccuracy = pose.HorizontalAccuracy;

        //位置情報が安定していない場合
        if (verticalAccuracy > VERTICAL_THRESHOLD && horizontalAccuracy > HOLIZONTAL_THRESHOLD) return false;
        
        return true;
    }

    /// <summary>
    /// アンカーの位置にオブジェクトを出す
    /// </summary>
    private void Adjust()
    {
        arObject.SetPositionAndRotation(anchor.transform.position,anchor.transform.rotation);
    }

    /// <summary>
    /// アンカーの存在を確認
    /// なければ追加
    /// </summary>
    private bool IsExistGeoSpatialAnchor(double lat, double lng, double alt)
    {
        //EarthTrackingStateの準備ができていない場合は処理しない
        if (arEarthManager.EarthTrackingState != TrackingState.Tracking)
        {
            return false;
        }

        if (anchor == null)
        {
            //GeoSpatialアンカーを作成
            //この瞬間に反映されるわけではなくARGeospatialAnchorのUpdate関数で毎フレーム補正がかかる
            var offsetRotation = Quaternion.AngleAxis(180f, Vector3.up);
            anchor = arAnchorManager.AddAnchor(lat, lng, alt, offsetRotation);
        }

        return anchor != null;
    }
}
```
https://qiita.com/OKsaiyowa/items/c2a2da503d71eb3af5bd

参考記事内で紹介されているサンプルコードとほぼ同じで不必要な機能だけ削った形になっています。このスクリプトを適当なオブジェクトにアタッチし、latitude, longitude, altitudeのそれぞれに緯度、経度、**ジオイド高**を入力してください。

また、取得した**標高**は都市モデルの親オブジェクト(PLATEAU Instanced City Model のスクリプトがアタッチされているオブジェクト)のY座標に入力してください。

### 高度について補足
急にジオイド高や標高が出てきましたが、ここでちょっと補足。
GeospatialAPIでアンカーにその地点の高度を渡す場合、楕円体高というものが必要となります。楕円体高は標高とジオイド高の和なのでこの2つを取得したということです。

![Geospatial_altitude.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111921/ffa317dd-c565-d3ba-b55d-5d5845e585a1.jpeg)

参考 : [ARCore Geospatial APIハンズオン byAR Fukuoka | ドクセル](https://www.docswell.com/s/Tks_Yoshinaga/Z86Q2K-geospatial-api#p201)

以上で都市モデルのインポートと現実との位置合わせは完了です！
ビルドを行って実機で確認すれば、建物とほぼピッタリに都市モデルが配置されると思います。(完全に位置合わせされるまでに少々時間がかかる場合があります。)

あとはこれにオクルージョンの機能などを追加すれば、AR空間上の物体が自然な形で出現したり、建物に隠れて見えなくなったりすることができ、ARアプリ開発に役立ちそうです。

# 参考文献(記事内重複あり)

https://zenn.dev/tkada/articles/04b44474149130

https://synesthesias.github.io/PLATEAU-SDK-for-Unity/index.html

https://qiita.com/OKsaiyowa/items/c2a2da503d71eb3af5bd

https://www.docswell.com/s/Tks_Yoshinaga/Z86Q2K-geospatial-api#p201

https://qiita.com/OKsaiyowa/items/8dd405a76c5edfb5a64e

# 終わりに

今回はPLATEAU SDKを用いた都市モデルのインポートと現実との位置合わせについてまとめました。初めての記事投稿なので、分かりにくい部分・間違っている部分などありましたら教えていただけると幸いです。