using UnityEngine; using UnityEngine.XR; using System.Collections.Generic; #if ZED_STEAM_VR using Valve.VR; #endif #if UNITY_EDITOR using UnityEditor; #endif /// /// Causes the GameObject it's attached to to position itself where a tracked VR object is, such as /// a Touch controller or Vive Tracker, but compensates for the ZED's latency. This way, virtual /// controllers don't move ahead of its real-world image. /// This is done by logging position data from the VR SDK in use (Oculus or OpenVR/SteamVR) each frame, but only /// applying that position data to this transform after the delay in the latencyCompensation field. /// Used in the ZED GreenScreen, Drone Shooter, Movie Screen, Planetarium and VR Plane Detection example scenes. /// public class ZEDControllerTracker : MonoBehaviour { /// /// Type of VR SDK loaded. 'Oculus', 'OpenVR' or empty. /// private string loadeddevice = ""; #if ZED_STEAM_VR //Only enabled if the SteamVR Unity plugin is detected. /// /// OpenVR System class that lets us get inputs straight from the API, bypassing SteamVR. /// This is necessary because different versions of SteamVR use completely different input systems. /// protected CVRSystem openvrsystem = OpenVR.System; /// /// State of the controller's buttons and axes. Used to check inputs. /// protected VRControllerState_t controllerstate; /// /// State of the controller's buttons and axes last frame. Used to check if buttons just went down or up. /// protected VRControllerState_t lastcontrollerstate; /// /// Size of VRControllerState_t class in bytes. Used to call GetControllerState from the OpenVR API. /// protected const uint controllerstatesize = 64; /// /// Enumerated version of the uint index SteamVR assigns to each device. /// Converted from OpenVR.GetTrackedDeviceIndexForControllerRole(ETrackedControllerRole). /// public enum EIndex { None = -1, Hmd = (int)OpenVR.k_unTrackedDeviceIndex_Hmd, Device1, Device2, Device3, Device4, Device5, Device6, Device7, Device8, Device9, Device10, Device11, Device12, Device13, Device14, Device15 } [HideInInspector] public EIndex index = EIndex.None; /// /// How long since we've last checked OpenVR for the specified device. /// Incremented by Time.deltaTime each frame and reset when it reached timerMaxSteamVR. /// private float timerSteamVR = 0.0f; /// /// How many seconds to wait between checking if the specified device is present in OpenVR. /// The check is performed when timerSteamVR reaches this number, unless we've already retrieved the device index. /// private float timerMaxSteamVR = 0.25f; private Devices oldDevice; /// /// If true, will use a direct API check to OpenVR's API to check if a button is down. /// Does not work if using the SteamVR plugin 2.0 or higher as well as the action system it includes. /// [Tooltip("If true, will use a direct API check to OpenVR's API to check if a button is down.\r\n" + "Does not work if using the SteamVR plugin 2.0 or higher as well as the action system it includes. ")] public bool useLegacySteamVRInput = false; #endif /// /// Per each tracked object ID, contains a list of their recent positions. /// Used to look up where OpenVR says a tracked object was in the past, for latency compensation. /// public Dictionary> poseData = new Dictionary>(); /// /// Types of tracked devices. /// public enum Devices { RightController, LeftController, ViveTracker, Hmd, }; /// /// Type of trackable device that should be tracked. /// [Tooltip("Type of trackable device that should be tracked.")] public Devices deviceToTrack; /// /// Latency in milliseconds to be applied on the movement of this tracked object, so that virtual controllers don't /// move ahead of their real-world image. /// [Tooltip("Latency in milliseconds to be applied on the movement of this tracked object, so that virtual controllers don't" + " move ahead of their real-world image.")] [Range(0, 200)] public int latencyCompensation = 78; /// /// If true, and this is a controller, will offset controller's position by the difference between /// the VR headset and the ZED's tracked position. This keeps controller's position relative to the ZED. /// [Tooltip("If true, and this is a controller, will offset controller's position by the difference between " + "the VR headset and the ZED's tracked position. This keeps controller's position relative to the ZED. ")] [LabelOverride("Enable Controller Drift Fix")] public bool correctControllerDrift = true; /// /// The Serial number of the controller/tracker to be tracked. /// If specified, it will override the device returned using the 'Device to Track' selection. /// Useful for forcing a specific device to be tracked, instead of the first left/right/Tracker object found. /// If Null, then there's no calibration to be applied to this script. /// If NONE, the ZEDControllerOffset failed to find any calibration file. /// If S/N is present, then this script will calibrate itself to track the correct device, if that's not the case already. /// Note that ZEDOffsetController will load this number from a GreenScreen calibration file, if present. /// [Tooltip("The Serial number of the controller/tracker to be tracked." + " If specified, overrides the 'Device to Track' selection.")] public string SNHolder = ""; /// /// Cached transform that represents the ZED's head, retrieved from ZEDManager.GetZedRootTransform(). /// Used to find the offset between the HMD and tracked transform to compensate for drift. /// protected Transform zedRigRoot; /// /// Reference to the scene's ZEDManager component. Used for compensating for headset drift when this is on a controller. /// [Tooltip("Reference to the scene's ZEDManager component. Used for compensating for headset drift when this is on a controller. " + "If left blank, will be set to the first available ZEDManager.")] public ZEDManager zedManager = null; /// /// Sets up the timed pose dictionary and identifies the VR SDK being used. /// protected virtual void Awake() { #if ZED_STEAM_VR openvrsystem = OpenVR.System; #endif poseData.Clear(); //Reset the dictionary. poseData.Add(1, new List()); //Create the list within the dictionary with its key and value. //Looking for the loaded device loadeddevice = XRSettings.loadedDeviceName; if (!zedManager) { zedManager = FindObjectOfType(); //If there are multiple cameras in a scene, this arbitrary assignment could be bad. Warn the user. if (ZEDManager.GetInstances().Count > 1) { //Using Log instead of LogWarning because most users don't enable warnings but this is actually important. Debug.Log("Warning: ZEDController automatically set itself to first available ZED (" + zedManager.cameraID + ") because zedManager " + "value wasn't set, but there are multiple ZEDManagers in the scene. Assign a reference directly to ensure no unexpected behavior."); } } if (zedManager) zedRigRoot = zedManager.GetZedRootTansform(); } /// /// Update is called every frame. /// For SteamVR plugin this is where the device Index is set up. /// For Oculus plugin this is where the tracking is done. /// protected virtual void Update() { #if ZED_OCULUS //Used only if the Oculus Integration plugin is detected. //Check if the VR headset is connected. if (OVRManager.isHmdPresent && loadeddevice.ToString().ToLower().Contains("oculus")) { if (OVRInput.GetConnectedControllers().ToString().ToLower().Contains("touch")) { //Depending on which tracked device we are looking for, start tracking it. if (deviceToTrack == Devices.LeftController) //Track the Left Oculus Controller. RegisterPosition(1, OVRInput.GetLocalControllerPosition(OVRInput.Controller.LTouch), OVRInput.GetLocalControllerRotation(OVRInput.Controller.LTouch)); if (deviceToTrack == Devices.RightController) //Track the Right Oculus Controller. RegisterPosition(1, OVRInput.GetLocalControllerPosition(OVRInput.Controller.RTouch), OVRInput.GetLocalControllerRotation(OVRInput.Controller.RTouch)); if (deviceToTrack == Devices.Hmd) //Track the Oculus Hmd. { #if UNITY_2019_3_OR_NEWER InputDevice head = InputDevices.GetDeviceAtXRNode(XRNode.CenterEye); head.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition); head.TryGetFeatureValue(CommonUsages.deviceRotation, out Quaternion headRotation); RegisterPosition(1, headPosition, headRotation); #else RegisterPosition(1, InputTracking.GetLocalPosition(XRNode.CenterEye), InputTracking.GetLocalRotation(XRNode.CenterEye)); #endif } //Use our saved positions to apply a delay before assigning it to this object's Transform. if (poseData.Count > 0) { sl.Pose p; //Delay the saved values inside GetValuePosition() by a factor of latencyCompensation in milliseconds. p = GetValuePosition(1, (float)(latencyCompensation / 1000.0f)); transform.position = p.translation; //Assign new delayed Position transform.rotation = p.rotation; //Assign new delayed Rotation. } } } //Enable updating the internal state of OVRInput. OVRInput.Update(); #endif #if ZED_STEAM_VR UpdateControllerState(); //Get the button states so we can check if buttons are down or not. timerSteamVR += Time.deltaTime; //Increment timer for checking on devices if (timerSteamVR <= timerMaxSteamVR) return; timerSteamVR = 0f; //Checks if a device has been assigned if (index == EIndex.None && loadeddevice.Contains("OpenVR")) { //We look for any device that has "tracker" in its 3D model mesh name. //We're doing this since the device ID changes based on how many devices are connected to SteamVR. //This way, if there's no controllers or just one, it's going to get the right ID for the Tracker. if (deviceToTrack == Devices.ViveTracker) { var error = ETrackedPropertyError.TrackedProp_Success; for (uint i = 0; i < 16; i++) { var result = new System.Text.StringBuilder((int)64); OpenVR.System.GetStringTrackedDeviceProperty(i, ETrackedDeviceProperty.Prop_RenderModelName_String, result, 64, ref error); if (result.ToString().Contains("tracker")) { index = (EIndex)i; break; //We break out of the loop, but we can use this to set up multiple Vive Trackers if we want to. } } } //Looks for a device with the role of a Right Hand. if (deviceToTrack == Devices.RightController) { index = (EIndex)OpenVR.System.GetTrackedDeviceIndexForControllerRole(ETrackedControllerRole.RightHand); } //Looks for a device with the role of a Left Hand. if (deviceToTrack == Devices.LeftController) { index = (EIndex)OpenVR.System.GetTrackedDeviceIndexForControllerRole(ETrackedControllerRole.LeftHand); } //Assigns the HMD. if (deviceToTrack == Devices.Hmd) { index = EIndex.Hmd; } //Display a warning if there was supposed to be a calibration file, and none was found. if (SNHolder.Equals("NONE")) { Debug.LogWarning(ZEDLogMessage.Error2Str(ZEDLogMessage.ERROR.PAD_CAMERA_CALIBRATION_NOT_FOUND)); } else if (SNHolder != null && index != EIndex.None) // { //If the Serial number of the Calibrated device isn't the same as the current tracked device by this script... var snerror = ETrackedPropertyError.TrackedProp_Success; var snresult = new System.Text.StringBuilder((int)64); OpenVR.System.GetStringTrackedDeviceProperty((uint)index, ETrackedDeviceProperty.Prop_SerialNumber_String, snresult, 64, ref snerror); if (!snresult.ToString().Contains(SNHolder)) { Debug.LogWarning(ZEDLogMessage.Error2Str(ZEDLogMessage.ERROR.PAD_CAMERA_CALIBRATION_MISMATCH) + " Serial Number: " + SNHolder); //... then look for that device through all the connected devices. for (int i = 0; i < 16; i++) { //If a device with the same Serial Number is found, then change the device to track of this script. var chsnresult = new System.Text.StringBuilder((int)64); OpenVR.System.GetStringTrackedDeviceProperty((uint)i, ETrackedDeviceProperty.Prop_RenderModelName_String, snresult, 64, ref snerror); if (snresult.ToString().Contains("tracker")) { index = (EIndex)i; OpenVR.System.GetStringTrackedDeviceProperty((uint)i, ETrackedDeviceProperty.Prop_SerialNumber_String, snresult, 64, ref snerror); } if (snresult.ToString().Contains(SNHolder)) { index = (EIndex)i; string deviceRole = OpenVR.System.GetControllerRoleForTrackedDeviceIndex((uint)index).ToString(); if (deviceRole.Equals("RightHand")) deviceToTrack = Devices.RightController; else if (deviceRole.Equals("LeftHand")) deviceToTrack = Devices.LeftController; else if (deviceRole.Equals("Invalid")) { var error = ETrackedPropertyError.TrackedProp_Success; var result = new System.Text.StringBuilder((int)64); OpenVR.System.GetStringTrackedDeviceProperty((uint)index, ETrackedDeviceProperty.Prop_RenderModelName_String, result, 64, ref error); if (result.ToString().Contains("tracker")) deviceToTrack = Devices.ViveTracker; } Debug.Log("A connected device with the correct Serial Number was found, and assigned to " + this + " the correct device to track."); break; } } } } oldDevice = deviceToTrack; } if (deviceToTrack != oldDevice) index = EIndex.None; #endif } #if ZED_STEAM_VR /// /// Whether a given set of poses is currently valid - contains at least one pose and attached to an actual device. /// public bool isValid { get; private set; } /// /// Track the devices for SteamVR and applying a delay. /// protected void OnNewPoses(TrackedDevicePose_t newpose) { if (index == EIndex.None) return; var i = (int)index; isValid = false; if (!newpose.bDeviceIsConnected) return; if (!newpose.bPoseIsValid) return; isValid = true; //Get the position and rotation of our tracked device. var pose = new SteamVR_Utils.RigidTransform(newpose.mDeviceToAbsoluteTracking); //Saving those values. RegisterPosition(1, pose.pos, pose.rot); //Delay the saved values inside GetValuePosition() by a factor of latencyCompensation in milliseconds. sl.Pose p = GetValuePosition(1, (float)(latencyCompensation / 1000.0f)); transform.localPosition = p.translation; transform.localRotation = p.rotation; } protected void OnEnable() { if (openvrsystem == null) { enabled = false; return; } } protected void OnDisable() { isValid = false; } protected virtual void UpdateControllerState() { lastcontrollerstate = controllerstate; //Update position. if (index > EIndex.Hmd) { if (OpenVR.Compositor != null){ ETrackingUniverseOrigin tracktype = OpenVR.Compositor.GetTrackingSpace(); TrackedDevicePose_t[] absoluteposes = new TrackedDevicePose_t[16]; openvrsystem.GetDeviceToAbsoluteTrackingPose(tracktype, 0, absoluteposes); TrackedDevicePose_t newposes = absoluteposes[(int)index]; OnNewPoses(newposes); } } //We need to check for this always in case the user uses the deprecated GetVRButton methods. if (useLegacySteamVRInput) { openvrsystem.GetControllerState((uint)index, ref controllerstate, controllerstatesize); } } /// /// Returns if the VR controller button with the given ID was pressed for the first time this frame. /// /// EVR ID of the button as listed in OpenVR. [System.ObsoleteAttribute("ZEDControllerTracker's GetVRButton methods are deprecated.\r\n " + "Use ZEDControllerTracker_DemoInputs.GetVRButtonDown_Legacy instead., false")] public bool GetVRButtonDown(EVRButtonId buttonid) { if (openvrsystem == null) return false; //If VR isn't running, we can't check. bool washeldlastupdate = (lastcontrollerstate.ulButtonPressed & (1UL << (int)buttonid)) > 0L; if (washeldlastupdate == true) return false; //If the key was held last check, it can't be pressed for the first time now. bool isheld = (controllerstate.ulButtonPressed & (1UL << (int)buttonid)) > 0L; return isheld; //If we got here, we know it was not down last frame. } /// /// Returns if the VR controller button with the given ID is currently held. /// /// EVR ID of the button as listed in OpenVR. [System.ObsoleteAttribute("ZEDControllerTracker's GetVRButton methods are deprecated.\r\n " + "Use ZEDControllerTracker_DemoInputs.GetVRButtonHeld_Legacy instead.", false)] public bool GetVRButtonHeld(EVRButtonId buttonid) { if (openvrsystem == null) return false; //If VR isn't running, we can't check. bool isheld = (controllerstate.ulButtonPressed & (1UL << (int)buttonid)) > 0L; return isheld; } /// /// Returns if the VR controller button with the given ID was held last frame, but released this frame. /// /// EVR ID of the button as listed in OpenVR. [System.ObsoleteAttribute("ZEDControllerTracker's GetVRButton methods are deprecated.\r\n " + "Use ZEDControllerTracker_DemoInputs.GetVRButtonReleased_Legacy instead.", false)] public bool GetVRButtonReleased(EVRButtonId buttonid) { if (openvrsystem == null) return false; //If VR isn't running, we can't check. bool washeldlastupdate = (lastcontrollerstate.ulButtonPressed & (1UL << (int)buttonid)) > 0L; if (washeldlastupdate == false) return false; //If the key was held last check, it can't be released now. bool isheld = (controllerstate.ulButtonPressed & (1UL << (int)buttonid)) > 0L; return !isheld; //If we got here, we know it was not up last frame. } /// /// Returns the value of an axis with the provided ID. /// Note that for single-value axes, the relevant value will be the X in the returned Vector2 (the Y is unused). /// /// [System.ObsoleteAttribute("ZEDControllerTracker.GetAxis is deprecated.\r\n " + "Use ZEDControllerTracker_DemoInputs.GetVRAxis_Legacy instead.", false)] public Vector2 GetAxis(EVRButtonId buttonid) { //Convert the EVRButtonID enum to the axis number and check if it's not an axis. uint axis = (uint)buttonid - (uint)EVRButtonId.k_EButton_Axis0; if (axis < 0 || axis > 4) { Debug.LogError("Called GetAxis with " + buttonid + ", which is not an axis."); return Vector2.zero; } switch (axis) { case 0: return new Vector2(controllerstate.rAxis0.x, controllerstate.rAxis0.y); case 1: return new Vector2(controllerstate.rAxis1.x, controllerstate.rAxis1.y); case 2: return new Vector2(controllerstate.rAxis2.x, controllerstate.rAxis2.y); case 3: return new Vector2(controllerstate.rAxis3.x, controllerstate.rAxis3.y); case 4: return new Vector2(controllerstate.rAxis4.x, controllerstate.rAxis4.y); default: return Vector2.zero; } } #endif /// /// Compute the delayed position and rotation from the history stored in the poseData dictionary. /// /// /// /// private sl.Pose GetValuePosition(int keyindex, float timeDelay) { sl.Pose p = new sl.Pose(); if (poseData.ContainsKey(keyindex)) { //Get the saved position & rotation. p.translation = poseData[keyindex][poseData[keyindex].Count - 1].position; p.rotation = poseData[keyindex][poseData[keyindex].Count - 1].rotation; float idealTS = (Time.time - timeDelay); for (int i = 0; i < poseData[keyindex].Count; ++i) { if (poseData[keyindex][i].timestamp > idealTS) { int currentIndex = i; if (currentIndex > 0) { //Calculate the time between the pose and the delayed pose. float timeBetween = poseData[keyindex][currentIndex].timestamp - poseData[keyindex][currentIndex - 1].timestamp; float alpha = ((Time.time - poseData[keyindex][currentIndex - 1].timestamp) - timeDelay) / timeBetween; //Lerp to the next position based on the time determined above. Vector3 pos = Vector3.Lerp(poseData[keyindex][currentIndex - 1].position, poseData[keyindex][currentIndex].position, alpha); Quaternion rot = Quaternion.Lerp(poseData[keyindex][currentIndex - 1].rotation, poseData[keyindex][currentIndex].rotation, alpha); //Apply new values. p = new sl.Pose(); p.translation = pos; p.rotation = rot; //Add drift correction, but only if the user hasn't disabled it, it's on an actual controller, and the zedRigRoot position won't be affected. if (correctControllerDrift == true && (deviceToTrack == Devices.LeftController || deviceToTrack == Devices.RightController || deviceToTrack == Devices.ViveTracker) && (zedManager != null && zedManager.IsStereoRig == true && !zedManager.transform.IsChildOf(transform))) { //Compensate for positional drift by measuring the distance between HMD and ZED rig root (the head's center). #if UNITY_2019_3_OR_NEWER InputDevice head = InputDevices.GetDeviceAtXRNode(XRNode.Head); head.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition); Vector3 zedhmdposoffset = zedRigRoot.position - headPosition; #else Vector3 zedhmdposoffset = zedRigRoot.position - InputTracking.GetLocalPosition(XRNode.Head); #endif p.translation += zedhmdposoffset; } //Removes used elements from the dictionary. poseData[keyindex].RemoveRange(0, currentIndex - 1); } return p; } } } return p; } /// /// Set the current tracking to a container (TimedPoseData) to be stored in poseData and retrieved/applied after the latency period. /// /// Key value in the dictionary. /// Tracked object's position from the VR SDK. /// Tracked object's rotation from the VR SDK. private void RegisterPosition(int keyindex, Vector3 position, Quaternion rot) { TimedPoseData currentPoseData = new TimedPoseData(); currentPoseData.timestamp = Time.time; currentPoseData.rotation = rot; currentPoseData.position = position; poseData[keyindex].Add(currentPoseData); } /// /// Structure used to hold the pose of a controller at a given timestamp. /// This is stored in poseData with RegisterPosition() each time the VR SDK makes poses available. /// It's retrieved with GetValuePosition() in Update() each frame. /// public struct TimedPoseData { /// /// Value from Time.time when the pose was collected. /// public float timestamp; /// /// Rotation of the tracked object as provided by the VR SDK. /// public Quaternion rotation; /// /// Position of the tracked object as provided by the VR SDK. /// public Vector3 position; } } #if UNITY_EDITOR /// /// Custom editor for ZEDControllerTracker. /// If no VR Unity plugin (Oculus Integration or SteamVR plugin) has been loaded by the ZED plugin but one is found, /// presents a button to create project defines that tell ZED scripts that this plugin is loaded. /// These defines (ZED_STEAM_VR and ZED_OCULUS) are used to allow compiling parts of ZED scripts that depend on scripts in these VR plugins. /// Note that this detection will also be attempted any time an asset has been imported. See nested class AssetPostProcessZEDVR. /// [CustomEditor(typeof(ZEDControllerTracker)), CanEditMultipleObjects] public class ZEDVRDependencies : Editor { [SerializeField] static string defineName; static string packageName; public override void OnInspectorGUI() //Called when the Inspector is visible. { //if (CheckPackageExists("OpenVR")) if (CheckPackageExists("SteamVR_Camera.cs")) { defineName = "ZED_STEAM_VR"; packageName = "SteamVR"; } //else if (CheckPackageExists("Oculus") || CheckPackageExists("OVR")) else if (CheckPackageExists("OVRManager")) { defineName = "ZED_OCULUS"; packageName = "Oculus"; } if (EditorPrefs.GetBool(packageName)) //Has it been set? { DrawDefaultInspector(); } else //No package loaded, but one has been detected. Present a button to load it. { GUILayout.Space(20); if (GUILayout.Button("Load " + packageName + " data")) { if (CheckPackageExists(packageName)) { ActivateDefine(); } } if (packageName == "SteamVR") EditorGUILayout.HelpBox(ZEDLogMessage.Error2Str(ZEDLogMessage.ERROR.STEAMVR_NOT_INSTALLED), MessageType.Warning); else if (packageName == "Oculus") EditorGUILayout.HelpBox(ZEDLogMessage.Error2Str(ZEDLogMessage.ERROR.OVR_NOT_INSTALLED), MessageType.Warning); } } /// /// Finds if a folder in the project exists with the specified name. /// Used to check if a plugin has been imported, as the relevant plugins are placed /// in a folder named after the package. Example: "Assets/Oculus". /// /// Package name. /// public static bool CheckPackageExists(string name) { string[] packages = AssetDatabase.FindAssets(name); return packages.Length != 0; } /// /// Activates a define tag in the project. Used to enable compiling sections of scripts with that tag enabled. /// For instance, parts of this script under a #if ZED_STEAM_VR statement will be ignored by the compiler unless ZED_STEAM_VR is enabled. /// public static void ActivateDefine() { EditorPrefs.SetBool(packageName, true); string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(BuildTargetGroup.Standalone); if (defines.Length != 0) { if (!defines.Contains(defineName)) { defines += ";" + defineName; } } else { if (!defines.Contains(defineName)) { defines += defineName; } } PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Standalone, defines); } /// /// Removes a define tag from the project. /// Called whenever a package is checked for but not found. /// Removing the define tags will prevent compilation of code marked with that tag, like #if ZED_OCULUS. /// public static void DeactivateDefine(string packagename) { EditorPrefs.SetBool(packagename, false); string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(BuildTargetGroup.Standalone); if (defines.Length != 0) { if (defineName != null && defines.Contains(defineName)) { defines = defines.Remove(defines.IndexOf(defineName), defineName.Length); if (defines.LastIndexOf(";") == defines.Length - 1 && defines.Length != 0) { defines.Remove(defines.LastIndexOf(";"), 1); } } } PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Standalone, defines); } } #endif