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;
}
}
}