VRChatでOSCを使ってパーフェクトシンクできるようにした話 

# はじめに
先日、[VRC PerfectSyncer](https://natsunatsu.booth.pm/items/3687658)というツールをリリースしました。
これは、VRChatでパーフェクトシンクに対応したアバターを使えるようにするツールです。

https://twitter.com/VRNatsuVR/status/1497774309015957504?s=20&t=lLYou1OYKOZKvEdtYZFOzA

仕組みとしては、VRChat 2022.1.1で実装されたOSCを使って表情等を制御しています。
しかし、[VRChatのOSCで使えるデータは128ビットしかなく](https://docs.vrchat.com/docs/animator-parameters#parameter-types)、そのままではパーフェクトシンクを扱うことはできません
([パーフェクトシンクでは52個のBlendShapeを使い](https://hinzka.hatenablog.com/entry/2020/08/15/145040#1%E3%83%91%E3%83%BC%E3%83%95%E3%82%A7%E3%82%AF%E3%83%88%E3%82%B7%E3%83%B3%E3%82%AF%E3%81%A8%E3%81%AF)、それぞれ0～100の値を取るため)。

この記事では、この制限の中でどうやってパーフェクトシンクを実現したのかについて説明します。
自分が試行錯誤した知見の共有も兼ねて、VRChatでOSCを利用するユーザーの一助になればと思います。


# VRC PerfectSyncerの仕組み
まず、VRC PerfectSyncerの仕組みについて説明します。  
VRC PerfectSyncerは、専用の**セットアップ**をしたアバターを**OSCで制御する**ことによってパーフェクトシンクを実現しています。

具体的に書くと、
- **Client**アプリで
  - [iFacialMocap](https://www.ifacialmocap.com/home/japanese/)からパーフェクトシンク用のBlendShapeの値 x 52 を受け取り
  - Expression Parameter x 12 にエンコードし
- アバターに仕込んだAnimatorで
  - Expression Parameter x 12 をデコードし
  - 対応するAnimationClip x 52 を再生することで
  - BlendShape x 52 を適用

しています。
また、アバターにAnimatorを仕込む作業は**SetUpper**で行います。

この仕組みを図にすると、以下のようになります。
![VRC PerfectSyncerの仕組み (5)C.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285829/69b66ffb-b384-c411-a79a-b8c367ef8efb.png)
それでは、具体的にどうやってデータをやり取りしているのかについて、次章から説明していきます。


# データをやり取りする方法
52個のBlendShapeの値を、128ビット以内でどのようにやり取りしているのかについて説明します。  
結論から言うと、値の精度を落としてやり取りしています。

OSCで使えるビット数は`128`です。  
BlendShapeの個数は`52`です。

1つのBlendShapeで使えるビット数を計算すると
``` 128 ÷ 52 ≒ 2 ```
となります。

つまり、1つのBlendShapeにつき2ビット使えるという計算になります。

さて、ここで2ビットだとどれぐらいの精度でBlendShapeが扱えるのかについて考えてみます。  
2ビットというのは、2桁の2進数と考えられるので、  
| bits |
|---:|
| 00 |
| 01 |
| 10 |
| 11 |

の4つの値を取ることができます。

つまり、4段階でBlendShapeの値を表すことができます。  
具体的に書くと、以下のようにBlendShapeの値を表すことができます。
| bits | BlendShapeの値 |
|---:|---:|
| 00 |  0 |
| 01 | 33 |
| 10 | 66 |
| 11 | 99 |

こうすることで、値は飛び飛び(33刻み)になるものの、4段階で0~99までの値を表せます(本当は0~100にするべきですが現状はこうなっています)。  

ところで、元々0～100の値をたったの4段階にしてしまって大丈夫なのか、と思われるかと思います。  
ですが、(最初の動画を見れば分かるかと思いますが)意外と大丈夫でした。  
このあたりは「補間」の章で説明します。

さて、ではこれらの値を実際にどのようにやり取りしているのかについて、次章で説明します。


# エンコードとデコード
## エンコード
前章で、1つのBlendShapeにつき2ビットで表すと説明しました。  
一方で、[OSCで扱える値の型](https://docs.vrchat.com/docs/osc-avatar-parameters#avatar-parameters--config-files)には2ビットのものはありません。

OSCで扱うためには、BlendShapeの値を何らかの型に変換してあげる必要があります。  
この変換のことを、ここではエンコードと呼びます。  
エンコードの方法としては、ざっと以下の選択肢が思いつきます。

1. Int値(8ビット) x 1 にエンコードする → 2ビット x 4を1つのInt値にする
1. Bool値(1ビット) x 2 にエンコードする → 2ビットを1ビットずつに分割する

現在の実装では1の方法を取っています。
(実は2の方法の方が色々と都合が良いことに後から気づいたのですが、現状は1の実装になっています)

さて、いきなりエンコードと言われても想像がつかないと思うので、具体的な例を挙げます。

VRC PerfectSyncerでは、`eyeLookOutLeft`、`eyeLookInLeft`、`eyeLookDownLeft`、`eyeBlinkLeft`の値を`PerfectSyncValue0`というInt値にエンコードしています。
例えば、これらのBlendShapeの値がそれぞれ`99`、`66`、`33`、`0` の場合、以下のようにエンコードされます。
![VRC PerfectSyncerの仕組み (2)C.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285829/0cef7a21-212f-3fc8-c979-1bcaf984cd32.png)
ここで、2進数の`11100100`を符号無しで解釈すると、10進数では`228`となります。
つまり、この場合は`PerfectSyncValue0`に`228`という値が入ることになります。

以上がエンコードについての説明になりますが、これで分かりましたでしょうか？
正直、2進数は普段は触れないと思うのでパッと分からないかもですが、ひとまずこういうことをやっています。

さて、値をエンコードするということは、使う際にはデコードしてあげる必要があるということです。

## デコード
<!-- 前節ではエンコードについて説明しました。
この章ではエンコードされた値をどのようにデコードし、対応するBlendShapeを再生するのかについて説明します。 -->

エンコードは、BlendShapeの値 x 4をInt値に変換することでした。  
デコードは、その逆を行います。  
これを実現するのがAnimator ~~(闇)~~ です。  

OSCで送った値は、AnimatorのParameterに反映されます。  
先ほどの例だと、`PerfectSyncValue0`が`228`になるわけですね。  

これをエンコード前の値(`eyeLookOutLeft: 11`, `eyeLookInLeft: 10`, `eyeLookDownLeft: 01`, `eyeBlinkLeft: 00`)に復元することを、ここではデコードと呼びます。

では、具体的にどのようにAnimatorでデコードを行うのかについて説明します。  

Animatorには`State`という状態を表す物があり、これらを`Transition`で繋ぐことができます。  
このとき、`Transition`に条件を設定すると、その条件を満たした時のみ状態が遷移するようになります。  
つまり、「Parameter Xの値がYのとき、Aに遷移する」という処理が書けます。  

これを利用してデコードを行い、対応するBlendShapeを持つAnimationClipを再生します。
具体的な例を挙げると、以下のようにAnimationClipを再生するAnimatorを作ります。
- `PerfectSyncValue0`が`0`の場合(つまり2進数で`00000000`の場合)
  - `eyeLookOutLeft: 0`, `eyeLookInLeft: 0`, `eyeLookDownLeft: 0`, `eyeBlinkLeft: 0`
- `PerfectSyncValue0`が`1`の場合(つまり2進数で`00000001`の場合)
  - `eyeLookOutLeft: 0`, `eyeLookInLeft: 0`, `eyeLookDownLeft: 0`, `eyeBlinkLeft: 1`
- ...
- `PerfectSyncValue0`が`255`の場合(つまり2進数で`11111111`の場合)
  - `eyeLookOutLeft: 11`, `eyeLookInLeft: 11`, `eyeLookDownLeft: 11`, `eyeBlinkLeft: 11`

ちなみに、BlendShape1つにつき、1つのAnimatorLayerを使用します。
パーフェクトシンクを実現するためには全てのBlendShapeを足し合わせる必要があるため、各BlendShapeを並列して処理する必要があるためです。

実際のAnimatorを示したのが以下の図です。
![Layer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285829/89917648-db3b-7677-3cd9-90739ffe31c9.png)
図は`eyeDownLeft`に関するAnimatorLayerです。
見た目はシンプルですが、条件付きの`Transition`が沢山繋がれています
(表示は省略されてますが、各Stateに入る`Transition`は64本ずつあります)。

BlendShapeは52個あるため、この様なAnimatorLayerが計52層あります(量が多いためコードを書いて生成しています)。

さて、これでエンコードとデコードについて説明し終えました。
これで52個のBlendShapeの値をOSCでやりとりする仕組みが分かったかと思います。

ただし、このままだと値の精度を4段階に落としている都合上、動作がカクカクします。  
VRC PerfectSyncerではこれをスムーズにするため、補間を行ってます。


# 補間
精度を落とした値をそのまま使うと、当然ですが動作がカクカクします。

https://twitter.com/VRNatsuVR/status/1494737392078495747?s=20&t=lLYou1OYKOZKvEdtYZFOzA

これをスムーズにするため、補間を行います。

補間の行い方は簡単で、`ExitState`への`Transition`の`Transition Duration`を適当な値にするだけです。
VRC PerfectSyncerでは0.1秒に設定しています。

これにより、旧Stateから新Stateへ0.1秒かけて補間されます。
これで動作がだいぶスムーズになります。

https://twitter.com/VRNatsuVR/status/1494888514692939777?s=20&t=lLYou1OYKOZKvEdtYZFOzA

補間ナシと比較して、補間アリの場合は動作がスムーズになりました。


# 最後に
以上で、VRC PerfectSyncerのパーフェクトシンク部分についての説明を終わります。
VRC PerfectSyncerでは他にも頭と視線の制御を行っており、これらはパーフェクトシンク部分と違った処理をしているのですが、
そこまで書くと記事が長くなるため、知見だけオマケとして共有しておきます。
ここまで読んでいただきありがとうございました！

(もし良ければ、最初のツイートをRTいいねしてもらえると嬉しいです！)

# オマケ
## Stateをすぐに遷移させる方法
結論から言うと
- 常に`true`になるparameterをAnimatorに追加しておく(例として名前を`True`とする)
- `Transition`の`Condition`に`True`=`true`を追加

これでStateがすぐに次のStateに遷移する。

普通に考えると、Stateの`Has Exit Time`にチェックを入れて`Transition Duration`を`0`を設定すれば良さそうだが、何故か`0`に設定すると遷移しなくなるという罠(仕様？)がある(推測だが、内部的に無効な設定とみなされてそう)。

このため、Stateをすぐに遷移させるためには上記のようなハックをする必要がある。
今回はAnimatorでデコーダを実装する必要があったので(それ自体が異常だが)、このテクは必須だった。

Animatorで何かしらのロジックを組む人は覚えておいた方が良さそうと思ったので共有しておきます。
(他に良い方法があれば教えてもらえると助かります🙏)

## Avatar Parameter Driver について
https://docs.vrchat.com/docs/state-behaviors#avatar-parameter-driver
- AnimatorのStateに追加して使うコンポーネント
- これが追加されたStateに遷移したとき、好きなパラメータを好きな値にできる
- これの何が嬉しいかと言うと、複数のAnimatorLayerにまたがるような処理を書けるようになる

### 実際に使った例
頭の回転値のデコード
- 回転を表すには3つのパラメータが必要
- 当然、パラメータは同時にデコードしたい
- しかしAnimatorの仕様上、同時に処理を行うためにはレイヤーを分ける必要がある
- そこで`Avatar Parameter Driver`を使い、別々のレイヤーでデコードした値を別のパラメータにセットしている

## Animator Tracking Control
https://docs.vrchat.com/docs/state-behaviors#animator-tracking-control
アニメーションによる頭の回転を有効にするのに使った。
具体的には、`Head`を`Animation`にした。

これをしないと、頭が回転しない代わりに肩から下が少し動くという状態になった
(おそらくIKによる頭の回転が優先されるため)。

また、使い方に少しクセがあり、Animatorが動き始めてからすぐに実行しても無視されるので注意。
これは推測になるが、おそらくアバターの初期化処理中に実行されてしまい、設定が上書きされている。

これを避けるため、VRC PerfectSyncerでは`Wait State`を作って2秒間待機している。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285829/8d4e2441-800e-5893-09a6-229de0e79eb0.png)

## Avatar Descriptor の Lower Body の設定
VRC PerfectSyncerでは`Use Auto-Footsteps ...`の設定をオフにしている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285829/e4c09b3e-2320-d4d7-b96a-41bf62ddd854.png)
これをしないと、何故かアバターが勝手に前に進む現象が発生したため。
移動操作してないのにアバターが勝手に足踏みし始めて、勝手に歩き始める…(カメラは元の位置に残る)。
最初見た気はマジで怖かった。

同じ現象で悩んでいる人が居るかもしれないので共有です。

<!--
### ローカルでの動作確認
通常だと`Avatar Parameter Driver`の挙動はローカルでは確認できないが、以下のスクリプトをプロジェクトに入れておけばUnityをPlayモードにすると挙動が確認できる(自作スクリプト)。

本当にVRChatで行われる処理と同じかどうかは分からないが、値をセットするだけなのでおそらくこういったシンプルな処理をしているだけのはず…という推測で書いたコード。

実際、VRC PerfectSyncerの動作確認で使った分には問題なかったので共有しておく。

```
using System;
using System.Linq;
using UnityEngine;
using VRC.SDKBase;

public static class VrcAvatarParameterDriverInitializer
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    public static void Initialize()
    {
        VRC_AvatarParameterDriver.Initialize += avatarParameterDriver =>
        {
            avatarParameterDriver.ApplySettings += (driver, animator) =>
            {
                foreach (var parameter in driver.parameters)
                {
                    switch (animator.parameters.First(p => p.name == parameter.name).type)
                    {
                        case AnimatorControllerParameterType.Float:
                            animator.SetFloat(parameter.name, parameter.value);
                            break;
                        case AnimatorControllerParameterType.Int:
                            animator.SetInteger(parameter.name, (int)parameter.value);
                            break;
                        case AnimatorControllerParameterType.Bool:
                            animator.SetBool(parameter.name, parameter.value == 1);
                            break;
                        case AnimatorControllerParameterType.Trigger:
                            animator.SetTrigger(parameter.name);
                            break;
                        default:
                            throw new ArgumentOutOfRangeException();
                    }
                }
            };
        };
    }
}
```
-->