【Unity】HTC Vive で両手を使った transform 操作を実現する

はじめに

この記事はカヤックUnityアドベントカレンダー2018の17日目の記事になります。

こんにちは、ソーシャルゲーム事業部の浅利(@kasari39)です。 今年は Oculus Go の登場や、VTuber の流行により VR がより身近な存在になったように感じますね。私も Oculus Go で初めてVRを体験した一人です。

Oculus Go はコントローラーが右手しか無く、自由度が低いために私はあまり開発欲がそそられませんでしたが、最近おもむろにVIVEを購入してしまい、ついに私もVR世界で両手を扱えるようになりました。

というわけで本日は、VRで両手があったらやりたいことTOP3のひとつ、「両手によるtransform操作」を実装してみた話です。

つくったもの

両手によるtransform操作コンポーネント for VIVE Input Utility · GitHub

f:id:kasari:20181214183257g:plain

VIVE Input Utility

VR開発にあたり SteamVR Plugin と VIVE Input Utility を候補にしました。 初めのうちはVR開発のスタンダートっぽい SteamVR Plugin で実装していましたが以下の理由で今回は使用をやめました。

  • Event 周りの実装が SendMessage で構成されていてコードリーディングが捗らない
  • 掴んでいるときの挙動はコントローラー側の実装に依存しており、改良のためにはコントローラー側の実装を理解する必要があった

対照的に VIVE Input Utility では

  • Event 周りは EventSystem compatible のためエディタ機能で飛べる
  • 掴んでいるときの挙動はオブジェクト側の実装に依存しており、コントローラー側の実装を理解する必要がなかった

というわけで今回は VIVE Input Utility を採用しました。 assetstore.unity.com

この VIVE Input Utility、名前からして VIVE 専用の plugin なのかと勘ぐってしまいそうですが、Asset Store の記載の通り複数デバイスをサポートしています。 また、VRデバイスが無い場合でもマウスやキーボードでのシーンのテストを行える Simulator があるのが特徴的です。

Supporting device
* Simulator
* VIVE & VIVE Pro (OpenVR compatible device) 
* Oculus Rift & Touch & Go 
* Daydream 
* VIVE Focus (WaveVR compatible device)

実装

全体はこちら(100行ほど)

両手によるtransform操作コンポーネント for VIVE Input Utility · GitHub

簡単な解説

RigidPose

VIVE Input Utility 依存しているため、実装の中に RigidPose という struct が現れます。 これは Transform から Scale を無くしたものだと考えてよいです。

public struct RigidPose
{
    public Vector3 pos;
    public Quaternion rot;
}

片手掴みのとき

片手掴みのときは、オブジェクトをコントローラーの子に設定したかのようにコントローラーに追従する挙動にしました。 ただ実際に親子関係を操作するわけにもいかないため内部で、localPosition, localRotation 相当のものを保持して world position を計算してあげることでこれを実現します。

片手掴みに関しては各 Plugin で Component が用意されており、同様の挙動となっています。 SteamVR Plugin では Interactable、Vive Input Utility では BasicGrabbable がこれに相当します。

f:id:kasari:20181214092641g:plain

  • grabberOrigin が黄色の球
  • grabOffset 白色の線
  • grabPose 赤色の球
public class Grabber
{
    // コントローラー
    public IColliderEventCaster eventCaster { get; private set; }
    // 掴んでいるオブジェクト
    public GameObject grabbingObj { get; private set; }

    // コントローラーの位置と向き
    public RigidPose grabberOrigin { get { return new RigidPose(eventCaster.transform); } }
    // コントローラーからオブジェクトまでの offset
    // (コントローラーを親、オブジェクトを子としたときの localPosition のようなもの)
    public RigidPose grabOffset { get; private set; }
    // コントローラーの現在の位置と向き、offset から計算したオブジェクトの位置と向き
    // (コントローラーを親、オブジェクトを子としたときの position のようなもの)
    public RigidPose grabPose { get { return grabberOrigin * grabOffset; } }

    public Grabber(IColliderEventCaster eventCaster, GameObject obj)
    {
        this.eventCaster = eventCaster;
        this.grabbingObj = obj;
        this.grabOffset = RigidPose.FromToPose(new RigidPose(eventCaster.transform), new RigidPose(obj.transform));
    }
}
public void UpdateTransform()
{
      // 片手で掴んでいるとき
      if (isSoloGrabbed)
      {
            targetTransform.position = oneGrabber.grabPose.pos;
            targetTransform.rotation = oneGrabber.grabPose.rot;
      }

掴んでいるコントローラーに追従すればよいだけなので単純です。 対象の Transform を grabPose(GIFの赤色の球) のものに更新します。

両手掴みのとき

両手掴みのときの挙動については、想像しづらかったため試行錯誤しながら違和感の少ない設定を探しました。

f:id:kasari:20181214095555g:plain

Position

Position は両手の grabPose(GIFの赤色の球) の中間にしました。

targetTransform.position = (oneGrabber.grabPose.pos + otherGrabber.grabPose.pos) / 2;

Scale

Scale は両手掴みを開始したときの両手の距離を保持しておき、その距離と現在の距離の差から求めました。

ここでは grabPose ではなく grabberOrigin の position を計算に用いているのがポイントです。

var currentMagnitude = (otherGrabber.grabberOrigin.pos - oneGrabber.grabberOrigin.pos).magnitude;
var scale = currentMagnitude / _initMagnitude;

targetTransform.localScale = scale * _initScale;

Rotation

Rotation の計算には Quaternion.FromToRotaion を使用しました。Quaternion.FromToRotaion は fromDirection から toDirection への回転を作成してくれます。

public static Quaternion FromToRotation (Vector3 fromDirection, Vector3 toDirection);

これに以下の引数を渡すことで掴んだオブジェクトの現在の Rotation を求めました。

  • fromDirection: 両手掴みを開始したときの「片方の手からもう片方の手への向き」
  • toDirection: 現在の「片方の手からもう片方の手への向き」

Quaternion.FromToRotaion のデモ

f:id:kasari:20181214153434g:plain

静止している線を _initDir、移動している線を currentDir として見てください。

var currentDir = (otherGrabber.grabberOrigin.pos - oneGrabber.grabberOrigin.pos).normalized;
var rot = Quaternion.FromToRotation(_initDir, currentDir);

targetTransform.rotation = rot * _initRot;

おわりに

今回はVRで両手があったらやりたいことTOP3のひとつである「両手によるtransform操作」を実装してみました。TOP3にランクインしたのはみゅみゅ教授の生放送を初めて見たときの感動からです。モチベをありがとうございます。

VIVE Input Utility イケてる設計だと思うのでもっと盛り上がって欲しいですね。

明日は荒井による「Android App Bundleを触ってみた」の話になります。