VRで自分の手がオブジェクトを貫通しないようにして"ものに触っている感"を出す
こんにちは!新しく代表になりました学部二年のにとです。この記事はA-PxLのアドベントカレンダーに向けて執筆しました。最近はHalf-Life:AlyxやGreen Hell、ルインズメイガスなどいろんなVRゲームで遊んでいますが、やはり没入感を高める細かい工夫がたくさんあってすごく参考になりますね。今回は地味ながらも没入感がアップする"ものに触っている感"を作っていこうと思います。
環境
HMD : Meta Quest2
Unity : 2021.3.11f1(URPを導入)
XR Interaction Toolkit : 2.0.4
作るもの
今回はVR内の手でオブジェクトに触れて、貫通しないようにしようと思います。
赤丸がコントローラーの位置
youtu.be
今まではXR Interaction Toolkitが提供しているXR Originのアンカーの子オブジェクトに手を配置していたんですが、そうするとVRで壁などの動かないオブジェクトに触れようとするとコライダーをつけていても貫通してしまいます。意外と盲点でした。
実装方法を考える
作るにあたって3つの実装方法案を考えました。
Rayを使ってオブジェクトとの距離を測り、手が埋まらないようにtransformを変えていく
Rigidbodyとコライダーを信じてオブジェクトと手を接触させる
- Jointを使う
まず1は主にベクトルの計算をしていくのですが、オブジェクトがただの平面のときはよくても曲面や凸凹面になるとややこしくなってしまったのでダメでした。
2に関しては簡単に作れたのですが、↓のように挙動が安定しないのと、オブジェクトに触れた後に手の位置が変わってしまうなどの不具合があってダメでした。
3のJointを使う方法が一番うまくいったので紹介しようと思います。
Fixed Jointを使って実装する
1. XR Interaction ToolkitでVR環境を作る
まずはXR Interaction Toolkitの導入をしていきます。
いつも参考にさせていただいている記事↓
framesynthesis.jp
これで環境が整ったらXR Originをシーンに配置します。一応移動とスナップターンができるよう、XR Originに
- Locomotion System
- Continuous Move Provider
- Snap Turn Provider
をアタッチします。Continuous Move ProviderとSnap Turn ProviderはAssets>Samples>XR Interaction Toolkit>2.0.4(バージョン)>Starter Assetsの中の「XRI Default Continuous Move」と「XRI Default Snap Turn」をインスペクタにドラッグアンドドロップすると細かい設定をせずにアタッチできます。
こんな感じになれば大丈夫です。
XR Originを入れるところ忘れずに!
2. シーンに手のモデルを配置する
次に手をシーンに配置します。無料のモデルがAsset Storeになかったので「手 3Dモデル フリー」などと調べて出てきたものを適当に追加しました。手の位置や大きさを調整していい感じになったらOKです。(QuestLinkなどで実行しながら調整して値をコピーし、実行停止してペーストするのがラク)
今回はオブジェクトに触れる手を作るので、RigidbodyとColliderも追加しましょう。また、RididbodyのConstrainsのFreeze Rotationのxyz全てにチェックを入れます。
僕が使ったモデルは軸がおかしかったので空のGameObjectを作ってその中に手のモデルを入れました。(画像は空のGameObjectのインスペクタ)
3. Fixed Jointを設定する
次にFixed Jointを追加していきます。
XR Originの子オブジェクトの中の「Left Hand Controller」と「Right Hand Controller」にFixed Jointコンポーネントを追加します。追加されたRigidbodyのIs Kinematicにはチェックを入れておきましょう。
ここでFixed JointのConnected Bodyに手を入れても動くのですが、動きがガクガクしてしまうので、オブジェクトに触れている間だけ接続して、触れていない間はコントローラーを追従するようにします。
4. スクリプトを書く
手のモデルをコントローラーに追従させ、オブジェクトに触れたらFixed Jointに接続するコードを書いていきます。
using UnityEngine; public class HandJointBehavior : MonoBehaviour { [SerializeField] private Transform _controller; private bool _isTouch; private Rigidbody _rigidbody; private FixedJoint _joint; private void Start() { _rigidbody = GetComponent<Rigidbody>(); _joint = _controller.GetComponent<FixedJoint>(); } private void Update() { // 回転は常に追従 transform.rotation = _controller.rotation; // 触れていないときはコントローラーを追従する if (!_isTouch) { transform.position = _controller.position; } } private void OnCollisionStay(Collision other) { _isTouch = true; _joint.connectedBody = _rigidbody; // FixedJointに接続 } private void OnCollisionExit(Collision other) { _isTouch = false; _joint.connectedBody = null; // FixedJointから切断 } }
シンプルに実装できました。
最後に、書いたスクリプトを手のオブジェクトにアタッチします。アタッチしたらインスペクタのControllerに対応するコントローラーを入れます。
完成
さいごに
VR内で手を衝突させることで現実の手の位置とのズレが生じるのですが、違和感なく手がオブジェクトに触れている感じを表現することができました。触れたときにSEを出す、触れている間はコントローラーを振動させるなどでさらに没入感を得られると思います。
今年は勉強会の講師やチーム開発のリーダー、IVRCなど初めてのことをたくさん経験し、大きく成長できた一年でした。これからはA-PxLの代表としてより一層がんばっていこうと思います。そして来年はXRの面白さをもっと知り、いろいろな技術に触れていきたいです。
2023年もよろしくお願いします!