はじめに
先日、VRC PerfectSyncerというツールをリリースしました。
これは、VRChatでパーフェクトシンクに対応したアバターを使えるようにするツールです。
仕組みとしては、VRChat 2022.1.1で実装されたOSCを使って表情等を制御しています。
しかし、VRChatのOSCで使えるデータは128ビットしかなく、そのままではパーフェクトシンクを扱うことはできません
(パーフェクトシンクでは52個のBlendShapeを使い、それぞれ0~100の値を取るため)。
この記事では、この制限の中でどうやってパーフェクトシンクを実現したのかについて説明します。
自分が試行錯誤した知見の共有も兼ねて、VRChatでOSCを利用するユーザーの一助になればと思います。
VRC PerfectSyncerの仕組み
まず、VRC PerfectSyncerの仕組みについて説明します。
VRC PerfectSyncerは、専用のセットアップをしたアバターをOSCで制御することによってパーフェクトシンクを実現しています。
具体的に書くと、
-
Clientアプリで
- iFacialMocapからパーフェクトシンク用のBlendShapeの値 x 52 を受け取り
- Expression Parameter x 12 にエンコードし
- アバターに仕込んだAnimatorで
- Expression Parameter x 12 をデコードし
- 対応するAnimationClip x 52 を再生することで
- BlendShape x 52 を適用
しています。
また、アバターにAnimatorを仕込む作業はSetUpperで行います。
この仕組みを図にすると、以下のようになります。
それでは、具体的にどうやってデータをやり取りしているのかについて、次章から説明していきます。
データをやり取りする方法
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で扱える値の型には2ビットのものはありません。
OSCで扱うためには、BlendShapeの値を何らかの型に変換してあげる必要があります。
この変換のことを、ここではエンコードと呼びます。
エンコードの方法としては、ざっと以下の選択肢が思いつきます。
- Int値(8ビット) x 1 にエンコードする → 2ビット x 4を1つのInt値にする
- Bool値(1ビット) x 2 にエンコードする → 2ビットを1ビットずつに分割する
現在の実装では1の方法を取っています。
(実は2の方法の方が色々と都合が良いことに後から気づいたのですが、現状は1の実装になっています)
さて、いきなりエンコードと言われても想像がつかないと思うので、具体的な例を挙げます。
VRC PerfectSyncerでは、eyeLookOutLeft
、eyeLookInLeft
、eyeLookDownLeft
、eyeBlinkLeft
の値をPerfectSyncValue0
というInt値にエンコードしています。
例えば、これらのBlendShapeの値がそれぞれ99
、66
、33
、0
の場合、以下のようにエンコードされます。
ここで、2進数の11100100
を符号無しで解釈すると、10進数では228
となります。
つまり、この場合はPerfectSyncValue0
に228
という値が入ることになります。
以上がエンコードについての説明になりますが、これで分かりましたでしょうか?
正直、2進数は普段は触れないと思うのでパッと分からないかもですが、ひとまずこういうことをやっています。
さて、値をエンコードするということは、使う際にはデコードしてあげる必要があるということです。
デコード
エンコードは、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を示したのが以下の図です。
図はeyeDownLeft
に関するAnimatorLayerです。
見た目はシンプルですが、条件付きのTransition
が沢山繋がれています
(表示は省略されてますが、各Stateに入るTransition
は64本ずつあります)。
BlendShapeは52個あるため、この様なAnimatorLayerが計52層あります(量が多いためコードを書いて生成しています)。
さて、これでエンコードとデコードについて説明し終えました。
これで52個のBlendShapeの値をOSCでやりとりする仕組みが分かったかと思います。
ただし、このままだと値の精度を4段階に落としている都合上、動作がカクカクします。
VRC PerfectSyncerではこれをスムーズにするため、補間を行ってます。
補間
精度を落とした値をそのまま使うと、当然ですが動作がカクカクします。
これをスムーズにするため、補間を行います。
補間の行い方は簡単で、ExitState
へのTransition
のTransition Duration
を適当な値にするだけです。
VRC PerfectSyncerでは0.1秒に設定しています。
これにより、旧Stateから新Stateへ0.1秒かけて補間されます。
これで動作がだいぶスムーズになります。
補間ナシと比較して、補間アリの場合は動作がスムーズになりました。
最後に
以上で、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 について
- 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秒間待機している。
Avatar Descriptor の Lower Body の設定
VRC PerfectSyncerではUse Auto-Footsteps ...
の設定をオフにしている。
これをしないと、何故かアバターが勝手に前に進む現象が発生したため。
移動操作してないのにアバターが勝手に足踏みし始めて、勝手に歩き始める…(カメラは元の位置に残る)。
最初見た気はマジで怖かった。
同じ現象で悩んでいる人が居るかもしれないので共有です。