HoloLens 2(以下HL2)の開発を始めて戸惑う部分が、ポインターの扱いではないでしょうか。
HoloLens 1(以下HL1)の時には、ポインターはHead Gazeと呼ばれる視線(頭の動き)由来のものだけでした。ポインターの方向はCamera.main.transform.forwardと一致しますし、その方向でRayを作成してRaycastを行えば、ポインターが指し示しているものを容易に判別することができました。
ところがHL2では、複数のコントローラーに対して複数のポインターが定義されており、HL1に比べてかなり複雑になっています。
MRTKv2はVR(Windows MRやOpen VR、ViveやRiftなど)も対象としているため、まずコントローラーの種類がたくさんあります。HL2で一般的に使用するのは両手になりますが、右手と左手から同時にHand Rayが発生してそれぞれポインターが存在するため、ポインターの情報を得るにはどうしても複数のポインターが存在することを前提にする必要があります。
また、HL1のHead GazeをHL2でも有効にして使用することができるので、3つ以上のポインターが同時に存在するケースも発生します。
HandとHeadって共存できるんだな…何か勘違いしていた #MRTK pic.twitter.com/j7K7QHmpBD
— とりカシュ (@torikasyu) January 12, 2020
では、ポインターの情報を得るにはどうしたらいいでしょうか?
まず、ポインターがオブジェクトに当たっていれば、そのオブジェクトにアタッチしたスクリプトを利用して情報を取得できます。
MRTKのExamplesに、PointerResultExample というシーンがあります。
上記解説の図のように、ポインターが当たった位置に別のオブジェクトをSpawnしています。
中身を見ると、対象のオブジェクトにPointerHandler.csがアタッチされており、On Pointer Clickedイベントを拾って、MixedRealityPointerEventDataを引数として、SpwanOnPointerEvent.csを読んでいます。
MixedRealityPointerEventDataの、Pointer.Result.DetailsにPoint(位置)やNormal(向き)が入っています。
では、ポインターがどのオブジェクトにも当たっていない場合はどのようにすればいいでしょうか?
PointerHander.csをインスペクタで見てみると、「Is Focus Required」というチェックボックスがあります。これを外せば対象のオブジェクトに当たっていないときでも同様にポインターの情報が取得できます。
Pointer.Result.Details.Objectにはクリックされたオブジェクトが格納されているので、上記の「Is Focus Required」をoffにしておけば、シーン内に存在する任意のオブジェクト(Colliderが必要)について、クリックされたかどうかが判別できます。
インスペクタではなく、スクリプトでイベントに追加することもできるので柔軟な書き方ができます。
上記のようにPointer Clickedなどのイベントを経由して取る方法もありますが、常に監視したい場合もあります。たとえばFPSシューティングのように、Hand Rayの方向に照準を表示したい、などです。これもHL1の時にはCameraオブジェクトの子オブジェクトとして照準を置いてしまえば勝手に頭の方向に着いてきたのですが、HL2の場合はそうもいきません。
まずポインターそのものではなくRayの情報は、InputRayUtilsという便利なクラス経由で取得します。
使い方としては、ExamplesのInputDataExampleシーンを見ると理解が早いです。
TryGetHandRayでHand Ray、GetHeadGazeRayでHead GazeのRayが取得できますが、サンプルシーンではTryGetRayでどちらも取得しています。
そしてポインターの情報を取得するには、PointerUtilsを使用します。
GetPointers()などでポインターの情報を取得できます。
また、PointerUtilは、どのポインターを使うかの設定をする重要な機能も持っています。(基本的にはプロファイラで設定するのですが、動的に変更する場合)
下記のサンプルでは、デフォルトでは使用されないHead Gazeを有効にしています。
(テスト的に書いた不完全なサンプルなので使用する場合は精査してください)
左手と頭はRayの1m先にオブジェクトを配置し、右手はポインターの位置に配置しています。
public GameObject rightReticle; //Colliderを外したCubeなど public GameObject leftReticle; public GameObject headReticle; void Start() { PointerUtils.SetGazePointerBehavior(PointerBehavior.AlwaysOn); //Head Gazeを有効化 } private (InputSourceType, Handedness)[] inputSources = new (InputSourceType, Handedness)[] { (InputSourceType.Controller, Handedness.Right) , (InputSourceType.Controller, Handedness.Left) , (InputSourceType.Eyes, Handedness.Any) , (InputSourceType.Head, Handedness.Any) , (InputSourceType.Hand, Handedness.Left) , (InputSourceType.Hand, Handedness.Right) }; void Update() { foreach (var t in inputSources) { Ray myRay; if (InputRayUtils.TryGetRay(t.Item1, t.Item2, out myRay)) { if (t.Item2 == Handedness.Left) { //Rayの1m先に配置 leftReticle.transform.position = myRay.GetPoint(1f); } // if (t.Item2 == Handedness.Right) // { // rightReticle.transform.position = myRay.GetPoint(1f); // } if (t.Item1 == InputSourceType.Head) { //Rayの1m先に配置 headReticle.transform.position = myRay.GetPoint(1f); } } } //rightReticelはポインタの位置に配置してみる rightReticle.transform.position = PointerUtils.GetPointer<LinePointer>(Handedness.Right).Result.Details.Point; rightReticle.transform.forward = PointerUtils.GetPointer<LinePointer>(Handedness.Right).Result.Details.Normal; }