VRChatのOSC-Trackerを使ってQuest単機でフルトラを試す

VRChat v2022.4.1にてOSC経由でのトラッカー機能が実装されました。
今回はPythonでOpenVRのトラッカーのデータを取得し、そのデータをOSC経由でQuest2のVRChatに送信してみます。
OpenVR周りはほとんど分からないので、ツッコミどころがあるかと思いますがご容赦ください...

:::note warn
現時点(2022/11/25)ではまだベータ版の機能なので、VRChatのクライアントはベータ版を使用する必要があります。
:::

:::note info
(SteamVRのトラッカーを使ってQuest単機でフルトラしたい人向け)
実際にプログラム書いて動かしてみたいとかでなければ、私が作ったツールの使用をおすすめします。
使い方はリンク先をご確認ください。
https://github.com/takana-v/quest_steamvr_fbt_tool
:::

## 事前準備

### SteamVRをHMD無しで使えるようにする

デフォルトではSteamVRはHMDが無いと動きません。
ダミーのHMDを使用するように設定を変更します。
(デフォルトでは`C:\Program Files (x86)\Steam\config\steamvr.vrsettings`に設定ファイルがあります)

`activateMultipleDrivers`を`true`にし、`driver_null`の項目を追加します。

```json
{
   ...（略）
   "steamvr" : {
      "activateMultipleDrivers" : true,
      ...(略)
   },
   "driver_null" : {
      "enable" : true
   },
   ...（略）
}
```

### PCとQuestを同じネットワークに繋げる

PCから到達可能なように同じネットワークにQuestを繋げなければ、情報を送信できません。
自分の環境はこんな感じです。
以下、この環境を前提に書くので適宜読み替えてください。

```
[ルーター]   ----------- [PC]
192.168.11.1     |      192.168.11.2
       　　　　   ------ [Quest2]
                        192.168.11.3
```

### Pythonの環境構築

様々な流派があると思うので、好きな方法で準備してください。
私はvenvで3.8.10を使用しています。
使用するパッケージは以下の通りです。
```
openvr==1.23.701
python-osc==1.8.0
```
pipなどでインストールしておいてください。

```
pip install openvr python-osc
```

## 実際に試してみる

まずは、SteamVRを起動し、トラッカーなどを認識させておきます。

### OpenVRのinitialize

OpenVRモジュールを使い始めるには、まずinitializeを行う必要があります。
このコードはVRChatを起動した裏で動かすプログラムから流用したので、`VRApplication_Overlay`を引数に指定しています。
(`VRApplication_Scene`辺りが本当は良さそうですが、使える関数とかが変わってきてめんどくさいので...)

```python
import openvr
openvr.init(openvr.VRApplication_Overlay)
```

### デバイスの確認

トラッカーが何番目のデバイスかを確認する必要があるので、情報を取得します。

```python
i = 0
while True:
    serial = openvr.IVRSystem().getStringTrackedDeviceProperty(i, openvr.Prop_SerialNumber_String)
    if serial == "":
        break
    print(f"{i}: {serial}")
    i += 1
```

```
0: Null Serial Number
1: LHR-5BE0522E
2: LHR-4E26D999
3: LHR-D3553062
4: LHB-2866D50D
5: LHB-233754BA
```

LHRから始まるものがトラッカー、LHBから始まるものがベースステーションなので、1から3が目的のデバイスです。
（トラッカーの機種によって異なります。）

### 姿勢の取得

`getLastPoses`関数で、デバイスの情報が取得できます。
関数の戻り値はデバイスごとの情報のリストなので、さっき確認したトラッカーの番号を指定します。
その中の`mDeviceToAbsoluteTracking`がHmdMatrix34_t型のデータなので、リストに変形して扱いやすい形にします。

https://github.com/ValveSoftware/openvr/wiki/Matrix-Usage-Example

```python
poses, _ = openvr.VRCompositor().getLastPoses([], None)
pose = poses[1] # ここの1はさっき確認したデバイスの番号
m = [list(l) for l in list(pose.mDeviceToAbsoluteTracking)]
print(m)
```
（分かりやすいように整形済み）
```
[[0.5089882016181946, -0.8607551455497742, 0.005615833681076765, -0.009101968258619308], 
[0.06500636041164398, 0.03193291649222374, -0.9973737597465515, -0.2737535536289215], 
[0.8583152890205383, 0.5080165266990662, 0.07220806181430817, -2.6730711460113525]]
```
リンク先の通り、左の3x3の部分が回転行列、右の1x3部分が位置です。
VRChatにデータを渡すには、回転行列ではなくオイラー角にしないといけないので、こちらのページを参考に変換します。

https://qiita.com/aa_debdeb/items/3d02e28fb9ebfa357eaf

