見出し画像

Magic Leap 1 PCFs:ローカル - Unity

概要

Magic Leap 1では、マッピングされた現実世界と紐づけてデジタルコンテンツのポジションを復元することができます。

これを実現する上でPCFsと呼ばれるアンカーの座標を作成が必要です。今回は、Unity Editor 2020.3.x LTS + Lumin SDK 0.26 の環境下で、簡易なサンプルアプリケーションの構築方法を説明します。

この記事は、Magic Leap Developer Portal の 「Content Persistence: Local - Unity (Last updated: September 27, 2021)」の内容を元に作成した記事になります。

Persistent Coordinate Frames(PCF)について

Magic Leap 1は、環境のローカライズとマッピングを同時に行うことができます。 これを実現するためにに、Magic Leap 1は、Persistent Coordinate Frames(PCFs)というアンカーとなる静的な座標情報を作成します。 次に、作成されたPCFsをグループ化してマップを作成。そして、PCFsをグループ化してマップを作成し、Magic Leap 1はこのマップを使い、この場所が新しい場所であるか、既にマップ済みの場所なのかを判断します。

ユーザーは、マッピング設定(Settings>Privacy > Mapping)を行うことで、マッピングされた座標をセッション間で保持するかどうかを決定できます。

On Device

On Device のストレージでは、ユーザーのマップがMagic Leap 1のローカルに保存され、Magic Leap のクラウドサーバーには送信されません。マップをデバイスに保存すると、マップの生成とローカライズは、OS によって処理されます。マップは、ユーザーとアプリケーションで引き続き使用できます。ただし、デバイスに保存できるマップの量には制限があり、デバイスのメモリがいっぱいになると、過去のマップは上書きされます。その結果、Magic Leap 1が過去のエリアに紐づくマップの作成に、多くの時間を費やす可能性があります。

Personal World

Personal World に World Features(※1) はMagic Leap のクラウドサーバーに保存されます。(World Features は Magic Leap 1 ユーザーには、共有されません。)Personal World に World Features を保存すると、アプリはデバイスセッション間でコンテンツを保持し、次回アクセスしたときに同じ場所にコンテンツが表示されます。

(※1)World Features とは、周囲の点群データであり、マップの基本的な構成要素として機能しています。World Features のポイントは、コンテンツの配置および保存するための情報として使用しています。

Shared World

Shared World を使用すると、同じエリアで且つ、別々のMagic Leap 1 デバイスによって作成された各マップの World Featuresは、Magic Leap のクラウドサーバーに保存され、別々のマップがマージされて保存します。つまり、マッピングされたエリア内の World Featuresの結果は、そのエリアの集合マップとして提供され、すべての Shared World ユーザーが使用できるようになります。Shared World を使用すると、一度もエリアに訪れたことがないMagic Leap 1 ユーザー(Shared Worldが設定している状態)が
そのエリアに訪れたとしてもマッピングすることなく、必要がなくなります。マップを Shared World に提供すると、Magic Leap のクラウドサーバーに保存されマップを利用することができます。これにより共有および他のマルチユーザーの体験が向上し、同じエリアで共有体験することができます。

構築 / 実装

ここではUnity Editorを使用し、Magic Leap 1上でPCFsと紐づけるオブジェクトを配置するサンプルアプリケーションを構築します。(以下のようなことを行うアプリケーションになります。)

・Controlの位置にオブジェクトをインスタンス化。
・PCFsサービスを初期化。
・PCFsを検索。
・オブジェクトをPCFsの位置にバインド。
・アプリの再起動時にオブジェクトを再配置。

開発環境

Lumin SDK 0.26 + Unity Editor 2020.3

はじめる前に

プロジェクトがLuminに設定され、Lumin SDKパッケージがインポートされていることを確認します。

Privileges(特権)

PcfReadの設定が必要です。今回、Controlを使用するためController Poseの設定も行います。

API Level

Level 6 以上の設定が必要です。

1. 新規シーンの作成とCameraの置き換え

新規シーンを作成後、デフォルトの「Main Camera」を「Magic Leap Main Camera」に置き換えます。

ヒエラルキーにある、Main Cameraを削除します。

Package → Magic Leap SDK → Tools  → Prefabs → Main Camera prefabをヒエラルキーにドラッグ&ドロップします。

画像1

Persistent Object Prefab の作成

Persistent Object を可視化するためのPrefabを作成します。

シーン内にCubeを新規作成します。名前は PersistentObject とします。
スケールを 0.20 , 0.20 , 0.20 に設定します。
これを Assets フォルダにドラッグしてPrefabを作成します。
ヒエラルキーにあるPersistentObjectを削除します。

