目次
関連記事一覧
OculusQuest2(その1) VRで物を掴む投げる離す←今回の記事
はじめに
10月にOculusQuest2が発売され、クオリティの高いVRを手軽に楽しめる時代がきました。
その理由として、
- PCなしでスタンドアローンで動作する
- 6Dof対応(3軸移動、3軸回転)
- 外部センサーが必要ない
- PC用のOculus Rift Sよりも
- 解像度が高い
- Oculus Rift S 片目当たり 1,280×1,440
- Oculus Quest 2 片目当たり 1832×1920
- リフレッシュレートが高い
- Oculus Rift S 80Hz
- Oculus Quest 2 90Hz
- 値段が安い
- Oculus Rift S 5万円台
- Oculus Quest 2 3万円後半(64GB) 約5万円(256GB)
- 解像度が高い
- スタンドアローンでも動作し、PCと接続してOculusStoreやSteamのゲームも楽しめる
PCと接続する場合は、PCにそれなりの性能が必要です。
VR自体新しい技術ではありませんが、
何回かに分けてOculus Quest 2を使用したVRの基本的な実装をブログにまとめていきたいと思います。
今回は物をつかむ・投げる実装について書いていきます。
(最後にソースコードとその使い方も書きます)
今回作成したサンプル
公式にも物をつかむ・投げるのサンプルはありましたが、シンプルでなく理解しづらかったので、
自分なりに考えて実装してみることにしました。
今回作成したものは以下の動画になります。
赤いキューブは物理挙動をせず、
白いキューブは物理挙動をしています。
最初は物理挙動をしない状態で考えたほうが良いので、物理挙動なしの状態から説明していきます。
環境構築
使い慣れているUnity(2019.4.9f1)で実装していきます。
こちらで詳しく環境構築の説明はしません。
公式の環境構築方法はこちらになります。
https://developer.oculus.com/documentation/unity/unity-gs-overview/
所感としましては、結構簡単に環境構築をすることができました。
Oculus Quest 2とPCをつないで、Unityを実行するだけでVR空間が表示されます。
プレイヤーの作成
環境構築時にインポートしたOculus Integrationに含まれているPrefabのOVRCameraRigを配置することで、ヘッドマウントディスプレイのカメラが自動的に設定されます。
※シーンに存在しているMainCameraは削除してください
この状態ですと頭の動きに合わせてカメラが移動回転してくれますが、コントローラーが表示されません。
今回は物をつかむので、手を表示させます。
先ほど追加したOVRCameraRigのLeftHandAnchorとRightHandAnchorにそれぞれ、
CustomHandLeftとCustomHandRightを追加します。
こちらもOculus Integrationに含まれているプレファブです。
以上で視点と手の動きの設定はOKです。
これだけで、コントローラーを操作したときに手も勝手に動いてくれるので、本当に簡単ですね、、、
つかむオブジェクトの判定
つかむオブジェクトを判定するためにSphereCastを使用します
https://docs.unity3d.com/ja/current/ScriptReference/Physics.SphereCast.html
こちらで特筆して説明する関数でもないですが、
球を指定した地点から指定した方向に飛ばして何かにぶつかったらそのオブジェクトの情報を返す関数です。
途中でぶつかったらその時点で判定を終了します。
CustomHandのtransform.forwardで手をパーにしている状態の時の人差し指の方向が取得できるので、こちらを利用して球を飛ばして判定します。
判定する際、つかめるオブジェクトのみを判定したいので、LayerにGrabbableを指定して、こちらのみを判定しています。
オブジェクトを手元まで引き寄せる
つかめるオブジェクトの判定が出来ましたら、次に手まで引き寄せます。
画像のボタンを押すことで引き寄せられるようにします。
こちらで押した瞬間のタイミングが取得できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void Update() { // 右手の場合 if(OVRInput.GetDown(OVRInput.RawButton.LHandTrigger)) { // 押されたときの処理 } // 左手の場合 if(OVRInput.GetDown(OVRInput.RawButton.RHandTrigger)) { // 押されたときの処理 } } |
引き寄せる処理にはUnirxを利用しました。
コードの抜粋になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
_grabDisposable = this.UpdateAsObservable() // 手まで限りなく近くなるまで自分を近づける .TakeWhile(_ => (transform.position - handTrans.position).sqrMagnitude > 0.1f * 0.1f) .Subscribe( _ => { // 手までオブジェクトを近づける Vector3 dir = (handTrans.position - transform.position).normalized; transform.position += dir * (10f * Time.deltaTime); }, () => { // 購読完了時 // 手まで限りなく近くなった場合、親を手にして完了 transform.parent = handTrans; transform.position = handTrans.position; }).AddTo(this); |
Unirxでアップデートの購読を開始します。
TakeWhileで、アップデートを継続する条件(限りなくオブジェクトが手に近くなるまで)を指定しています。
sqrMagnitudeはベクトルの長さをルートの計算をしない状態で返すプロパティです。
ルートの計算を毎フレーム実行するのは重いのでこちらを使用します。
条件を満たしているとき、Subscribeの第一引数が呼ばれ続けます。
そして、オブジェクトが限りなく手に近くなりTakeWhileの条件に合わなくなったとき、Subscribeの第2引数の処理が呼ばれ、購読が完了します。
掴んでいる間は、手と一緒にオブジェクトが動いてほしいので、
購読完了時、手の子オブジェクトになるよう設定します。
離す
ボタンを離したときは以下のようにしてタイミングを取得できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void Update() { // 右手の場合 if(OVRInput.GetUp(OVRInput.RawButton.LHandTrigger)) { // 離されたときの処理 } // 左手の場合 if(OVRInput.GetUp(OVRInput.RawButton.RHandTrigger)) { // 離されたときの処理 } } |
離した際に、離したオブジェクトの親を手からもともとの親に戻してあげます。
1 |
transform.parent = _defaultParent; |
こちらまでの説明が、オブジェクトに物理挙動を与えない場合の説明です。
最初に紹介しました動画の、赤いキューブをつかんで離すまでの内容です。
RigidBody適用
こちらから動画の、白いキューブをつかんで離したときの内容になります。
オブジェクトに物理挙動を与えたい場合、RigidBodyコンポーネントを追加しただけでは期待した動作を得ることはできません。
作るゲームによって、どういう挙動にするか変わると思います。
いくつかのVRゲームを遊んでみると、物を投げるときの感覚は異なっています。
はじめに必要な設定
始めに、CustomHandLeft(Right)はデフォルトでColliderが設定されているので、
不必要な場合は削除するか、レイヤーでオブジェクトと衝突しないように設定します。
コントローラー加速度の取得
次に、コントローラの加速度の情報を取得する必要があるので、取得方法です。
コントローラーは以下で取得できます。
1 2 |
OVRInput.Controller controller = OVRInput.Controller.LTouch; OVRInput.Controller controller = OVRInput.Controller.RTouch; |
こちらで、加速度が取得できます。
1 |
Vector3 acc = OVRInput.GetLocalControllerAcceleration(controller); |
取得した加速度をそのまま使用すると、値が大きすぎたので調整する必要がありました。
1 2 3 |
[SerializeField, Range(0f, 1f)] private float _powerAdjust = 0.2f; [SerializeField] private float _minReleasePower = 1f; [SerializeField] private float _maxReleasePower = 100f; |
1 2 3 4 5 6 7 8 9 10 11 |
float mag = OVRInput.GetLocalControllerAcceleration(controller).magnitude; if (mag < _minReleasePower) { mag = 0f; } if (mag > _maxReleasePower) { mag = _maxReleasePower; } Vector3 acc = transform.forward * (mag * _powerAdjust); |
加速度の方向を使用しなかった理由
今回はコントローラーの加速度の方向を使用せずに、コントローラーの正面方向で固定しています。
1 |
Vector3 acc = transform.forward * (mag * _powerAdjust); |
自分の好みになってしまいますが、実際に前方向に物を投げる感覚と近づけたかったのでこのようにしました。
加速度の方向に飛ばすことが実装的には正しいと思いますが、現実世界でボールを投げる感覚で投げると下の方向に落ちてしまいます。
前に投げたい場合、腕を下に振り下ろすので、投げたい方向に押し出すようには投げないと思います。
(砲丸投げのように重いものを投げる場合は押し出すと思いますが、、、)
腕を振り下ろしたときの加速度の向きは、投げたい方向よりは下向きの方向になるので下のほうに飛んで行ってしまいます。
この実装方法ですと、投げたい方向にコントローラーを向ける必要があるので、
横に投げるときなど期待通りの動作になりません。
実装と確認を何回も繰り返して、自分が理想としている動作に近づけていく必要があります。
投げられるオブジェクト側の挙動
次に投げられるオブジェクト側の実装です。
こちらはシンプルなので特に難しいことはありません。
掴み始めたとき(ボタンを押した瞬間)から、投げる瞬間(ボタンを離した瞬間)までRigidBodyのisKinematicをtrueにします。
1 |
_rigidbody.isKinematic = true; |
こちらは物理挙動を無視して、プログラム上やアニメーションで移動するときにtrueにする必要があります。
そして、離した瞬間にこちらをfalseにして、力を加えてあげます。
1 2 |
_rigidbody.isKinematic = false; _rigidbody.AddForce(controllerAcc, ForceMode.Impulse); |
他に必要な実装もありますが、最後にすべてのソースを載せますのでそちらをご参照ください。
まとめ
今回はVRの導入として、物をつかむ・離す基本的な動作について書きました。
VRのコアな部分の設定と処理はプラグインとSDKがやってくれるので、手軽に実装することができますね。
最近、デバイスの性能が上がってきていて価格も安くなっているので、
これまでできなかったことができるようになるデバイスの登場が楽しみです。
ソースコードとその使い方
今回作成したソースコードです。
コード内で以下のライブラリを使用しています。
UniRx
https://assetstore.unity.com/packages/tools/integration/unirx-reactive-extensions-for-unity-17276
QuickOutline
https://assetstore.unity.com/packages/tools/particles-effects/quick-outline-115488
MyDistanceGrabber.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
using System.Collections; using System.Collections.Generic; using UniRx; using UniRx.Triggers; using UnityEngine; public class MyDistanceGrabber : MonoBehaviour { public enum HandType { LEFT, RIGHT } [SerializeField] private HandType _handType = HandType.LEFT; [SerializeField] private float _castCircleRadius = 0.2f; [SerializeField] private float _maxCastDistance = 3f; [SerializeField, Range(0f, 1f)] private float _powerAdjust = 0.2f; [SerializeField] private float _minReleasePower = 1f; [SerializeField] private float _maxReleasePower = 100f; private MyGrabbable _targetGrabbable; private MyGrabbable _grabbedGrabbable; void Start() { this.FixedUpdateAsObservable().Subscribe(_ => { // 掴めるオブジェクトの判定 DetectGrabbable(); }).AddTo(this); // 右か左かによってボタン、コントローラーの切り分け OVRInput.RawButton buttonType = OVRInput.RawButton.LHandTrigger; OVRInput.Controller controller = OVRInput.Controller.LTouch; switch (_handType) { case HandType.LEFT: buttonType = OVRInput.RawButton.LHandTrigger; controller = OVRInput.Controller.LTouch; break; case HandType.RIGHT: buttonType = OVRInput.RawButton.RHandTrigger; controller = OVRInput.Controller.RTouch; break; } // 中指ボタンが押されたとき this.UpdateAsObservable() .Where(_ => OVRInput.GetDown(buttonType)) .Subscribe(_ => { if (_targetGrabbable != null) { _targetGrabbable.Grab(transform); _grabbedGrabbable = _targetGrabbable; } }).AddTo(this); // 中指ボタンが離されたとき this.UpdateAsObservable() .Where(_ => OVRInput.GetUp(buttonType)) .Subscribe(_ => { if (_grabbedGrabbable != null) { Vector3 dir = transform.forward; float mag = OVRInput.GetLocalControllerAcceleration(controller).magnitude; if (mag < _minReleasePower) { mag = 0f; } if (mag > _maxReleasePower) { mag = _maxReleasePower; } Vector3 acc = dir.normalized * (mag * _powerAdjust); _grabbedGrabbable.Release(acc); _grabbedGrabbable = null; } }).AddTo(this); } /// <summary> /// 掴めるオブジェクトを検知する /// </summary> void DetectGrabbable() { RaycastHit hit; Vector3 p1 = transform.position; int layerMask = LayerMask.GetMask(new string[] {"Grabbable"}); if (Physics.SphereCast(p1, _castCircleRadius, transform.forward, out hit, _maxCastDistance, layerMask)) { var grabbable = hit.collider.GetComponent<MyGrabbable>(); if (grabbable) { if (_targetGrabbable != null) { _targetGrabbable.OutLineEnabled = false; } grabbable.OutLineEnabled = true; _targetGrabbable = grabbable; } } else { if (_targetGrabbable != null) { _targetGrabbable.OutLineEnabled = false; _targetGrabbable = null; } } } } |
MyGrabbable.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
using System; using UnityEngine; using UniRx; using UniRx.Triggers; [RequireComponent(typeof(Outline))] public class MyGrabbable : MonoBehaviour { public bool IsGrabbed { get => _isGrabbed; } private Rigidbody _rigidbody; private Transform _defaultParent; private Outline _outline; private bool _isGrabbed = false; public bool OutLineEnabled { get { return _outline.enabled; } set { _outline.enabled = value; } } void Awake() { _rigidbody = GetComponent<Rigidbody>(); _outline = GetComponent<Outline>(); _outline.enabled = false; } void Start() { _defaultParent = transform.parent; } private IDisposable _grabDisposable; public void Grab(Transform handTrans) { if (_isGrabbed) { return; } if (_rigidbody != null) { _rigidbody.isKinematic = true; } _isGrabbed = true; _grabDisposable?.Dispose(); _grabDisposable = this.UpdateAsObservable() // 手まで限りなく近くなるまで自分を近づける .TakeWhile(_ => (transform.position - handTrans.position).sqrMagnitude > 0.1f * 0.1f).Subscribe( _ => { // 手までオブジェクトを近づける Vector3 dir = (handTrans.position - transform.position).normalized; transform.position += dir * (10f * Time.deltaTime); }, () => { // 購読完了時 // 手まで限りなく近くなった場合、親を手にして完了 transform.parent = handTrans; transform.position = handTrans.position; }).AddTo(this); } public void Release(Vector3 controllerAcc) { if (!_isGrabbed) { return; } if (_rigidbody != null) { _rigidbody.isKinematic = false; _rigidbody.AddForce(controllerAcc, ForceMode.Impulse); } _grabDisposable?.Dispose(); _isGrabbed = false; transform.parent = _defaultParent; } } |
MyDistanceGrabber
投げる側(手)で使用するコンポーネントです。
設定するプロパティ
名前 | 説明 |
---|---|
HandType | 左or右手を設定する |
CastCircleRadius | つかめるかを判定するときの球の半径 |
MaxCastDistance | つかめる距離 |
PowerAdjust | 投げる力の調整用の値 |
MinReleasePower | 許容するコントローラーの最小の加速度 |
MaxReleasePower | 許容するコントローラーの最大の加速度 |
MyGrabbable
投げられる側(キューブ)に使用するコンポーネントです。
投げられるオブジェクトのレイヤーにはGrabbableを指定し、
こちらのコンポーネントをアタッチするだけです。
(Colliderコンポーネントも必要です)
Outlineコンポーネントも自動的にセットされるので、こちらでアウトラインの色と太さを設定できます。
こちらは設定をプリセットに保存しておくと便利です(参考:Unity Preset機能を試しました)
物理挙動をさせたい場合は、RigidBodyを追加するだけでOKです。