VOYAGE GROUP VR室ブログ

VOYAGE GROUP VR室のブログです。コンテンツの紹介や制作方法、イベントレポートなどについて書きます。原則毎週水曜日更新。

HTC Vive向けにアプリケーションを開発する〜コントローラでインタラクション編〜

最近糖質制限を始めたVR室の@daybysayです。

明日はPSVRの発売日!弊社ももちろん購入しているので、到着が楽しみです!

さて、今回は前回の続きとして、HTC Vive向けのアプリにインタラクションの機能を追加したいと思います。

やはりVRコンテンツは、プレイヤーの身体を用いたインタラクションでVRの世界に影響を与える体験が楽しいですよね。

ということで今回はコントローラを用いたインタラクションの実装についてご紹介します。

ちなみに今回開発したアプリは下記にあげてあります。

github.com

目次

Viveコントローラについて

コントローラのインタフェースは次の画像の通りです。

f:id:DayBySay:20161012111135p:plain

基本的にはこれらのインタフェースと、コントローラ本体の位置、角度などを掛け合わせてインタラクションを構築することになります。

Viveコントローラのイベントを利用する

Viveコントローラのイベントを使うには、SteamVRプラグイン内に用意されているSteamVR_TrackedControllerクラスを利用すると簡単です。

今回は下記のようなクラスを用意しました。

using UnityEngine;
using System.Collections;

public class MY_TrackedController : MonoBehaviour {
    SteamVR_TrackedController trackedController;

    void Start () {
        trackedController = gameObject.GetComponent<SteamVR_TrackedController> ();

        if (trackedController == null) {
            trackedController = gameObject.AddComponent<SteamVR_TrackedController> ();
        }

        trackedController.MenuButtonClicked += new ClickedEventHandler (DoMenuButtonClicked);
        trackedController.MenuButtonUnclicked += new ClickedEventHandler (DoMenuButtonUnClicked);
        trackedController.TriggerClicked += new ClickedEventHandler (DoTriggerClicked);
        trackedController.TriggerUnclicked += new ClickedEventHandler (DoTriggerUnclicked);
        trackedController.SteamClicked += new ClickedEventHandler (DoSteamClicked);
        trackedController.PadClicked += new ClickedEventHandler (DoPadClicked);
        trackedController.PadUnclicked += new ClickedEventHandler (DoPadClicked);
        trackedController.PadTouched += new ClickedEventHandler (DoPadTouched);
        trackedController.PadUntouched += new ClickedEventHandler (DoPadTouched);
        trackedController.Gripped += new ClickedEventHandler (DoGripped);
        trackedController.Ungripped += new ClickedEventHandler (DoUngripped);
    }

    public void DoMenuButtonClicked(object sender, ClickedEventArgs e) {
        Debug.Log ("DoMenuButtonClicked");
    }

    public void DoMenuButtonUnClicked(object sender, ClickedEventArgs e) {
        Debug.Log ("DoMenuButtonUnClicked");
    }

    public void DoTriggerClicked(object sender, ClickedEventArgs e) {
        Debug.Log ("DoTriggerClicked");
    }

    public void DoTriggerUnclicked(object sender, ClickedEventArgs e) {
        Debug.Log ("DoTriggerUnclicked");
    }

    public void DoSteamClicked(object sender, ClickedEventArgs e) {
        Debug.Log ("DoSteamClicked");
    }

    public void DoPadClicked(object sender, ClickedEventArgs e) {
        Debug.Log ("DoPadClicked");
    }

    public void DoPadUnclicked(object sender, ClickedEventArgs e) {
        Debug.Log ("DoPadUnclicked");
    }

    public void DoPadTouched(object sender, ClickedEventArgs e) {
        Debug.Log ("DoPadTouched");
    }

    public void DoPadUntouched(object sender, ClickedEventArgs e) {
        Debug.Log ("DoPadUntouched");
    }

    public void DoGripped(object sender, ClickedEventArgs e) {
        Debug.Log ("DoGripped");
    }

