Oculus Quest 2でROS Sharpを使う


Oculus Quest 2でROS#を使ってみたところ、Oculus Quest 2からのデータ送信がそのままでは正しく動きませんでした。
一部コードに変更を加えることで動くようになったので、その方法をまとめました。

(2021/2/14 追記)
Githubのリンクを追記しました
(追記ここまで)

今回はROS#を使って、
- ヘッドセットの位置・向きの出力
- コントローラ入力の出力
- カメラ画像の受信
をします。
一部コードに変更を加えた以外はOculus QUestと同じ手順で、ほぼこちらの記事そのままです。

環境

環境構築

  1. Unityの開発環境構築
    • UnityHubを起動する
    • メニューで「インストール」を選択し、右上の「インストール」ボタンを選択
    • Unity 2019.4.17f1 (LTS)を選択する。その際、「Android Build Support」プラグインを追加する
  2. Oculus Quest2の開発者モードを有効にする
  3. プロジェクトの作成
    • UnityHubからプロジェクトを新規作成する
    • テンプレートに3Dを選択する

ビルド設定

  1. Build Settingの変更
    • File -> Build Settingを選択
    • PlatformでAndroidを選択し、Switch Platformボタンを押す
  2. Oculus Integrationをインポート
    • Asset StoreからOculus Integration for Unityをインポートする
  3. XR Plug-in Managementのインストール
    • Window -> Package Managerを選択
    • XR Plug-in Managementを選択し、インストール
  4. Player Settingの変更
    • Edit -> Project Settingsを選択
    • Player -> Other Settingsを選択
      • Package Nameに任意の名前を入力
      • Minimum API LevelをAndroid 7.1 'Nougat' (API level 25)にする
      • Api Compatibility Levelを.NET 4.xにする
    • XR Plug-in Managementを選択
      • Android と Standalone の “Oculus” にチェック 入れる
  5. ROS#のインポート
    • こちらからRosSharp.unitypackageをダウンロード
    • Asset -> Import Package -> Custome Packageからダウンロードしたパッケージを選択しインポート

アプリ実装

ソースコードはこちらのGitHubにあげました。

Oculus Quest2のカメラとコントローラ

  1. カメラ
    • HierarchyからMain Cameraを削除
    • ProjectからAssets/Oculus/VR/Prefabsを選択し、OVRCameraRigをHierarchyドラッグ&ドロップ
    • OVRCameraRigのOVR Managerスクリプトで、Target Devicesで、Quest2にチェック
  2. コントローラ
    • ProjectからAssets/Oculus/VR/Prefabsを選択し、OVRControllerPrefabをHierarchyのLeftControllerAnchorの下にドラッグ&ドロップ
      • LeftControllerAnchorにOVRControllerPrefabをドラッグ&ドロップ
      • ControllerをL Touchに変更
    • ProjectからAssets/Oculus/VR/Prefabsを選択し、OVRControllerPrefabをHierarchyのRightControllerAnchorの下にドラッグ&ドロップ
      • RightControllerAnchorにOVRControllerPrefabをドラッグ&ドロップ
      • ControllerをR Touchに変更
    • AndroidManifest.xmlを変更
      • をに変更

ROS Sharp

  1. RosConnectorの設定
    • Hierarchyで右クリック -> Create EmptyでGameObjectを作成、名前をRosConnectorに変更
    • ProjectでAssets/RosSharp/Scripts/RosBridgeClient/RosCommunicationを開き、RosConnectorスクリプトをRosConnectorオブジェクトにアタッチ
    • RosConnectスクリプトで、ROSのIPアドレスを設定
  2. カメラ画像の受信
    • Hierarchyで右クリック > 3D Object > Planeで平面を作成、名前をImageReceiverに変更
    • InspectorでPositionを(0, 0, 10)に、Rotationを(90, 0, 0)に、Scaleを(-1, -1, 0.75)に設定
    • ProjectでAssets/RosSharp/Scripts/RosBridgeClient/RosCommunicationを開き、ImageSubscriberスクリプトをRosConnectorオブジェクトにアタッチ
    • ImageSubscriberスクリプトのTopicを設定(自分の環境では/image_raw/compressedとしました)
    • ImageSubscriberスクリプトのMessageReceiverに、ImageReceiverオブジェクトをドラッグ&ドロップ
  3. ヘッドセットの位置・向きの出力
    • ProjectからAssets/RosSharp/Scripts/RosBridgeClient/RosCommunicationを開き、PoseStampedPublisherスクリプトをRosConnectorオブジェクトにアタッチ
    • PoseStampedPublisherスクリプトのTopicを設定(自分の環境では/oculus/head_set/poseとしました)
    • PoseStampedPublisherスクリプトのPublished TransformにCenterEyeAnchorをドラッグ&ドロップ
  4. コントローラ入力の送信
    • ProjectからAssets/RosSharp/Scripts/RosBridgeClient/RosCommunicationを開き、JoyPublisherスクリプトをRosConnectorオブジェクトにアタッチ
    • JoyPublisherスクリプトのTopicのTopicを設定(自分の環境では/oculus/controllerとしました)
    • ProjectのAssets/RosSharp/Scripts/RosBridgeClient/MessageHandlingからJoyAxisReaderを4つアタッチ
      • 自分は左右コントローラのジョイスティックのみ使うためこのようにしました。ボタンを使う場合はJoyButtonReaderを使用するボタンの数だけアタッチします
    • 4つのJoyAxisReaderのNameにそれぞれ以下の値を設定
      • Oculus_CrossPlatform_PrimaryThumbstickHorizontal
      • Oculus_CrossPlatform_PrimaryThumbstickVertical
      • Oculus_CrossPlatform_SecondaryThumbstickHorizontal
      • Oculus_CrossPlatform_SecondaryThumbstickVertical
    • 使用可能なボタンのNameの一覧は、Edit -> Project Settings -> Input Managerから確認できます

