[VRChat] 相対位置を維持するワールド固定の解説

本記事は[VRChat Advent Calendar 2021](https://adventar.org/calendars/6466)の13日目の記事です。前回はあーあーあ～さんの[Cauchy-Binetの公式による行列木定理の証明](https://aaavrc.hatenablog.com/entry/2021/12/12/000008)でした。
#はじめに
自由な場所を持てる指ペンを作りました。

https://twitter.com/INUSHOUGUN/status/1375762412503109638?s=20

これで使ってる「好きな所を持ち直せる」タイプのワールド固定(仮想ピックアップ)の解説です。
※簡単に導入するための説明はないです。原理の解説なのでアニメーション等には触れません。
#(従来型)ワールド固定の原理
まずは復習
##仮想原点
ワールド中を動き回るアバターの中に、常にワールド原点に固定される仮想原点オブジェクトを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2376708/0589846e-4686-6912-12a0-89f523214180.png)
いつものやつです。自分の位置回転を打ち消します。
![原点.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2376708/841dda36-a157-4cdf-466d-daed53bda6aa.gif)

:::note warn
設定時に仮想原点がワールド原点からズレているとうまく固定されません
:::
##Constraintの無効化
ParentConstraintは有効にしてる間、ターゲットに自身の親であるかのように追随しますが、無効にすると__現在の位置回転を保ったまま__追随が切れます。
![Constraint.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2376708/e30c930c-05de-7094-f805-1caa9372a497.gif)

これを合わせると、原点に対して固定されたりされなかったりするオブジェクトをアバターに仕込めます。
#(新型)仮想ピックアップの原理
以下説明のために単語を定義します

|単語|意味|
|:--|---|
|Hand|手など持ち上げるのに使うボーン|
|Origin|仮想原点|
|Sphere|固定するオブジェクト|
|Point|持ち上げる際の追随点|
|Pickup|持ち上げた状態|
|Drop|固定した状態|

ワールド固定を解除した際にSphereが手元に飛んでくるのは、PointとSphereが離れているからです。
![Fly.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2376708/7e319dcf-0fbf-44ee-0631-05ead59587cf.png)
置いた場所で持ち上げる=PointとSphereが同じ位置・回転であれば飛んで来ないですね。
![NoFly.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2376708/ba1ddf00-0f31-5a0a-2be9-c9964b8bbb15.png)
そういうわけで、Drop中にPointがSphereを置いた地点から動かないようにすれば、飛んでくることはなくなります。
Drop中はPointがSphereに追随するようにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2376708/a1bca43c-e9ab-d15a-590c-74fc82a15dbe.png)
Drop時にSphereのParentConstraintを無効にすると同時に、PointのParentConstraintを有効化します。これによって、親子関係が入れ替わります。
Pickup時は逆に、Sphereのを有効にしてPointのを無効化します。

- Pickup中: SphereはPointに追随 PointはHandの子 => Sphereがアバターに付いてくる
- Drop中: PointはSphereに追随 SphereはOriginの子 => Sphereはワールドに固定される この状態でPickupへ切り替えると、SphereがPointの位置へワープするが同位置にあるので飛ばない

![Pickup.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2376708/2538b6fe-c7d6-07e1-5a8e-8b6a5bd54c60.gif)
これで仮想ピックアップが実現できました。

これだけだと位置のリセットができなくて不便なのでそちらの仕組みも作ります[^1]。
Transformを(0,0,0)とかやるとWriteDefaultsが悪さするので、ParentConstraintで、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2376708/7d682a7f-0127-b54f-8c46-65f97dc1ac1c.png)
PointをHandに親子付けしてリセットしましょう。リセット時以外はHandへのWeightを0、Spereへのを1に。リセット時はHandへのWeightを1、Sphereへのを0に切り替えればいいです。
#応用
PPPenでは手と仮想原点に対して相互に固定できる仕組みですが、左右の手にそれぞれ固定させれば武器の持ち替えのようなことができます。
頭と手にすれば帽子の位置を調節するみたいなギミックにもなります。
うまくやれば3点以上での持ち替えもできそう。
#留意点
PickupとDropを繰り返す度に位置ズレが累積するので長時間や素早い持ち替えには注意が必要です。
ExpressionParametersよりGestureの方がIKと同期タイミングが近い[^2]ようなので、アクションに使う場合などはハンドサインの方がよさそう。
LateJoinerや持ち替え時にアバターが読み込まれていない相手にも無力です。適当なタイミングで位置リセットしてください。
#おわりに
解説中のParentConstraintは全部Offsetを0にしています。
Constraintの設定は細かい注意点が多いし、アニメーションは手順が多いし、導入手順を解説するには複雑過ぎて諦めました。
何かと活用できそうなギミックなので、理解できたら使ったり、解説したり、販売などしてもらえると嬉しいですね。

明日はmkc1370さんの[Udonでプリクラを作った話](https://qiita.com/mkc1370/items/1f311a8944f9ff627202)になります。

[^1]: ペンは消せばいいので位置リセットしてません。
[^2]: ペンの筆記を最初はメニュー操作でやっていたのですが、他人からまともに読めません。ジェスチャ操作にしたら素早く書いても十分程度読めるようになりました。