Unity
Oculus
VRIK

VRIK(FinalIK)をunityちゃんに設定する手法(Oculus) VR(Oculus)でキャラクタに憑依して操るアプリの作り方

完成図

VRIKをunityちゃんに設定した際に四苦八苦したのでその際のメモ。
完成図は以下のような感じ。

VRIKでunityちゃんに憑依し操るデモ。 pic.twitter.com/1VboBecNts

— unagi (@UnagiHuman) 2017年7月23日

unityちゃんを選択するとプレイヤーがunityちゃんに憑依し、なりきる事ができるデモになります。

必要なアセットの用意

  1. unityちゃん(最新版を公式HPよりダウンロード)
  2. FinalIK(VRIK)
  3. oculus-avatar-sdk
  4. VRTK
  5. oculus-utilities-for-unity-5

VRIKの設定

FinalIKのサンプルシーンにVRIKのデモがあるのですがunityちゃんへのVRIKの設定にはあまり参考になりません。
違和感なく成りきるには頭、手の位置と角度の調整をしてあげないといけません。結構大変です。

自分は下記のようにオブジェクトを構築し、VRIKを設定しました。

VRIKComponentSet.PNG

hierarchyのオブジェクトの構造にコメントを加えると以下のような感じです。

- CameraRoot(カメラ位置のルート)
    - HeightOffset (yにモデルの身長分のオフセットを加える)
        - OVRCameraRig
            - TrackingSpace 
                - LeftEyeAnchor
                - centerEyeAnchor
                - LocalAvator (Avator)
                - RightEyeAnchor
                - LeftHandAnchor
                - RightHandAnchor

- CharaRoot(キャラのルート)
    - TrackingSpace Dummy(VRIKが参照する頭、手の位置)
        - CenterEyeAnchor (OVRCameraRig下のcenterEyeAnchorのlocalPosition,localRotationと同期)
            - RotateOffset (回転offset)
                - headAnchor (VRIKの頭の位置)
        - LeftHandAnchor (OVRCameraRig下のアンカーと同期)
            - RotateOffset (回転offset)
                - LeftHandOffset(VRIKの左手の位置)
        - RightHandAnchor (OVRCameraRig下のアンカーと同期)
            - RotateOffset (回転offset)
                - RightHandOffset(VRIKの右手の位置)
    - unityChan (unityちゃん本体。VRIKをアタッチする)

TrackingSpace以下のアンカーのポジションをTrackingSpace Dummy以下のアンカーに同期させているのが下記のスクリプトです。

TrackingTarget.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
using UniRx.Triggers;

public class TrackingTarget : MonoBehaviour
{
    public enum Anchor
    {
        CENTEREYE,
        RIGHTHAND,
        LEFTHAND,
        RIGHTFOOT,
        LEFTFOOT,
    }

    [SerializeField]
    bool m_isTraking = true;

    [SerializeField]
    Anchor m_anchor;

    [SerializeField]
    OVRCameraRig m_cameraRig;

    public float height = 1.5f;


    private void Awake ()
    {

        this.UpdateAsObservable ().Subscribe (p => TrakingTransform ());
    }


    private void Start ()
    {
        if (OVRManager.instance.trackingOriginType == OVRManager.TrackingOrigin.FloorLevel) {
            height = 0;
        }

    }


    void TrakingTransform ()
    {
        if (!m_isTraking)
            return;
        transform.localPosition = GetAnchor ().transform.localPosition + new  Vector3 (0, height, 0);
        transform.localRotation = GetAnchor ().transform.localRotation;
    }


    Transform GetAnchor ()
    {
        switch (m_anchor) {
        case Anchor.CENTEREYE:
            return m_cameraRig.centerEyeAnchor;
            break;
        case Anchor.LEFTHAND:
            return m_cameraRig.leftHandAnchor;
            break;
        case Anchor.RIGHTHAND:
            return m_cameraRig.rightHandAnchor;
            break;
        default:
            return m_cameraRig.centerEyeAnchor;
            break;

        }
    }
}

設定のポイントは何点かあって、

  1. カメラの位置をモデルの身長に合わせる。

    1. OVRCameraRigのTrakingOriginTypeをeyeLevelにし、TrackingSpace にunityちゃんの身長(足から目まで)分のy方向のオフセット1.2mを加える。こうする事によって、自分より身長の小さい3Dモデルに対してカメラの高さをUnity側で調整できる
  2. OVRCameraRig以下のポジションアンカーと実際にVRIKが参照するポジションアンカーのオブジェクトは別にする

    1. TrackingSpace Dummy以下の各アンカーはOVRCameraRig以下のアンカーのlocalPosition+身長分のoffset,localRotationを毎フレーム捕捉しており、VRIKはDummyの方を参照している形になる。こうする事でCameraRootをどの位置においてもOKになり、Cameraを unityChanの位置に合わせて操ることも、Cameraの位置をunityChanとは別の位置において遠隔操作して操るみたいな感じにできる。
  3. VRIKが参照する各部位(頭、右手、左手)には回転と位置のオフセットを調整できるようにしている。

    1. オフセットを調整して手と頭がunityちゃんの位置に完全に一致するようにしないと、違和感がありまくる。