ROS Sharpスクリプトの変更

以上でまでのコードをビルドして動かしたところカメラ画像は表示されるのですが、/oculus/head_set/pose/oculus/controllerが出力されませんでした。
ログを確認すると、

Error Unity NullReferenceException: Object reference not set to an instance of an object
Error Unity   at RosSharp.RosBridgeClient.UnityPublisher`1[T].Start () [0x00018] in <c1466e84f5f0406bad7dd0f2189d032c>:0 
Error Unity   at RosSharp.RosBridgeClient.PoseStampedPublisher.Start () [0x00000] in <c1466e84f5f0406bad7dd0f2189d032c>:0 

Error Unity NullReferenceException: Object reference not set to an instance of an object
Error Unity   at RosSharp.RosBridgeClient.PoseStampedPublisher.UpdateMessage () [0x00000] in <c1466e84f5f0406bad7dd0f2189d032c>:0 
Error Unity   at RosSharp.RosBridgeClient.PoseStampedPublisher.FixedUpdate () [0x00000] in

という例外が発生していました。

調査したところ、UnityPublisherのStart()内にあるrosConnector.RosSocket.Advertise<T>(Topic);で例外が発生していることがわかりました。
rosConnectorの初期化等に時間がかかっているのかと考え、直前にスリープを入れたり、PoseStampedPublisher.csスクリプトとJoyPublisher.csスクリプトの実行順を一番最後にしたりと試したのですが、解決できませんでした。
そこで、UnityPublisherのPublish()内で、rosConnectorがnullの場合は、GetComponent()とAdvertise()を呼ぶようにしました。

また、Start()で例外が発生したままにするとPoseStampedPublisherとJoyPublisherのStart()でのメッセージの初期化が行われないため、Publish()でメッセージのNullReferenceExceptionも発生してしまいます。そのためStart()内で例外をcatchしメッセージを初期化するようにしました。
以下が変更したソースコードになります。

UnityPublisher.cs
namespace RosSharp.RosBridgeClient
{
    [RequireComponent(typeof(RosConnector))]
    public abstract class UnityPublisher<T> : MonoBehaviour where T : Message
    {
        public string Topic;
        private string publicationId;

        private RosConnector rosConnector;
        protected bool initialized = false;

        protected virtual void Start()
        {
            try
            {
                rosConnector = GetComponent<RosConnector>();
                publicationId = rosConnector.RosSocket.Advertise<T>(Topic);
            } catch (Exception e)
            {
                // nop
            }  
        }

        protected void Publish(T message)
        {
            if (!rosConnector)
            {
                rosConnector = GetComponent<RosConnector>();
            }
            if (string.IsNullOrEmpty(publicationId)) {
                publicationId = rosConnector.RosSocket.Advertise<T>(Topic);
            }
            rosConnector.RosSocket.Publish(publicationId, message);
        }
    }
}

Build & 実行

  1. Oculus Quest 2を接続してBuild & Run
  2. Oculus Quest 2で、カメラ画像が表示されることを確認する
  3. ROS側で、$ topic listコマンドを実行し、/oculus/head_set/pose/oculus/controllerがあることを確認する
  4. $ rostopic echo /oculus/head_set/pose$ rostopic echo /oculus/controllerをそれぞれ実行し、値が出力されていることを確認する(/oculus/controllerの出力例)
$ rostopic echo /oculus/controller
---
header: 
  seq: 177
  stamp: 
    secs: 1612795853
    nsecs:  23462057
  frame_id: "Unity"
axes: [0.0, 0.0, 0.0, 0.0]
buttons: []
---
header: 
  seq: 178
  stamp: 
    secs: 1612795853
    nsecs:  30868053
  frame_id: "Unity"
axes: [0.0, 0.0, 0.0, 0.0]
buttons: []
---

最後に

UnityとOculus Quest 2について詳しくないため以上の対応をしましたが、根本的な解決方法があるのではないかと思います。
もじご存じの方がいれば教えていただけると嬉しいです。
また、ROS#の最新版では解決されているかもしれないので、最新のソースコードをダウンロードしてビルドし、試してみてもいいかと思います。(自分はビルド環境がないため試していません)

参考

OculusQuestをROSと通信させる
ROS Sharp v1.6