VR空間での自然なインタラクションのための問題と解決法

　この記事は『[ョョョねこ Advent Calendar 2020](https://adventar.org/calendars/5281)』の15日目の記事です。

　ョョョねこの集団にこっそり入り込んだ”あらいぐま”のアライラクです(ねこじゃないにゃ)

　ワールドとかアバターとかを人に語れるような知識はないので、代わりに少し知識のあるハプティクスについてお話したいと思います。
　間違いや指摘があれば教えてもらえると助かります。
#目標とすること

　ハプティクスとは、VRの中で、触覚に関連する研究分野です。今回お話するのはその中でも、VR上の物体と自然なインタラクションをするための方法の1つであるVirtual Coupling法[^1]を紹介します。

　“自然な”インタラクションといっても色々ありますが、今回は、現実のような力学でユーザの手とVR上のオブジェクトが干渉できるための方法について言及します。

　やりたいことを明確化するために、ベースケースとして両手で側面から箱を持ち上げるということをしましょう。

　現実では持ち上げる動作に図のような力が発生しています。これから説明するのは、これと同じようなことをVR上で実現するには？ということです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170475/fcd9b77f-612e-97e4-e8c2-6690d8217d0f.png" >
#VRの問題

　まず、これを実現するために障害となるVRの問題から説明していきます。

　VRCをプレイしているとVR上の手がVR上の壁を貫通することがよくあります。
　なぜVRではVR上の手が物体に貫通するのでしょうか？
　現実では手が机などに埋まることはないですよね？
　この差を考えるとVR上では見えてても現実に物体が存在しないことが原因であると言えます。
　現実では壁を押すと力学的に反力が返って、釣り合うことでその場に静止します。VR上ではその場に物体が視覚的に見えていても質量と形が存在している物体が存在しないため、当然力学的な力が働かず、手が物体を通り抜けてしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170475/4b0201ba-d08f-7d67-5a7a-a73b585cea96.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170475/5c7ec465-e3e4-2dee-e467-170000ce9bb8.png)

　今の話は、手にはコライダーが存在せず、物理シミュレーションをしていない時の話です。当然、物理演算をしなければ力学の挙動は起こりえません。
　次に、トラッキングした手の位置にrigidbodyとコライダーを入れた箱を手に見立ててベースケースである横から物体を持ち上げることをしましょう。

　動画の緑色の箱が手の代わりです。この箱はUdonを用いて手の位置と姿勢を同期します。

```c#
    private void FixedUpdate()
    {
        var rightHandData = Networking.LocalPlayer.GetTrackingData(VRCPlayerApi.TrackingDataType.RightHand);
        var leftHandData = Networking.LocalPlayer.GetTrackingData(VRCPlayerApi.TrackingDataType.LeftHand);

        rightHand.position = rightHandData.position;
        rightHand.rotation = rightHandData.rotation;
        leftHand.position = leftHandData.position;
        leftHand.rotation = leftHandData.rotation;
    }
```
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">箱が持てないあらいぐま <a href="https://t.co/9jg0GKXkw8">pic.twitter.com/9jg0GKXkw8</a></p>&mdash; アライラク (@AraiRacu) <a href="https://twitter.com/AraiRacu/status/1338424739665047553?ref_src=twsrc%5Etfw">December 14, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
　そうすると動画のようになかなかうまく持てません。
　なぜこれが起きるかを掘り下げて考えてみましょう。ポイントは、手が自由に動かせることにあります。現実ではVR上の物体が存在しないため、手が物体によって拘束されることはありません。そうするとVR物体にめり込ませることが簡単にできてしまいます。
　物理シミュレーションでは、こういった同じ場所に複数のオブジェクト(Unityの場合はコライダー)が存在しないように辻褄を合わせようとします。その辻褄合わせの手段として、例えば、めり込んだらめり込んだだけ押し返すようなことをします。通常のシミュレーションならこれで辻褄があうのですが、VR、特にユーザに直接、反力が返ってこないようなデバイスを使用している場合、プレイヤーの手はVR物体に大きく侵入してしまいます。押し返したいのに押し返せないため、非常に大きな力が働き、オブジェクトが吹っ飛んだりするようなおかしなシミュレーション結果になります。
　要するに、プレイヤーの手をシミュレーション側で押し戻すことができないため、とてつもない力が働いてしまい、シミュレーションが破綻してしまうのです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170475/80d22224-af6f-dc71-337e-aa76d2ceaeec.png)
　これがVRの問題の大きな1つです。現実にはVR物体が存在せず、ユーザに反力をフィードバックできないため、手がVR物体に大きく侵入し、シミュレーションが破綻してしまうのです。

