//#define OCULUS_SDK //<-- define this in your project settings if you have the OVR API #if UNITY_2017_2_OR_NEWER using System; using System.Collections.Generic; using System.Text; using UnityEngine; using UnityEngine.XR; using ZenFulcrum.EmbeddedBrowser.VR; using ZenFulcrum.VR.OpenVRBinding; namespace ZenFulcrum.EmbeddedBrowser.VR { public enum InputAxis { LeftClick,//main trigger RightClick,//alt trigger MiddleClick,//alt alt trigger JoyStickX, JoyStickY,//joystick x/y position TouchPadTouch,//touchpad is touched TouchPadX, TouchPadY,//touchpad x/y position (only valid when TouchPadTouch > 0) } [Flags] public enum JoyPadType { None = 0, Unknown = 1 << 1, Joystick = 1 << 2, TouchPad = 1 << 3, } /// /// Unity's VR input system is sorely lacking in usefulness. /// We can finally get pose data in a more-or-less straightforward manner (InputTracking.GetNodeStates, /// state.TryGetPosition, etc.), but getting the axis and buttons is an awful mess. /// /// This page https://docs.unity3d.com/Manual/OpenVRControllers.html says we can access the inputs via /// the input system! And, you can look at (of all things) the Joystick names to see which joystick /// is a VR controller at runtime. But guess what? There's no Unity API to fetch the value of a given /// axis on a given controller! Extra-stupid, right? You can define a specific input in the input menu, /// but that doesn't help when you switch to a different computer and the joystick numbers change! /// Short of defining an "input" for every possible controller * every axis you use, there's not a /// way to get the controller axis inputs in a way that will work reliably across different machines. /// /// (We can fetch buttons manually via Input.GetKey(KeyCode.Joystick1Button0 + buttonId + joystickIdx * 20), /// but this don't address the axis issue at all.) /// /// Anyway, this is a workaround. Around another Unity problem. Implemented by hooking into backend APIs directly. /// /// /// /// ...and then there's SteamVR 2.x and its input system. /// public abstract class VRInput { public static VRInput Impl { private set; get; } public struct Pose { public Vector3 pos; public Quaternion rot; } public static void Init() { if (Impl == null) Impl = GetImpl(); } public abstract float GetAxis(XRNodeState node, InputAxis axis); /// /// Returns where the hand is pointing right now. (z+ forward) /// /// /// public virtual Pose GetPose(XRNodeState node) { Pose ret = new Pose(); node.TryGetPosition(out ret.pos); node.TryGetRotation(out ret.rot); return ret; } public virtual JoyPadType GetJoypadTypes(XRNodeState node) { return JoyPadType.None; } public virtual string GetNodeName(XRNodeState node) { return InputTracking.GetNodeName(node.uniqueID); } private static VRInput GetImpl() { if (XRSettings.loadedDeviceName == "OpenVR") { return new OpenVRInput(); } else if (XRSettings.loadedDeviceName == "Oculus") { #if OCULUS_SDK return new OculusVRInput(); #else Debug.LogError("To use the Oculus API for input, import the Oculus SDK, delete ZFBrowser/**.asmdef (2 files), and define OCULUS_SDK"); return new NoVRInput(); #endif } else { Debug.LogError("Unknown VR input system: " + XRSettings.loadedDeviceName); return new NoVRInput(); } } } class NoVRInput : VRInput { public override float GetAxis(XRNodeState node, InputAxis axis) { return 0; } } partial class OpenVRInput : VRInput { enum InputMode { Direct, MappedActions, } /// /// Previous controller state (direct input mode) /// private VRControllerState_t direct_lastState; private ulong pointPose, leftClick, middleClick, rightClick; private ulong joystickScroll, touchPadScrollTouch, touchPadScrollPos; private ulong leftHand, rightHand; public OpenVRInput() { if (mode == InputMode.MappedActions) { if (OpenVR.Input == null) throw new ApplicationException("Cannot start VR input"); pointPose = GetActionHandle(PointPose, "PointPose"); leftClick = GetActionHandle(LeftClickAction, "LeftClickAction"); middleClick = GetActionHandle(MiddleClickAction, "MiddleClickAction"); rightClick = GetActionHandle(RightClickAction, "RightClickAction"); joystickScroll = GetActionHandle(JoystickScrollAction, "JoystickScrollAction"); touchPadScrollTouch = GetActionHandle(TouchpadScrollTouch, "TouchpadScrollTouch"); touchPadScrollPos = GetActionHandle(TouchpadScrollPosition, "TouchpadScrollPosition"); var res = OpenVR.Input.GetInputSourceHandle("/user/hand/left", ref leftHand); if (res != EVRInputError.None) throw new ApplicationException("Failed to find hand " + res); res = OpenVR.Input.GetInputSourceHandle("/user/hand/right", ref rightHand); if (res != EVRInputError.None) throw new ApplicationException("Failed to find hand " + res); } } private ulong GetActionHandle(string handleName, string name) { if (string.IsNullOrEmpty(handleName)) return 0; ulong ret = 0; var res = OpenVR.Input.GetActionHandle(handleName, ref ret); if (res != EVRInputError.None) { throw new ApplicationException("Failed to set up VR action " + res); } return ret; } public static string GetStringProperty(uint deviceId, ETrackedDeviceProperty prop) { var buffer = new StringBuilder((int)OpenVR.k_unMaxPropertyStringSize); ETrackedPropertyError err = ETrackedPropertyError.TrackedProp_Success; OpenVR.System.GetStringTrackedDeviceProperty( deviceId, prop, buffer, OpenVR.k_unMaxPropertyStringSize, ref err ); if (err != ETrackedPropertyError.TrackedProp_Success) { throw new Exception("Failed to get property " + prop + " on device " + deviceId + ": " + err); } return buffer.ToString(); } public override string GetNodeName(XRNodeState node) { var deviceId = (uint)GetDeviceId(node); try { return GetStringProperty(deviceId, ETrackedDeviceProperty.Prop_ModelNumber_String); } catch (Exception ex) { Debug.LogError("Failed to get device name for device " + deviceId + ": " + ex.Message); return base.GetNodeName(node); } } protected void Direct_ReadState(XRNodeState node) { if (OpenVR.System == null) { Debug.LogWarning("OpenVR not active"); direct_lastState = default(VRControllerState_t); return; } var controllerId = GetDeviceId(node); if (controllerId < 0) { direct_lastState = default(VRControllerState_t); return; } //Debug.Log("Id is " + controllerId); var res = OpenVR.System.GetControllerState( (uint)controllerId, ref direct_lastState, (uint)System.Runtime.InteropServices.Marshal.SizeOf(typeof(VRControllerState_t)) ); if (!res) { Debug.LogWarning("Failed to get controller state"); } } private bool GetDigitalInput(ulong action, XRNodeState node) { InputDigitalActionData_t data = new InputDigitalActionData_t(); var res = OpenVR.Input.GetDigitalActionData( action, ref data, (uint)System.Runtime.InteropServices.Marshal.SizeOf(typeof(InputDigitalActionData_t)), node.nodeType == XRNode.LeftHand ? leftHand : rightHand ); if (res != EVRInputError.None) throw new ApplicationException("Failed to get digital input data " + node.nodeType + ": " + res); return data.bState; } private Vector2 GetVector2Input(ulong action, XRNodeState node) { InputAnalogActionData_t data = new InputAnalogActionData_t(); var res = OpenVR.Input.GetAnalogActionData( action, ref data, (uint)System.Runtime.InteropServices.Marshal.SizeOf(typeof(InputAnalogActionData_t)), node.nodeType == XRNode.LeftHand ? leftHand : rightHand ); if (res != EVRInputError.None) throw new ApplicationException("Failed to get vector input data " + node.nodeType + ": " + res); return new Vector2(data.x, data.y); } private Pose GetPoseInput(ulong action, XRNodeState node) { InputPoseActionData_t data = new InputPoseActionData_t(); var res = OpenVR.Input.GetPoseActionData( action, XRDevice.GetTrackingSpaceType() == TrackingSpaceType.RoomScale ? ETrackingUniverseOrigin.TrackingUniverseStanding : ETrackingUniverseOrigin.TrackingUniverseSeated, 0, ref data, (uint)System.Runtime.InteropServices.Marshal.SizeOf(typeof(InputPoseActionData_t)), node.nodeType == XRNode.LeftHand ? leftHand : rightHand ); if (res != EVRInputError.None) throw new ApplicationException("Failed to get pose input data " + node.nodeType + ": " + res); var matRaw = data.pose.mDeviceToAbsoluteTracking; var mat = new Matrix4x4( new Vector4(matRaw.m0, matRaw.m1, matRaw.m2, 0), new Vector4(matRaw.m4, matRaw.m5, matRaw.m6, 0), new Vector4(matRaw.m8, matRaw.m9, matRaw.m10, 0), new Vector4(0, 0, 0, 1) ); var rot = mat.rotation; // rot.x *= -1; rot.z *= -1; return new Pose { pos = new Vector3(matRaw.m3, matRaw.m7, -matRaw.m11), rot = rot }; } public override float GetAxis(XRNodeState node, InputAxis axis) { if (mode == InputMode.Direct) { Direct_ReadState(node); switch (axis) { case InputAxis.LeftClick: return direct_lastState.rAxis1.x; case InputAxis.MiddleClick: return (direct_lastState.ulButtonPressed & (1ul << (int)EVRButtonId.k_EButton_Axis0)) != 0 ? 1 : 0; case InputAxis.RightClick: return (direct_lastState.ulButtonPressed & (1ul << (int)EVRButtonId.k_EButton_Grip)) != 0 ? 1 : 0; //We don't use joystck and touchpad at the same time, GetJoypadTypes returns only one type in this mode case InputAxis.JoyStickX: case InputAxis.TouchPadX: return direct_lastState.rAxis0.x; case InputAxis.JoyStickY: case InputAxis.TouchPadY: return direct_lastState.rAxis0.y; case InputAxis.TouchPadTouch: return (direct_lastState.ulButtonTouched & (1ul << (int)EVRButtonId.k_EButton_SteamVR_Touchpad)) != 0 ? 1 : 0; default: throw new ArgumentOutOfRangeException("axis", axis, null); } } else { switch (axis) { case InputAxis.LeftClick: if (leftClick == 0) return 0; return GetDigitalInput(leftClick, node) ? 1 : 0; case InputAxis.MiddleClick: if (middleClick == 0) return 0; return GetDigitalInput(middleClick, node) ? 1 : 0; case InputAxis.RightClick: if (rightClick == 0) return 0; return GetDigitalInput(rightClick, node) ? 1 : 0; case InputAxis.JoyStickX: case InputAxis.JoyStickY: { if (joystickScroll == 0) return 0; var v = GetVector2Input(joystickScroll, node); if (axis == InputAxis.JoyStickX) return v.x; else return v.y; } case InputAxis.TouchPadX: case InputAxis.TouchPadY: { if (touchPadScrollPos == 0) return 0; var v = GetVector2Input(touchPadScrollPos, node); if (axis == InputAxis.TouchPadX) return v.x; else return v.y; } case InputAxis.TouchPadTouch: if (touchPadScrollTouch == 0) return 0; return GetDigitalInput(touchPadScrollTouch, node) ? 1 : 0; default: throw new ArgumentOutOfRangeException("axis", axis, null); } } } public override Pose GetPose(XRNodeState node) { if (mode == InputMode.Direct) { return base.GetPose(node); } else { return GetPoseInput(pointPose, node); } } private int GetDeviceId(XRNodeState node) { var targetRole = node.nodeType == XRNode.LeftHand ? ETrackedControllerRole.LeftHand : ETrackedControllerRole.RightHand; for (uint i = 0; i < OpenVR.k_unMaxTrackedDeviceCount; i++) { var role = OpenVR.System.GetControllerRoleForTrackedDeviceIndex(i); if (role == targetRole) return (int)i; } return -1; } private JoyPadType jpType = JoyPadType.Unknown; public override JoyPadType GetJoypadTypes(XRNodeState node) { if (jpType != JoyPadType.Unknown) return jpType; if (mode == InputMode.Direct) { var name = GetNodeName(node); if (name.Contains("Oculus Touch Controller") || name.StartsWith("Oculus Rift CV1")) { //OpenVR gives us "Oculus Rift CV1 (Left Controller)" etc. where I wish it would mention the type of controller (Touch) return jpType = JoyPadType.Joystick; } else if (name.StartsWith("Vive Controller")) { return jpType = JoyPadType.TouchPad; } else { Debug.LogWarning("Unknown controller type: " + name); return jpType = JoyPadType.None; } } else { return jpType = (JoyPadType.Joystick | JoyPadType.TouchPad); } } } #if OCULUS_SDK class OculusVRInput : VRInput { public override float GetAxis(XRNodeState node, InputAxis axis) { var controller = GetController(node); OVRPlugin.ControllerState4 state = OVRPlugin.GetControllerState4((uint)controller); switch (axis) { case InputAxis.LeftClick: return controller == OVRInput.Controller.LTouch ? state.LIndexTrigger : state.RIndexTrigger; case InputAxis.RightClick: return controller == OVRInput.Controller.LTouch ? state.LHandTrigger : state.RHandTrigger; case InputAxis.MiddleClick: { var buttonId = controller == OVRInput.Controller.LTouch ? 0x00000400 : 0x00000004; //see enum ovrButton_ in OVR_CAPI.h return (state.Buttons & buttonId) != 0 ? 1 : 0; } case InputAxis.JoyStickX: case InputAxis.JoyStickY: { var joy = controller == OVRInput.Controller.LTouch ? state.LThumbstick : state.RThumbstick; return axis == InputAxis.JoyStickX ? joy.x : joy.y; } default: return 0; } } private OVRInput.Controller GetController(XRNodeState node) { switch (node.nodeType) { case XRNode.LeftHand: return OVRInput.Controller.LTouch; case XRNode.RightHand: return OVRInput.Controller.RTouch; default: return OVRInput.Controller.None; } } public override JoyPadType GetJoypadTypes(XRNodeState node) { //assuming Oculus Touch return JoyPadType.Joystick; } } #endif } #endif