たるこすの日記

たるこすの日記

リアルからバーチャルへ、バーチャルからリアルへ

HoloLens と PC でマウスカーソルを行き来させる

こんにちは、たるこすです。

今回は、HoloLens と PC でマウスカーソルを行き来させるというのをやってみました。

やりたかったこと

以下のデモのように、PC 上のマウスカーソルを画面外に移動させるとマウスカーソルが HoloLens 側に移動して HoloLens 側の操作ができるというものを目指しました。


Microsoft HoloLens: Partner Demo with Maya by Autodesk

できたもの

以下の動画のように、PC のモニターに表示されているマウスカーソルが画面の外に出て HoloLens 側で表示され、物体をクリックできるようなものが作れました。


PC & HoloLens share mouse

実装概要

実装の中身について、簡単に解説します。ただし、この通りにやれば実装できる、というところまで細かくは書いていません。もし詳細が知りたいところがあれば、お気軽に @tarukosu までメッセージを送ってください。

PC 側アプリと HoloLens 側アプリはそれぞれ Unity で作成しています。

PC 側アプリ

PC 側アプリでは、HoloLens がモニターの位置を認識できるようにマーカーとなる画像を表示します。 また、マウスカーソルが PC 側に表示されている状態と、マウスカーソルが HoloLens 側に表示されている状態の2つの状態があり、それぞれで以下のような処理を行っています。

カーソルが PC 側に表示されている状態では、マウスカーソルの位置を取得し、カーソルが画面の左端に達した際にカーソルが画面外に出たというイベントを HoloLens に通知します。その際、どの位置から出たかという情報も送っています。

HoloLens 側でこのイベントを受け取ると Hololens 側でカーソルが表示されるので、PC 側ではカーソルを非表示・ロック状態にしてカーソルが表示されないようにします。

Cursor.visible = false;
Cursor.lockState = CursorLockMode.Locked;

カーソルが HoloLens 側に表示されている間は、以下のようにして取得したマウスの入力状態を HoloLens 側に送り続けます。

var x = Input.GetAxis("Mouse X");
var y = Input.GetAxis("Mouse Y");
var lButton = Input.GetMouseButton(0);
var rButton = Input.GetMouseButton(1);

HoloLens から、カーソルが PC 側に移動したというイベントを受け取ると、再度マウスカーソルを表示します。その際、どの位置に表示させるかという情報も同時に受け取り、以前のブログで書いた MouseHook.SetCursorPos メソッドを呼ぶことでその位置にカーソルを移動させています。

ただ、 MouseHook.SetCursorPos はビルドしたUWPアプリでは動かなかったので、今回は PC側アプリは Unity エディタ上で動かすということで妥協しました。

HoloLens とのデータのやりとりは、HoloToolkit に含まれる Sharing を利用しています。

HoloLens 側アプリ

PC のモニター位置は、PC 側アプリで表示されているマーカ画像を Vuforia を利用して認識することで取得します。

HoloLens 側でのマウスカーソルの表示は MR Design Labs に含まれる World Cursor を利用しています。MR Design Labs にある HoloLens の子要素には、以下のように WorldCursor が含まれています。

さらに、HoloLens の子要素にある HoloLens Focuser を複製し、Input Source が WorldCursor となったものを作成します。こうすることで、WorldCursor があたっているオブジェクトにフォーカスがあたるようになります。

PC 側アプリから、カーソルが画面外に出たというイベントを受け取ったら、2次元のカーソル位置を3次元の位置へと変換し、worldCursor.SetCursorPosition を呼んで WorldCursor の位置を移動させます。

また、World Cursor にアタッチされているInputSourceWorldCursorGamepad を変更し、PC 側アプリから送られてきたマウスの入力状態を World Cursor に反映させるようにします。以下のように、getSelectButtonPressed, getMenuButtonPressed, getInputDelta でボタンが押されているかどうかやマウスの移動量を返すようにすることで、World Cursor がマウスの動きによって移動するようになります。本当は、InputSourceWorldCursorGamepad はそのままにして、新しく InputSourceWorldCursorSharingMouse を作成して WorldCursor にアタッチしようとしたのですが、なぜかそれだとマウスクリック時に Gaze のフォーカスがあたっているほうがクリックされてしまうということが起こったため、仕方なく InputSourceWorldCursorGamepad の中を変更しました。

    public override bool getSelectButtonPressed()
    {
        return HoloMouseSharing.MouseSharingManager.Instance.LButton;
    }

    public override bool getMenuButtonPressed()
    {
        return HoloMouseSharing.MouseSharingManager.Instance.RButton;
    }

    public override Vector2 getInputDelta()
    {
        return HoloMouseSharing.MouseSharingManager.Instance.MouseMovement;
    }

PC とは違って、HoloLens で表示されるマウスカーソルはカーソルがどこにあるのかすぐに見失ってしまいます。そのため、右クリックが押されると、worldCursor.SnapToGazeDir(); を呼び出して Gaze の位置にカーソルが移動するようにしています。

マウスカーソルを PC 側に戻す動作は以下のように行っています。 Vuforia を使って認識したモニターの位置にオブジェクトを作成し、InteractibleObject を継承したスクリプトをアタッチします。FocusEnter メソッドでオブジェクトにフォーカスが当たっていることを取得します。それが WorldCursor によるものであれば、カーソルがモニター内に移動したというイベントを PC 側アプリに通知し、HoloLens 側の World Cursor を非表示にします。

public class Monitor : InteractibleObject
{
...
    public void FocusEnter(FocusArgs args)
    {
    var focuser = args.Focuser.gameObject.GetComponent<InputSourceFocuser>();     
    if (focuser != null && focuser.TargetingInputSource.ToString().Contains("WorldCursor"))
        {
        var screenPoint = WorldToScreenPoint(args.Focuser.FocusHitInfo.point);
        MouseSharingManager.Instance.IntoMonitor(screenPoint);
        InputShell.Instance.worldCursor.SetCursorActive(false);
        }
    }
}

以上が簡単な実装についての解説です。同じようなものを実装したいという場合には参考にしてみてください。