見出し画像

VRChatワールドの同期を考える(アバター編)

誰が言ったか、「同期のコツは、同期しないこと」なんて言葉もあります。
この記事は、私がVRChatのアバターギミックを幾つか作っていく中で気づいた、同期のコツみたいなもののアウトプットです。
Avatars3.0が何かとか、Unity Animator用語とかの基礎的な話は、有志による入門向け記事がもう充実しているので、ここでは省略します。


Avatars3.0の同期のしくみ

ワールド編では事象同期( SendCustomNetworkEvent() )と状態同期( UdonSynced )の2種類が使えると説明しました。
アバターでは状態同期のみが可能です。以下、詳しく見ていきましょう。

Avatars3.0における同期の概略図

同期の説明の前に、VRChatの(Avatars3.0の)アバターがどう動かされているのかをおさらいしておきましょう。
アバターには、(それがアニメーション操作を可能とするなら)ルートオブジェクトにAnimatorコンポーネントが付いています。図では省略していますが、VRC Avatar DescriptorのPlayable Layersにセットされた各Animator Controller(以下Controller)が一つのControllerとして合成され、このAnimatorコンポーネントに渡されることで、アバターがどう動くかが決められます。

ローカルプレイヤーの元では、アバターのアニメーション(歩行などのモーション、エモート、FXレイヤーでやってるようなギミック全般)は基本このAnimatorを経由して実行されます。
HumanoidボーンやVRC Avatar Descriptorに設定したボーン、ブレンドシェイプに関しては、その部位のTracking ControlがTrackingモードに入っていると、更に上書きされるような形でVRChatが制御します。

さて、ではリモートプレイヤーはというと、アバターデータ自体はローカルプレイヤーと同じものがロードされます。
あとはローカルプレイヤーの状態を同期して反映してあげれば、同じアバター状態が再現できるはずですが…ネットワーク同期にはタイムラグや通信速度の問題がつきものです。そのため、最低限の通信量にすることで可能な限り応答速度を落ちにくくするのが常となっています。

やっと本題です。では何が同期するのか?というと…

  • VRCが定義済みのパラメーター

  • Expression Parameters

  • Tracking用IK情報 (主にHumanoidボーン用)

  • (プレイヤーの座標、アバタースケール)

  • (PhysboneコンポーネントのGrab/Pose中の座標)

現状これだけです。
これ以外で同期しているように見えるもの(Local OnlyをオフにしたVRC Avatar Parameter Driverの挙動、Contactsの接触判定など)は全て、これらの同期状態からリモートプレイヤーの手元(ローカル)で再計算されて再現されたもの、という事です。
VRC Constraintsコンポーネントには公式機能としてワールド固定機能も持っていますが、これも一切がローカル計算なので、"ワールド固定した"事を別途同期しないと同期しません。
以降、同期するものを直接同期、同期してきた情報を元にローカルで計算することで状態を再現しているものを間接同期と呼ぶことにします。

直接同期するもの

VRCが定義済みのパラメーター

Controller内に設定したパラメーターに、上記一覧と「名前(大文字小文字も)」と「型」が一致するものがある場合、それはVRChat上ではRead Only扱いとなり、強制的に制御されます。そしてこの値は同期されます。
誰もが一度は見たことのあるGestureRight/Leftも、これの一部だった訳ですね。

Expression Parameters

ExpressionParametersのInspector

直接同期の一つ、Expression Parameters (以下ExParams)。
Expression Menu (以下ExMenu)やOSCからAnimatorのパラメーターを操作するには、このExParamsを仲介する必要がありますが、それとは関係無く、ここに追加してSyncedにチェックを入れたパラメーターは、ローカルプレイヤーの元で値に変更があった時に、リモートプレイヤーに対し同期送信されます。
※かつてはExParamsにセットしたパラメーターは問答無用で同期対象となっていたため、古い解説記事だとSyncチェックボックスの説明が無いので注意

Tracking用IK情報

未検証ですが、どうもローカルプレイヤーのTracking状況にかかわらず、Humanoidボーンの座標(位置・向き)情報はリモートプレイヤーに直接送信されているようです。(正確な情報求む)
※2023/8/22追記
Humanoidボーン座標が直接同期する情報はガセでした。