VRTKの設定

VRTKを適切にセッティングして、unityちゃんを持ち運んだり、oculusTouchの操作で憑依できるようにします。

下記がOculus用のVRTKセッティングになります。
VRTKmanager.PNG

下記がコントローラー用の設定になります。
GrabとUseを有効にするのと、コントローラーから出るPointerがunityちゃんに接触した時点でアクションができるようなセッティングになっています。
vrtkcontroller.PNG

下記が対象物に対する設定になります。VRTK_interactable_objectでgrab,Use可能な設定と、VRTK_FixedJointGrabAttachでGrabした場合の持つポジションの設定をしています。(grabSnapオブジェクトの位置を掴むことになる)
vrtkObjectSetting1.PNG

コントローラーで操作して憑依するロジックの実装

ロジックは大まかにこんな感じになります。
1. VRTKのセッティングをしたunityちゃんに対してuseを実行するとOnUseEventが呼ばれ、
2. unityちゃんの位置にカメラの位置をセッティングし、
3. vrikをONにして憑依する
4. 10秒後にvrikをオフにしてカメラの位置をリセットする

GrabManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RootMotion.FinalIK;
using VRTK;
using DG.Tweening;

/// <summary>
/// Grab manager.
/// </summary>
public class GrabManager : MonoBehaviour
{
    [SerializeField] Transform m_cameraRoot;
    [SerializeField] Transform m_cameraHeightOffset;
    [SerializeField] float m_playerHeight = 1.5f;
    [SerializeField] float m_unityChanHeight = 1.2f;

    void Awake ()
    {
        m_cameraHeightOffset.localPosition = new Vector3 (0, m_playerHeight, 0);
    }

    /// <summary>
    /// Raises the use event event.
    /// </summary>
    /// <param name="o">O.</param>
    /// <param name="e">E.</param>
    public void OnUseEvent (object o, InteractableObjectEventArgs e)
    {
        var chara = o as VRTK_InteractableObject;
        SetVRIK (chara.transform);
    }


    /// <summary>
    /// Sets the VRI.
    /// </summary>
    void SetVRIK (Transform target)
    {
        StartCoroutine (SetVRIKCoroutine (target));
    }


    /// <summary>
    /// Sets the VRIK coroutine.
    /// </summary>
    /// <returns>The VRIK coroutine.</returns>
    IEnumerator SetVRIKCoroutine (Transform target)
    {
        //Grab,Use等ができないよう、Colliderは切っておく
        target.GetComponent<Collider> ().enabled = false;
        var vrik = target.GetComponentInChildren<VRIK> (true);

        var startPosition = m_cameraRoot.position;
        var startRotation = m_cameraRoot.rotation;

        //Playerがunityちゃんに向かって移動
        var tween1 = m_cameraRoot.DOMove (target.position, 0.5f, false).SetEase (Ease.InOutQuad);
        yield return tween1.WaitForCompletion ();

        //unityちゃんの回転をリセット。(x,z軸の回転は厳しい)
        target.localRotation = Quaternion.Euler (new Vector3 (0, target.localRotation.eulerAngles.y, 0));
        target.position = new Vector3 (target.position.x, 0, target.position.z);

        //カメラ座標をunityちゃんに移動
        m_cameraRoot.transform.SetParent (target);
        m_cameraRoot.transform.localPosition = Vector3.zero;
        m_cameraRoot.transform.localRotation = Quaternion.identity;

        //カメラ高さoffsetをunityちゃんに合わせる
        m_cameraHeightOffset.localPosition = new Vector3 (0, m_unityChanHeight, 0);

        //VRIKせっと
        vrik.enabled = true;

        //VRIKをリセットする
        yield return new WaitForSeconds (0.1f);
        vrik.solver.Reset ();

        //10秒操る
        yield return new WaitForSeconds (10f);

        //VRIKを無効にして元の位置に戻る
        vrik.enabled = false;
        var tween2 = m_cameraRoot.DOMove (startPosition, 0.5f, false).SetEase (Ease.InOutQuad);
        yield return tween2.WaitForCompletion ();
        vrik.transform.localPosition = Vector3.zero;
        vrik.transform.localRotation = Quaternion.identity;
        m_cameraRoot.transform.SetParent (null);
        m_cameraRoot.SetPositionAndRotation (startPosition, startRotation);
        m_cameraHeightOffset.localPosition = new Vector3 (0, m_playerHeight, 0);
        target.GetComponent<Collider> ().enabled = true;
    }

}

上記のOnUseEventをunityちゃんにセットしたVRTK_interactable_object_unity_EventsにUnity Eventとして設定します。
grabEvent.PNG