Quest3でMR×ROS2を試してみる

　今回はMetaQuest3のMR機能とROS2を組み合わせて遊んでみることにします。といってもQuest3は実質的にAndroidみたいなものなので，ROS2との連携方法については以前作った[スマホコントローラ](https://qiita.com/hirekatsu0523/items/b91031a7a6fcc95839dc)の時と全く同じだったりします。なので今回のメインはMRの部分ですかね。

# 環境
 - Windows11(Unity用)
 - Ubuntu22.04(ROS2用)
 - ROS2 Humble
 - Unity 2022.3.11f1

# 開発環境の構築
### Meta Quest Developer Hub
[こちらのページ](https://developer.oculus.com/meta-quest-developer-hub/)の`Download for Windows`からMeta Quest Developer Hubをダウンロードし，インストールしてください。

### 開発者設定
[こちらのページ](https://developer.oculus.com/manage/verify/)からアカウントの認証を行い，新しい組織を作成します。分からないことがあれば[公式チュートリアル](https://developer.oculus.com/documentation/native/android/mobile-device-setup/)を参照してください。

開発者アカウントを作れたら，次にQuest3を開発者モードにします。
スマートフォンのMeta Questアプリを開いて，
`Menu`>`デバイス`>`Meta Quest 3`>`ヘッドセットの設定`>`開発者モード`と進んで，開発者モードをオンにします。
そのままQuest3とPCを有線接続すると，画面に`USBデバッグを許可しますか`と表示されるので，許可or常に許可を選びます。

### Meta Quest Link
[公式記事](https://www.meta.com/ja-jp/help/quest/articles/headsets-and-accessories/oculus-link/connect-link-with-quest-2/)を参考にしてQuest3でQuest Linkを使えるようにしてください。
PCのOculusアプリを立ち上げて，`設定`>`ベータ`から`開発者ランタイム機能`をオンにします。すると，その下に追加項目が表示されるので，`Oculus Link経由でのパススルー`もオンにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/5cee87ee-915d-186e-d857-4fdc2a839787.png)


### Unity
 - プロジェクトの作成
Unity Hubを開いてプロジェクトを作成していきます。
`プロジェクト`>`新しいプロジェクト`>`3D(URP)`と進んで，プロジェクト名と保存場所を設定したら`プロジェクトを作成`を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/f46d00f3-8597-f9d0-697f-7d56978b0f08.png)

 - Oculus統合SDKの導入
[アセットストア](https://assetstore.unity.com/packages/tools/integration/oculus-integration-82022)からOculus Integration SDKを導入します。
`マイアセットに追加`>`Unityで開く`と選択すると，Unityで下の画面が開くので`インポート`をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/9cb7d6f8-862b-1ba4-b601-caf06e2a19b1.png)

全てインポートします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/0e7e4a17-9f37-d272-f45a-82344aab2d08.png)

途中でポップアップが出てきても，基本的にはデフォルトの値を選んでおけば問題ありません。
再起動をしたらOculus統合SDKの導入は完了です。

 - プロジェクト設定
`編集`>`プロジェクト設定...`からProject Setting画面を開きます。

    - グラフィックス
`スクリプタブルレンダーパイプライン設定`をURP-Balancedに変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/6c27ee4a-3f57-2bc8-3334-e99969ebb2d7.png)

    - Oculus
Androidタブを選んで，`Fix All`をクリックします。何回かクリックすると警告が消えるようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/e42a24d0-3d36-9919-f958-65adb5627c69.png)

    - プレイヤー
一番上の`企業名`を適当なものに変更します。
Androidタブの`その他の設定`>`識別`，`設定`の部分を下の画像のように変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/d5b8522c-16e4-919f-4761-26f89d2ff8f9.png)

    - 品質
クリックしてBalancedに変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/c46b5b3d-65ce-dc5a-fe0e-4ae2ec8bd9e6.png)

    - XR Pulug-in Management > Oculus