Tracking ControlがTrackingに入っている部位は本人のトラッキング状態がアバターに適用される訳ですが、これを他人に対しても再現する必要があります。
ここでは詳細を省いた説明をしますが、Trackingが何をしているかというと、アバターの姿勢はローカルプレイヤーのトラッキング座標で直接制御しているわけでは無く、トラッキング座標を元にIK情報を算出し、そのIK情報を元にアバターを制御しています。同期されるのはこのIK情報の部分で、他人のクライアント上ではIK情報を元にアバターのTracking部位が操作されます。(そういう意味ではHumanoidボーンも間接同期と言えます)
※IKというのは、一言で言うとアバターに対するあやつり糸みたいなものです。

VRC Animator Tracking Control

直接同期を使った同期設計

その他の同期するものは主題から逸れるため横に置きます。
とにかく、ここまでに出てきたパラメーターは直接同期され、リモートプレイヤーのクライアント上でロードされた貴方のアバターのパラメーターを直接書き換えます。
つまり、このパラメーターを元に遷移条件を組む場合、シンプルに遷移条件を組めば良い事になります。

直接同期するパラメーターによる遷移図(例)

間接同期するもの

ここまではAvatars3.0初期の根幹機能という事もあり、新情報が無かったという人も居るかと思います。ところでVRChatは、需要に合わせて、複雑になってでも自由な機能を追加/変更してきました。ここからは、そんな機能を使う場合の同期設計を見ていきます。ギアを上げていくぞっ

同期してきた情報を元に間接的に同期するもの、という言い方をしましたが、これは裏を返せば、それ自体は同期をしていない事を示しています。
故に考慮漏れがあると、同期ズレや所謂チャタリングが起きかねません。原則としては「同期したものを同期するな」といった所でしょうか。
これも機能ごとに見ていきましょう。

VRC Avatar Parameter Driver

Avatars3.0ではプレイヤーが独自スクリプトを持ち込むことは出来ませんが、代わりにVRChatがController内のステート/サブステートマシンに追加するStateMachine Behaviourスクリプト(以下ビヘイビア)の形で、幾つかアニメーション操作の便利機能を提供しています。

VRC Avatar Parameter Driver

他のビヘイビアもそうですが、VRC Avatar Parameter Driverはステートに添付する形で追加します。そしてVRChat内では「そのステートへの遷移が確定した瞬間」に実行され、Controller内の指定のパラメーターを書き換える事が出来ます。
先述の通り、VRC Avatar Parameter Driver自体には同期機能は存在しませんが、(他の)同期パラメーターによる遷移条件で遷移するなら、本人と他人とで同じステートに同じタイミングで遷移するので、同じVRC Avatar Parameter Driverが実行される事になります。まさに間接同期する機能という訳です。
ここで紛らわしいのがLocal Onlyのチェックボックスですが、これにチェックを入れていてもステートには遷移します。ここでいうLocalとはアバターを着ている本人の事で、「実行したのが本人だった時のみこのVRC Avatar Parameter Driverの内容を実行する」、言い換えると、「実行したのが他人プレイヤーなら何もしない」機能のチェックボックスです。
例えばランダムな値が作りたいけど、その結果は同期して欲しい、という時には、そのパラメーターをExParamsにSynced登録しておいて、Local Onlyにチェックを入れたVRC Avatar Parameter DriverでRandomに値を書き換える遷移を組む、といった機構が考えられます。こうする事で、本人の元でのみパラメーターの書き換えが実行され、その結果のみがExParamsを通して他人に同期され書き換えられることになります。


ランダム値の同期イメージ

他にもステートを大量に行き来するような複雑なレイヤーは必要だが、その中で書き換わる値が全てExParamsに登録されていてリモートプレイヤーには遷移すら必要無い、というような場合は、VRChatの定義済みパラメーターである IsLocal が使えます。
以下のようにデフォルト(オレンジ)ステート/Any Stateからの遷移条件にIsLocal = trueを加える事で、本人の元でのみ複雑な遷移が実行されるレイヤーを構築できます。

