Pico Neo 2を無理やりVIVE Input Utilityに対応させる

#はじめに
Pico Neo 2はPico社が出した新しいHMDですが、以前のPicoのHMDと異なりWave Unity SDK (HTCが提供しているモバイルHMD向けのSDK)に対応しておらず、そのためVRでのUI設計に便利な[VIVE Input Utility](https://github.com/ViveSoftware/viveinpututility-unity)にも対応していません。そうなると、Pico SDKについてくるUIサンプルを使うかとなるのですが、これが今一つ使いづらくて残念なのです。

そこで、Pico Neo 2を強引にVIVE Input Utilityに対応させることにしました。あくまでも付け焼き刃的な対応であり、VIVE Focus PlusやOculus Questのように正式対応しているモバイルHMDと全く同じ使い方はできませんが、レーザーポインターでボタンを押す程度のことは可能です。

具体的には、VIVE Input UtilityのSimulatorの機能を応用しています。[こちら](https://github.com/ViveSoftware/ViveInputUtility-Unity/wiki/Simulator)にあるとおり、Simulatorを使っているときは、`HTC.UnityPlugin.VRModuleManagement.VRModule.Simulator.onUpdateDeviceState`というイベントが発生し、その際にコントローラーの状態を書き込むことで、マウス、キーボードや非対応のHMDからの入力をおこなうことが出来ます。

#方法

まず、[Pico Unity SDK](https://developer.pico-interactive.com/sdk)とVIVE Input UtilityのUnity Packageを入手してプロジェクトにインポートしてください。

Pico Neo 2でコントローラーを使うときは、`PicoMobileSDK/Pvr_Controller/Scenes`の`Pvr_Controller_UI`シーンの`Pvr_UnitySDK`をそのまま使うのが良いです。

さらに`HTC.UnityPlugin/ViveInputUtility/Prefabs`にある`VivePointers`と`[ViveInputUtility]`をシーンに追加してください。

最後に、新しい空のGameObjectを追加してください。ここでは`ViuOnPico`という名前にします。

ここまで追加したものをまとめると、以下のような感じになるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58503/54da19ee-019b-e67a-aac0-f65ded53597a.png)

つぎに、`[ViveInputUtility]`にアタッチされている`VR Module`コンポーネントで`Select Module`を`Simulator`にします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58503/70f211b1-6f5f-6764-77d4-40efe5a23f1f.png)

つぎに、`Preference`の`VIU Setting`で`Simulator`にチェックを入れ、以下のように設定してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58503/88248e7f-8daa-4270-9c2e-c5bca3c2e2b6.png)

そうすると`Project Settings`の`"Player"`で`Other Settings`のScripting Define Symbolに`VIU_PLUGIN;VIU_SIUMULATOR_SUPPORT`が追加されているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58503/62ca2c0f-e309-133b-091b-701be6b2e403.png)

さらに`XR Settings`が以下のように`Virtual Reality Supported`にチェックが入り`Mock HMD`が選択されていることをビルド前に確認しましょう（なぜか勝手にチェックが外れていることがある）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58503/7575ecb2-3c24-7c48-a444-c44018400cd8.png)

次に、`ViuOnPico.cs`というファイルを新規作成し、このページの最後にあるスクリプトをコピペしてください。`ViuOnPico.cs`は`ViuOnPico`ゲームオブジェクトにアタッチします。