AndroidタブのQuest 3にチェックを入れておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/6968ac78-2652-b6cb-ac74-4758bf7b732b.png)

        そもそもOculusタブがないという人は，XR Plugin Managementをインストールし，AndroidタブからOculusプラグインにチェックを入れてください。[こちらの記事](https://qiita.com/b0bmat0ca/items/bc5ce8e38979d4680402)が参考になるかもしれません。

 - ビルドターゲットの変更
`ファイル`>`ビルド設定...`から`Build Setting`を開き，プラットフォームをAndroidに変更したら`ターゲットの切り替え`をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/0509d1c3-5c56-d6eb-3a8b-834025662fd6.png)


最後にプロジェクトウィンドウから`Asetts`>`Setting`>`URP-Balanced-Renderer`を選択して，インスペクターウィンドウで`スクリーンスペースアンビエントオクルージョン`のチェックを外しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/c3e8297c-7648-65f4-d3cc-e2db20b379f4.png)

### ROS2forUnity
ros2-for-unityのunitypackageを作成したので[こちら](https://github.com/Azuma413/ros2-for-unity-win-android)からダウンロードしてください。
プロジェクトウィンドウ上で右クリックをすると以下の画像のように表示が出てくるので，`パッケージをインポート`>`カスタムパッケージ...`をクリックします。
<img width="956.25" alt="image.png (62.8 kB)" src="https://img.esa.io/uploads/production/attachments/19744/2023/05/26/142382/fe297c70-f648-4cc9-8f47-aa5a097969b9.png">

先ほどダウンロードしたフォルダの中に`ros2-for-unity-win-android.unitypackage`というファイルがあるので，それを選択しインポートします。





# MRの設定
### パススルー
MRをUnityで実現するには通常レイヤーに加えて，パススルーレイヤ―を追加しなければなりません。パススルーレイヤーは実行時にカメラから合成された映像に置き換えられます。

 - OVR Camera Rigの追加
ヒエラルキーウィンドウから`Directional Light`以外のオブジェクトを削除して，プロジェクトウィンドウから`Assets`>`Oculus`>`VR`>`Prefabs`>`OVRCameraRig`をヒエラルキーウィンドウへ追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/5be97857-e5a9-e107-e49e-610ca9d63d67.png)

 - OVR Camera Rigの設定
ヒエラルキーウィンドウで`OVRCameraRig`を選択しインスペクターウィンドウを開きます。
    - `OVRCameraRig`>`OVR Manager`>`Quest 3`にチェックを入れる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/65ddb30c-ba26-2141-5673-5ef04753c354.png)
    - `OVRManager`>`Quest Features`>`Passthrough Support`を`必須`にする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/46275e7f-154a-c004-5868-68685d264a79.png)
    - `OVRManager`>`Insight Passthrough`>`Enable Passthrough`にチェックを入れる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/e5d8a90b-3cf8-660c-f527-9564872c2d24.png)
    - `コンポーネントを追加`から`OVR Passthrough Layer`を追加する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/7531f6ff-3876-cb5c-67ed-7384ed53f6b4.png)
    - `OVR Passthrough Layer`>`Placement`を`Underlay`に変更する(パススルーレイヤーを通常レイヤーの下側に配置する)。

 - Sky boxの無効化
`ウィンドウ`>`レンダリング`>`ライティング`>`環境`と進みます。
`スカイボックスマテリアル`に`None`を設定し，`環境ライティング`>`ソース`を`色`に，`環境光の色`を黒にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/c3fcf23f-ea2c-fd7b-8bb1-26e721e3de05.png)

ちなみにQuest Linkを有効にした状態でUnity上の`Play`を選択すると，わざわざapkファイルをインストールしなくてもQuest3でシーンを実行できます。もし実行されない場合は`編集`>`プロジェクト設定...`>`Oculus`からProject Setup Toolを使ってAndroidとWindowsのエラーや警告を修正してからもう一度試してみてください。

### シーンモデル
　壁や床などの現実の物体を認識して，Unityで利用するにはシーンモデルという機能を用いる必要があります。シーンモデルとは現実の物体にシーンアンカーを紐づけたもので構成され，シーンアンカーは図形＋意味を要素として持ちます。たとえば床のシーンアンカーには平面図形と，床という意味が紐づけられます。
　また，シーンモデルにアクセスするにはOVRSceneManagerというUnityコンポーネントを利用します。
 - アンカーサポートの有効化
