UnityでOculusコントローラのセンサーデータを取得する

このチュートリアルでOculusコントローラの速度や加速度データを取得して、ボールを投げるシーンを作ってみましょう。

Input Systemを準備する

以前のチュートリアルでOculusコントローラ用にUnityのInput Systemの設定を説明しました。今回はこれをベースに「投げる」アクションを追加したいと思います。前のチュートリアルでは入力設定がこんな感じでした。

今回は、「Drop」アクションが要らないので右クリックして、削除を選んでも構いませんが、前のチュートリアルで作ったシーンが正しく動作しなくなります。もし「Drop」アクションを残したいなら、これから作るアクションと操作がかぶらないように修正してください。

以前のチュートリアルで説明した通り、「Throw」という新しいアクションを追加しましょう。

ボタンを離した時にボールを投げたいので、ここでPressインタラクションを追加して「Release Only」にしました。Oculusコントローラの右トリガーにバインドします。

入力設定を忘れずに保存してウィンドウを閉じます。

ボールのプレハブを作る

投げ方を変えて様々な種類のボールを投げたいので、球を2種類用意します。分かりやすくするためにそれぞれに違う色を付けます。

ボール用のマテリアルを準備します。

新しシーンにスフィアを追加して、スケールを変えて大きさを調整します。

20㎝のボールになります

SphereにRigidbodyコンポーネントを追加して、名称を「Ball」に変えて、用意したマテリアルを付けます。

ボールはゲームの弾と同様に永遠に持続せず、一定の時間が過ぎたら自動的に消えるようにしたいですね。「Lifetime」という新しいスクリプトを作っておきましょう。

このスクリプトはUnityのコルーチンを使っています。書き方が少し分かりにくいかも知れませんが、Unityで「〇〇秒待ってからこれをやれ」という指示の最も標準的な書き方です。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lifetime : MonoBehaviour
{
    public float lifetimeSeconds = 3f;

    void Start()
    {
        // コルーチンを開始する
        StartCoroutine(DestroyAfterWait());
    }

    IEnumerator DestroyAfterWait()
    {
        // lifetimeSecondsで指定した秒数を待つ(念のために0以下にならないようにする)
        yield return new WaitForSeconds(Mathf.Max(0, lifetimeSeconds));

        // 時間が来たので自らのゲームオブジェクトを削除する
        Destroy(gameObject);
    }
}

LifetimeスクリプトをBallに付けて、寿命を調整します。

これで球が完成したのでプロジェクトウィンドウにドラッグしてプレハブとして保存します。その後、シーンから削除します。

Ballプレハブを選択して、複製(Windows: Ctrl+D、macOS: command+D)します。複製されたプレハブの名称を変えて、もう一つのマテリアルを付けます。

シーンを準備する

新しいシーンを作成して、最初のOculusチュートリアルで作成したXRRigのプレハブを追加します。ボールを投げるターゲットとして適当にキューブなどを配置します。

ボールを投げた時にボールが出る位置を調整できるようにしたいので、XRRigを少し修正します。ここでは右手で投げますが、入力設定で指定した手を展開してその中に空のオブジェクトを追加します。名称を「Anchor」にします。

Anchorはボールが出る位置になって、ハンドル表示を「ローカル」に設定した時に表示される青の矢印を手の「前」方向になります。

Anchorの位置と向きを調整します。

Oculusコントローラのセンサーデータを取得する

これはチュートリアルの最も肝心な作業ですね。しかし、Unityの公式ドキュメントを調べても分かりやすい説明がなかなか出てきません。試したところ、次で方法で何とかセンサーデータを取得できました。

他のところで使えるために、その機能を独立したコンポーネントで実装します。「VRController」という新しいスクリプトを作成して、XRRigのRightController又はLeftControllerに付けます。

UnityのXR機能を使うのでスクリプトの冒頭に「using UnityEngine.XR」を指定します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;

