見出し画像

【LiDAR×ARFoundation】天井切り抜きお天気ARを試作してみた

今年の春ごろに発売された新型iPadProに搭載されている LiDAR という機能があります。
この LiDAR を ARKit3.5 と組み合わせることでより一層ハイエンドなARアプリの作成が可能となりました。
また、ARFoundation のアップデートにより、ARKit3.5 と同様の機能が利用可能となっています。(一部機能を除く)

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

参考記事:
・LiDARとは?→ LiDAR + ARKit3.5 を調査してみました!
・ARFoundationとは?→ ARFoundationをさわってみた

バージョン情報
・Unity 2019.3.4f1
・iPad Pro 2020 / iPadOS 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メッシュ化”に該当する機能は、現状ありません。

画像1

画像引用元:About AR Foundation

そうは言ってもさすがはUnityさんです。
検知した平面にラベルを貼り付けることが可能な機能に関しては用意してくれていました。

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

PlaneClassificationLabeler をつかってラベリングする

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

画像2

画像3

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


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を使って雨量を取得する

それでは実装です。

まずは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 のアップデートも頻繁に行われているようなので今後も要チェックです。

この記事が気に入ったらサポートをしてみませんか?