Unity
VR
OculusGo

Oculus Go 開発メモ - レーザーポインタとuGUI編

はじめに

レーザーポインタを表示して、uGUI部品を操作したいときがある。

基本的には、Oculus GoのコントローラからレーザーポインタによりUIを操作するまでの記事のとおりにやればOK。

気をつけるべきなのは、上記の記事で使用している"Assets > OVRInputSelection > InputSystem"以下にあるスクリプトと、"Assets > Oculus > VR > Scripts"以下にある同姓同名のスクリプトとは互換性がないので、混ぜて使用しないようにしないといけない。
この記事では"Assets > OVRInputSelection > InputSystem"以下にあるスクリプトしか使わない。

以下では、上記記事の内容のとおりに構築済みであることを前提にする。

レーザーの長さを調節する

レーザーの長さをUI部品までの距離に調節したかった。

考え方としては、"Assets > OVRInputSelection > OVRInputModule"を改造して、レーザーが何かのUIにヒットした時にはその座標を、ヒットしていないときにはヒットしていないことを通知する機能があれば良いだろう。

具体的には、以下のようなコードを追加する。

OVRInputModule.cs(抜粋)
// UIにヒットしたときのイベント
public delegate void RayHitDelegate(Vector3 hitPosition, Vector3 hitNormal);
// ヒットしなかったときのイベント
public delegate void RayNoHitDelegate();

public RayHitDelegate OnSelectionRayHit;
public RayNoHitDelegate OnNoRayHit;

また、これらのイベントをどこかで呼び出す必要がある。
具体的には、GetGazePointerData()を以下のように書き換える。

OVRInputModule.cs(抜粋)
        protected MouseState GetGazePointerData()
        {
            // Get the OVRRayPointerEventData reference
            OVRRayPointerEventData leftData;
            GetPointerData(kMouseLeftId, out leftData, true );
            leftData.Reset();


            leftData.worldSpaceRay = OVRInputHelpers.GetSelectionRay(activeController, trackingSpace);
            leftData.scrollDelta = GetExtraScrollDelta();

            //Populate some default values
            leftData.button = UnityEngine.EventSystems.PointerEventData.InputButton.Left;
            leftData.useDragThreshold = true;
            // Perform raycast to find intersections with world
            eventSystem.RaycastAll(leftData, m_RaycastResultCache);
            var raycast = FindFirstRaycast(m_RaycastResultCache);
            leftData.pointerCurrentRaycast = raycast;
            m_RaycastResultCache.Clear();

            OVRRaycaster ovrRaycaster = raycast.module as OVRRaycaster;
            bool noHit = true;
            // We're only interested in intersections from OVRRaycasters
            if (ovrRaycaster) 
            {
                // The Unity UI system expects event data to have a screen position
                // so even though this raycast came from a world space ray we must get a screen
                // space position for the camera attached to this raycaster for compatability
                leftData.position = ovrRaycaster.GetScreenPosition(raycast);


                // Find the world position and normal the Graphic the ray intersected
                RectTransform graphicRect = raycast.gameObject.GetComponent<RectTransform>();
                if (graphicRect != null)
                {
                    // Set are gaze indicator with this world position and normal
                   // Vector3 worldPos = raycast.worldPosition;
                    //Vector3 normal = GetRectTransformNormal(graphicRect);

                    if (OnSelectionRayHit != null) {
                        noHit = false;
                        OnSelectionRayHit(raycast.worldPosition, raycast.worldNormal);
                    }
                }
            }
            OVRPhysicsRaycaster physicsRaycaster = raycast.module as OVRPhysicsRaycaster;
            if (physicsRaycaster)
            {
                leftData.position = physicsRaycaster.GetScreenPos(raycast.worldPosition);

                if (OnSelectionRayHit != null) {
                    noHit = false;
                    OnSelectionRayHit(raycast.worldPosition, raycast.worldNormal);
                }
            }

            if(noHit){
                OnNoRayHit();
            }

            // Stick default data values in right and middle slots for compatability

            // copy the apropriate data into right and middle slots
            OVRRayPointerEventData rightData;
            GetPointerData(kMouseRightId, out rightData, true );
            CopyFromTo(leftData, rightData);
            rightData.button = UnityEngine.EventSystems.PointerEventData.InputButton.Right;

            OVRRayPointerEventData middleData;
            GetPointerData(kMouseMiddleId, out middleData, true );
            CopyFromTo(leftData, middleData);
            middleData.button = UnityEngine.EventSystems.PointerEventData.InputButton.Middle;


            m_MouseState.SetButtonState(UnityEngine.EventSystems.PointerEventData.InputButton.Left, GetGazeButtonState(), leftData);
            m_MouseState.SetButtonState(UnityEngine.EventSystems.PointerEventData.InputButton.Right, UnityEngine.EventSystems.PointerEventData.FramePressState.NotChanged, rightData);
            m_MouseState.SetButtonState(UnityEngine.EventSystems.PointerEventData.InputButton.Middle, UnityEngine.EventSystems.PointerEventData.FramePressState.NotChanged, middleData);
            return m_MouseState;
        }