IsLocalパラメーターによる本人限定処理

Contacts/Physbone (パラメーター)

Avatar Dynamicsと称して実装された、待望の物理演算系コンポーネントであるContacts/Physboneには、その状態変化をAnimator Controllerのパラメーターに直接渡す機能を持ちます。
これらのコンポーネントはアバターのボーンを直接指定して追従させる機構となっています。Humanoidボーンは同期するので、その子孫オブジェクトに付ければ、必然的にこのコンポーネントが持つ"当たり判定"も位置同期する、同じ動きをするから接触判定も概ね同時に発生する。という理屈になっており、そのためこれらのコンポーネントも直接的に同期することはありません。

PhysboneからAnimatorに渡せるパラメーター
Contact ReceiverからAnimatorに渡せるパラメーター

ここまで読んでピンと来た方、その感性は重要です。そうです。アバターの動きには当然タイムラグがあるので、各クライアントの状況次第では、接触判定が発生したりしなかったりします。また、Physboneの各種値やContactsのProximityのようなアナログ的な値は、基本一致しておらず、それぞれのクライアントの環境化で説得力のある値を取っているもの、と考えるべきです。
更にこれらのコンポーネントはExMenuと違い、Bool値であっても毎フレーム書き込もうとするようです。そのため、Contacts/Physboneのパラメーターを直接ExParamsに登録するのは非推奨です。同期していないように見えるか、チャタります。
Contact Receiverの方にはLocal Onlyオプションもありますが、それ込みでもOnEnterだと同期抜けの可能性があるので非推奨です。特にOnEnterは、接触した1フレームだけtrueになったのちfalseになる受信タイプです。一方でExParamsは10~1Hz間隔でしか値を同期しないので、取りこぼしの可能性が高いです。Constantでも接触期間によって、同じく取りこぼしの可能性があると言えます。

ではどうするか。

Physboneの場合は、そもそもそういう物として、同期の正確性が不要な用途に限定して使う、という考え方も出来ます。これにはメリットもあり、それぞれのクライアント上では見たままの動きが直接パラメーターに反映されている事になるので、タイムラグを感じさせないようにできます。応答性が重要なら"あえて同期しない"選択は一考の余地があります。

Contactsの場合も応答性>正確性であれば同じ考え方でも良いのですが、こちらはどちらかというと正確性が優先される、Bool値を書き換える使われ方の方が多いでしょう。
そこで、Contact Receiverで書き込むパラメーターと、ExParamsで同期するパラメーターを別々にします。そして本人の手元でのみContactsの接触判定を元にした状態遷移を行い、そこでVRC Avatar Parameter Driverを使って同期用パラメーターを書き換えるのです。このワンクッションを挟む事により、他人にはExParams経由で確実にパラメーターの値(=状態)が同期されます。
ExParamsによる値の同期もされるので、後から来た人にも同期が保証されます。

以下の例は、Contact ReceiverのOnEnterを使い、接触判定が発生する度にオブジェクトのオンオフを切り替えるギミックです。Contact_OnEnter条件の遷移のところのDurationを調整する事で、ローカルでのチャタリング対策にもなるでしょう。
1レイヤーでContactsの接触判定と同期パラメーターの状態遷移までやってしまっていますが、パラメーター制御専用のレイヤーと、同期パラメーターを元にしてアニメーションを実行するだけの表示用レイヤーの、2レイヤー構造にした方が良いでしょう。

Contactsで同期するトグルスイッチのサンプル

おわりに

最後に紹介したContactsの同期制御は、一見「同期したものを同期するな」原則に反しているように見えますが、「Humanoidボーンの同期による接触判定の間接同期」、「同じステートに遷移する事によるVRC Avatar Parameter Driverの間接同期」を回避し、「オブジェクトを表示する」という結果だけが同期しています。
このように、何が同期して、何が(同期しているようにみえているだけで、その実)同期していないかを見極め、1回だけの同期で"状態"が一致するように組む、というのを意識する事が、同期設計では重要です。
慣れるまでは難しいところですが、パラメーター名を分かりやすくするなど工夫していきましょう。

いいなと思ったら応援しよう!