ヒエラルキーウィンドウで`OVRCameraRig`を選択し，インスペクターウィンドウから`OVRManager`>`Quest Features`>`Anchor Support`を有効にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/6e59e134-d836-6452-f1e2-5ae1639b79f5.png)


 - OVRSceneManagerの追加と設定
    - プロジェクトウィンドウから`Assets`>`Oculus`>`VR`>`Prefabs`>`OVRSceneManager`をヒエラルキーウィンドウに追加する。
    - `Assets`>`Oculus`>`SampleFramework`>`Usage`>`SceneManager`>`Prefabs`フォルダから`InvisiblePlane`と`InvisibleVolume`を`OVRSceneManager`のインスペクターウィンドウの`OVR Scene Manager`>`Plane Prefab`と`Volume Prefab`にそれぞれドラッグアンドドロップして割り当てる。
(OVR Scene Anchorスクリプトが割り当てられてあれば他のPrefabも割り当てられます。また，Prefab Overridesからシーンアンカーに紐づけられた意味毎に異なるPrefabを割り当てることもできるようです)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/f50c639c-a043-498b-f152-729a5371b7d7.png)


MRではこれら以外にも空間アンカーという重要な要素がありますが，今回は必要ないので説明しません。
https://developer.oculus.com/documentation/unity/unity-spatial-anchors-overview/

# メイン実装
　今回はテストとしてコントローラーのJoy Stickの値をPCに送り，同じ値をPCから受け取ってオブジェクトを動かしてみることにします。
　最初にプロジェクトウィンドウの`Asetts`下に`Scripts`というフォルダを作っておいてください。今後スクリプトはこのフォルダ内で作成します。
### ROSの実装
 - ROSControl.cs
プロジェクトウィンドウのScriptsフォルダ内で右クリックをして，`作成`>`C#スクリプト`と進んでスクリプトを作成します。ファイル名はROSControlとしています。
```cs
using ROS2;
using UnityEngine;

namespace MyROS
{
    public class ROSControl : MonoBehaviour
    {
        internal static readonly ROSConfig Instance = new ROSConfig();
        // Start is called before the first frame update
        void Start()
        {
            TryGetComponent(out Instance.ros2Unity);
        }

        // Update is called once per frame
        void Update()
        {
            if (Instance.ros2Unity.Ok())
            {
                if (Instance.ros2Node == null)
                {
                    Instance.ros2Node = Instance.ros2Unity.CreateNode("unity_node");
                    Instance.joy_data_pub = Instance.ros2Node.CreatePublisher<geometry_msgs.msg.Twist>("joy_data");
                    Instance.cmd_vel_sub = Instance.ros2Node.CreateSubscription<geometry_msgs.msg.Twist>("cmd_vel", Instance.cmd_vel_cb);
                }
            }
        }
    }
    class ROSConfig
    {
        public ROS2UnityComponent ros2Unity;
        public ROS2Node ros2Node;
        public IPublisher<geometry_msgs.msg.Twist> joy_data_pub;
        public ISubscription<geometry_msgs.msg.Twist> cmd_vel_sub;
        public geometry_msgs.msg.Twist cmd_vel_data;
        public void cmd_vel_cb(geometry_msgs.msg.Twist msg)
        {
            cmd_vel_data = msg;
        }
    }
}
```
　Twist型のjoy_dataというトピックをPCに送り，PCからは同じくTwist型のcmd_velを受け取ります。ROSConfigクラスでPublisherやSubscriberを設定し，ROSControlクラスでそのインスタンスを作成することで，他のファイルからもPublisherやPCから受け取った値を利用できるようにしています。

ヒエラルキーウィンドウで右クリックをして，`空のオブジェクトを作成`から`ROSObject`という名前のオブジェクトを作成してください(名前は任意)。
そのオブジェクトに今作成した`ROSControl`スクリプトをドラッグアンドドロップでアタッチします。
さらにプロジェクトウィンドウから`Assets`>`Ros2ForUnity`>`Scripts`>`ROS2UnityComponent.cs`を探して，同じく`ROSObject`にアタッチしてください。
インスペクターウィンドウでこのようになっていれば正しくアタッチできています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/26c3f19d-955f-9c36-3e33-77dab9914ce0.png)

