Unity + Steam VR Plugin でオブジェクトを掴めるようにする

SteamVR2 の InteractionSystem を使って実現します。

# 環境

- Windows 10 PC
- Unity 2019.3.2f1
- Steam VR Plugin 2.5.0
- Oculus Quest + Oculus Link / Valve index

# 手順

## Unity の新しいプロジェクトを作成する

空の３Dのプロジェクトを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/d5eb53e9-fba1-3e7a-d006-94a5ff091f02.png)

## Steam VR Plugin を Asset Store で探して import する

Asset Store で "SteamVR" と入力して、SteamVR Plugin を探します。

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/02943486-4749-26cc-04d4-d30cde323674.png)

見つかったらクリックして Import します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/2a8f2d44-7c4f-6bb6-3a35-3c51c997aed2.png)

途中で下のようなパネルが出たら、Accept All を押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/c540c72f-69d1-8e54-ac46-775f2a107d3f.png)

似たような Steam VR と書かれたパネルが何度か出るかもしれません。出るたびに Accept All します。

## Player Prefab の追加

Import が終了したら、Assets の SteamVR → InteractionSystem → Core → Prefabs の中にある Player を Hierarchy ウィンドウの中にドラッグドロップして追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/07cfedfb-cc13-2ac6-631c-09aa313d7178.png)

追加したら、元からある Main Camera を右クリックして Delete します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/a5ff2920-4259-2de4-7f47-39a0132008a8.png)

## 地面の追加

この状態でプログラムを実行することもできますが、プレイヤーがVR空間に出た瞬間、地面がなくて無限に落下してしまいます。とりあえず、落ちないようにするために地面を追加しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/25311e57-4df5-5322-7eb9-eaef5bd606d4.png)

Hierarchy ウィンドウで右クリックして、Create → 3DObject → Plane を選びます。すると地面（Plane）が追加されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/66d558ee-05fb-5eaa-24ae-70515f9adeb5.png)

これで、プログラムを実行しても落ちなくなります。

## つかむオブジェクトの追加

つかむオブジェクトの例として、Cube を配置します。Hierarchy ウィンドウで右クリックして、Create → 3DObject → Cube を選びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/8421eea4-082a-1baa-3fef-5ce88d256787.png)

Cube を選択して、Inspector の Transform を図のように設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/f969730b-355b-7086-fbcb-b759c7e87d0f.png)

プログラム開始時に、プレイヤーは 0,0,0 の位置に出現し、目線の高さがだいたい y=1.5 弱くらいの位置で、z 軸のプラス方向を向いた状態になります。プレイヤーが開始直後に触ることができる位置に、つかむオブジェクトを配置してます。サイズもデフォルトの 1,1,1 だとかなり巨大なので、手でつかめる程度に小さくしてます。

ここでは y を 2 (2mの高さ）にしてますが、座った状態で実行するなら 1 ～ 1.5 くらいにしたほうがいいかもしれません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/77462ac2-992b-d597-0493-be2cd0b1f2ff.png)

調整後の Scene ウィンドウは上のような感じになります。

## Ineractable.cs の追加

Assets の SteamVR → InteractionSystem → Core → Scripts の中にある Interactable.cs を Cube にドラッグドロップします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/916dacd0-5ba4-7c20-d28c-a38743eba6e1.png)

これで、Cube は SteamVR の InteractionSystem から「プレイヤーが干渉できるオブジェクト」として認識されるようになります。

Interactable.cs を張り付けなくても、自力でプログラムを書けば、コントローラのボタンが押されたかどうかとか、コントローラが物体とぶつかったかどうかを取得することもできます。「Unity SteamVR つかむ」とかで検索するとその方法の記事がたくさん見つかります。しかし、複雑なことをしないのなら Interactable.cs を張り付けるだけでも十分物体の操作ができるので、ここでは Interactable.cs を張り付ける方法を使います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/4398c7f7-1982-0ba2-e168-34f7d9b1fc93.png)

