今回はOculus Touchの開発ネタをやっていきます。
使う機材はOculus Questですが、Oculus Rift Sでもそのまま使うことができる内容です。
Oculus Questをお持ちの方は公式チュートリルアプリであるFirst Stepsをプレイ済みかと思われます。
このコンテンツは物を掴んで投げるだけでなく、拳(握った状態)で殴ることができ、
人差し指を立てた状態でツンツンすることもできます。
これは今までやってきたAvatarGrabなどにあるOVR Grabberではできないことです。
そこで今回は拳で物を殴ったり、指でつんつんするなど、手を使った物理挙動を作ってみます。
- 開発環境
- アプローチ
- セットアップ
- サンプルシーンの確認
- OVR Fist
- 実際に使ってみる
- 手の用意
- 手に当たり判定をつける
- OVRFistの設定
- 操作方法
- まとめ
- 参考文献
- おまけ OVR Fistのコードについて
- おまけ Handコンポーネントのバグ?
開発環境
Windows 10
Unity 2018.4.5f
Oculus Integration ver1.39
Oculus Quest
ALVR
アプローチ
OVR Grabber(とそれを継承しているDistance Grabberなど)では
掴むといった判定はすべてIsTriggerで行われます。
つまり、CustomHandやAvatartGrabでは物理衝突を行うことができません。
探してみたところ、OculusIntegration内に物を殴るといった機能はないようです。(あったらごめん)
そのため、手に沿ったコライダーを付け、物理衝突を制御するコンポーネントを独自実装していきます。
セットアップ
始めてVR開発をする方はこちらを参考にしてみてください
既にこちらを終えてあることを前提として進めていきます。
サンプルシーンの確認
Assets/SampleFramework/Usageに様々なサンプルシーンがあります。
学習や挙動の確認にとても有益です。見てみましょう。
今回はお馴染み、AvatarGrabシーンを使っていきます。
【追記】 2020/02/18
AvatarGrabシーンが削除されました。詳しくはこちら。
OVR Fist
今回はOculusIntegrationにない機能ですので独自コンポーネントを実装していきます。
その名もOVRFistコンポーネントです。
インスペクターパラメーター
Controller
HandやOVRGrabberにあるものと同じです。
右手であればR Touch、左手であればL Touchを選択します。
実際に使ってみる
手の用意
あらかじめ手を用意しておきましょう。
手っ取りばやく用意するのであれば、CustomHandプレハブをそのまま使いましょう。
AvatarGrabにあるLocalAvatarWithGrabの中に右手と左手を加えます。
最初から入っているAvatarGrabber2つは非アクティブにしておきましょう。
ついでにLocalAvatarWithGrabの中にあるOvrAvatarコンポーネントも無効にしておきます。
余談ですが、デフォルトのCustomHandプレハブは左右の手でマテリアルが違います。
見た目を統一したい場合はマテリアルを差し替えましょう。
変え方は前回の記事で紹介しています。
手に当たり判定をつける
まずは手に当たり判定をつけましょう。
AvatarGrabなどの手には最初からCapsuleColliderがついていますが、これはGrab用のIsTriggerなものです。
当然物理衝突はしません。
そのため物理衝突をするコライダーを手の形に沿ってつけていきます。
手にはSkinnedMeshColliderがついているのでMeshColliderをつけたいところですが、
MeshColliderはメッシュの変形に対応していないので、プリミティブなコライダーを組み合わせて再現します。
使うのはこちらのアセット「SAColliderBuilder」
複雑な形の3Dモデルでも簡単に当たり判定をつけることができるアセットです。
詳しくはこちらで解説されています。
SAColliderBuilderの設定
SA Bone Collider Builderのアタッチ
このコンポーネントはAnimatorのあるオブジェクトにアタッチする必要があります。
CustomHand内にある、hand_skeltal_lowersにSA Bone Collider Builderをアタッチします。
Animatorがアタッチされているのが目印。
コライダーの生成
SABoneColliderBuilderのインスペクターを編集します。
・Shape TypeをCapsule
・Optimize Rotationのチェックを外す
・Rigidbody IsCreateのチェックを外す
最後にProcessを押して生成
これでいい感じにコライダーが生成されます。
片手が終わったらもう片方の手もやりましょう。
テスト段階で偶然見かけましたが、コライダー生成時にRigidbodyをつけ、IsKinematicを外すと。
手のメッシュが崩れとんでもないことになります。
似たような現象が起きた方はRigidbodyあたりを確認してみましょう。
動作確認
ここまでできたら実際に動かしてみたいところですが、このままでは問題があります。
なんとせっかくつけたコライダーが実行時に無効化されてしまいます。
犯人はHandコンポーネントです。
Handがアタッチされている子オブジェクトのコライダー(IsTriggerでないもの)はStart時に無効化されます。
// Collision starts disabled. We'll enable it for certain cases such as making a fist. m_colliders = this.GetComponentsInChildren<Collider>().Where(childCollider => !childCollider.isTrigger).ToArray(); CollisionEnable(false);
OVRFistではここも考慮しています。
OVRFistの設定
GitHubからOVRFistを持ってくる
GitHubからOVRFistを持ってきます。
Handオブジェクトにアタッチ
これをHandコンポーネントやOVRGrabberコンポーネントがある同じオブジェクトにアタッチします。
ちなみにOVRGrabberコンポーネントは必須ではありません。
インスペクターのControllerパラメーターを変更します。
右手であればR Touch、左手であればL Touchに設定します。
これで終わりです。
操作方法
First Stepsとほとんど同じ操作感です。
HandTrigger(中指のボタン)を握っている間のみ拳の当たり判定が有効になります。
ただし、OVRGrabbableが掴める範囲内にある場合は掴むことを優先します。
物を放り投げて殴り飛ばすこともできます。
まとめ
SAColliderBuilderを使えば簡単に手に当たり判定をつけることができる。
OVRFistコンポーネントで拳に物理的な衝突判定を実装できる。
参考文献
おまけ OVR Fistのコードについて
正直OVR Grabberのm_grabCandidatesをPublicにして直接参照すればOnTriggerなどは不要になりますが、
できるだけOculusIntegrationのコードに手を加えたくないためこういう書き方になっています。
これに限らずOculusIntegrationのコードにいろいろ手を加えている方は、
アップデートのたびに手直しする必要がでてくるので注意しましょう。
あるいはアップデートしないというのも手です。
おまけ Handコンポーネントのバグ?
OculusIntegrationに含まれているHandコンポーネントにはバグがあります。
Handには子オブジェクトにあるIsTriggerではないコライダーを全て無効化するCollisionEnable()があります。
このメソッドが子オブジェクト全てのコライダーの有効化無効化を切り替えていますが、
同時にコライダーのあるオブジェクトのスケールも変更しています。
この時、有効化時にはCOLLIDER_SCALE_MAXを適応しなければならない所がCOLLIDER_SCALE_MINになっています。m_collisionScaleCurrentもなんかおかしい。
このままでは、例えコライダーが有効化されてもスケールが小さいままです。。
private void CollisionEnable(bool enabled) { if (m_collisionEnabled == enabled) { return; } m_collisionEnabled = enabled; if (enabled) { m_collisionScaleCurrent = COLLIDER_SCALE_MIN; for (int i = 0; i < m_colliders.Length; ++i) { Collider collider = m_colliders[i]; collider.transform.localScale = new Vector3(COLLIDER_SCALE_MIN, COLLIDER_SCALE_MIN, COLLIDER_SCALE_MIN); collider.enabled = true; } } else { m_collisionScaleCurrent = COLLIDER_SCALE_MAX; for (int i = 0; i < m_colliders.Length; ++i) { Collider collider = m_colliders[i]; collider.enabled = false; collider.transform.localScale = new Vector3(COLLIDER_SCALE_MIN, COLLIDER_SCALE_MIN, COLLIDER_SCALE_MIN); } } }
正しくはこうなるはずです。
if (enabled) { m_collisionScaleCurrent = COLLIDER_SCALE_MAX; for (int i = 0; i < m_colliders.Length; ++i) { Collider collider = m_colliders[i]; collider.transform.localScale = new Vector3(COLLIDER_SCALE_MAX, COLLIDER_SCALE_MAX, COLLIDER_SCALE_MAX); collider.enabled = true; } } else { m_collisionScaleCurrent = COLLIDER_SCALE_MIN; for (int i = 0; i < m_colliders.Length; ++i) { Collider collider = m_colliders[i]; collider.enabled = false; collider.transform.localScale = new Vector3(COLLIDER_SCALE_MIN, COLLIDER_SCALE_MIN, COLLIDER_SCALE_MIN); } }
それとも、このままの方が正しいのだろうか、よくわからん。
今までGistなどを使ってましたが、今回初めてGitHubでコードを公開しました。
READMEの書き方とかおかしい所、他間違っている箇所がありましたらコメントにお願いします。