using System.Collections; using System.Collections.Generic; using UnityEngine; /// /// Handles the ZED camera's offset from its tracked object within the MR calibration scene. /// Provides interfaces for translating/rotating gradually or instantly, along with related /// functions like undo/redo and loading/saving. /// public class CameraAnchor : MonoBehaviour { /// /// The scene's ZEDManager instance. Should be a child of this object's transform. /// If not assigned, will be set to the ZEDManager for camera index 1. /// [Tooltip("The scene's ZEDManager instance. Should be a child of this object's transform. " + "If not assigned, will be set to the ZEDManager for camera index 1.")] public ZEDManager zedManager; /// /// ZEDControllerTracker assigned to this object. That's what keeps the object's position in sync with the real-world object. /// [HideInInspector] public ZEDControllerTracker controllerTracker; /// /// Max speed the ZED will move when calling TranslateZEDIncrementally(). This happens when using the manual translation arrows. /// [Space(5)] [Tooltip("Max speed the ZED will move when calling TranslateZEDIncrementally(). This happens when using the manual translation arrows.")] public float maxTranslateMPS = 0.05f; /// /// Max speed the ZED will move when calling RotateZEDIncrementally(). This happens when using the manual rotation rings. /// [Tooltip("Max speed the ZED will move when calling RotateZEDIncrementally(). This happens when using the manual rotation rings.")] public float maxRotateDPS = 30f; /// /// Delegate that provides a reference for a CameraAnchor, indended to be to this one. /// public delegate void CameraAnchorCreatedDelegate(CameraAnchor anchor); /// /// Event called in Start(), confirming that the anchor is ready to be used. /// public static event CameraAnchorCreatedDelegate OnCameraAnchorCreated; /// /// Index of the layer hidden from the ZED camera, preventing it from being visible in the 2D view. /// This is NOT enforced in the camera itself via script; you must exclude it from the camera's layer mask manually. /// public const int HIDE_FROM_ZED_LAYER = 17; /// /// File path to where the app loads and saves the calibrated offset values - the whole point of the MR calibration scene existing. /// private string filePath { get { string folder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData); string specificFolder = System.IO.Path.Combine(folder, @"Stereolabs\steamvr\ZED_Position_Offset.conf"); return specificFolder; } } /// /// How many undo/redo poses to save before we start discarding them. /// private const int MAX_UNDO_HISTORY = 50; private CappedStack undoStack = new CappedStack(MAX_UNDO_HISTORY); private CappedStack redoStack = new CappedStack(MAX_UNDO_HISTORY); //When we call TranslateZEDIncrementally or the Rotate version, we only want to register an undoable pose //when you first call it. So we have a timer we start after registering the pose that blocks re-registering //until neither incremental function has been called for MIN_TIME_ect seconds. private const float MIN_TIME_BETWEEN_INCREMENTAL_UNDOS = 0.25f; private float incrementalUndoTimer = 0f; private bool canRegisterIncrementalUndo { get { return incrementalUndoTimer <= 0f; } } /// /// Get necessary references and handle startup events. /// void Start() { if (!controllerTracker) controllerTracker = GetComponent(); if (!zedManager) zedManager = controllerTracker.zedManager; zedManager.OnZEDReady += LoadCalibFile; //Dispatch event that this anchor is ready. if (OnCameraAnchorCreated != null) OnCameraAnchorCreated.Invoke(this); } /// /// Sets the ZED position and rotation to the specified values, instantly. /// Also stores the pose for undoing later. /// public void SetNewZEDPose(Vector3 localpos, Quaternion localrot) { RegisterUndoablePose(); zedManager.transform.localPosition = localpos; zedManager.transform.localRotation = localrot; } /// /// Adds the specified translation and rotation to the current ZED transform values. /// /// Direction and amount to translate the ZED. /// Direction and amount to rotate the ZED. /// If true, applies to localPosition/localRotation. Otherwise, applies to world-space values. public void MoveZEDPose(Vector3 posoffset, Quaternion rotoffset, bool uselocal) { RegisterUndoablePose(); if (uselocal) //Local space. { zedManager.transform.localPosition += posoffset; zedManager.transform.localRotation = rotoffset * zedManager.transform.localRotation; } else //World space. { zedManager.transform.position += posoffset; zedManager.transform.rotation = rotoffset * zedManager.transform.rotation; } } /// /// Move the ZED in a direction by an amount governed by maxTranslateMPS. /// Provided values should be clamped to -1 and 1, representing the multiple of that max speed used to translate. /// For instance, if max speed is 1 meter per second and this is called with 0, 0.5f, 0, for 1 second, it'll /// move 0.5 meters over the course of that second. /// Meant to be called every frame while dragging a control (like the translate arrows) to slide the ZED gradually. /// /// What percentage (-1 to 1) of the max speed to move the ZED. public void TranslateZEDIncrementally(Vector3 translation) //Input should be clamped to -1 and 1. 1 moves in max direction speed. { if (canRegisterIncrementalUndo) //Clear to register the undo. { RegisterUndoablePose(); StartCoroutine(BlockIncrementalUndoRegister()); } else //Timer already started. Don't start a new coroutine, but reset the timer. { incrementalUndoTimer = MIN_TIME_BETWEEN_INCREMENTAL_UNDOS; } Vector3 velocity = translation * maxTranslateMPS * Time.deltaTime; zedManager.transform.localPosition += zedManager.transform.localRotation * velocity; } /// /// Rotates the ZED in a direction by an amount governed by maxRotateDPS. /// Provided values should be clamped to -1 and 1, representing the multiple of that max speed used to rotate. /// For instance, if max speed is 100 degrees per second, and this is called with 0,0.5f,0 for 1 second, it'll /// rotate 50 degrees on the Y axis over the course of that second. /// Meant to be called every frame while dragging a control (like the translate arrows) to slide the ZED gradually. /// /// public void RotateZEDIncrementally(Vector3 rotation)//Input should be clamped to -1 and 1. 1 moves in max rotation speed. { if (canRegisterIncrementalUndo) //Clear to register the undo. { RegisterUndoablePose(); StartCoroutine(BlockIncrementalUndoRegister()); } else //Timer already started. Don't start a new coroutine, but reset the timer. { incrementalUndoTimer = MIN_TIME_BETWEEN_INCREMENTAL_UNDOS; } Vector3 angvelocity = rotation * maxRotateDPS * Time.deltaTime; zedManager.transform.localRotation *= Quaternion.Euler(angvelocity); //zedManager.transform.localEulerAngles += angvelocity; } /// /// Temporarily blocks an undoable position from being registered from the 'incremental' move functions. /// This is done so that when you first start sliding/rotating the ZED over time, it only saves the position /// in the first frame, and not repeatedly or at any time until you finish thta action. /// private IEnumerator BlockIncrementalUndoRegister() { incrementalUndoTimer = MAX_UNDO_HISTORY; while (incrementalUndoTimer > 0) { incrementalUndoTimer -= Time.deltaTime; yield return null; } } /// /// Moves the ZED to the top position on the Undo stack, undoing the last change. /// Also adds the current position to the Redo stack. /// public void Undo() { if (undoStack.Count == 0) return; //No action to undo. AnchorPose undoPose = undoStack.Pop(); //Get last pose from the stack and remove it. //Save the current pose to the redo stack. AnchorPose pose = new AnchorPose(zedManager.transform.localPosition, zedManager.transform.localRotation); redoStack.Push(pose); //Apply historical pose to the ZED. zedManager.transform.localPosition = undoPose.position; zedManager.transform.localRotation = undoPose.rotation; //redoStack.Push(undoPose); //Remember what you undid so it can be redone. } /// /// Moves the ZED to the top position on the Redo stack, if any, which is added after /// the user calls Undo, so long as another Undo is not registered in the meantime. /// public void Redo() { if (redoStack.Count == 0) return; //No action to redo. AnchorPose redoPose = redoStack.Pop(); //Get last pose from the stack and remove it. //Re-apply that pose to the ZED. zedManager.transform.localPosition = redoPose.position; zedManager.transform.localRotation = redoPose.rotation; undoStack.Push(redoPose); //Put that action back on the undo stack so you could repeat this process again if you wanted. } /// /// Call before the ZED is moved to make it so you can go back to this position by pressing Undo. /// If you are moving it gradually, call before the gradual movement starts and don't update during. /// Also clears the Redo stack as you've now branched away from it. /// private void RegisterUndoablePose() { RegisterUndoablePose(zedManager.transform.localPosition, zedManager.transform.localRotation); } /// /// Call before the ZED is moved to make it so you can go back to this position by pressing Undo. /// If you are moving it gradually, call before the gradual movement starts and don't update during. /// Also clears the Redo stack as you've now branched away from it. /// private void RegisterUndoablePose(Vector3 pos, Quaternion rot) { AnchorPose pose = new AnchorPose(pos, rot); undoStack.Push(pose); //Clear the Redo stack as we've now branched away from whatever history it had. redoStack.Clear(); } /// /// Saves the ZED's offset from the tracked object as a file to be loaded within Unity or from any ZED application /// built to load such a calibration file. /// Destination directory defined by the CALIB_FILE_PATH constant. /// public void SaveCalibFile() { int slashindex = filePath.LastIndexOf('\\'); string directory = filePath.Substring(0, slashindex); print(directory); if (!System.IO.Directory.Exists(directory)) { System.IO.Directory.CreateDirectory(directory); } using (System.IO.StreamWriter file = new System.IO.StreamWriter(filePath)) { Vector3 localpos = zedManager.transform.localPosition; //Shorthand. Vector3 localrot = zedManager.transform.localRotation.eulerAngles; //Shorthand. file.WriteLine("x=" + localpos.x); file.WriteLine("y=" + localpos.y); file.WriteLine("z=" + localpos.z); file.WriteLine("rx=" + localrot.x); file.WriteLine("ry=" + localrot.y); file.WriteLine("rz=" + localrot.z); #if ZED_STEAM_VR string indexstring = "indexController="; if (controllerTracker.index > 0) { var snerror = Valve.VR.ETrackedPropertyError.TrackedProp_Success; var snresult = new System.Text.StringBuilder((int)64); indexstring += Valve.VR.OpenVR.System.GetStringTrackedDeviceProperty((uint)controllerTracker.index, Valve.VR.ETrackedDeviceProperty.Prop_SerialNumber_String, snresult, 64, ref snerror); } else { indexstring += "NONE"; } file.WriteLine(indexstring); #endif file.Close(); print("Calibration saved to " + filePath); MessageDisplay.DisplayTemporaryMessageAll("Saved calibration file to:\r\n" + filePath); } } /// /// Loads a previously-saved calibration file, if any, and applies the position to the ZED. /// Called when the ZED is first initialized, so you left off where you started. /// public void LoadCalibFile() { if (!System.IO.File.Exists(filePath)) { print("Did not load values as no previously-saved file was found: " + filePath); return; } string[] lines = null; try { lines = System.IO.File.ReadAllLines(filePath); } catch (System.Exception e) { Debug.LogError(e.ToString()); } if (lines == null) { print("Loaded calibration file was empty: " + filePath); return; } Vector3 localpos = Vector3.zero; Vector3 localrot = Vector3.zero; //Euler angles. foreach (string line in lines) { string[] splitline = line.Split('='); if (splitline != null && splitline.Length >= 2) { string key = splitline[0]; string value = splitline[1].Split(' ')[0].ToLower(); //Removed space after values if present. if (key == "indexController") continue; //We don't need to load this. //We'll parse the field ahead of time for simplicity, but this only works because all needed values are floats. //This needs to be amended if we ever need to load other values. float parsedval = float.Parse(value, System.Globalization.CultureInfo.InvariantCulture); if (key == "x") { localpos.x = parsedval; } else if (key == "y") { localpos.y = parsedval; } else if (key == "z") { localpos.z = parsedval; } else if (key == "rx") { localrot.x = parsedval; } else if (key == "ry") { localrot.y = parsedval; } else if (key == "rz") { localrot.z = parsedval; } } } zedManager.transform.localPosition = localpos; zedManager.transform.localRotation = Quaternion.Euler(localrot); print("Loaded past calibration from file: " + filePath); } /// /// Very simple version of UnityEngine's Pose class, as it doesn't exist in older versions of Unity. /// internal class AnchorPose { internal Vector3 position; internal Quaternion rotation; internal AnchorPose(Vector3 pos, Quaternion rot) { position = pos; rotation = rot; } } }