はじめに

今年の春ごろに発売された新型iPadProに搭載されている
LiDARという機能があります。

このLiDARをARKit3.5と組み合わせることで
より一層ハイエンドなARアプリの作成が可能となりました。

また、ARFoundationのアップデートにより、
ARKit3.5と同様の機能が利用可能となっています。(一部機能を除く)

今回はそのLiDARとARFoundationを組み合わせて
簡単なデモアプリの作成を行いました。

LiDARとは?→ LiDAR + ARKit3.5を調査してみました!
ARFoundationとは?→ ARFoundationを触ってみた


バージョン情報

Unity  2019.3.4f1
iPad Pro 2020  13.4
OS Mac Catalina  10.15.4
Xcode  11.5


デモ


まずはこちらのデモ動画をご覧ください。
緯度経度を入力してその土地の実際の雨量を反映させています。

仕組みは下記です。
①天井を認識
②リクエストヘッダーに位置情報をのせて気象情報のAPIにリクエスト送信
③レスポンスフィールドの雨量情報に応じて表示内容を変更

今回はUnityさんが公開してくださっているサンプルをベースとして
話を進めていきます。


下準備

下記サンプルをクローンしてきます。
Unityのバージョンは2019.3以降推奨です。

サンプルのリンク:AR Foundation Samples

こちらの記事でも紹介していますが、
ARKit3.5にはScene Geometryという周囲の空間を3Dメッシュ化し、
検知したメッシュにラベルを貼り付けることが可能な機能があります。

しかし、残念ながらARFoundationには
Scene Geometryの”周囲の空間を3Dメッシュ化”に該当する機能は
現状、ありません

画像引用元:About AR Foundation

そうは言ってもさすがはUnityさんです。

検知した平面にラベルを貼り付けることが可能な機能に関しては
用意してくれていました。

今回はその機能が盛り込まれたPlaneClassification
というシーンをベースとして、
天気の情報を天井及びAR空間に反映するアプリを作成しました。


PlaneClassificationLabeler

検知した平面のラベリング処理を担っているコードは
AR Session Originにアタッチされている
AR Plane Classification VisualizerというPrefabの中の
PlaneClassificationLabelerというクラスの中にあります。


コード

以下がコードの中身です。


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

/// <summary>
/// Manages the label and plane material color for each recognized plane based on 
/// the PlaneClassification enumeration defined in ARSubsystems. 
/// </summary>

[RequireComponent(typeof(ARPlane))]
[RequireComponent(typeof(MeshRenderer))]
public class PlaneClassificationLabeler : MonoBehaviour
{
    ARPlane m_ARPlane;
    MeshRenderer m_PlaneMeshRenderer;
    TextMesh m_TextMesh;
    GameObject m_TextObj;
    Vector3 m_TextFlipVec = new Vector3(0, 180, 0);

    void Awake()
    {
        m_ARPlane = GetComponent<ARPlane>();
        m_PlaneMeshRenderer = GetComponent<MeshRenderer>();

        // Setup label
        m_TextObj = new GameObject();
        m_TextMesh = m_TextObj.AddComponent<TextMesh>();
        m_TextMesh.characterSize = 0.05f;
        m_TextMesh.color = Color.black;
    }

    void Update()
    {
        UpdateLabel();
        UpdatePlaneColor();
    }

    void UpdateLabel()
    {
        // Update text
        m_TextMesh.text = m_ARPlane.classification.ToString();

        // Update Pose
        m_TextObj.transform.position = m_ARPlane.center;
        m_TextObj.transform.LookAt(Camera.main.transform);
        m_TextObj.transform.Rotate(m_TextFlipVec);
    }

    void UpdatePlaneColor()
    {
        Color planeMatColor = Color.cyan;

        switch (m_ARPlane.classification)
        {
            case PlaneClassification.None:
                planeMatColor = Color.cyan;        
                break;
            case PlaneClassification.Wall:
                planeMatColor = Color.white;        
                break;
            case PlaneClassification.Floor:
                planeMatColor = Color.green;        
                break;
            case PlaneClassification.Ceiling:
                planeMatColor = Color.blue;        
                break;
            case PlaneClassification.Table:
                planeMatColor = Color.yellow;        
                break;
            case PlaneClassification.Seat:
                planeMatColor = Color.magenta;        
                break;
            case PlaneClassification.Door:
                planeMatColor = Color.red;        
                break;
            case PlaneClassification.Window:
                planeMatColor = Color.clear;        
                break;
        }

        planeMatColor.a = 0.33f;                
        m_PlaneMeshRenderer.material.color = planeMatColor;
    }

    void OnDestroy()
    {
        Destroy(m_TextObj);
    }
}

ARPlaneというクラスに
PlaneClassificationというEnumが定義されています。

あとは認識したプレーンが
PlaneClassificationのどのステートに該当しているかを判定し、
処理を分岐させれば良いだけです。

