【XR Kaigi 2023解説用】MRTK3とpassthrough, 空間マッピングを使ったコンテンツ作成（ Lenovo VRX向け）

# クロスプラットフォームライブラリ「Mixed Reality Toolkit 3」を使ってみよう

この記事は、XR Kaigi 2023で登壇した「Mixed Reality Toolkit 3 で始めるクロスプラットフォーム開発」の中で解説した実装に関する記事です。
<iframe width="560" height="315" src="https://www.youtube.com/embed/xHpYMK9HWuE?si=g6XxUE1b0Q3klbbQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

2023年9月にリリースされた「Mixed Reailty Tookit 3」の環境構築とUX部品を使ってみたいと思った人向けに作成しました。本記事はMRTK3で構築したコンテンツに対して以下の作業を実施する手順を記載しています。

* 空間マッピングの機能を追加と可視化
* passthrough機能の実装
* モーションコントローラ時のHandMenu対応
* Lenovo VRXにデプロイ

|<iframe width="560" height="315" src="https://www.youtube.com/embed/NcxKsPX86xE?si=4htTiKcbBrr10U7l" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>|<iframe width="560" height="315" src="https://www.youtube.com/embed/-k7DGRGaS-Q?si=NBEx8fp2WWgf44SY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>|
|:-:|:-:|

本記事の手順は事前に以下の記事でUnityプロジェクトを作成済みの状態からスタートすることを前提にしていますが、作業手順自体は汎用的なものなので必要に応じて活用してください。

https://qiita.com/miyaura/private/72c0f9e2c1ac2521ee0f

MRTK3の概要については XR Kaigi内のセッションでも紹介していますが、関連ドキュメントなど詳細は以前に記事にしたものがあるので合わせて参考にしてください。

https://qiita.com/miyaura/items/a36d89a8e708d1bcd722

本記事に関連する記事は複数あります。

https://qiita.com/miyaura/private/c8443173e05056ee3041

https://qiita.com/miyaura/private/be7e1ad2450d7aeff072

https://qiita.com/miyaura/private/50e017d9feb0d5b23d88

Lenovo VRX以外のデバイスでのMRTK3を使うことに興味がある方は参考にしてください。

## 開発環境について

Lenovo VRXにデプロイするために必要なツールとライブラリは以下の通りです。

* ツール
   * Unity 2021.3.22f1
      * Android Build Support
          * Open JDK
          * Android SDK & NDK Tools
   * Mixed Reality Feature Tool
* 導入するライブラリ（インポート時に自動導入される依存ライブラリは未記載）
   * MRTK3関連
       * MRTK Input 3.0.0
      * MRTK Spatial Manipulation 3.0.0
      * MRTK Standard Assets 3.0.0
      * MRTK UX Components 3.1.0
      * MRTK UX Core Scripts 3.1.0
      * MRTK Graphics Tools 0.5.12
      * MRTK Core Definitions 3.0.0
      * Mixed Reality OpenXR Plugin 1.9.0
   * Snapdragon Spaces
      * QHCT Unity Interaction 4.1.0
      * Snapdragon Spaces SDK 0.17.0
      * Snapdragon Spaces Dual Render Fusion 0.17


## Snapdragon Spacesの導入

Lenovo VRXの独自機能を利用するためにはMRTK3に加えて[Snapdragon Spaces](https://spaces.qualcomm.com/)が必要になります。

https://spaces.qualcomm.com/

### Snapdragon Spacesについて
Snapdragon SpacesはQualcommが提供するXRデバイス向けの開発プラットフォームになります。現在このプラットフォームがサポートしているデバイスは以下のLenovo製のXRデバイスとなっています。

* Lenovo ThinkReality A3
* Lenovo VRX

Snapdragon SpacesのSDKについて公式サイトからダウンロードすることが可能です（要無償ユーザ登録）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/6427b0c4-0a00-3c14-f996-83fec0080910.png)

現在はUnity以外にもUnreal Engine向けのライブラリも提供されています。