public class VRController : MonoBehaviour
{

追跡可能な「ノード」がいくつもあります。

LeftEye左目
RightEye右目
CenterEye両目の中央点
Head
LeftHand左手
RightHand右手
GameControllerゲームコントローラ
TrackingReferenceトラッキング用のリファレンス
HardwareTrackerトラッキング用のハードウェア装置

初代のOculus Riftは専用のコントローラがなくて、通常のXboxコントローラを使用していたので、ここにゲームコントローラがあります。また、初代Riftは外部の「カメラ」という装置を使ってユーザーの頭の位置を追跡していました。これが「トラッキング用のリファレンス」です。最後の「ハードウェア装置」は追加の未定のデバイスです(だと思います)。

使用するノードを切り替えたいので、スクリプトに以下のパラメータを追加します。

public class VRController : MonoBehaviour
{
    public XRNode node;

インスペクターでこれを「Right Hand」または「Left Hand」にします。

ノードから取得したデータを他のスクリプトで簡単に使えるようにしたいので、以下のパラメータをスクリプトに追加します。

public class VRController : MonoBehaviour
{
    public XRNode node;

    public bool tracked = false; // データ取得可能か
    public Vector3 position; // 位置
    public Quaternion rotation; // 向き
    public Vector3 velocity; // 速度
    public Vector3 acceleration; // 加速度
    public Vector3 angularVelocity; // 角速度
    public Vector3 angularAcceleration; // 角加速度

角速度と角加速度はコントローラの向きの回転の速さと加速を表しています。

これらのパラメータをUpdateメソッドで更新します。

void Update()
{
    // すべてのノードのデータを取得する

    // データ用のListを用意する
    List<XRNodeState> states = new List<XRNodeState>();
    // 最新のデータを取得する(全ノード分)
    InputTracking.GetNodeStates(states);
    // 取得したデータを確認する
    foreach (XRNodeState s in states)
    {
        if (s.nodeType == node) // ノードが合えば...
        {
            // データの取得を試す
            tracked = s.tracked;
            s.TryGetPosition(out position);
            s.TryGetRotation(out rotation);
            s.TryGetVelocity(out velocity);
            s.TryGetAcceleration(out acceleration);
            s.TryGetAngularVelocity(out angularVelocity);
            s.TryGetAngularAcceleration(out angularAcceleration);
            break; // これ以上ループを続けない
        }
    }
}

VRコントローラによって取得できないデータがあるので、ここで少し珍しい方法でデータを取得します。データが取得できているかどうかはtrackedパラメータで記憶します。

これでセンサーデータの取得スクリプトが完成です。

ボールを投げる

早速、ボールを投げてみたいです。VRControllerと同じゲームオブジェクトに「VRThrow」という新しいスクリプトを追加します。

スクリプトの始まりをこういう風に修正します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(VRController))]
public class VRThrow : MonoBehaviour
{
    public Transform anchor = null;
    public GameObject ballPrefab = null;

冒頭に、「using UnityEngine.InputSystem」を追加して、Input Systemの機能が使えるようにします。

「RequireComponent」の部分は同じオブジェクトに「VRController」コンポーネントが必ず付いている事を要求します。「anchor」は前に作ったAnchorオブジェクトへの参照です。位置と回転だけが必要ですので、型をGameObjectではなく、Transformにします。「ballPrefab」を投げる球のプレハブです。これらのパラメータをインスペクターで設定します。

Input Systemのチュートリアルと同様にPlayer Inputコンポーネントが必要ですが、どこに付けるかはある程度自由です。一応、XRRigオブジェクト本体に追加しておきます。

忘れずに、入力設定アセットを指定しておきましょう。そして、以前のチュートリアルと違って、Behaviorを「Invoke Unity Events」にします。この方法だとプレーヤーの操作に反応するオブジェクトとその結果を実行するオブジェクトを分離させる事ができます。

「Invoke Unity Events」にした時にエディタでゲームをプレーした時に反応しなくなるようになる事があります。原因はスクリプトのエラーやバグでもなく、ゲームウィンドウがフォーカスされていない時にUnityイベントが送信されないからです。インスペクターで値を編集した時にゲーム画面をクリックするとまた反応してくれます。

VRThrowスクリプトにこのようなメソッドを追加します。

public void Throw(InputAction.CallbackContext context)
{
    Debug.Log("Throw!");
}

実装は後で説明しますが、その前にこのメソッドが呼び出されるようにPlayer Inputを修正します。

「イベント」と「Oculus Actions」を展開して、「Throw」アクションが起きた時に実行されるイベントを追加(+)します。忘れずに「Editor And Runtime」を選択して、エディタでも実行されるようにします。反応するオブジェクトをVRControllerとVRThrowが付いているオブジェクトにします。実行するメソッドはVRThrowコンポーネントのThrowです。

この時点で初めて動作確認ができるようになります。プレイして、Oculusコントローラのトリガーボタンを離したらコンソールに「Throw!」が表示されます。

あれ?トリガーを一回しか押していないのに、メッセージが3回も表示されてしまいました。実は、Unity Eventsを使うと、ジェスチャーが「始まった時」、「実行された時」、「キャンセルされた時」に通知が来ます。これを引数の「phase」で確認できます。

public void Throw(InputAction.CallbackContext context)
{
    Debug.Log("Throw! " + context.phase);
}

入力設定でボタンを離した時だけ反応するようにしましたが、この3つの内の「Performed」がいつ送信されるかという設定になっていました。(しかし、Behaviorを「Send Messages」にしたら、StartedとCanceledが送信されませんね。ややこしい…)

では、Throwに確認を入れます。

public void Throw(InputAction.CallbackContext context)
{
    if (context.phase == InputActionPhase.Performed)
    {
        Debug.Log("Throw!");
    }
}

じゃあ、これで大丈夫です。球を投げましょう。

球のスピードを簡単に調節したいのでVRThrowに以下のパラメータを追加します。

public float ballSpeed = 20f;

Throwメソッドの実装がこうなります。

public void Throw(InputAction.CallbackContext context)
{
    if (context.phase == InputActionPhase.Performed)
    {
        // インスペクターで設定されている事を確認する
        if (ballPrefab != null && anchor != null)
        {
            // プレハブから新しいオブジェクトを生成する
            GameObject ball = Instantiate(ballPrefab);

            // 球の位置をアンカーの位置にする
            ball.transform.position = anchor.position;

            // 球のRigidbodyを取得する
            Rigidbody rb = ball.GetComponent<Rigidbody>();
            if (rb != null)
            {
                // ボールの速度をアンカーの「前」方向に合わせて、スピードで調整する
                rb.velocity = anchor.forward * ballSpeed;
            }
        }
    }
}

Anchorを動かして、ボールが飛ぶ位置と角度を調整する事ができます。

さて、コントローラの速度に合わせて、ボールを投げたなら、VRControllerコンポーネントへの参照を取得します。

VRController vrController;

// Start is called before the first frame update
void Start()
 {
    vrController = GetComponent<VRController>();
 }

そして、Throwメソッドでそのvelocityを使って、球のRigidbodyの速度を初期化します。

// 球のRigidbodyを取得する
Rigidbody rb = ball.GetComponent<Rigidbody>();
if (rb != null)
{
    // ボールの速度をアンカーの「前」方向に合わせて、スピードで調整する
    //rb.velocity = anchor.forward * ballSpeed;

    // ボールの速度をコントローラの速度に合わる
    rb.velocity = vrController.velocity * ballSpeed;

この投げ方ではボールの初期速度を下げるとより自然に投げる事ができます。

前だけでなく、上、右、左、手が動いている方向にボールが投げられます。

さらに工夫する

これで自然にボールを投げる事ができますが、最初からボールのプレハブを2つ用意しましたね。「投げる」ジェスチャーとコントローラを「突き刺す」ジェスチャーを認識して、投げるボールの種類を切り替えてみたいです。

2つのジェスチャーの主な違いは、コントローラが回転しながら動いているかどうかです。投げる時にコントローラはこう回転します。

この回転の速度は、VRControllerのangularVelocityで分かります。x、yとz軸の回転速度がありますが、この動きはx軸の回転になります。「投げ」と「突き」の閾値と「突き」用のプレハブを追加しておきましょう。

public class VRThrow : MonoBehaviour
{
    public Transform anchor = null;
    public GameObject ballPrefab = null;
    public GameObject stabBallPrefab = null; // 突き用のプレハブ
    public float throwMinAngularVelocity = 10f; // 投げと突きの閾値
    public float ballSpeed = 20f;

インスペクターで突きのプレハブを指定します。

Throwメソッドの実装をこういう風に修正します。

if (context.phase == InputActionPhase.Performed)
{
    // インスペクターで設定されている事を確認する
    if (ballPrefab != null && stabBallPrefab != null && anchor != null)
    {
        bool isStab = vrController.angularVelocity.x < throwMinAngularVelocity;

「突き」用のプレハブがちゃんと設定されているチェックと、ジェスチャーが「突き」かどうかの判定を行います。

プレハブから球を生成するコードも修正します。

GameObject prefab = isStab ? stabBallPrefab : ballPrefab;

// プレハブから新しいオブジェクトを生成する
GameObject ball = Instantiate(prefab);

ここで三項演算子を使って、ジェスチャーの種類によって使うプレハブを切り替えます。(全くの余談ですが、参考演算子を嫌うプログラマーがいます。確かに下手に使うとかなり分かりにくいコードになってしまいますが、ここでは個人的に最もシンプルですっきりしたコードになります。)

このコードを試してみると、確かに投げる感じで手を動かすと青いボールが出て、突き刺すようにジェスチャーでは赤いボールがでます。

すばらしい、すばらしい。でも、まだです。色々試してみると投げはいいですが、横、上、下方向に突き刺すと赤いボールが出ます。まあ、ゲームによってこの動作が正しいかも知れませんが、ここは剣の様にまっすぐ前に突き刺した時だけ赤いボールを出して、横方向などに「刺す」と何もしないようにします。

ここで登場する助っ人はベクトル演算の内積です。

Unityなど、ゲームエンジンでよく方向を長さが「1」のベクトルで表します。長さが1のベクトル同士の内積を計算するとその角度関係が簡単に分かります。90度のベクトルの内積はいつも0で、反対方向向いているベクトルの内積はいつも0以下です。長さが1なら全く同じ方向に向いているベクトルの内積は1です。

という訳で、「突き」をした時に、コントローラの移動方向のベクトルとアンカーの「前」方向のベクトルの内積を計算すればその二つのベクトルがどれほど同じ方向に向いているかが分かります。

内積の閾値パラメータを準備します。

public class VRThrow : MonoBehaviour
{
    public Transform anchor = null;
    public GameObject ballPrefab = null;
    public GameObject stabBallPrefab = null;
    public float throwMinAngularVelocity = 10f;
    public float stabThreshold = 0.8f; // 突き用の内積の閾値
    public float ballSpeed = 20f;

Throwメソッドで「突き」をした時に内積を確認します。幸いにUnityで内積を計算するのは簡単です。(というか内積はそもそも簡単な演算です。)

GameObject prefab = isStab ? stabBallPrefab : ballPrefab;

if (isStab)
{
    // 速度ベクトルの長さを1にして方向ベクトルに変える
    Vector3 movementDirection = vrController.velocity.normalized; 
    float dot = Vector3.Dot(anchor.forward, movementDirection); // 内積(英語:Dot Product)
    if (dot < stabThreshold)
    {
        // 閾値以下なので、ここで何も投げずに終了
        return;
    }
}

これで動作を確認してみたら、投げた時に青いボールが出て、アンカーが示す方向にまっすぐに突き刺すと赤いボールが出て、上や横にコントローラを動かしても何も起こりません。これでチュートリアルが終わります!

1 トラックバック

Leave a Reply

Your email address will not be published. Required fields are marked *

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください