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
Personal World
Shared World
構築 / 実装
ここではUnity Editorを使用し、Magic Leap 1上でPCFsと紐づけるオブジェクトを配置するサンプルアプリケーションを構築します。(以下のようなことを行うアプリケーションになります。)
開発環境
はじめる前に
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をヒエラルキーにドラッグ&ドロップします。
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 イベントにサブスクライブします。
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のインスタンスを作成します。
このスクリプトを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の位置を使用します。
最も近い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 "を使用します。渡された値は保存され、再起動時にオブジェクトを復元するために参照することができます。
これで、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