【Unity】StreetscapeGeometryAPIの使い方

## StreetscapeGeometryAPIとは？
周辺の都市データを3Dモデルとして取得可能です。実際の建造物と同じ位置に表示されるので、オクルージョンやARコンテンツの演出として利用可能です。

参考リンク：[Use buildings and terrain around you on Unity](https://developers.google.com/ar/develop/unity-arf/geospatial/streetscape-geometry)

## バージョン
| ツール、ライブラリ | バージョン |
|:-:|:-:|
|  Unity |  2022.3.46f1 |
|  [ARCore Extensions](https://github.com/google-ar/arcore-unity-extensions) | 1.46.0  |

## デモ
取得した都市モデルを半透明な状態で表示するサンプル、その都市モデルをオクルージョンに利用するサンプルです。地形のモデルも取れるので、ついでに表示しています。

LOD1が取得される場所

![SGOcclusionLOD1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/126977e0-f421-0b8b-bd08-6e8e355242bf.gif)

LOD2が取得される場所

![SGOcclusionLOD2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/64eab6e8-2c28-19cd-a158-259a5e11164e.gif)

## サンプルコード
Googleのサンプルだとカメラのfar clipが小さな値になっているので注意が必要です。

```CS
using System.Collections.Generic;
using Google.XR.ARCoreExtensions;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR.ARSubsystems;

public class StreetscapeGeometrySample : MonoBehaviour
{
    [SerializeField] private ARStreetscapeGeometryManager _streetscapeGeometryManager;
    [SerializeField] private Button _changeMaterialButton;
    [SerializeField] private Material _streetscapeGeometryMaterialForOcclusion;
    [SerializeField] private Material _streetscapeGeometryMaterial;
    [SerializeField] private GameObject _arObject;

    private readonly Dictionary<TrackableId, GameObject> _streetscapegeometryGOs = new();

    private List<ARStreetscapeGeometry> _addedStreetscapeGeometries = new();
    private List<ARStreetscapeGeometry> _updatedStreetscapeGeometries = new();
    private List<ARStreetscapeGeometry> _removedStreetscapeGeometries = new();

    private enum MaterialType
    {
        Default,
        Occlusion
    }

    private MaterialType _currentMaterialType = MaterialType.Default;

    public void Awake()
    {
        _streetscapeGeometryManager.StreetscapeGeometriesChanged += GetStreetscapeGeometry;
        _changeMaterialButton.onClick.AddListener(ChangeMaterial);
    }

    private void Update()
    {
        UpdateGeometryGOs();

        _arObject.SetActive(_currentMaterialType == MaterialType.Occlusion);
    }

    public void OnDestroy()
    {
        _streetscapeGeometryManager.StreetscapeGeometriesChanged -= GetStreetscapeGeometry;
        _changeMaterialButton.onClick.RemoveListener(ChangeMaterial);
    }

    /// <summary>
    /// StreetscapeGeometryが生成したオブジェクトのマテリアルを変更する。
    /// </summary>
    private void ChangeMaterial()
    {
        var streetscapeGeometryGameObjectList = new List<GameObject>(_streetscapegeometryGOs.Values);
        if (streetscapeGeometryGameObjectList.Count == 0) return;

        if (_currentMaterialType == MaterialType.Default)
        {
            streetscapeGeometryGameObjectList.ForEach(go =>
                SetMaterialToGeometry(go, _streetscapeGeometryMaterialForOcclusion));
            _currentMaterialType = MaterialType.Occlusion;
        }
        else
        {
            streetscapeGeometryGameObjectList.ForEach(go => SetMaterialToGeometry(go, _streetscapeGeometryMaterial));
            _currentMaterialType = MaterialType.Default;
        }
    }


    /// <summary>
    /// StreetscapeGeometryによるオブジェクトの生成、更新、削除を行う。
    /// </summary>
    private void UpdateGeometryGOs()
    {
        foreach (var geometry in _addedStreetscapeGeometries)
        {
            InstantiateRenderObject(geometry);
        }

        foreach (var geometry in _updatedStreetscapeGeometries)
        {
            InstantiateRenderObject(geometry);
            UpdateRenderObject(geometry);
        }

        foreach (var geometry in _removedStreetscapeGeometries)
        {
            DestroyRenderObject(geometry);
        }
    }

    /// <summary>
    /// StreetscapeGeometryの機能を利用して地形/ビル群のオブジェクトを生成する。
    /// </summary>
    private void InstantiateRenderObject(ARStreetscapeGeometry streetscapeGeometry)
    {
        if (streetscapeGeometry.mesh == null) return;
        if (_streetscapegeometryGOs.ContainsKey(streetscapeGeometry.trackableId)) return;

        var geometry = new GameObject("StreetscapeGeometryMesh", typeof(MeshFilter), typeof(MeshRenderer));
        geometry.transform.position = streetscapeGeometry.pose.position;
        geometry.transform.rotation = streetscapeGeometry.pose.rotation;
        geometry.GetComponent<MeshFilter>().mesh = streetscapeGeometry.mesh;
        SetMaterialToGeometry(geometry, _streetscapeGeometryMaterial);
        _currentMaterialType = MaterialType.Default;
        _streetscapegeometryGOs.Add(streetscapeGeometry.trackableId, geometry);
    }

    /// <summary>
    /// StreetscapeGeometryが生成した既存のオブジェクトの位置と回転を更新する。
    /// </summary>
    private void UpdateRenderObject(ARStreetscapeGeometry streetscapeGeometry)
    {
        if (_streetscapegeometryGOs.TryGetValue(streetscapeGeometry.trackableId, out var geometry))
        {
            geometry.transform.position = streetscapeGeometry.pose.position;
            geometry.transform.rotation = streetscapeGeometry.pose.rotation;
        }
    }

    /// <summary>
    /// StreetscapeGeometryが生成したオブジェクトを削除する。
    /// </summary>
    private void DestroyRenderObject(ARStreetscapeGeometry streetscapeGeometry)
    {
        if (_streetscapegeometryGOs.TryGetValue(streetscapeGeometry.trackableId, out var geometry))
        {
            _streetscapegeometryGOs.Remove(streetscapeGeometry.trackableId);
            Destroy(geometry);
        }
    }

    /// <summary>
    /// 対象のオブジェクトにマテリアルを設定する。
    /// </summary>
    private void SetMaterialToGeometry(GameObject go, Material material)
    {
        go.GetComponent<MeshRenderer>().material = material;
    }

    /// <summary>
    /// StreetscapeGeometryの変更イベントを受け取り、リストを更新する。
    /// </summary>
    private void GetStreetscapeGeometry(ARStreetscapeGeometriesChangedEventArgs eventArgs)
    {
        _addedStreetscapeGeometries = eventArgs.Added;
        _updatedStreetscapeGeometries = eventArgs.Updated;
        _removedStreetscapeGeometries = eventArgs.Removed;
    }
}
```

## LOD1/LOD2について
StreetscapeGeometryで取得できるモデルにはLOD1、LOD2の2種類が存在します。
LOD2について、冒頭で紹介したようなオクルージョンやARコンテンツの演出として利用可能な品質ですが、LOD1は正直使えません。デモで示した通り、形状はただの立方体で、高さも合っていません。

そして、これは開発者側でコントロールできるものではなく、Googleの気まぐれでどちらが取得できるかが決まっています。

よって、アプリ側ではジオメトリの[quality](https://developers.google.com/ar/reference/unity-arf/class/Google/XR/ARCoreExtensions/ARStreetscapeGeometry#quality)の値をチェックして、LOD1は弾くような実装があると良いかと思います。

ただ、未解決の問題として、"事前にそのエリアのLODの品質を判別できないこと"が残されています。この件について、issueを立てて問い合わせてみたところ、どうやら、"LOD1が存在する可能性"の高い場所はGoogleMapから読み取ることができるようです。

issue：[How can I check LOD before using Streetscape Geometry API?](https://github.com/google-ar/arcore-unity-extensions/issues/221)

以下GIFのようにGoogleMapを航空写真として表示し、マップを斜めにずらすことでそのエリアにLOD1が存在する可能性を確認できます。

![SG_Map.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/4f57935c-4481-7967-a111-a3dc4145ed39.gif)

もし、建物の場所に3次元表示された都市モデルではなく、2Dの平面的な表示(以下画像赤枠部分)となっている場合、その近辺はLOD1の存在する可能性が高くなるそうです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/64428b9f-5f3c-49cb-df06-0ab45afc966d.png)