### ROS_DOMAIN_IDの設定

 - SetID.cs
こちらのファイルでROS_DOMAIN_IDを設定します。
```cs
using System;
using UnityEngine;

public class SetID : MonoBehaviour
{
    void Start()
    {
        // ROS_DOMAIN_IDに123を設定する
        Environment.SetEnvironmentVariable("ROS_DOMAIN_ID", "123");
        // 上手く設定できているか確認する
        string value = Environment.GetEnvironmentVariable("ROS_DOMAIN_ID");
        Debug.Log("ROS_DOMAIN_ID:" + value + "\n");
    }
}
```
例としてIDを123に設定しているので，各自変更してください。

このスクリプトも`ROSObject`にアタッチしておきます。
次にメニューから`編集`>`プロジェクト設定...`>`スクリプト実行順序`と開き，右下の+から`SetID`を追加してください。
`SetID`の右側の欄に`-1`と入力し，`Apply`で適用します。こうすることでROS_DOMAIN_IDがROSよりも早く設定され，エラーを防ぐことができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/711c0316-4d9a-f1c8-9b5a-1513dccbd242.png)

### オブジェクトの実装
ROSから送られてきた値を読み込んで動く物体を実装します。

今回は適当にヒエラルキーウィンドウに`Robot`という名前の球を追加しました。
インスペクターウィンドウで`コンポーネントを追加`から`Rigidbody`を追加しておきます。
 - RobotControl.cs
```cs
using MyROS;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RobotControl : MonoBehaviour
{
    private Rigidbody robot;
    // Start is called before the first frame update
    void Start()
    {
        robot = GetComponent<Rigidbody>();
    }

    // Update is called once per frame
    void Update()
    {
        //Unityはyが上
        robot.AddForce((float)ROSControl.Instance.cmd_vel_data.Linear.X, 0.0f, (float)ROSControl.Instance.cmd_vel_data.Linear.Y);
    }
}
```
ROSから受け取った値はコールバック関数内で`cmd_vel_data`に格納されるので，その値をそのまま加速度としてRigitbodyに加えています。

スクリプトを`Robot`にアタッチすると画像のようになると思います(サイズとマテリアルを調整しています)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/d2a67fe8-0052-8b50-6105-157e60ad5f84.png)

インスペクターウィンドウからプロジェクトウィンドウに`Robot`をドラッグアンドドロップしてPrefab化したら，インスペクターウィンドウの`Robot`は削除しておきましょう。


### コントローラーの値を読む
コントローラーの値を読むにはシーンに`OVR Manager`を配置する必要がありますが，これは`OVRCameraRig`に既にアタッチされているので大丈夫です。

 - コントローラーの表示
まずはコントローラーのモデルを表示するためのPrefabを追加します。
`Assets`>`Oculus`>`VR`>`Prefabs`>`OVRControllerPrefab`を，画像の通りにヒエラルキーウィンドウの`LeftHandAnchor`と`RightHandAnchor`の下に配置してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/0ccb9fe8-c474-1aac-683b-3263ac2e82d2.png)

    インスペクターウィンドウから，`LeftHandAnchor`下に配置した方は`コントローラー`を`L Touch`に，`RightHandAnchor`下に配置した方は`R Touch`に変更します。
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/f7a70bcd-c678-66dc-ff3b-b79ac67e8635.png)

 - GetInput.cs