#解決法の考え方
　以上から、これら問題をどうすればいいか？それは、今まで使っていた手と物体のインタラクションの間にもう1つ辻褄合わせのための手を挟んで、この手をVR上の物体に干渉させればよいのです。
　この手は便宜上VR手と呼びますが、物体と干渉していないときは基本的にトラッキングした手と同じ位置にあります。しかし、物体と干渉する際にはトラッキングした手は物体に大きく侵入してもVR手は物体表面に存在し続けます。さらに、物体へ力を加えられるような手があれば、現実のような動作をすることができますよね？
　これでは、トラッキングした手(つまり現実の手)とVR上で見えている手がずれてしまいますが、よほど大きくずれなければ、気づかないです。(たしかそういう研究もあったはず…)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170475/d0c08f06-3e95-4a3a-ee03-994f4bb6f5fa.png)

#Virtual Coupling法
　では、トラッキングした手とVR手はどのように関連付ければいいのか？
それにはいくつかの方法があるのですが、力学のバネの考え方を導入するのが、今回紹介するVirtual Coupling法[^1](以下、VC)と呼ばれる方法です。
　トラッキングした手とVR手を仮想的なバネでつなぎます。そうして、VR手をトラッキングした手で引っ張るイメージです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170475/5ea40b40-6941-62b7-b875-9502c8066c73.png)
　そうするとVR手はバネによって常にトラッキングした手に近づこうとします。物体に干渉していない状態ではトラッキングした手とVR手はほぼ同じ場所にいます。物体に干渉するとトラッキングした手は物体とは干渉せずに物体に侵入しますが、VR手は物体と干渉し、表面で止まります。さらに、トラッキングした手の物体への侵入量を大きくするとその分VR手との距離が大きくなり、戻そうとする力が大きくなり、それが物体へ与えられます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170475/834d4cde-55ec-9079-f08e-11a093dc0395.png)

#VRChat Udonを用いた実装
　では実際にやってみましょう。
　今回はSpringJointを使わずに、自分でバネの挙動をするコードを書きます。(SpringJointは狙った動きをしなかったので…)

　まずは並進力です。バネ力は高校物理で$F=kx$ですね。ただ、これだけではバネが振動し続けて、収束しません。そこで、バネに粘性を加えます。ダンパってやつですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170475/7c40a31c-98b9-eff4-01d0-a3b98be63077.png)
　これを加えた式は

```math
F=kx+B\dot x
```
となります。それぞれの変数を説明すると
$F$　:　VR手に与える並進力
$k$　:　並進バネの係数
$B$　:　ダンパの係数
$x$　:　変位(トラッキングした手とVR手の位置差(ベクトル))
$\dot x$　:　xの微分(トラッキングした手から見たVR手の速度)
です。
　変位の微分がなぜ速度になるのかなどについては本筋から外れるのと少し長くなるので説明は省略します。$B\dot x$をつけると振動が収束するんだなという認識で大丈夫です。

　これまでは、位置のみの話でしたが、手に形が存在すると姿勢についても考える必要があります。
　姿勢を追従するための力はは並進力ではなく、回転トルクになります。
　ただ、考え方は並進力と同じで、変位を位置ではなく、姿勢としてみるだけです。よって、式は

```math
T=k\theta+B\dot \theta
```
となります。
　$\theta$を求めるためには、クォータニオンの計算が必要ですが、ここも説明は省略します。