```python
from math import asin, atan
rot_y = asin(m[0][2])
if rot_y != 0:
    rot_x = atan(-m[1][2]/m[2][2])
    rot_z = atan(-m[0][1]/m[0][0])
else:
    rot_x = atan(m[2][1]/m[1][1])
    rot_z = 0
```

姿勢の方も変数に代入しておきます。

```python
pos_x = m[0][3]
pos_y = m[1][3]
pos_z = m[2][3]
```

これで渡すデータの準備は完了です。

### OSCでの送信

こんな感じのコードになります。
自分の場合は`pos_x`をマイナスにしないと正しい向きになりませんでした。
また、ルームの設定が悪いのか、トラッカーが地面に埋まってしまうので、適当に`pos_y + 0.05`にしています。
VRChatでOSC機能をOnにするのを忘れないようにしてください。

```python
from pythonosc import udp_client
client = udp_client.SimpleUDPClient("192.168.11.3", 9000)

client.send_message(
    f"/tracking/trackers/1/position",
    [-pos_x, pos_y+0.05, pos_z]
)
client.send_message(
    f"/tracking/trackers/1/rotation",
    [rot_x, rot_y, rot_z]
)
```

## 実際に動くコード

[このコード](https://gist.github.com/takana-v/ab0ef6cf1509db1dbd1df8d962c049e2)をちょっと変更したものです。
ビジーループが気になる方は適当にsleepを入れてください。

```python
import math
from pythonosc import udp_client
import openvr

client = udp_client.SimpleUDPClient("192.168.XXX.XXX", 9000)

openvr.init(openvr.VRApplication_Overlay)

def get_device_index(serial):
    i = 0
    while True:
        serial_num = openvr.IVRSystem().getStringTrackedDeviceProperty(i, openvr.Prop_SerialNumber_String)
        if serial_num == serial:
            return i
        if serial_num == "":
            raise RuntimeError("指定されたデバイスが見つかりませんでした。")
        i += 1

def run_tracker_server(tracker_indexes):
    poses = []
    while True:
        poses, _ = openvr.VRCompositor().getLastPoses(poses, None)
        for i,j in enumerate(tracker_indexes):
            pose = poses[j]
            m = [list(l) for l in list(pose.mDeviceToAbsoluteTracking)]
            pos_x = m[0][3]
            pos_y = m[1][3]
            pos_z = m[2][3]
            rot_y = math.asin(m[0][2])
            if rot_y != 0:
                rot_x = math.atan(-m[1][2]/m[2][2])
                rot_z = math.atan(-m[0][1]/m[0][0])
            else:
                rot_x = math.atan(m[2][1]/m[1][1])
                rot_z = 0
            client.send_message(
                f"/tracking/trackers/{i+1}/position",
                [
                    -pos_x,
                    pos_y,
                    pos_z,
                ]
            )
            client.send_message(
                f"/tracking/trackers/{i+1}/rotation",
                [
                    rot_x,
                    rot_y,
                    rot_z,
                ]
            )

if __name__ == "__main__":
    device_serials = ["LHR-XXXXXXXX", "LHR-YYYYYYYY", "LHR-ZZZZZZZZ"]
    tracker_indexes = [get_device_index(s) for s in device_serials]
    run_tracker_server(tracker_indexes)
```

## 実際にやってみた動画

https://twitter.com/taka7n/status/1595722840976027649

## まだやってないこと

`/tracking/trackers/head/`にデータを送ることで、自動で位置合わせができるみたいです。
（自分はトラッカーが無いので、Auto-center OSC Trackers機能で位置合わせをしています。）

## 感想

やっぱりちょっとラグを感じますね...
VRChatは動かないけど、SteamVRなら動く、みたいなスペックのPCをお持ちのQuestユーザーなら有用かもしれません。
そういえばSlimeVRは公式で対応しているらしいですね。
一般ユーザーは、トラッカー公式の対応を待つのがいいかと思います。

## 余談

最初の方に触れた、「VRChatを起動した裏で動かすプログラム」についてです。
このプログラムは、修理中で無い左手コントローラーの代わりに仮想トラッカーを握ってることにして、フルトラ？（5点）を実現するというものです。
（両手のコントローラーが無いと強制3点になる仕様でした。）
仮想トラッカーは、こちらのVirtual Motion Trackerを使用しました。

https://qiita.com/gpsnmeajp/items/9c41654e6c89c6b9702f

正直OpenVRのドライバー周りを触ろうと思うと地獄なので、こちらのソフトを使用するのをおすすめします。
こんな便利なものを作ってくださった @gpsnmeajp さんには感謝してもしきれません...

## リンク

公式のヘルプページ

https://docs.vrchat.com/docs/osc-trackers