スクリプトを張り付けると、Cube の Inspector に Interactable という項目が追加されるはずです。設定できる項目がたくさんありますが、つかむだけなら設定を変更する必要はありません。

## プレイヤーが Cube に触れているときにボタンを押したときの処理を書く

Interactable.cs では、プレイヤーが物体に「触った」ことまでは検出してくれますが、触った後に「つかむ」という処理まではしてくれません。つかむ処理は、自分でスクリプト中に処理を書く必要があります。そのために、Cube に Add Component でスクリプトを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/93c4f59c-95a1-a4de-66b0-5b34528205c7.png)

Cube の Inspector の一番したにある Add Component のボタンをおして、New Script を選んでから、スクリプトの名前をキーボードで入力します。ここでは "CubeController" としてみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/1af1c4a9-1b58-d3c4-bf71-4a9d552c8e84.png)

Enter を押すと、Inspector に CubeController という項目が追加されます。もしこの段階で、SteamVR のウィンドウが出た場合は、Accept All を押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/6db758b6-3934-130e-e3d4-bab95794e05d.png)

Cube Controller の項目に右側にあるボタンをクリックして、Edit Script を選びます。すると Visual Studio が起動して、CubeController.cs を編集できるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/79e5c136-c39a-3c7f-d650-ef83993d5370.png)

初期状態ではこうなっているはずです。このコードの部分を、下記のように書き換えます。

```CubeController.cs
using UnityEngine;
using Valve.VR.InteractionSystem;

[RequireComponent(typeof(Interactable))]

public class CubeController : MonoBehaviour
{

    private Vector3 oldPosition;
    private Quaternion oldRotation;

    private Hand.AttachmentFlags attachmentFlags = Hand.defaultAttachmentFlags & (~Hand.AttachmentFlags.SnapOnAttach) & (~Hand.AttachmentFlags.DetachOthers) & (~Hand.AttachmentFlags.VelocityMovement);
    private Interactable interactable;

    void Awake()
    {
        interactable = this.GetComponent<Interactable>();
    }

    private void HandHoverUpdate(Hand hand)
    {
        GrabTypes startingGrabType = hand.GetGrabStarting();
        bool isGrabEnding = hand.IsGrabEnding(this.gameObject);

        if (interactable.attachedToHand == null && startingGrabType != GrabTypes.None)
        {
            oldPosition = transform.position;
            oldRotation = transform.rotation;

            hand.HoverLock(interactable);
            hand.AttachObject(gameObject, startingGrabType, attachmentFlags);
        }
        else if (isGrabEnding)
        {
            hand.DetachObject(gameObject);

            hand.HoverUnlock(interactable);

            transform.position = oldPosition;
            transform.rotation = oldRotation;
        }
    }
}
```

このコードは、Assets の SteamVR → InteractionSystem → Core → Scripts の中にある InteractableExample.cs の中身の一部をコピペしただけのものです。（コメントは削除しています）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/c0a0153e-3b91-fafc-fa39-698f10feed84.png)

書き込んだら、Control + S などでセーブして、Unity に戻ります。自動的にコードはコンパイルされます。（多分）

これで、実行の準備はほぼ完了です。

## VR機器の準備をする。

ここまでできたら、Oculus や Valve Index を接続して、SteamVR で動く適当なVRアプリを起動し、SteamVR が問題なく動くことを確認しておきます。Oculus Quest の場合は、Oculus Link がオンになっていることも確認しておきます。

## コントローラの bindings の初期設定をする（1回目のみ）

VR機器の準備ができたら、Unity の上部にある実行ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/4c8cc72d-9f16-df9e-1ab6-87cc10f4bcaf.png)

これが出たらOKを押します。最初の1回目だけ出ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/43b1e48d-2de2-1b32-8534-f431d081ec56.png)

続けてこれも出るのでOKを押します。これも最初の1回目だけ出ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/9ec87bfb-5076-9c18-a122-8cd7ecca8b07.png)