Unityに導入する場合はPackage Managerからインポートします。
最初に[(作成したUnityプロジェクトフォルダ)\ProjectUnity2021.3\Packages]内に[SnapdragonSpaces]というフォルダ名を作成し、その中にダウンロードしたSnapdragon SpacesのSDKファイルを格納します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/611669b6-01c5-c503-a18f-eb25b4838f40.png)

次にUnity Editorのメニューから[Window]-[Package Manager]を選択しPackage Managerウインドウを開きます。左上の[+]を押して[add package from tarball ...]を選択しインポートを行ってください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/b5391482-4511-6f63-f5e8-ad4a753a51db.png)

問題なく導入できると以下のようにQualCommのパッケージとして追加されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/11ce94b6-c063-3cfe-fde3-fdd194aea693.png)

## プロジェクトを作成する

MRTK3でのアプリ実装については以下の記事を参照してUI部分の構築を行ってください。以降はこの手順で作成したプロジェクトを変更しながら機能を追加していきます。

https://qiita.com/miyaura/private/72c0f9e2c1ac2521ee0f

## 空間マッピングを有効にする

Lenovo VRXで空間認識の機能を利用する場合、ARFoundationを経由してLenovo VRXの機能を利用できます。
実装については以下の手順に従って設定を行います。

1. [MRTK XR Rig]-[Camera Offset]の下に空のGameObjectを追加
1. 追加したオブジェクト名をSpatialMappingに変更（任意）
1. SpatialMappingのInspectorパネルから以下のコンポーネントを追加/設定する
    1. [ARMeshManager]を追加しMesh Prefabを設定
    1. [Spaces AR Mesh Manager Config]を追加

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/bf8038e7-eaa6-19ae-e930-715a85c40405.png)

AR Mesh Managerに設定するMeshObjectは以下のコンポーネントを定義したオブジェクトを作成し設定してください。

* Mesh Filter
* Mesh Renderer
* Mesh Collider (※床など空間に干渉するコンテンツを作成する場合に必要)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/a3edd403-7d81-90d0-71a4-2e474c838cf0.png)


以上でVRXでMRTK3と空間マッピングの機能を使ったコンテンツを作ることが可能です。

## passthroughを設定と実装

VRXはデバイスとしてパススルーモードをサポートしています。パススルーの切り替え自体はデバイスにもボタンがついているのですが、アプリケーション内でパススルーを利用する場合は少し実装が必要になります。
VRXのパススルー機能を変更するためにはSnapdragon SpacesのBaseRuntimeFeatureコンポーネントを経由して行います。これは実装で対応する形になります。
まずはパススルー機能を有効/無効切り替えるための実装としてPassthroughToggleSwitchコンポーネントを作成しました。
Snapdragon SpacesのBaseRuntimeFeatureコンポーネントが有効であることを確認後、パススルーの状態を切替えるための処理を作成しました。後で、この機能をメニューに組み込みます。

```PassthroughToggleSwitch.cs
// Copyright (c) 2023 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php

using Qualcomm.Snapdragon.Spaces;
using UnityEngine;
using UnityEngine.XR.OpenXR;

public class PassthroughToggleSwitch : MonoBehaviour{

    public bool IsPassthroughOn{ get; private set; }

    private BaseRuntimeFeature _baseRuntimeFeature;

    public virtual void OnEnable()    {
        _baseRuntimeFeature = OpenXRSettings.Instance.GetFeature<BaseRuntimeFeature>();
    }

    // Start is called before the first frame update
    private void Start()    {
        if (!_baseRuntimeFeature)        {
            Debug.LogWarning("Base Runtime Feature isn't available.");
            return;
        }

        if (!_baseRuntimeFeature.IsPassthroughSupported()) return;

        IsPassthroughOn = _baseRuntimeFeature.GetPassthroughEnabled();
        SetPassthrough(IsPassthroughOn); 
    }

    public void SetPassthrough(bool isPassthroughOn)    {
        if (!_baseRuntimeFeature.IsPassthroughSupported())        {
            Debug.LogWarning("Passthrough isn't supported.");
            return;
        }
        IsPassthroughOn = isPassthroughOn;
        _baseRuntimeFeature.SetPassthroughEnabled(isPassthroughOn);
    }
}
```