また、レーザーポインタでイベントを通知してもらう必要があるので、"Assets > OVRInputSelection > InputSystem > OVRPointerVisualizer"を、以下のように書き換える。

OVRPointerVisualizer.cs
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.EventSystems;

namespace ControllerSelection {

    public class OVRPointerVisualizer : MonoBehaviour {

        [Header("(Optional) Tracking space")]
        [Tooltip("Tracking space of the OVRCameraRig.\nIf tracking space is not set, the scene will be searched.\nThis search is expensive.")]
        public Transform trackingSpace = null;
        [Header("Visual Elements")]
        [Tooltip("Line Renderer used to draw selection ray.")]
        public LineRenderer linePointer = null;
        [Tooltip("Fallback gaze pointer.")]
        public Transform gazePointer = null;
        [Tooltip("Visually, how far out should the ray be drawn.")]
        public float rayDrawDistance = 500;
        [Tooltip("How far away the gaze pointer should be from the camera.")]
        public float gazeDrawDistance = 3;

        [HideInInspector]
        public OVRInput.Controller activeController = OVRInput.Controller.None;

        [SerializeField] private OVRInputModule inputModule;
        private bool gazePointerVisibility = false;

        void Awake() {
            if (trackingSpace == null) {
                Debug.LogWarning("OVRPointerVisualizer did not have a tracking space set. Looking for one");
                trackingSpace = OVRInputHelpers.FindTrackingSpace();
            }
            if(gazePointer != null){
                // gazePointerにコライダが付いていると動作に支障をきたすのでオフっておく。
                var collider = gazePointer.GetComponent<Collider>();
                if(collider != null){
                    collider.enabled = false;
                }
            }
            if(inputModule == null){
                inputModule = EventSystem.current.currentInputModule as OVRInputModule;
            }
        }

        void HandleSelectionRayHit(Vector3 hitPosition, Vector3 hitNormal){
            activeController = OVRInputHelpers.GetControllerForButton(OVRInput.Button.PrimaryIndexTrigger, activeController);
            Ray ray = OVRInputHelpers.GetSelectionRay(activeController, trackingSpace);
            SetPointerVisibility();

            if (linePointer != null) {
                linePointer.SetPosition(0, ray.origin);
                linePointer.SetPosition(1, hitPosition);
            }

            if (gazePointer != null) {
                gazePointer.gameObject.SetActive(true);
                gazePointer.position = hitPosition;
            }
        }

        void HandleNoRayHit(){
            activeController = OVRInputHelpers.GetControllerForButton(OVRInput.Button.PrimaryIndexTrigger, activeController);
            Ray ray = OVRInputHelpers.GetSelectionRay(activeController, trackingSpace);
            SetPointerVisibility();

            if (linePointer != null) {
                linePointer.SetPosition(0, ray.origin);
                linePointer.SetPosition(1, ray.origin + ray.direction * rayDrawDistance);
            }

            if (gazePointer != null) {
                gazePointer.gameObject.SetActive(gazePointerVisibility);
                gazePointer.position = ray.origin + ray.direction * gazeDrawDistance;
            }
        }

        void OnEnable() {
            SceneManager.sceneLoaded += OnSceneLoaded;
            if(inputModule != null){
                inputModule.OnSelectionRayHit += HandleSelectionRayHit;
                inputModule.OnNoRayHit += HandleNoRayHit;
            }
        }

        void OnDisable() {
            SceneManager.sceneLoaded -= OnSceneLoaded;
            if(inputModule != null){
                inputModule.OnSelectionRayHit -= HandleSelectionRayHit;
                inputModule.OnNoRayHit -= HandleNoRayHit;
            }
        }

        void OnSceneLoaded(Scene scene, LoadSceneMode mode) {
            if (trackingSpace == null) {
                Debug.LogWarning("OVRPointerVisualizer did not have a tracking space set. Looking for one");
                trackingSpace = OVRInputHelpers.FindTrackingSpace();
            }
        }

        public void SetPointerVisibility() {
            if (trackingSpace != null && activeController != OVRInput.Controller.None) {
                if (linePointer != null) {
                    linePointer.enabled = true;
                }
                if (gazePointer != null) {
                    gazePointer.gameObject.SetActive(false);
                    gazePointerVisibility = false;
                }
            }else{
                if (linePointer != null) {
                    linePointer.enabled = false;
                }
                if (gazePointer != null) {
                    gazePointer.gameObject.SetActive(true);
                    gazePointerVisibility = true;
                }
            }
        }

    }
}

あとは、OVR Pointer VisualizerのInput Module欄にEventSystemのOVRInputModuleを突っ込んでおけば動くはず。

スクリーンショット 2018-06-06 14.20.14.png