　ここで得られた並進力とトルクをVR手にAddForce/AddTorqueによって作用させます。

　以上をUdonで実装するとこのようになります。(クォータニオンからオイラー角に変換する際に色々追加処理をしています。)

```c#
    [SerializeField] private GameObject realHand;
    [SerializeField] private float PSpring;
    [SerializeField] private float PDamper;
    [SerializeField] private float RSpring;
    [SerializeField] private float RDamper;

    private Rigidbody Vrigid;

    private Vector3 beforeDiffPos;
    private Quaternion beforeDiffQuat;

    void Start()
    {
        Vrigid = this.GetComponent<Rigidbody>();
        beforeDiffPos = new Vector3(0, 0, 0);
        beforeDiffQuat = new Quaternion(0, 0, 0, 1);
    }

    private void FixedUpdate()
    {
        //変位の計算
        Vector3 diffPos = realHand.transform.position - this.transform.position;
        //速度の計算
        Vector3 diffVel = (diffPos - beforeDiffPos) / Time.deltaTime;
        //並進力の計算
        Vector3 Force = diffPos * PSpring + diffVel * PDamper;
        beforeDiffPos = diffPos;

        //姿勢差の計算
        Quaternion diffQuat = realHand.transform.rotation * Quaternion.Inverse(this.transform.rotation);
        //角速度の計算
        Vector3 diffAngleVel = Quat2Euler(diffQuat * Quaternion.Inverse(beforeDiffQuat)) / Time.deltaTime;
        //トルクの計算
        Vector3 Torque = Quat2Euler(diffQuat) * RSpring + diffAngleVel * RDamper;
        beforeDiffQuat = diffQuat;

        //rigidbodyに追加
        Vrigid.AddForce(Force);
        Vrigid.AddTorque(Torque);
    }

    private Vector3 Quat2Euler(Quaternion quat)
    {
        //クォータニオンをオイラー角に変換
        Vector3 eularRad = quat.eulerAngles;

        //オイラー角を-180<theta<180の範囲に置換(udonだと何故か配列でエラーが出た？)
        if (eularRad.x > 180.0f)
        {
            eularRad.x = eularRad.x - 360.0f;
        }
        if (eularRad.y > 180.0f)
        {
            eularRad.y = eularRad.y - 360.0f;
        }
        if (eularRad.z > 180.0f)
        {
            eularRad.z = eularRad.z - 360.0f;
        }

        //ラジアンに変換
        return eularRad * Mathf.Deg2Rad;
    }
```

　これを実装した結果がこの動画です。先程の動画から赤色の箱であるVR手が追加されました。
　そうすると、緑色のトラッキングされた手がVR物体をめり込み、それに引っ張られるVR手が力を出すことで持ち上げることができました。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">箱が持てるあらいぐま <a href="https://t.co/VMSLZmPSim">pic.twitter.com/VMSLZmPSim</a></p>&mdash; アライラク (@AraiRacu) <a href="https://twitter.com/AraiRacu/status/1338424855184564224?ref_src=twsrc%5Etfw">December 14, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

###余談：アニメーションを用いた方法との違いについて
　自然なインタラクション”風に見せる”方法の1つとしてアニメーションを使った手法があります。わかりやすい事例としてはHalf-Life Alyxにおいて、色々なオブジェクトを持つ、操作する動作に使われています。
　VCとこのアニメーション方式の決定的な違いは物体に力を与えられるかどうかです。アニメーションの場合は、持つか持たないかなど、作ったアニメーション分の決められた動きしかできません。それに対してVCは、実際に物体に力を加えられるため、例えば、物を持ってから手を緩めて、ゆっくり滑り落とすなどといった力の加減によって、できることが無段階に増やすことができます。