このコンポーネントを任意のGameObjectに追加しておきます。
次にアプリ内でメニューから変更できるようにHandMenuを改修します。
プロジェクト内に作成済みのHandMenuBaseを展開し[Buttons-GridLayout]の下に[CanvasButtonToggleSwitch]を追加します。[CanvasButtonToggleSwitch]はProjectパネルからパッケージ内を検索して追加することができます。これもMRTK3のUX部品の１つでボタンを押すごとにOn/Offが切り替わるボタンになります。これに合わせてイベントも発生するので、先ほどGameObjectに追加した[PassthroughToggleSwitch]のSetPassthroughメソッドでパススルーの有効無効化処理をOnToggled,OnUntggoledイベントに割り当てておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/3eb7615e-9a55-af83-722c-4a8f3b652116.png)

これでHandmenuでこのボタンを押すことでパススルーの有効/無効化をアプリ内で実現できます。

## モーションコントローラを使う場合の対応

Lenovo VRXはハンドトラッキングと付属のモーションコントローラが利用可能です。モーションコントローラを利用したい場合、今回のアプリで提供しているHandMenuの機能は利用できません。このメニュー部品のUXはハンドトラッキング用に作成されているため「ハンドトラッキングで手のひらを返す時にメニューを表示する」ように作られています。今回はこのメニュをモーションコントローラのPrimaryButtonが押された時に表示/非表示するように実装します。MenuScriptを継承してVRX用に拡張して実装します。
HandMenuの挙動を変更する部分はOnEnalbeメソッドで実装しています。アプリ実行中に入力デバイスの情報を検索しモーションコントローラを検出している場合は、HandMenuの挙動を変更しています。具体的には以下の実装を変更しています。

* HandMenuに実装されているSolverHandlerを変更
    * メニューの追従は頭の動きに合わせる
    * 手のひらで行うためのコンポーネントは無効化
    * UGUIコンポーネントのRaycastTargetを有効にする（おまじない）
* 左右のモーションコントローラのPrimary Buttonが押された際にメニュー制御するイベントを設定
　　* PrimaryButtonが押されるたびにメニューの表示/非表示を実行 

```MenuScriptsForVRX.cs
// Copyright (c) 2023 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php

using System.Collections.Generic;
using MixedReality.Toolkit;
using MixedReality.Toolkit.SpatialManipulation;
using MixedReality.Toolkit.UX;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
using UnityEngine.XR;
using InputDevice = UnityEngine.XR.InputDevice;

public class MenuScriptsForVRX : MenuScripts{
    private bool _isXRController;
    private SolverHandler _solverHandler;

    public GameObject HandMenuBase;

    public InputActionReference LeftControllerPrimary;

    public PassthroughToggleSwitch PassthroughFeature;
    public PressableButton PassthroughToggle;
    public InputActionReference RightControllerPrimary;

    public virtual void OnEnable() {
        LeftControllerPrimary.action.performed += OnPrimaryButtonPressed;
        RightControllerPrimary.action.performed += OnPrimaryButtonPressed;
        var inputDevices = new List<InputDevice>();
        InputDevices.GetDevices(inputDevices);
        foreach (var inputDevice in inputDevices) _isXRController = inputDevice.name.Contains("Oculus");

        if (_isXRController) {
            HandMenuBase.GetComponent<HandBounds>().enabled = false;
            HandMenuBase.GetComponent<HandConstraintPalmUp>().enabled = false;
            _solverHandler = HandMenuBase.GetComponent<SolverHandler>();
            _solverHandler.TrackedTargetType = TrackedObjectType.Head;
            _solverHandler.UpdateSolvers = false;
            var orbital = HandMenuBase.EnsureComponent<Orbital>();
            orbital.LocalOffset = new Vector3(0, 0, 0.5f);
            SetButtonRaycastTarget(true);
        } else {
            HandMenuBase.GetComponent<HandBounds>().enabled = true;
            HandMenuBase.GetComponent<HandConstraintPalmUp>().enabled = true;
            _solverHandler.TrackedTargetType = TrackedObjectType.HandJoint;
            SetButtonRaycastTarget(false);
        }
    }

    private void SetButtonRaycastTarget(bool raycastTarget) {
        foreach (var button in HandMenuBase.GetComponentsInChildren<PressableButton>(true)) {
            button.transform.Find("Frontplate").GetComponent<RawImage>().raycastTarget = raycastTarget;
        }
    }

    public virtual void OnDisable() {
        LeftControllerPrimary.action.performed -= OnPrimaryButtonPressed;
        RightControllerPrimary.action.performed -= OnPrimaryButtonPressed;
    }

    // Start is called before the first frame update
    public override void Start() {
        base.Start();

        PassthroughToggle.ForceSetToggled(PassthroughFeature.IsPassthroughOn);
    }

    private void OnPrimaryButtonPressed(InputAction.CallbackContext ctx) {
        if (!_isXRController) return;
        var menu = HandMenuBase.transform.GetChild(0).gameObject;
        menu.SetActive(!menu.activeSelf);
        _solverHandler.UpdateSolvers = !_solverHandler.UpdateSolvers;
    }
}
```