`GetInput`というスクリプトを作成し，コントローラーの値を読み取ります。
```cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MyROS;

public class GetInput : MonoBehaviour
{
    [SerializeField]
    private GameObject robot; //インスペクターウィンドウからrobotオブジェクトを設定
    private OVRInput.Controller controller;
    private geometry_msgs.msg.Twist send_data;
    // Start is called before the first frame update
    void Start()
    {
        //コントローラーを取得
        controller = GetComponent<OVRControllerHelper>().m_controller;
        //Twistの初期化
        send_data = new geometry_msgs.msg.Twist();
    }

    // Update is called once per frame
    void Update()
    {
        //Button One = Aボタンが押された時の処理
        if (OVRInput.GetDown(OVRInput.Button.One, controller))
        {
            //コントローラーの位置と方向に合わせてrobotを配置
            Instantiate(robot, transform.position, transform.rotation);
        }
        //右のコントローラーのstickから値を読み込んでPublish
        send_data.Linear.X = OVRInput.Get(OVRInput.Axis2D.SecondaryThumbstick).x;
        send_data.Linear.Y = OVRInput.Get(OVRInput.Axis2D.SecondaryThumbstick).y;
        ROSControl.Instance.joy_data_pub.Publish(send_data);
    }
}
```
スクリプトを`RightHandAnchor`下に配置した`OVRControllerPrefab`にアタッチしてください。
インスペクターウィンドウから`Get Input`>`Robot`に，先ほど作成した`Robot`のPrefabをドラッグアンドドロップにより割り当てます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/6dcfef37-59ef-98be-ba54-2d3ee4675c24.png)


### ビルド&インストール
以上でUnity側の実装は終わったので，`ファイル`>`ビルド設定...`>`ビルド`からapkファイルとして出力します。
`Meta Quest Developer Hub`を起動し，Quest3を接続した状態でapkファイルをファイルをウィンドウ上にドラッグアンドドロップすると，インストール先を聞かれるので，接続デバイスを選んでapkファイルをインストールします。
`Device Manager`タブで下のように表示されていれば大丈夫です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/4392605e-d3e7-e284-b89f-0c81f7ad9f6f.png)



### PC側の実装
こちらは特に説明することもないので，コードだけ置いておきます。
```cpp
#include <chrono>
#include <memory>
#include <string>
#include "rclcpp/rclcpp.hpp"
#include "geometry_msgs/msg/twist.hpp"

using namespace std::chrono_literals;

class MinimalPubSub : public rclcpp::Node{
    public:
    MinimalPubSub() : Node("minimal_pubsub"){
        auto topic_callback = [this](const geometry_msgs::msg::Twist& msg) -> void{
            auto message = geometry_msgs::msg::Twist();
            message = msg;
            std::cout << "X:" << message.linear.x << "\nY:" << message.linear.y << std::endl;
            publisher_->publish(message);
        };

        rclcpp::QoS qos(rclcpp::KeepLast(10));
        publisher_ = this->create_publisher<geometry_msgs::msg::Twist>("cmd_vel", qos);
        subscription_ = this->create_subscription<geometry_msgs::msg::Twist>("joy_data", qos, topic_callback);
    }
    private:
    rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr publisher_;
    rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr subscription_;
};

int main(int argc, char* argv[]){
    rclcpp::init(argc,argv);
    rclcpp::spin(std::make_shared<MinimalPubSub>());
    rclcpp::shutdown();
    return 0;
}
```

# テスト
PC側で先ほど作成したROS2ノードを立ち上げます。
Quest3を起動して，メニューバー右端の`アプリライブラリ`から`アプリを検索`と進んで，`全て`と書かれたタブをクリックして`提供元不明`を選択します。すると先ほどインストールしたアプリが出てくるので起動します。
Aボタンで球体出現，ジョイスティックで球体が動けばうまくいっています。PC上でもQuest3から値が送られてきていることが分かります。

https://youtube.com/shorts/44lVhdVcY64?si=lizlFxD8X8qNg0Eg

※デバッグ用にコンソールを表示しています。

# おわりに
想像以上にSDKや公式ドキュメントが充実しており，ほとんど詰まることなく実装できてしまいました。これからどんなものを作ろうかと，夢が広がります。

# 参考文献
https://qiita.com/b0bmat0ca/items/bc5ce8e38979d4680402

https://qiita.com/b0bmat0ca/items/944f540448f9dca53ed9

https://lawrenceturner.hatenablog.com/entry/2023/10/13/025329

https://developer.oculus.com/documentation/unity/unity-passthrough/

https://www.jyuko49.com/entry/2019/07/14/155816

https://qiita.com/sakastudio_/items/4a96e900949843719271

https://xr-hub.com/archives/11132

https://nekojara.city/unity-object-acceleration

https://developer.oculus.com/documentation/unity/unity-tutorial-basic-controller-input/

https://www.jyuko49.com/entry/2019/07/14/155816