#VC法の問題点
　もちろん、この方式にも欠点があります。
　一番の欠点が、これがバネであるということと現実の力学に似せた物理シミュレーション上で行われているということです。
現実では、常に力学的な作用が行われています。しかし、シミュレーションの世界では、一定時間ごとに物理の計算->それをもとにVCの計算->それを物理の計算に使うというループが回っています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170475/64c3b8d5-d0a4-92af-5f1a-07cb3e4b6f2f.png)
　その間隔が有限であることから、必ず計算誤差が生じてしまいます。その誤差が含まれている結果をもとにVCの計算をするとそこに誤差が含まれます。もし、VCのバネやダンパの係数の値を大きく設定すると計算結果の誤差が大きくなります。これを繰り返すと誤差が無視できなくなるほど大きくなり、VR手が振動するなどの不安定な挙動を起こしてしまいます。
　そのため、バネをもっと強くしたくても強くできないという問題が生じます。例えば、トルクのバネとダンパの係数を大きくするとこのように振動してしまいます。
　(動画では角速度にリミッターが掛かっているのと、動画のfpsが小さいという理由でそこまで大きく振動はしていませんが…)
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">振動する箱 <a href="https://t.co/047ossw9pZ">pic.twitter.com/047ossw9pZ</a></p>&mdash; アライラク (@AraiRacu) <a href="https://twitter.com/AraiRacu/status/1338424942711373824?ref_src=twsrc%5Etfw">December 14, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

　バネの特性から来る問題もあります。薄い物体が持ちづらいという問題です。トラッキングした手とVR手の位置差が大きくなればなるほど大きな力が発生します。逆に、位置差が小さいと小さな力になります。薄い物体ではトラッキングした手の侵入量が小さくなってしまい、持ち上げるために十分な力が得られない問題もあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170475/5841aefa-b21e-e24e-d7a2-23570d091bad.png)
 さらに、物理演算を使っているため、アニメーションを用いる方法に比べ、必要な処理が大きいという問題があります。物理演算はかなり計算量の大きな計算を高速に回す必要があり、処理のウェイトが大きくなります。
#おわりに
　Half-Life Alyxをプレイした方ならわかると思いますが、実際にVR空間のオブジェクトを自然に持ったり、操作したりという体験はユーザへとても強い印象を与えることができます。さらに、現実と同じ操作であることから操作を習得するまでの時間が短くて済むことも重要です。このように、現実と同様の物体とのインタラクションができることの意義はとても大きいと思います。
　ただ、上げた振動の問題など解決が難しい点が多く、これに関しては現在進行系でいろいろな研究がなされています。

　また、今回はフィードバックに関しては言及していません(この話も色々長いので…)。本来であれば、VCで求めた力を何らかの形でユーザへフィードバックされます。どれだけの強さをユーザにフィードバックするかについても、VR手に与えるVC力をそのまま利用できることもこの方法の強みです。

　今回説明した方法は活用しづらいかもしれません。しかし、知っておいてほしいところは、「VR物体は現実には存在しないこと」です。当たり前といえば当たり前ですが、これによって様々な問題が起こります。このVC法は解決策の1例に過ぎず、他にもいろいろな解決方法があります。この問題をどうするかということがVRを作る側の考えなければならないところです。

　頭の片隅にでもそういうことをあるんだなぐらいに置いて貰えれば幸いです。

　冒頭に述べたように間違いや指摘、疑問などがあれば、答えられる範囲で答えますので、下のツイッターのアカウントへDMかリプなどを送ってください。

P.S.
　今回の話とは逸れますが、VRChat向けの3Dアバターや小物などをBoothにて置いているので、覗いてもらえると嬉しいです！！~~(宣伝乙)~~
Booth:[RacoonStudio](https://arairacu.booth.pm/)

Twitter:[@AraiRacu](https://twitter.com/AraiRacu)

[^1]COLGATE, J. Edward; STANLEY, Michael C.; BROWN, J. Michael. Issues in the haptic display of tool use. In: Intelligent Robots and Systems 95.'Human Robot Interaction and Cooperative Robots', Proceedings. 1995 IEEE/RSJ International Conference on. IEEE, 1995. p. 140-145.