【Unity(C#)】VR空間内で掴んだObjectとKinematicなオブジェクトを衝突させる

##OculusのGrab

まずはこちらをご覧ください。
掴んだキューブがデスクをすり抜けているのがわかるかと思います。

![Tutrial.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/00cfea98-6da8-b5f2-fad1-85f43666b12b.gif)


実はこれ、OculusQuestのチュートリアルで、
**技術力がどうとかの問題ではなく、すり抜けるのはVRのベストプラクティスになりつつある**んですよね。

というのも、手はコントローラーの動きに追従して動くので、
もし、キューブがデスクに衝突してそれ以上先に進めない...となった場合でも、
手は現実空間でフィードバックを受けていないのでそのまま動かせてしまいます。

そうなると、**VR空間内の手の動きと現実の手の動きに乖離が生じて違和感を生み出す**要因になるということです。


##現実世界をコピーした挙動なんて存在しない
VR空間での手の挙動にプレイヤーの自由を与えた場合、
現実世界と同様の物理挙動を再現する方法は現時点のデバイスには存在し得ません。

もし、現実世界と一致する完璧な挙動をVR空間内でも再現したいのであれば、
VR空間での映像にリンクさせて、現実空間でも実際に物体を掴み、
周りのオブジェクトとの衝突も考慮する...といった かなり大掛かりな仕掛けが必要になります。

そうは言っても、手に持っているオブジェクトがすり抜けるのを見てしまうことで、
没入感が減少しているのは紛れもない事実です。

##Kinematicな物体に衝突させる
今回はケースによってはベストプラクティスとなり得る可能性を秘めた案を提唱します。

そのために犠牲にしたのは、この２つです。
**①オブジェクトを掴んだ時の手は消える**
**②実際の手の位置とVR空間内で掴んだオブジェクトの位置がずれる**

実際にご覧になってもらった方が早いので見てください。
![PhysicsGrab.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/db81472b-c6e3-059f-4cf9-124ee7bf183e.gif)

白いキューブ(Kinematic)に、掴んだ赤いキューブがめり込むことなく、
動作しているのが見てとれます。

もともと、**Kinematicに設定していて、コライダーの付いているオブジェクト**は
**静的で外部からの力を受けているようには見えない**、いわば自然界の岩のような存在です。

よって、掴んだオブジェクトとの関係性も
GIFのようになるのが望んだ挙動である場合もあり得るのではないか　と思い、
今回のような実装を試みました。

実は、VIVEのHome画面では実際に似たような実装がされています。

##コード

コードはOculusIntegrationの`OVRGrabbable`を継承しています。


```C#:掴みたいオブジェクトにアタッチ
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PhysicsGrabbable : OVRGrabbable
{
    GameObject anchor; 
    GameObject grabHand;
    Rigidbody rb;
    Vector3 offsetPos; 
    Vector3 thisObjPos;

    Vector3 tmpGripTrans;

    bool isCollision;
    bool grabMomentKinematic;
    float acceptableDistance = 0.1f;

    protected override void Start()
    {
        rb = this.gameObject.GetComponent<Rigidbody>();
        
        anchor = GameObject.Find("RightControllerAnchor");
        grabHand = GameObject.Find("hands:Rhand");
    }

    public override void GrabBegin(OVRGrabber hand, Collider grabPoint)
    {
        m_grabbedBy = hand;
        m_grabbedCollider = grabPoint;
        grabHand.SetActive(false);
        this.gameObject.transform.parent = anchor.transform;
        tmpGripTrans = this.gameObject.transform.localPosition;
        rb.useGravity = false;
        grabMomentKinematic = rb.isKinematic;
        rb.isKinematic = false;
        rb.constraints = RigidbodyConstraints.FreezeRotation;
    }

    public override void GrabEnd(Vector3 linearVelocity, Vector3 angularVelocity)
    {
        grabHand.SetActive(true);
        this.gameObject.transform.parent = null;
        rb.useGravity = true;
        rb.isKinematic = grabMomentKinematic;
        rb.constraints = RigidbodyConstraints.None;
        rb.velocity = linearVelocity;
        rb.angularVelocity = angularVelocity;
        m_grabbedBy = null;
        m_grabbedCollider = null;
    }

    void OnCollisionEnter(Collision other)
    {
        isCollision = true;
    }

    void OnCollisionExit(Collision other)
    {
        isCollision = false;
    }
   
    void Update()
    {
        if (isGrabbed)
        {
            rb.velocity = Vector3.zero;
            rb.angularVelocity = Vector3.zero;

            offsetPos = anchor.transform.position;
            thisObjPos = this.gameObject.transform.position;
            
            if (isCollision == false && Vector3.Distance(thisObjPos,offsetPos)>acceptableDistance)
            {
                this.gameObject.transform.localPosition = tmpGripTrans;
            }
        }
    }
}

```

先ほど犠牲にしたと説明した、
**②実際の手の位置とVR空間内で掴んだオブジェクトの位置がずれる**
の箇所ですが、少しでもマシにするために、
**○○cm以上離れたら掴んだ時点のポジションに戻す**　という実装で対処しています。

##親子関係で掴んだ表現
今回の掴み実装は親子関係により再現しています。
トリガーを引いた瞬間、掴んだオブジェクトを手のアンカーの子にすることで、
あたかも掴んだかのような表現を実現しています。

そして、掴んだオブジェクトの`useGravity`、`isKinematic`をfalseにしています。

これだけでは、無重力空間に浮かんだような
外部からの力に対して摩擦が無い限り、無限に影響を受け続ける状態になるので、
`velocity`、`angularVelocity`を0に更新し続けています。

ちなみに、通常の`OVRGrabbable`は
`MovePosition`,`MoveRotation`で掴んだ表現が実装されています。

```C#:OVRGrabbable

    protected virtual void MoveGrabbedObject(Vector3 pos, Quaternion rot, bool forceTeleport = false)
    {
        if (m_grabbedObj == null)
        {
            return;
        }

        Rigidbody grabbedRigidbody = m_grabbedObj.grabbedRigidbody;
        Vector3 grabbablePosition = pos + rot * m_grabbedObjectPosOff;
        Quaternion grabbableRotation = rot * m_grabbedObjectRotOff;

        if (forceTeleport)
        {
            grabbedRigidbody.transform.position = grabbablePosition;
            grabbedRigidbody.transform.rotation = grabbableRotation;
        }
        else
        {
            grabbedRigidbody.MovePosition(grabbablePosition);
            grabbedRigidbody.MoveRotation(grabbableRotation);
        }
    }
```

##より現実世界に寄せるには...
そもそも論ですが、現実世界にKinematicなオブジェクトなんて存在しないんですよね。
どんなに大きい岩でも、山でも、それこそ大陸だってとてつもないパワーで押せば
原型をとどめるかどうかは置いといて、理論上動かせるはずです。
でもあまりにも必要なパワーが大きすぎて、全くもって動かせない存在(Kinematic)と言われても通用します。

なので、より現実に近い世界をVRで創造したいのであれば、
**全てのオブジェクトをNot Kinematicな状態にして、
動かない理由をDragやPhysics Materialで与えてあげる**ってのも
一つの正解の形なんじゃないでしょうか。


あと、全然関係ないんですが、ゲームオブジェクト名で手のアンカーやモデルのオブジェクトを
取得しているダサダサコーディングなので両手にも対応できるようにしてからGithubに載せます...

---
2019/09/21　追記

コメントでご指摘頂いてた、
**掴んだオブジェクトがKinematicと衝突すると手との相対位置がずれていき、
一定距離離れると強制リリース(掴みがキャンセル)される**
という実装を加えたものを用意しました。
`releasable`というbool値を用意したので、Inspectorでオンオフできます。

あと、左右どちらでもいけるようにしました。(ただし、名前指定に変わりはないです)

```C#:掴みたいオブジェクトにアタッチ
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PhysicsGrabbable : OVRGrabbable
{
    [SerializeField]  bool releasable;
    [SerializeField]  float acceptableDistance = 0.1f;
    
    GameObject anchor,anchorL,anchorR; 
    GameObject grabHand,grabHandL,grabHandR;
    Rigidbody rb;
    Vector3 offsetPos; 
    Vector3 thisObjPos;

    Vector3 tmpGripTrans;

    bool isCollision;
    bool grabMomentKinematic;
    

    protected override void Start()
    {
        rb = this.gameObject.GetComponent<Rigidbody>();
        
        grabHandL = GameObject.Find("hands:Lhand");
        anchorL = GameObject.Find("LeftControllerAnchor");
        
        grabHandR = GameObject.Find("hands:Rhand");
        anchorR = GameObject.Find("RightControllerAnchor");
    }

    public override void GrabBegin(OVRGrabber hand, Collider grabPoint)
    {
        m_grabbedBy = hand;
        
        if (m_grabbedBy.name =="CustomHandLeft")
        {
            if(grabHandL!=null) grabHand = grabHandL;
            if(anchorL!=null) anchor = anchorL;
        }
        else if (m_grabbedBy.name =="CustomHandRight")
        {
            if(grabHandR!=null) grabHand = grabHandR;
            if(anchorR!=null) anchor = anchorR;
        }
        
        m_grabbedCollider = grabPoint;
        grabHand.SetActive(false);
        this.gameObject.transform.parent = anchor.transform;
        tmpGripTrans = this.gameObject.transform.localPosition;
        rb.useGravity = false;
        grabMomentKinematic = rb.isKinematic;
        rb.isKinematic = false;
        rb.constraints = RigidbodyConstraints.FreezeRotation;
    }

    public override void GrabEnd(Vector3 linearVelocity, Vector3 angularVelocity)
    {
        grabHand.SetActive(true);
        this.gameObject.transform.parent = null;
        rb.useGravity = true;
        rb.isKinematic = grabMomentKinematic;
        rb.constraints = RigidbodyConstraints.None;
        rb.velocity = linearVelocity;
        rb.angularVelocity = angularVelocity;
        m_grabbedBy = null;
        m_grabbedCollider = null;
    }

    void OnCollisionEnter(Collision other)
    {
        isCollision = true;
    }

    void OnCollisionExit(Collision other)
    {
        isCollision = false;
    }
   
    void Update()
    {
        if (isGrabbed)
        {
            rb.velocity = Vector3.zero;
            rb.angularVelocity = Vector3.zero;

            offsetPos = anchor.transform.position;
            thisObjPos = this.gameObject.transform.position;
            
            if (isCollision == false && Vector3.Distance(thisObjPos,offsetPos)>acceptableDistance)
            {
                this.gameObject.transform.localPosition = tmpGripTrans;
            }
            
            if (releasable && Vector3.Distance(thisObjPos, offsetPos) > acceptableDistance)
            {
                m_grabbedBy.ForceRelease(this.gameObject.GetComponent<PhysicsGrabbable>());
            }
        }
    }
}
```

![ReleaseGrab.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/e8e5eb95-79cf-58dc-3fbb-6a6337acb250.gif)

無茶しようとすると掴みが自動解除されます。


なんかOculusの権利関係ややこしそうなので、
コピーライティング書いときます。

```
Copyright © Facebook Technologies, LLC and
its affiliates. All rights reserved.
```