Pico4 モーショントラッカーをUnity + SteamVRで使用する方法

# Pico4 モーショントラッカーをUnity + SteamVRで使用する方法

## 概要
Pico4のモーショントラッカーをUnity（PCVR）で利用するための手順を解説します。Android版はボディトラッキングのSDKが公開されておりドキュメントも充実していますが、PCVRで利用するための方法が調べてもなかなか出てこなかったので、備忘録として残しておきます。結論としては、OpenVRのAPI経由でトラッカーの位置情報を取得し、Unity上のオブジェクトに反映させることができます。

  (※ 2024/11/08時点では、OpenXR Pluginの`Input Action`を使用する方法では、ポーズ情報の取得ができませんでした。)

## 前提条件
PICO Connect経由で、SteamVRアプリ上でPicoのボディトラッキングデータが仮想トラッカーとして認識されている状態を前提とします。

## 環境
### ハードウェア
- Pico 4 Ultra (システムバージョン 5.12.0.U)
- Pico モーショントラッカー (ファームウェアバージョン TR0186)

### ソフトウェア
- Unity 2022.1.22
- SteamVR 2.8.6
- PICO Connect (V10.2.7)
- Motion Tracker (2.0.3)  

## セットアップ手順
### 1. Unity環境の準備
1. Unity Asset StoreからSteamVR Pluginをダウンロード
2. プロジェクトにSteamVR Pluginをインポート
3. 新規シーンを作成

### 2. トラッキング用スクリプトの実装  

#### トラッカーの情報を取得するスクリプト（SteamVR_TrackedPoseProvider.cs）
OpenVRのSDKを使用して、SteamVRから全てのトラッキングデバイスの情報を取得します。  
トラッカーのシリアルナンバーと紐付けて管理します。

```csharp
using System.Collections.Generic;
using UnityEngine;
using Valve.VR;

public class SteamVR_TrackedPoseProvider : MonoBehaviour
{
    [SerializeField] EVRApplicationType eVRApplicationType;

    private Dictionary<string, TrackedDevicePose_t> trackedDevices = new Dictionary<string, TrackedDevicePose_t>();
    private Dictionary<uint, string> deviceSerials = new Dictionary<uint, string>();
    private TrackedDevicePose_t[] poses = new TrackedDevicePose_t[OpenVR.k_unMaxTrackedDeviceCount];
    

    private void Start() {
        if (OpenVR.System == null) {
            var error = EVRInitError.None;
            OpenVR.Init(ref error, eVRApplicationType);
            if (error != EVRInitError.None) {
                throw new Exception("OpenVRの初期化に失敗しました: " + error);
            }
        }        
    }

    private void OnDestroy() {
        if (OpenVR.System != null) {
            OpenVR.Shutdown();
        }
    }

    private void Update() {
        if (OpenVR.System != null) {
            OpenVR.System.GetDeviceToAbsoluteTrackingPose(ETrackingUniverseOrigin.TrackingUniverseStanding, 0, poses);
            for (uint i = 0; i < OpenVR.k_unMaxTrackedDeviceCount; i++) {
                if (poses[i].bDeviceIsConnected && poses[i].bPoseIsValid) {
                    if (!deviceSerials.ContainsKey(i)) {
                        string serialNumber = GetTrackerSerialNumber(i);
                        if (!string.IsNullOrEmpty(serialNumber)) {
                            deviceSerials[i] = serialNumber;
                            trackedDevices[serialNumber] = poses[i];
                        }
                    }
                    else {
                        string serialNumber = deviceSerials[i];
                        trackedDevices[serialNumber] = poses[i];
                    }
                }
            }
        }        
    }

    private string GetTrackerSerialNumber(uint deviceIndex) {
        var error = ETrackedPropertyError.TrackedProp_Success;
        var serialNumber = new System.Text.StringBuilder(64);
        OpenVR.System.GetStringTrackedDeviceProperty(deviceIndex, ETrackedDeviceProperty.Prop_SerialNumber_String, serialNumber, 64, ref error);
        return error == ETrackedPropertyError.TrackedProp_Success ? serialNumber.ToString() : null;
    }

    public bool TryGetPose(string serialNumber, out Vector3 position, out Quaternion rotation) {
        if (trackedDevices.ContainsKey(serialNumber)) {
            var poseMatrix = trackedDevices[serialNumber].mDeviceToAbsoluteTracking;
            position = poseMatrix.GetPosition();
            rotation = poseMatrix.GetRotation();
            return true;
        }
        position = Vector3.zero;
        rotation = Quaternion.identity;
        return false;
    }
}
```