最後にコンポーネントの設定を行います。HandMenuBaseオブジェクトに設定していたMenuScriptコンポーネントを先ほど作成したMenuScriptsForVRXに置き換えます。パラメータの設定は以下の図を参考に設定してください。
primaryButtonについてはXRIの仕組みを使用してアクションを取得しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/44c8e080-cbc8-2a11-9b24-107ef6fcae5f.png)

以上で、passthroughと空間マッピングの実装が完了です。

## VRXへのデプロイ

VRXにデプロイするために設定は次の通りです。VRXはハンドトラッキングまたは、モーションコントローラを利用できるためそれぞれで一部設定が異なります。

1. Build Settingsの変更
1. ハンドトラッキングを利用
    1. MRTK3の設定
    1. XR Plugin Management(Open XR) の設定
1. モーションコントローラを利用
    1. MRTK3の設定
    1. XR Plugin Management(Open XR) の設定 
1. デプロイ

### 1. Build Settingsの変更

Unity Editorのメニューから[File]-[Build Settings]を選択しBuild Settingsパネルを表示します。VRX向けのアプリはAndroidを選択する必要があるので、Platformから[Android]を選択し、[Switch Platform]を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/f466f809-58c8-0175-0d20-a16eda3a3a40.png)

### 2. ハンドトラッキングを利用

VRXでMRTK3のハンドトラッキングを利用する場合の環境設定を行います。

#### 2-1. MRTK3の設定

次にAndroid用のMRTK3の設定を行います。Unityのメニューから[Edit]-[Project Settings]を選択しプロジェクト設定パネルを表示します。
左のメニューから「MRTK3」を選択します。右側にMRTK3のプロファイル設定についての項目が表示されます。プラットフォームごとのタブが表示されているので「Androidアイコン」のタブが選択されていることを確認してMRTK3のプロファイルを設定してください。
プロファイル設定については今回はデフォルトの設定で特に問題がないためパッケージに入っているデフォルトの[MRTKProfile]を割り当てます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/7e5e22be-456a-8513-f2f7-bdf149d347cd.png)


#### 2-2. XR Plugin Management(Open XR) の設定

次にVRX用にXR系の設定を行っていきます。引続き[Project Settings]の中から[XR Plug-in Management]を選択します。プラットフォームごとのタブが表示されているので「Windowsアイコン」のタブが選択されていることを確認したうえで以下の項目にチェックが入れてください。

* Initialize XR on Startup
* OpenXR
* Snapdragon Spaces feature group

上記の設定を行った後[Project Validation]の項目をチェックするといくつかの警告が表示されます。右上の[Fix All]を何回か押すことでSnapdragon Spaces向けにプロジェクトの設定が是正されます。
警告をすべて消すためにはいくつか手動パッケージの導入等必要になります。具体的にはデバイス毎のモーションコントローラのモデル表示関する設定です。また、今回はハンドトラッキングを利用するためモーションコントローラ周りの設定で警告が出ます。これは公式でも現状は警告がでてる記載があるので問題ありません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/c6db05cd-dcf2-9528-9a51-9bbc855e8797.png)