これで、Pico Neo 2でもVIVE Input Utilityを利用できます。VIVE Input Utilityの具体的な使い方はVIVE Input Utilityの[チュートリアル](https://github.com/ViveSoftware/ViveInputUtility-Unity/wiki/Example-0.Tutorial)や[Vive Focus Plus向けの記事](https://qiita.com/yanosen_jp/items/1ef94b3d32abb4dd0bf2)を参考にしてください。とりあえず、レーザーポインターの動作は確認済みです。

#注意
Pico Neo 2 SDKに付属のレーザーポインターは消去せず、スケールをゼロにして見えないようにしています。完全に消すこともできますが、Pico Neo 2 SDKをいじくる必要があるので今回はパスしました。

```ViuOnPico.cs
using UnityEngine;
using HTC.UnityPlugin.VRModuleManagement;
using Pvr_UnitySDKAPI;
using HTC.UnityPlugin.Utility;
using UnityEngine.XR;

public class ViuOnPico : MonoBehaviour
{
    Transform PvrUnitySdk;

    Transform controllerManagerTransform;
    Transform headTransform;

    private uint m_rightIndex = VRModule.INVALID_DEVICE_INDEX;
    private uint m_leftIndex = VRModule.INVALID_DEVICE_INDEX;

    private const uint HEAD_INDEX = 0u;
    private const uint RIGHT_HAND_INDEX = 1u;
    private const uint LEFT_HAND_INDEX = 2u;

    public uint GetRightControllerDeviceIndex() { return m_rightIndex; }
    public uint GetLeftControllerDeviceIndex() { return m_leftIndex; }

    // Start is called before the first frame update
    void Start()
    {
        PvrUnitySdk = FindObjectOfType<Pvr_UnitySDKManager>().gameObject.transform;
        VRModule.Simulator.onUpdateDeviceState += OnUpdate;
        controllerManagerTransform = PvrUnitySdk.Find("ControllerManager");
        headTransform = PvrUnitySdk.Find("Head");

        var controller0 = controllerManagerTransform.GetComponent<Pvr_Controller>().controller0.transform;
        var controller1 = controllerManagerTransform.GetComponent<Pvr_Controller>().controller1.transform;

        controller0.Find("dot").transform.localScale = Vector3.zero;
        controller1.Find("dot").transform.localScale = Vector3.zero;
        controller0.Find("ray_alpha").transform.localScale = Vector3.zero;
        controller1.Find("ray_alpha").transform.localScale = Vector3.zero;
        controller0.Find("ray_LengthAdaptive").transform.localScale = Vector3.zero;
        controller1.Find("ray_LengthAdaptive").transform.localScale = Vector3.zero;
    }

    // Update is called once per frame
    void OnUpdate(IVRModuleDeviceState[] prevState, IVRModuleDeviceStateRW[] currentState)
    {
        UpdateConnectedDevices(prevState, currentState);
        UpdateDevicePose(prevState, currentState);
        UpdateDeviceInput(prevState, currentState);
    }

    private void UpdateConnectedDevices(IVRModuleDeviceState[] prevState, IVRModuleDeviceStateRW[] currState)
    {
        if (!XRDevice.isPresent)
        {
            if (prevState[HEAD_INDEX].isConnected)
            {
                currState[HEAD_INDEX].Reset();
            }
        }
        else
        {
            if (!prevState[HEAD_INDEX].isConnected)
            {
                currState[HEAD_INDEX].isConnected = true;
                currState[HEAD_INDEX].deviceClass = VRModuleDeviceClass.HMD;
                currState[HEAD_INDEX].serialNumber = XRDevice.model + " HMD";
                currState[HEAD_INDEX].modelNumber = XRDevice.model + " HMD";
                currState[HEAD_INDEX].deviceModel = VRModuleDeviceModel.Unknown;
                currState[HEAD_INDEX].renderModelName = string.Empty;
            }
        }

        if (Controller.UPvr_GetControllerState(1) != ControllerState.Connected)
        {
            if (prevState[RIGHT_HAND_INDEX].isConnected)
            {
                currState[RIGHT_HAND_INDEX].Reset();
                m_rightIndex = VRModule.INVALID_DEVICE_INDEX;
            }
        }
        else
        {
            if (!prevState[RIGHT_HAND_INDEX].isConnected)
            {
                currState[RIGHT_HAND_INDEX].isConnected = true;
                currState[RIGHT_HAND_INDEX].deviceClass = VRModuleDeviceClass.Controller;
                currState[RIGHT_HAND_INDEX].serialNumber = XRDevice.model + " Controller Right";
                currState[RIGHT_HAND_INDEX].modelNumber = XRDevice.model + " Controller Right";
                currState[RIGHT_HAND_INDEX].deviceModel = VRModuleDeviceModel.Unknown;
                currState[RIGHT_HAND_INDEX].renderModelName = string.Empty;
                currState[RIGHT_HAND_INDEX].input2DType = VRModuleInput2DType.ThumbstickOnly;
                m_rightIndex = RIGHT_HAND_INDEX;
            }
        }

        if (Controller.UPvr_GetControllerState(0) != ControllerState.Connected)
        {
            if (prevState[LEFT_HAND_INDEX].isConnected)
            {
                currState[LEFT_HAND_INDEX].Reset();
                m_leftIndex = VRModule.INVALID_DEVICE_INDEX;
            }
        }
        else
        {
            if (!prevState[LEFT_HAND_INDEX].isConnected)
            {
                currState[LEFT_HAND_INDEX].isConnected = true;
                currState[LEFT_HAND_INDEX].deviceClass = VRModuleDeviceClass.Controller;
                currState[LEFT_HAND_INDEX].serialNumber = XRDevice.model + " Controller Left";
                currState[LEFT_HAND_INDEX].modelNumber = XRDevice.model + " Controller Left";
                currState[LEFT_HAND_INDEX].deviceModel = VRModuleDeviceModel.Unknown;
                currState[LEFT_HAND_INDEX].input2DType = VRModuleInput2DType.ThumbstickOnly;
                m_leftIndex = LEFT_HAND_INDEX;
            }
        }
    }

    private void UpdateDevicePose(IVRModuleDeviceState[] prevState, IVRModuleDeviceStateRW[] currState)
    {

        if (currState[HEAD_INDEX].isConnected)
        {
            currState[HEAD_INDEX].pose = new RigidPose(headTransform.position, headTransform.rotation);
            currState[HEAD_INDEX].isPoseValid = true;
        }
        else
        {
            currState[HEAD_INDEX].pose = new RigidPose(Vector3.zero, Quaternion.identity);
        }

        var controllerManagerPose = new RigidPose(controllerManagerTransform.position, controllerManagerTransform.rotation);

        if (currState[RIGHT_HAND_INDEX].isConnected)
        {
            var pose = new RigidPose(Controller.UPvr_GetControllerPOS(1), Controller.UPvr_GetControllerQUA(1));
            currState[RIGHT_HAND_INDEX].pose = controllerManagerPose * pose;
            currState[RIGHT_HAND_INDEX].isPoseValid = true;
        }

        if (currState[LEFT_HAND_INDEX].isConnected)
        {
            var pose = new RigidPose(Controller.UPvr_GetControllerPOS(0), Controller.UPvr_GetControllerQUA(0));
            currState[LEFT_HAND_INDEX].pose = controllerManagerPose * pose;
            currState[LEFT_HAND_INDEX].isPoseValid = true;
        }
    }

    private void UpdateDeviceInput(IVRModuleDeviceState[] prevState, IVRModuleDeviceStateRW[] currState)
    {
        if (currState[RIGHT_HAND_INDEX].isConnected)
        {
            var appPressed = Controller.UPvr_GetKey(1, Pvr_KeyCode.APP);
            var systemPressed = Controller.UPvr_GetKey(1, Pvr_KeyCode.HOME);
            var padPressed = Controller.UPvr_GetKey(1, Pvr_KeyCode.TOUCHPAD);
            var padTouched = Controller.UPvr_IsTouching(1);
            var padAxis = Controller.UPvr_GetTouchPadPosition(1);

            var triggerPressed = Controller.UPvr_GetKey(1, Pvr_KeyCode.TRIGGER);
            var gripPressed = Controller.UPvr_GetKey(1, Pvr_KeyCode.Right);

            var APressed = Controller.UPvr_GetKey(1, Pvr_KeyCode.A);

            currState[RIGHT_HAND_INDEX].SetButtonPress(VRModuleRawButton.Touchpad, padPressed);
            currState[RIGHT_HAND_INDEX].SetButtonPress(VRModuleRawButton.ApplicationMenu, appPressed);
            currState[RIGHT_HAND_INDEX].SetButtonPress(VRModuleRawButton.System, systemPressed);

            currState[RIGHT_HAND_INDEX].SetButtonPress(VRModuleRawButton.Trigger, triggerPressed);
            currState[RIGHT_HAND_INDEX].SetButtonPress(VRModuleRawButton.Grip, gripPressed);
            currState[RIGHT_HAND_INDEX].SetButtonPress(VRModuleRawButton.A, APressed);

            currState[RIGHT_HAND_INDEX].SetButtonTouch(VRModuleRawButton.Touchpad, padTouched);
            currState[RIGHT_HAND_INDEX].SetAxisValue(VRModuleRawAxis.TouchpadX, padAxis.x);
            currState[RIGHT_HAND_INDEX].SetAxisValue(VRModuleRawAxis.TouchpadY, padAxis.y);
        }

        if (currState[LEFT_HAND_INDEX].isConnected)
        {
            var appPressed = Controller.UPvr_GetKey(0, Pvr_KeyCode.APP);
            var systemPressed = Controller.UPvr_GetKey(0, Pvr_KeyCode.HOME);
            var padPressed = Controller.UPvr_GetKey(0, Pvr_KeyCode.TOUCHPAD);
            var padTouched = Controller.UPvr_IsTouching(0);
            var padAxis = Controller.UPvr_GetTouchPadPosition(0);

            var triggerPressed = Controller.UPvr_GetKey(0, Pvr_KeyCode.TRIGGER);
            var gripPressed = Controller.UPvr_GetKey(0, Pvr_KeyCode.Left);

            var APressed = Controller.UPvr_GetKey(0, Pvr_KeyCode.X);

            currState[LEFT_HAND_INDEX].SetButtonPress(VRModuleRawButton.Touchpad, padPressed);
            currState[LEFT_HAND_INDEX].SetButtonPress(VRModuleRawButton.ApplicationMenu, appPressed);
            currState[LEFT_HAND_INDEX].SetButtonPress(VRModuleRawButton.System, systemPressed);

            currState[LEFT_HAND_INDEX].SetButtonPress(VRModuleRawButton.Trigger, triggerPressed);
            currState[LEFT_HAND_INDEX].SetButtonPress(VRModuleRawButton.Grip, gripPressed);
            currState[LEFT_HAND_INDEX].SetButtonPress(VRModuleRawButton.A, APressed);

            currState[RIGHT_HAND_INDEX].SetButtonTouch(VRModuleRawButton.Touchpad, padTouched);
            currState[RIGHT_HAND_INDEX].SetAxisValue(VRModuleRawAxis.TouchpadX, padAxis.x);
            currState[RIGHT_HAND_INDEX].SetAxisValue(VRModuleRawAxis.TouchpadY, padAxis.y);
        }
    }
}

```