非常に使いやすくなっているので大助かりでした。


実装

それでは実装です。
まずはAPIの利用です。
今回はYAHOOさんの気象情報APIを利用しました。


using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

/// <summary>
/// APIを利用して降水量取得
/// </summary>
public class RainfallRequest : MonoBehaviour
{
    [SerializeField] private InputField _latitudeInputField;
    [SerializeField] private InputField _longitudeInputField;
    [SerializeField] private Text _debugText;

    private string _url;
    private Coroutine _runningCoroutine;

    /// <summary>
    /// 降水量
    /// </summary>
    public static float Rainfall { get; private set; }

    /// <summary>
    /// 現在の降水量を取得
    /// Buttonのイベントに登録
    /// </summary>
    public void GetRainfall()
    {
        if (_runningCoroutine == null)
        {
            StartCoroutine(rainfallGetWebRequest());
        }
    }
    
    private IEnumerator rainfallGetWebRequest()
    {
        _url =
            //APIのリクエストパラメータ含むURL
            "https://map.yahooapis.jp/weather/V1/place?coordinates=" +
            //経度,緯度
            _longitudeInputField.text + "," + _latitudeInputField.text + "&appid=" +
            //クライアントID
            "登録時のクライアントID";
        
        //リクエスト
        UnityWebRequest request = UnityWebRequest.Get(_url);

        //リクエストが渡るまで待つ
        yield return request.SendWebRequest();

        //成功時
        if (string.IsNullOrEmpty(request.error))
        {
            //ファイル読み込み
            XDocument xml = XDocument.Parse(request.downloadHandler.text);
            XNamespace ns = xml.Root.Name.Namespace;
            XElement root = xml.Root;

            Debug.Log(xml);

            //必要なレスポンスフィールドを取得 
            IEnumerable<XElement> titles = root.Descendants(ns + "Rainfall");
            if (titles != null)
            {
                //現在時刻の降水量取得
                Rainfall = float.Parse(titles.FirstOrDefault().Value);
                _debugText.text = "雨量"+ Rainfall;
            }
        }
        //失敗時
        else
        {
            _debugText.text = "取得失敗";
        }

        _runningCoroutine = null;
    }
}

今回取得してきているのは現在時刻の降水量(Rainfall)です。

次に、取得した雨量のデータを別のクラスで利用します。


using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

/// <summary>
/// 天井を認識して空模様のプレーンを貼り付ける
/// </summary>
[RequireComponent(typeof(ARPlane))]
[RequireComponent(typeof(MeshRenderer))]
public class WeatherManager : MonoBehaviour
{
    [SerializeField] private Material _rainyMaterial;
    [SerializeField] private Material _sunnyMaterial;
    [SerializeField] private Material _transparentMaterial;
    [SerializeField] private ParticleSystem _rainEffectParticleSystem;

    private ARPlane _aRPlane;
    private MeshRenderer _planeMeshRenderer;
    private ParticleSystem.EmissionModule _emissionModule;
    private ParticleSystem.MinMaxCurve _minMaxCurve;

    private void Awake()
    {
        _aRPlane = GetComponent<ARPlane>();
        _planeMeshRenderer = GetComponent<MeshRenderer>();
        _emissionModule = _rainEffectParticleSystem.emission;
        _minMaxCurve = _emissionModule.rateOverTime;
    }

    private void Update()
    {
        //認識した平面が"天井"か判定
        if (_aRPlane.classification == PlaneClassification.Ceiling)
        {
            //雨の位置を認識した天井の中央に
            _rainEffectParticleSystem.transform.position = _aRPlane.center;

            //取得してきた雨量の情報で判定
            if (RainfallRequest.Rainfall > 0)
            {
                //雨雲のマテリアルに変更
                _planeMeshRenderer.material = _rainyMaterial;
            }
            else
            {
                //晴れ空のマテリアルに変更
                _planeMeshRenderer.material = _sunnyMaterial;
            }

            //Emissionの量を操作して雨量を調整
            _minMaxCurve.constant = RainfallRequest.Rainfall * 100f;
            _emissionModule.rateOverTime = _minMaxCurve;
        }
        else
        {
            //透明
            _planeMeshRenderer.material = _transparentMaterial;
        }
    }
}

認識した平面のうち、利用したいのは天井だけなので
PlaneClassification.Ceilingで判定を行います。

他の認識した平面に関しては透明なマテリアルで描画しないようにしました。


最後に

分類(Classification)を利用すれば、これまでは困難であった
特定の平面に対して任意の処理を行うという実装が簡単に実現できます。

加えて、LiDARの平面検知の速度は目を見張るものがあります。
より一層、現実をシームレスに拡張できるようになりました。

ARFoundationのアップデートも頻繁に行われているようなので
今後も要チェックです。