#### トラッカーのポーズを適用するスクリプト（SteamVR_TrackedPose.cs）
上記のSteamVR_TrackedPoseProviderからトラッカーの位置と回転を取得し、GameObjectに反映させます。

```csharp
using UnityEngine;

public class SteamVR_TrackedPose : MonoBehaviour
{
    public string serialNumber; // インスペクターで設定
    private SteamVR_TrackedPoseProvider poseProvider;

    void Start() {
        poseProvider = FindObjectOfType<SteamVR_TrackedPoseProvider>();
        if (poseProvider == null) {
            Debug.LogError("SteamVR_TrackedPoseProvider not found in the scene.");
        }
    }

    void Update() {
        if (poseProvider != null && !string.IsNullOrEmpty(serialNumber)) {
            if (poseProvider.TryGetPose(serialNumber, out Vector3 position, out Quaternion rotation)) {
                transform.position = position;
                transform.rotation = rotation;
            }
        }
    }
}
```

#### コードの重要な部分の説明

1.OpenVRの初期化と終了（Start関数, OnDestroy関数）
```csharp
// OpenVRシステムの初期化
OpenVR.Init(ref error, eVRApplicationType);
        
// OpenVRシステムの終了
OpenVR.Shutdown();
 ```
SteamVR Pluginを使用する場合は勝手に起動するので必要ないと思います。
OpenXR Pluginで使用する場合は必要で、アプリケーションタイプはOtherに設定すると私の場合は滑らかに動作しました。  

2.トラッキングデータの取得（GetTrackerSerialNumber）  
```csharp
OpenVR.System.GetStringTrackedDeviceProperty(deviceIndex, 
ETrackedDeviceProperty.Prop_SerialNumber_String, serialNumber, 64, ref error);
```
デバイスのシリアルナンバーを取得します。（ロールの設定に利用します。）
Picoのボディトラッキングデータの場合は`Waist`や`LeftFoot`として認識されていました。

3.トラッキングデータの取得（Update関数）
```csharp
OpenVR.System.GetDeviceToAbsoluteTrackingPose(ETrackingUniverseOrigin.TrackingUniverseStanding, 0, poses);
```
SteamVRから接続されている全デバイスのトラッキング情報を取得します。TrackingUniverseStandingは立位での追跡空間を指定します。

4.ポーズ情報の変換（TryGetPose関数）
```csharp
var poseMatrix = trackedDevices[serialNumber].mDeviceToAbsoluteTracking;
position = poseMatrix.GetPosition();  // デバイスの位置
rotation = poseMatrix.GetRotation();  // デバイスの回転
```
SteamVRから取得したposeの情報から位置（Vector3）と回転（Quaternion）に変換します。


### 3. シーンのセットアップ
1. 空のGameObjectを作成し、`SteamVR_TrackedPoseProvider`スクリプトをアタッチ
2. トラッキングしたい位置を表すオブジェクト（例：Cube）を作成
3. 作成したオブジェクトに`SteamVR_TrackedPose`スクリプトをアタッチ
4. インスペクターでシリアルナンバー（例：`Waist`,`LeftFoot`）を設定

## 注意事項
- トラッカーのシリアルナンバーは、実際のデバイスに割り当てられているものと一致させる必要があります。

## まとめ
私の環境では、この実装でPico4のボディトラッキング情報をUnityで取得することができました。誰かの役に立てば幸いです。

## 参考リンク
- [OpenVR APIリファレンス - IVRSystem Overview](https://github.com/ValveSoftware/openvr/wiki/IVRSystem_Overview)