さらに続けてこのウィンドウが出ます。このウィンドウは、VR機器ごとにボタンの設定などを細かく設定するためのものですが、このアプリではデフォルトで用意されているアクション（機能）しか使わないので、特に何も設定せずに Save and generate を押します。すると、Compiling と表示されてデフォルトのボタンの割り当て設定が反映されます。Compiling が終わって、ウィンドウの表示が元に戻ったら、ウィンドウは右上のｘを押して閉じてしまって大丈夫です。

このあと、下のようなウィンドウが出た場合は OK を押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/e8d6007e-4fdf-d7fd-8bf7-e453eb2e204a.png)

Visual Studio 側で「外部のプログラムで更新されました」というようなウィンドウが出た場合は「再読み込み」を押します。

いろいろ出てくるパネルやウィンドウが閉じたら、Unity に戻って改めて実行しなおします。

## 改めて実行

2回目に実行すると、SteamVR が自動的に起動して、プログラムが実行されます。VRヘッドセットとコントローラで物体を操作できるようになります。

![unity2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/80484320-e391-daa8-7716-9070d07aabe9.png)

さきほど設置した Cube に手を近づけると、Cube に黄色枠がでます。枠が出ている状態で、人差し指か中指のところにあるボタン（コントローラによって違います）を押すと、物体をつかめます。ボタンを押したまま手を動かせば、物体を動かすことができます。

ボタンを離すと、物体はもとの位置に戻ります。下のスナップショットをクリックすると、動作の様子を撮影した youtube の動画に飛びます。（というかYoutubeの動画って Qiita に素直に埋め込めないのね？）

[![Cubeをつかむデモ動画](http://img.youtube.com/vi/yp-zSeW7ZPE/0.jpg)](http://www.youtube.com/watch?v=yp-zSeW7ZPE)

Cube に手が届かない場合は、Cube の Inspector の Transform で Z とか Y を微調整します。

# ちょっとした覚書

## bindings について

コントローラのボタンの設定（Binlings）については、下記の記事で詳しく書かれています。

- https://medium.com/@danielle.co3tz33/steamvr-2-2-0-new-input-system-tutorial-3fef5e275828

この記事では、”A"という名前の独自のアクションを追加して、そのアクションにコントローラのボタンを割り当て、さらに”A"のアクションの内容を実装する方法について説明されています。

上の記事のように、アクションが割り当てられていないボタンにアクションを紐づけたり、自分でアクションを新規に定義して、ボタンにそのアクションを紐づけたいときには、これらのウィンドウを使って設定をします。

bindings の設定ファイルを Export すると、デフォルトでは Documents\steamvr\input 以下に保存されます。このファイルは Unity の Assets には自動的には含まれません。プロジェクトをコピーしたり Assets を unitypackage に export する場合には、手動で同梱（配布）する必要があります。

## CubeController.cs の中身について

Interactable.cs がつけられたオブジェクトに、```private void HandHoverUpdate(Hand hand)``` という関数を定義しておくと、SteamVR の InteractionSystem が物体間の衝突判定の処理をするたびに、この関数を自動的に呼び出してくれるようです。

この関数の中で、衝突が開始されたのか、ボタンが押されたのか、衝突が終わったのか、などを自前で判定して、イベントの内容に合わせた処理を書くという感じになってます。

最初の if で「手が何も掴んでいない時に、何かを掴んだ時」の処理として、物体のデフォルトの位置を oldPosition に保存し、手に物体を Attach してます。

次の if は「物体を離したとき」の処理として、手から物体を Detach し、物体の位置を oldPosition に戻しています。戻す処理を書かなければ、物体は手を離した場所に残り続けるはずです。

物体を投げたいとか、転がしたいとか、押したいとか、他の物体を通過しないようにしたいときは、SteamVR のデモのシーンの中に、いろいろなサンプルがあるので、そのへんのコードが参考になりそうです（多分