2. Persistent Object の生成処理

ここでは、ControlのBumperキーを押したときに、Controlの位置と回転を反映させてPersistent Objectを生成するスクリプトを作成します。

スクリプトの作成

using UnityEngine.XR.MagicLeap;
using UnityEngine;

public class PersistentContentExample : MonoBehaviour
{
   // Start is called before the first frame update
   void Start()
   {
       
   }
}

PersistentContentExampleという新しいスクリプトを作成します。スクリプトを開き、Update()メソッドを削除します。スクリプトの先頭にusing UnityEngine.XR.MagicLeap宣言を追加します。

3. Control のボタンイベントを登録

using UnityEngine;
using UnityEngine.XR.MagicLeap;

public class PersistentContentExample : MonoBehaviour
{
   // Start is called before the first frame update
   void Start()
   {
 
#if PLATFORM_LUMIN
       MLInput.OnControllerButtonDown += MLInputOnOnControllerButtonDown;
#endif

   }

#if PLATFORM_LUMIN
   private void MLInputOnOnControllerButtonDown(byte controllerId, MLInput.Controller.Button button)
   {
 
     //TODO

   }
#endif

}

MLInput.OnControllerButtonDown`イベントを利用することで、Control のボタンが押されたことを検知することができます。

まず、MLInputOnOnControllerButtonDownというメソッドを作成し、byteとMLInput.Controller.Buttonを受け取ります。これらの値は、Control の IDと、どのボタンが押されたかを表します。次に、このメソッドを MLInput.OnControllerButtonDown イベントにサブスクライブします。

注意: Magic Leap特有のロジックは、他のプラットフォームでの競合を避けるために、#if PLATFORM_LUMINでラップしてください。

4. PersistentObjectのインスタンス化

using UnityEngine;
using UnityEngine.XR.MagicLeap;

public class PersistentContentExample : MonoBehaviour
{
   public GameObject PersistentObject;
   // Start is called before the first frame update
   void Start()
   {

#if PLATFORM_LUMIN
       MLInput.OnControllerButtonDown += MLInputOnOnControllerButtonDown;
#endif

   }

#if PLATFORM_LUMIN
   private void MLInputOnOnControllerButtonDown(byte controllerId, MLInput.Controller.Button button)
   {

       if (button == MLInput.Controller.Button.Bumper)
       {
           var controller = MLInput.GetController(controllerId);

           var persistentObject = Instantiate(PersistentObject, controller.Position, controller.Orientation);
       }

   }
#endif

}

PersistentObjectを作成する前に、そのオブジェクトを参照する必要があります。 Start()メソッドの上に、PersistentObjectという名前のパブリックなGameObjectを宣言します。

これで、MLInputOnOnControllerButtonDown(...)メソッドでPersistentObjectのインスタンスを生成することができます。 まず、ボタンを比較して、それが MLInput.Controller.Button.Bumper であることを確認し、次に controllerId を使ってプレイヤーのコントローラを参照します。最後に、コントローラの位置と回転を使って、PersistentObjectのインスタンスを作成します。

画像2

このスクリプトをGameObjectにアタッチして、PrafabにあるPersistentObjectを割り当てます。

5. Magic Leap 1再起動後も生成されたPersistentObjectの位置を復元させる方法

Magic Leap 1再起動後も生成されたPersistentObjectの位置を復元させるためには、生成されたPersistentObjectの位置情報と回転情報を保存する必要があります。

6. Persistent Coordinate Frames サービスの開始

void Start()
   {
#if PLATFORM_LUMIN

       MLResult result = MLPersistentCoordinateFrames.Start();

       if (!result.IsOk)
       {
           Debug.LogError("Error: Failed starting MLPersistentCoordinateFrames, disabling script. Reason:" + result);

           enabled = false;
           return;
       }

       MLInput.OnControllerButtonDown += MLInputOnOnControllerButtonDown;

#endif
   }

Startメソッドの中で、#if PLATFORM_LUMINを追加して、Lumin Platformをターゲットにしているときだけコードがコンパイルするようにします。 そして、Magic Leap 1に、MLPersistentCoordinateFrames.Start()を呼び出して、Persistent Coordinate Framesの検索を開始するように指示します。 メソッドの結果から、リクエストが成功したかどうかを知ることができます。もし成功しなかった場合は、Debug.LogError()を使ってユーザーにエラーを知らせ、スクリプトを無効にします。

7. PCFにオブジェクトをバインドする

private void MLInputOnOnControllerButtonDown(byte controllerId, MLInput.Controller.Button button)
   {

       if (button == MLInput.Controller.Button.Bumper && MLPersistentCoordinateFrames.IsLocalized)
       {
           var controller = MLInput.GetController(controllerId);

           var persistentObject = Instantiate(PersistentObject, controller.Position, controller.Orientation);

           MLPersistentCoordinateFrames.FindClosestPCF(controller.Position, out MLPersistentCoordinateFrames.PCF pcf); 

           var persistentBinding = new TransformBinding(persistentObject.GetInstanceID().ToString(), "exampleItem");

           persistentBinding.Bind(pcf, persistentObject.transform);
       }

   }

PCFサービスは動作しているため、オブジェクトをPCF座標にバインドするためのロジックを作成することができます。これは、MLInputOnOnControllerButtonDownメソッドで行います。

オブジェクトをバインドする前に...

MLPersistentCoordinateFrames.IsLocalizedを使って、Magic Leap 1がローカライズされているかどうかをチェックする必要があります。オブジェクトをインスタンス化する前に、この引数を条件チェックに追加します。

ローカライズされたことを確認し、新しいオブジェクトをインスタンス化した後、シーン内でPCFを見つける必要があります。オブジェクトを最も近いPCFにバインドするのがベストです。この場合、MLPersistentCoordinateFrames.FindClosestPCFを使用します。MLPersistentCoordinateFrames.FindClosestPCFは、ワールドポジションを入力として受け取り、最も近いPCFを返します。この例では、最初のパラメータにControlの位置を使用します。

追加情報:FindClosestPCFメソッドは、探しているPCFの種類や、PCFの位置を更新するかどうかなどの追加の引数を渡すことができます。

最も近いPCFが見つかった場合、Transform Bindingクラスにアクセスするために、using MagicLeap.Core宣言をスクリプトの先頭に追加します。

using UnityEngine;
using UnityEngine.XR.MagicLeap;
using MagicLeap.Core;

public class PersistentContentExample : MonoBehaviour
​

最も近い PCF を見つけたら、新しい TransformBindingを作成します。idパラメータにはpersistentObject.GetInstanceID()を使用してインスタンス化されたオブジェクトのInstanceIDを使用し、prefabTypeパラメータには "exampleItem "を使用します。渡された値は保存され、再起動時にオブジェクトを復元するために参照することができます。

TransformBinding のコンストラクタは 2 つの引数を取ります。1 つ目は ID 用のユニークな文字列で、2 つ目は複数の TransformBinding 間で共有できる文字列です。2 番目の引数は、タイプまたはプレハブへのパスを記述するために使用できます。

これで、Transform Binding を Bind、Unbind、および Update できるようになりました。新しく作成された Transform Binding で Bindを呼び出します。最も近い PCF と persistentObject の Transform を渡します。

8. バウンドオブジェクトの復元

using UnityEngine;
using UnityEngine.XR.MagicLeap;
using MagicLeap.Core;
using System.Collections.Generic;

public class PersistentContentExample : MonoBehaviour
{
   public GameObject PersistentObject;
   // Start is called before the first frame update
   void Start()
   {
#if PLATFORM_LUMIN
       MLResult result = MLPersistentCoordinateFrames.Start();

       if (!result.IsOk)
       {
           Debug.LogError("Error: Failed starting MLPersistentCoordinateFrames, disabling script. Reason:" + result);

           enabled = false;
           return;
       }

       MLPersistentCoordinateFrames.OnLocalized += HandleOnLocalized;

       MLInput.OnControllerButtonDown += MLInputOnOnControllerButtonDown;
#endif
   }

#if PLATFORM_LUMIN
   private void MLInputOnOnControllerButtonDown(byte controllerId, MLInput.Controller.Button button)
   {
       if (button == MLInput.Controller.Button.Bumper && MLPersistentCoordinateFrames.IsLocalized)
       {
           var controller = MLInput.GetController(controllerId);

           var persistentObject = Instantiate(PersistentObject, controller.Position, controller.Orientation);

           MLPersistentCoordinateFrames.FindClosestPCF(controller.Position, out MLPersistentCoordinateFrames.PCF pcf); 

           var persistentBinding = new TransformBinding(persistentObject.GetInstanceID().ToString(), "exampleItem");

           persistentBinding.Bind(pcf, persistentObject.transform);
       }
   }

   private void HandleOnLocalized(bool localized)
   {
       TransformBinding.storage.LoadFromFile();

       List<TransformBinding> allBindings = TransformBinding.storage.Bindings;

       foreach (TransformBinding storedBinding in allBindings)
       {
           // Try to find the PCF with the stored CFUID.
           MLResult result = MLPersistentCoordinateFrames.FindPCFByCFUID(storedBinding.PCF.CFUID, out MLPersistentCoordinateFrames.PCF pcf);

           if (pcf != null && MLResult.IsOK(pcf.CurrentResultCode))
           {
               GameObject gameObj = Instantiate(PersistentObject, Vector3.zero, Quaternion.identity);

               storedBinding.Bind(pcf, gameObj.transform, true);
           }
       }
   }
#endif
}

これで、オブジェクトはPCFにバインドされ、アプリケーションの再起動後にリストアできるようになりました。これを行うために、HandleOnLocalizedというメソッドを作成し、boolの引数を受け取れるようにします。この条件はローカリゼーションの状態を表します。Start()メソッドの中で、MLPersistentCoordinateFrames.OnLocalizedイベントに新しいメソッドをサブスクライブします。これにより、ローカリゼーションの状態が変化したときに、このイベントが呼び出されるようになります。

このメソッドでは、デバイスのストレージにアクセスして、以前のバインディングを探します。まず、スクリプトの先頭に using System.Collections.Generic 宣言を追加して、List<T> クラスにアクセスできるようにします。次に、HandleOnLocalizedメソッドで TransformBinding.storage.LoadFromFile()を呼び出し、保存されているデータを読み込みます。

TransformBinding のローカル参照を作成しallBindings を呼びます。このリストには、現在のセッションだけでなく前のセッションのバインディングも含まれます。その後、リストの繰り返し処理を実施し、バインディングのPCFの存在チェックを行います。最後にPersistentObjectを作成して、バインディングを再バインドします。バインディングで新しいオブジェクトを最後に保存した位置に移動させるためには、Bindメソッドを呼び出す際に第3引数に必ずtrueを設定する必要があります。

9. 重複したアイテムの修正

public class PersistentContentExample : MonoBehaviour
{

...
   //Track the objects we already created to avoid duplicates
   private Dictionary<string, GameObject> _persistentObjectsById = new Dictionary<string, GameObject>();

...

//Called when the Magic Leap's localization status changes.
   private void HandleOnLocalized(bool localized)
   {
       //Read the saved files from storage
       TransformBinding.storage.LoadFromFile();

       //Cache the reference to the Transform Bindings
       List<TransformBinding> allBindings = TransformBinding.storage.Bindings;

       //Debug that the bindings are being restored
       Debug.Log("Getting saved bindings..." );

       foreach (TransformBinding storedBinding in allBindings)
       {
           // Try to find the PCF with the stored CFUID.
           MLResult result = MLPersistentCoordinateFrames.FindPCFByCFUID(storedBinding.PCF.CFUID, out MLPersistentCoordinateFrames.PCF pcf);

           //If the current map contains the PCF and the PCF is being tracked and we have not created the object already...
           if (pcf != null && MLResult.IsOK(pcf.CurrentResultCode) && _persistentObjectsById.ContainsKey(storedBinding.Id) == false)
           {
               //Create a the persistent content
               GameObject gameObj = Instantiate(PersistentObject, Vector3.zero, Quaternion.identity);

               //Bind the new object to the Transform binding. Setting the "regain" condition true, will position the transform at the saved position.
               storedBinding.Bind(pcf, gameObj.transform, true);

               //Track the created object to avoid duplicates.
               _persistentObjectsById.Add(storedBinding.Id, gameObj);

               //Debug that a binding was restored.
               Debug.Log("Restored bound transform at PCF : " + pcf.CFUID);

           }
       }
   }
}

今、このアプリケーションを実行してみると、同じオブジェクトが何度も作成されていることに気づくかもしれません。この問題を解決するには、すでにオブジェクトを持っているバインディングを追跡する必要があります。そのためには、キーを文字列、値をGameObjectとしたDictionaryを作成します。

そして、HandleOnLocalizedメソッドでオブジェクトをインスタンス化する前に、オブジェクトが作成されているかどうかをチェックします。作成されていない場合は、インスタンス化してDictionaryに追加します。Transform Binding ID をキーとして、インスタンス化されたオブジェクトを値として使用します。

同様にMLInputOnControllerButtonDownメソッドでオブジェクトを生成した後、インスタンス化したオブジェクトをDictionaryに追加します。

10. PersistentContentExample のソースコード

今まで説明した全文のソースコードが以下になります。

using UnityEngine;
using UnityEngine.XR.MagicLeap;
using MagicLeap.Core;
using System.Collections.Generic;

public class PersistentContentExample : MonoBehaviour
{
   [Tooltip("The object that will be created when pressing the bumper, and will persist between reboots.")]
   public GameObject PersistentObject;

   //Track the objects we already created to avoid duplicates
   private Dictionary<string, GameObject> _persistentObjectsById = new Dictionary<string, GameObject>();

   // Start is called before the first frame update
   void Start()
   {
       //PCFs are only valid when building for Lumin
#if PLATFORM_LUMIN

       //Ask the Magic Leap to start looking for Persistent Coordinate Frames.
       //The result will let us know if the service could start.
       MLResult result = MLPersistentCoordinateFrames.Start();

       //If our request was not successful...
       if (!result.IsOk)
       {
           //Inform the user about the error in the debug log.
           Debug.LogError("Error: Failed starting MLPersistentCoordinateFrames, disabling script. Reason:" + result);

           //Since we need the service to start successfully, we disable the script if it doesn't.
           enabled = false;

           //Return to prevent further initialization.
           return;
       }

       //Handle Localization status changes.
       MLPersistentCoordinateFrames.OnLocalized += HandleOnLocalized;

       //Handle controller button down events.
       MLInput.OnControllerButtonDown += MLInputOnOnControllerButtonDown;

#endif
   }

//The Magic Leap Controller and PCFs can only be used when building for Lumin
#if PLATFORM_LUMIN

   //Called when the user pressed a button down on the Magic Leap controller
   private void MLInputOnOnControllerButtonDown(byte controllerId, MLInput.Controller.Button button)
   {
       //Check to see if the button that was pressed was the Bumper.
       if (button == MLInput.Controller.Button.Bumper && MLPersistentCoordinateFrames.IsLocalized)
       {
           //If it was the bumper, get the controller using the controllerId
           var controller = MLInput.GetController(controllerId);

           //Create a new object with the controller's position and rotation
           var persistentObject = Instantiate(PersistentObject, controller.Position, controller.Orientation);

           //Find the closest PCF relative to the controller's position.
           MLPersistentCoordinateFrames.FindClosestPCF(controller.Position, out MLPersistentCoordinateFrames.PCF pcf);

           //Create a new Transform Binding.
           var persistentBinding = new TransformBinding(persistentObject.GetInstanceID().ToString(), "exampleItem");

           //Bind the newly created transform to it. 
           persistentBinding.Bind(pcf, persistentObject.transform);

           //Track the created object to avoid duplicates.
           _persistentObjectsById.Add(persistentObject.GetInstanceID().ToString(),persistentObject);

           //Debug which PCF the new object was bound to.
           Debug.Log("Transform bound to PCF : " + pcf.CFUID);
       }

   }

   //Called when the Magic Leap's localization status changes.
   private void HandleOnLocalized(bool localized)
   {
       //Read the saved files from storage
       TransformBinding.storage.LoadFromFile();

       //Cache the reference to the Transform Bindings
       List<TransformBinding> allBindings = TransformBinding.storage.Bindings;

       //Debug that the bindings are being restored
       Debug.Log("Getting saved bindings..." );

       foreach (TransformBinding storedBinding in allBindings)
       {
           // Try to find the PCF with the stored CFUID.
           MLResult result = MLPersistentCoordinateFrames.FindPCFByCFUID(storedBinding.PCF.CFUID, out MLPersistentCoordinateFrames.PCF pcf);

           //If the current map contains the PCF and the PCF is being tracked and we have not created the object already...
           if (pcf != null && MLResult.IsOK(pcf.CurrentResultCode) && _persistentObjectsById.ContainsKey(storedBinding.Id) == false)
           {
               //Create a the persistent content
               GameObject gameObj = Instantiate(PersistentObject, Vector3.zero, Quaternion.identity);

               //Bind the new object to the Transform binding. Setting the "regain" condition true, will position the transform at the saved position.
               storedBinding.Bind(pcf, gameObj.transform, true);

               //Track the created object to avoid duplicates.
               _persistentObjectsById.Add(storedBinding.Id, gameObj);

               //Debug that a binding was restored.
               Debug.Log("Restored bound transform at PCF : " + pcf.CFUID);

           }
       }
   }
#endif

}

最後に

弊社では、Magic Leap 1を活用したアプリケーションをMagic Leap Worldに2つリリースしています!

OnePlanet XR

「OnePlanet XR」はAR/MR技術に専門特化したコンサルティングサービスです。豊富な実績を元に、AR/MR技術を活用した新たな事業の立ち上げ支援や、社内業務のデジタル化/DX推進など、貴社の必要とするイノベーションを実現いたします。

MRグラスを活用した3Dモデル設置シミュレーション

ご相談から受け付けております。ご興味ございましたら弊社までお問い合わせください。

お問い合わせ先: https://1planet.co.jp/xrconsulting.html

OnePlanet Tech Magazine

https://note.com/oneplanetinc/m/m25ceb06130d0