    public void DoUngripped(object sender, ClickedEventArgs e) {
        Debug.Log ("DoUngripped");
    }
}

コントローラのトリガーを引いた時と離した時にログが吐き出されるだけの非常にシンプルな実装です。SteamVR_TrackedControllerにシステムボタンのイベントは実装されていないようですが、openvr_api.csを見る限りはイベントの取得はできそうに見えます。

次に、このクラスをSteamVR_TrackedControllerクラスと共にViveのコントローラにセットします。

f:id:DayBySay:20161012114434p:plain

これで準備完了です。動かしてみましょう。

https://media.giphy.com/media/3oz8xYVHj1QjVsRUmk/giphy.gif

見づらくて恐縮ですが、ボタンを押すとログが流れています。

これでコントローラにイベントを介したインタラクションを実装できるようになりました。

SteamVR_TrackedControllerクラスの解説

余談ですがSteamVR_TrackedControllerクラス事態の解説です。

コントローラのイベントの取得方法はSteamVRプラグインに用意されているSteamVR_TrackedController.cs 内のUpdate関数を見ると、イベントの呼び出しがどうなっているかがわかりやすいです。

例えば下記の実装を見てみましょう

    // Update is called once per frame
    void Update()
    {
        var system = OpenVR.System;
        if (system != null && system.GetControllerState(controllerIndex, ref controllerState))
        {
            ulong trigger = controllerState.ulButtonPressed & (1UL << ((int)EVRButtonId.k_EButton_SteamVR_Trigger));
            if (trigger > 0L && !triggerPressed)
            {
                triggerPressed = true;
                ClickedEventArgs e;
                e.controllerIndex = controllerIndex;
                e.flags = (uint)controllerState.ulButtonPressed;
                e.padX = controllerState.rAxis0.x;
                e.padY = controllerState.rAxis0.y;
                OnTriggerClicked(e);

            }

4行目はCVRSystemクラスのGetControllerStateメソッドに対して、コントローラに振られたIndexとコントローラの状態を受け取る変数(controllerState)を渡しており、最終的にcontrollerStateにコントローラの状態を表すUnsigned Longの値が代入されます。

そのcontrollerStateの値をビットフラグとして扱っており、各ビットを見ることで現在のコントローラの状態がどうなっているかを判定できるようになっています。

トリガーイベントの場合はEVRButtonIdのk_EButton_SteamVR_Triggerの値、つまり33ビット分シフトされたUnsigned Long型の1を用いて論理積をとっています。 つまり、後ろから34桁目のビットが1である場合、トリガーが押下されている状態を表しています。

ここで返ってくる値にすべてのボタンのステートが含まれているので、ボタンの同時押しなどをハンドリングしたい場合は、このOpenVR APIを直接使うことでシンプルに実装が可能になります。

物をつかむスクリプトの実装

それでは、コントローラを使って物をつかむ処理を実装していきましょう。

ここでは下記を行います。

  • つかむオブジェクトを見つける
  • オブジェクトをつかむ

つかむオブジェクトを見つける

今回はコントローラに接触しているオブジェクトをつかむ対象とします。

接触しているオブジェクトを見つけるには、コントローラとの衝突判定を用いて実現したいと思います。

先ほどのMY_TrackedControllerクラスに次の処理を追加しました。接触したことがわかりやすいよう接触したオブジェクトを削除する実装ににしました。

 void OnTriggerEnter(Collider other) {
        Destroy(other.gameObject);
    }

OnTriggerEnter関数はColliderを持つオブジェクト同士であり、かつ最低片方にRigidBodyがセットされている場合に呼び出されます。otherには接触したオブジェクトがわたってくるので、コントローラが触れた対象を取得できます。

次に衝突対象のオブジェクトを配置しましょう。今回はCubeを利用します。

f:id:DayBySay:20161012135709p:plain

Cubeを置き、RigidBodyを設定しました。

次はコントローラ側の設定を変えます。デフォルトではColliderが存在しないのでとりあえずつけます。

BoxColliderを0.1サイズで用意しました。このときIs Triggerにチェックを入れるのを忘れないでください。

f:id:DayBySay:20161012135815p:plain

いったんこれで動かしてみましょう。

https://media.giphy.com/media/3o6ZtgbsEsGj3a3Bew/giphy.gif

ちゃんと消えますね。これで接触判定と接触した対象のオブジェクトを取得できるようになりました。

オブジェクトをつかむ

次に、削除の処理をつかむ(相対位置を固定する)処理に変えていきます。

ここではコントローラとオブジェクトの相対位置を固定するのにFixed Jointを利用します。

スクリプトは以下のように実装しました。

using UnityEngine;
using System.Collections;

public class MY_TrackedController : MonoBehaviour {
    SteamVR_TrackedController trackedController;
    GameObject grababbleObject;
    FixedJoint joint;

    void Start () {
        trackedController = gameObject.GetComponent<SteamVR_TrackedController> ();

        if (trackedController == null) {
            trackedController = gameObject.AddComponent<SteamVR_TrackedController> ();
        }

        trackedController.TriggerClicked += new ClickedEventHandler (DoTriggerClicked);
        trackedController.TriggerUnclicked += new ClickedEventHandler (DoTriggerUnclicked);

        joint = gameObject.GetComponent<FixedJoint> ();
    }

    public void DoTriggerClicked(object sender, ClickedEventArgs e) {
        grab ();
    }

    public void DoTriggerUnclicked(object sender, ClickedEventArgs e) {
        release ();
    }

    void grab() {
        if (grababbleObject == null || joint.connectedBody != null) {
            return;
        }

        joint.connectedBody = grababbleObject.GetComponent<Rigidbody> ();
    }

    void release() {
        if (joint.connectedBody == null) {
            return;
        }

        joint.connectedBody = null;
    }

    void OnTriggerEnter(Collider other) {
        grababbleObject = other.gameObject;
    }

    void OnTriggerExit(Collider other) {
        grababbleObject = null;
    }
}

OnTriggerEnter関数で接触しているオブジェクトを取得、メンバ変数に保持し、DoTriggerClickedで保持されたオブジェクトとコントローラの相対位置を固定することでつかむ処理を実装しています。

次にコントローラにFixedJointを追加します。

f:id:DayBySay:20161012151240p:plain

FixedJoint追加の際にRigidBodyコンポーネントが自動で追加されますが、このRigidBodyのUse Gravityはチェックを外し、Is Kinematicはチェックを入れてください。これをやっておかないと固定したオブジェクトの挙動が変な感じになります。

それではこれで動かしてみましょう。

オブジェクトをつかんで離すところまで実装できました!

物を投げてみる

実は上の実装では、つかんだ物オブジェクトは自由落下するだけで物を投げることができません。

オブジェクトを投げるためには、飛んで行くための力をかけてあげる必要があります。

オブジェクトを離した瞬間に、コントローラにかかっている速度と角速度をオブジェクトにかけてあげましょう。

MY_TrackedControllerクラスのrelease関数を下記のように修正します。

   void release() {
        if (joint.connectedBody == null) {
            return;
        }

        Rigidbody rigidBody = joint.connectedBody;
        joint.connectedBody = null;

        var device = SteamVR_Controller.Input((int)trackedController.GetComponent<SteamVR_TrackedObject> ().index);
        rigidBody.velocity = device.velocity;
        rigidBody.angularVelocity = device.angularVelocity;
        rigidBody.maxAngularVelocity = rigidBody.angularVelocity.magnitude;
    }

動かしてみます!

https://media.giphy.com/media/l2YSkwiE8GSE56pJm/giphy.gif

いい感じに飛んでいきましたね。これでつかんだものを投げられるようになりました!

まとめ

  • SteamVR_TrackedControlerを使うと簡単に実現できる
  • 物をつかむにはFixed Jointを使う
  • つかんだものを投げるにはコントローラにかかっている速度と角速度をオブジェクトにかける

今回はコントローラの使い方についてご紹介させていただきました。

次回は何を書くか未定です!