次に[OpenXR]で必要な設定を行います。[XR Plug-in Management]-[OpenXR]を選択します。以下の設定が行われているかを確認してください。

* Interaction Profileには**何も設定しない**
* OpenXR Feature Groupsの以下をチェック
    * Snapdragon Spaces
    * Base Runtime
    * Hand Tracking
    * Motion Controller Model 
    * Spatial Meshing (Experimental) 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/8b082ba4-cedf-8548-2f84-0199e87e055b.png)

#### 3. モーションコントローラを利用

VRXでMRTK3のハンドトラッキングを利用する場合の環境設定を行います。

#### 3-1. MRTK3の設定

次にAndroid用のMRTK3の設定を行います。Unityのメニューから[Edit]-[Project Settings]を選択しプロジェクト設定パネルを表示します。
左のメニューから「MRTK3」を選択します。右側にMRTK3のプロファイル設定についての項目が表示されます。プラットフォームごとのタブが表示されているので「Androidアイコン」のタブが選択されていることを確認してMRTK3のプロファイルを設定してください。今回のサンプルではモーションコントローラを使わない場合MRTK3のサブシステムはすべて使用しません。このため、ProjectパネルからAssets配下の任意の場所に空のMRTKProfileを作成し割り当ててください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/9e3caff8-0135-6485-1312-ff5f4ffa1fa1.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/5efeb6ff-2510-1de9-f1d7-abf5e9709464.png)


#### 3-2. XR Plugin Management(Open XR) の設定

次にVRX用にXR系の設定を行っていきます。引続き[Project Settings]の中から[XR Plug-in Management]を選択します。プラットフォームごとのタブが表示されているので「Androidアイコン」のタブが選択されていることを確認したうえで以下の項目にチェックが入れてください。

* Initialize XR on Startup
* OpenXR
* Snapdragon Spaces feature group

上記の設定を行った後[Project Validation]の項目をチェックするといくつかの警告が表示されます。右上の[Fix All]を何回か押すことでSnapdragon Spaces向けにプロジェクトの設定が是正されます。
警告をすべて消すためにはいくつか手動パッケージの導入等必要になります。具体的にはデバイス毎のモーションコントローラのモデル表示関する設定です。また、今回はハンドトラッキングを利用するためモーションコントローラ周りの設定で警告が出ます。これは公式でも現状は警告がでてる記載があるので問題ありません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/c6db05cd-dcf2-9528-9a51-9bbc855e8797.png)


次に[OpenXR]で必要な設定を行います。[XR Plug-in Management]-[OpenXR]を選択します。以下の設定が行われているかを確認してください。

* Interaction Profileに[Oculus Touch Controller Profile]を追加
* OpenXR Feature Groupsの以下をチェック
    * Snapdragon Spaces
    * Base Runtime
    * Motion Controller Model 
    * Spatial Meshing (Experimental) 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152548/a61f0ae9-d4a8-ed27-dcbb-0a564b79261e.png)

### 4. デプロイ

デプロイについてはAndroidアプリのそれと変わりありません。VRXのスマートフォンを開発者モードでPCにUSB接続しておけばUnityから直接デプロイすることも可能です。もしくはAPKをスマートフォンにコピーしてAPKを直接インストールします。

1点注意点としてはVRXはAndroidベースのデバイスなのですがAPKをインストールするためにはVRXの開発者モードを解放して設定を変える必要があります。やり方自体はAndroidの開発者モードの解放する手順と一緒です。公式サイトではすぐわかるところにはない（？）のでリンク張っておきます。ちなみに、私は最初に困った方です。

https://support.lenovo.com/id/en/solutions/ht515762-developer-mode-feature-lenovo-thinkreality-vrx-headset

## 参考サイト

https://spaces.qualcomm.com/

https://docs.spaces.qualcomm.com/unity/setup/SetupGuideUnity.html

https://docs.spaces.qualcomm.com/unity/samples/preview/MRTK3SampleWinOnly.html