//======= Copyright (c) Stereolabs Corporation, All rights reserved. ===============
using UnityEngine;
using System.Runtime.InteropServices;
using UnityEngine.XR;
using System.IO;
using System.Collections.Generic;
using UnityEngine.Rendering;
///
/// In pass-through AR mode, handles the final output to the VR headset, positioning the final images
/// to make the pass-through effect natural and comfortable. Also moves/rotates the images to
/// compensate for the ZED image's latency using our Video Asynchronous Timewarp.
/// ZEDManager attaches this component to a second stereo rig called "ZEDRigDisplayer" that it
/// creates and hides in the editor at runtime; see ZEDManager.CreateZEDRigDisplayer() to see this process.
///
/// The Timewarp effect is achieved by logging the pose of the headset each time it's available within the
/// wrapper. Then, when a ZED image is available, the wrapper looks up the headset's position using the timestamp
/// of the image, and moves the final viewing planes according to that position. In this way, the ZED's images
/// line up with real-life, even after a ~60ms latency.
///
public class ZEDMixedRealityPlugin : MonoBehaviour
{
#region DLL Calls
const string nameDll = sl.ZEDCommon.NameDLL;
[DllImport(nameDll, EntryPoint = "sl_compute_size_plane_with_gamma")]
private static extern System.IntPtr dllz_compute_size_plane_with_gamma(int width, int height, float perceptionDistance, float eyeToZedDistance, float planeDistance, float HMDFocal, float zedFocal);
[DllImport(nameDll, EntryPoint = "sl_compute_hmd_focal")]
private static extern float dllz_compute_hmd_focal(int width, int height, float w, float h);
/*****LATENCY CORRECTOR***/
[DllImport(nameDll, EntryPoint = "sl_latency_corrector_add_key_pose")]
private static extern void dllz_latency_corrector_add_key_pose(ref Vector3 translation, ref Quaternion rotation, ulong timeStamp);
[DllImport(nameDll, EntryPoint = "sl_latency_corrector_get_transform")]
private static extern int dllz_latency_corrector_get_transform(ulong timeStamp, bool useLatency,out Vector3 translation, out Quaternion rotation);
[DllImport(nameDll, EntryPoint = "sl_latency_corrector_initialize")]
private static extern void dllz_latency_corrector_initialize(int device);
[DllImport(nameDll, EntryPoint = "sl_latency_corrector_shutdown")]
private static extern void dllz_latency_corrector_shutdown();
/****ANTI DRIFT ***/
[DllImport(nameDll, EntryPoint = "sl_drift_corrector_initialize")]
public static extern void dllz_drift_corrector_initialize();
[DllImport(nameDll, EntryPoint = "sl_drift_corrector_shutdown")]
public static extern void dllz_drift_corrector_shutdown();
[DllImport(nameDll, EntryPoint = "sl_drift_corrector_get_tracking_data")]
public static extern void dllz_drift_corrector_get_tracking_data(ref TrackingData trackingData, ref Pose HMDTransform, ref Pose latencyCorrectorTransform, int hasValidTrackingPosition,bool checkDrift);
[DllImport(nameDll, EntryPoint = "sl_drift_corrector_set_calibration_transform")]
public static extern void dllz_drift_corrector_set_calibration_transform(ref Pose pose);
[DllImport(nameDll, EntryPoint = "sl_drift_corrector_set_calibration_const_offset_transform")]
public static extern void dllz_drift_corrector_set_calibration_const_offset_transform(ref Pose pose);
#endregion
///
/// Container for storing historic pose information, used by the latency corrector.
///
public struct KeyPose
{
public Quaternion Orientation;
public Vector3 Translation;
public ulong Timestamp;
};
///
/// Container for position and rotation. Used when timestamps are not needed or have already
/// been processed, such as setting the initial camera offset or updating the stereo rig's
/// transform from data pulled from the wrapper.
///
[StructLayout(LayoutKind.Sequential)]
public struct Pose
{
public Vector3 translation;
public Quaternion rotation;
public Pose(Vector3 t, Quaternion q)
{
translation = t;
rotation = q;
}
}
///
///
///
[StructLayout(LayoutKind.Sequential)]
public struct TrackingData
{
public Pose zedPathTransform;
public Pose zedWorldTransform;
public Pose offsetZedWorldTransform;
public int trackingState;
}
///
/// Gameobject holding the camera in the final ZEDRigDisplayer rig, which captures the final images sent to both HMD screens.
///
public GameObject finalCameraCenter;
///
/// 'Intermediate' left camera GameObject, which is the one on the regular, always-visible ZED stereo rig (ZED_Rig_Stereo),
/// usually called 'Left_eye'.
///
[Tooltip("'Intermediate' left camera GameObject, which is the one on the regular, always-visible ZED stereo rig (ZED_Rig_Stereo), "
+ "usually called 'Left_eye'. ")]
public GameObject ZEDEyeLeft;
///
/// 'Intermediate' right camera GameObject, which is the one on the regular, always-visible ZED stereo rig (ZED_Rig_Stereo),
/// usually called 'Right_eye'.
///
[Tooltip("'Intermediate' right camera GameObject, which is the one on the regular, always-visible ZED stereo rig (ZED_Rig_Stereo)," +
"usually called 'Right_eye'. ")]
public GameObject ZEDEyeRight;
///
/// 'Intermediate' left screen/canvas object in the always-visible ZED stereo rig.
///
[Tooltip("")]
public ZEDRenderingPlane leftScreen;
///
/// 'Intermediate' right screen/canvas object in the always-visible ZED stereo rig.
///
[Tooltip("")]
public ZEDRenderingPlane rightScreen;
///
/// Final center viewing plane/canvas object in the final ZEDRigDisplayer rig. Displays the image from the center
/// 'intermediate' cameras (ZEDEyeRight and ZEDEyeRight) and is offset for image comfort and moved each frame for the Timewarp effect.
///
[Tooltip("")]
public Transform quadCenter;
///
/// Camera object in 'finalCameraCenter', which captures the final image output to the headset's left and right screens.
///
// [Tooltip("")]
public Camera finalCenterEye;
///
/// Material from the final center plane. Usually a new instance of Mat_ZED_Unlit.
///
[Tooltip("Material from the final right plane. Usually a new instance of Mat_ZED_Unlit. ")]
public Material centerMaterial;
///
/// Base, pre-Timewarp offset between each final plane and its corresponding camera.
///
[Tooltip("Offset between each final plane and its corresponding camera.")]
public Vector3 offset = new Vector3(0, 0, (float)sl.Constant.PLANE_DISTANCE);
///
/// Distance to set each intermediate camera from the point between them. This is half of the post-calibration
/// distance between the ZED cameras, so X is usually very close to 0.0315m (63mm / 2).
///
[Tooltip("")]
public Vector3 halfBaselineOffset;
///
/// Reference to the ZEDCamera instance, which communicates with the SDK.
///
[Tooltip("Reference to the ZEDCamera instance, which communicates with the SDK.")]
public sl.ZEDCamera zedCamera;
///
/// Reference to the scene's ZEDManager instance, usually contained in ZED_Rig_Stereo.
///
[Tooltip("Reference to the scene's ZEDManager instance, usually contained in ZED_Rig_Stereo.")]
public ZEDManager manager;
///
/// Flag set to true when the target textures from the ZEDRenderingPlane overlays are ready.
///
[Tooltip("Flag set to true when the target textures from the ZEDRenderingPlane overlays are ready.")]
public bool ready = false;
///
/// Flag set to true when a grab is ready, used to collect a pose from the latest time possible.
///
[Tooltip("Flag set to true when a grab is ready, used to collect a pose from the latest time possible.")]
public bool grabSucceeded = false;
///
/// Flag set to true when the ZED is ready (after ZEDManager.OnZEDReady is invoked).
///
[Tooltip("Flag set to true when the ZED is ready (after ZEDManager.OnZEDReady is invoked).")]
public bool zedReady = false;
///
/// If a VR device is still detected. Updated each frame. Used to know if certain updates should still happen.
///
private bool hasVRDevice = false;
public bool HasVRDevice
{
get { return hasVRDevice; }
}
///
/// The current latency pose - the pose the headset was at when the last ZED frame was captured (based on its timestamp).
///
private Pose latencyPose;
///
/// The physical offset of the HMD to the ZED. Represents the offset from the approximate center of the user's
/// head to the ZED's left sensor.
///
private Pose hmdtozedCalibration;
///
/// Public accessor for the physical offset of the HMD to the ZED. Represents the offset from the
/// approximate center of the user's head to the ZED's left sensor.
///
public Pose HmdToZEDCalibration
{
get { return hmdtozedCalibration; }
}
///
/// Whether the latency correction is ready.
///
private bool latencyCorrectionReady = false;
///
/// Contains the last position computed by the anti-drift.
///
public TrackingData trackingData = new TrackingData();
///
/// Filename of the saved HMD to ZED calibration file loaded into hmdtozedCalibration.
/// //If it doesn't exist, it's created with hard-coded values.
///
[Tooltip("")]
[SerializeField]
private string calibrationFile = "CalibrationZEDHMD.ini";
///
/// Path of the saved HMD to ZED calibration file loaded into hmdtozedCalibration.
/// By default, corresponds to C:/ProgramData/Stereolabs/mr.
///
private string calibrationFilePath = @"Stereolabs\mr";
///
/// Delegate for the OnHMDCalibChanged event.
///
public delegate void OnHmdCalibrationChanged();
///
/// Event invoked if the calibration file that sets the physical ZED offset is changed at runtime.
/// Causes ZEDManger.CalibrationHasChanged() to get called, which re-initialized the ZED's position
/// with ZEDManager.AdjustZEDRigCameraPosition() at the next tracking update.
///
public static event OnHmdCalibrationChanged OnHmdCalibChanged;
///
/// Cached property id for _MainTex. use the mainTexID property instead.
///
private int? _maintexLeftid;
///
/// Property id for _MainTex, which is the main texture from the ZED.
///
private int mainTexLeftID
{
get
{
if (_maintexLeftid == null) _maintexLeftid = Shader.PropertyToID("_MainTexLeft");
return (int)_maintexLeftid;
}
}
///
/// Cached property id for _MainTex. use the mainTexID property instead.
///
private int? _maintexRightid;
///
/// Property id for _MainTex, which is the main texture from the ZED.
///
private int mainTexRightID
{
get
{
if (_maintexRightid == null) _maintexRightid = Shader.PropertyToID("_MainTexRight");
return (int)_maintexRightid;
}
}
List nodeStates = new List();
private bool hasXRDevice()
{
#if UNITY_2020_1_OR_NEWER
var xrDisplaySubsystems = new List();
SubsystemManager.GetInstances(xrDisplaySubsystems);
foreach (var xrDisplay in xrDisplaySubsystems)
{
if (xrDisplay.running)
{
return true;
}
}
return false;
#else
return XRDevice.isPresent;
#endif
}
private string getXRModelName()
{
#if UNITY_2019_1_OR_NEWER
return XRSettings.loadedDeviceName;
#else
return XRDevice.model;
#endif
}
private void Awake()
{
//Initialize the latency tracking only if a supported headset is detected.
//You can force it to work for unsupported headsets by implementing your own logic for calling
//dllz_latency_corrector_initialize.
hasVRDevice = hasXRDevice();
if (hasVRDevice)
{
if (getXRModelName().ToLower().Contains("vive")) //Vive or Vive Pro
{
dllz_latency_corrector_initialize(0);
}
else //Oculus Rift CV1, Rift S, Windows Mixed Reality, Valve Index, etc.
{
dllz_latency_corrector_initialize(1);
}
dllz_drift_corrector_initialize();
}
#if UNITY_2017_OR_NEWER
nodeState.nodeType = VRNode.Head;
nodes.Add(nodeState);
#endif
}
///
/// Sets references not set in ZEDManager.CreateZEDRigDisplayer(), sets materials,
/// adjusts final plane scale, loads the ZED calibration offset and other misc. values.
///
void Start()
{
hasVRDevice = hasXRDevice();
//iterate until we found the ZED Manager parent...
Transform ObjParent = gameObject.transform;
int tries = 0;
while (manager == null && tries < 50) {
if (ObjParent!=null)
manager= ObjParent.GetComponent ();
if (manager == null && ObjParent!=null)
ObjParent = ObjParent.parent;
tries++;
}
if (manager != null) {
manager.OnZEDReady += ZEDReady;
zedCamera = manager.zedCamera;
} else
return;
leftScreen = ZEDEyeLeft.GetComponent();
rightScreen = ZEDEyeRight.GetComponent();
finalCenterEye = finalCameraCenter.GetComponent();
centerMaterial = quadCenter.GetComponent().material;
finalCenterEye.SetReplacementShader(centerMaterial.shader, "");
finalCenterEye.depth = 0;
float plane_dist = (float)sl.Constant.PLANE_DISTANCE;
scale(quadCenter.gameObject, new Vector2(1.78f * plane_dist, 1.0f * plane_dist));
zedReady = false;
#if ZED_HDRP || ZED_URP
RenderPipelineManager.beginFrameRendering += SRPStartFrame;
#else
Camera.onPreRender += PreRender;
#endif
LoadHmdToZEDCalibration();
}
///
/// Computes the size of the final planes.
///
/// ZED's current resolution. Usually 1280x720.
/// Typically 1.
/// Distance from your eye to the camera. Estimated at 0.1m.
/// Distance to final quad (quadLeft or quadRight). Arbitrary but set by offset.z.
/// Focal length of the HMD, retrieved from the wrapper.
/// Focal length of the ZED, retrieved from the camera's rectified calibration parameters.
///
public Vector2 ComputeSizePlaneWithGamma(sl.Resolution resolution, float perceptionDistance, float eyeToZedDistance, float planeDistance, float HMDFocal, float zedFocal)
{
System.IntPtr p = dllz_compute_size_plane_with_gamma((int)resolution.width, (int)resolution.height, perceptionDistance, eyeToZedDistance, planeDistance, HMDFocal, zedFocal);
if (p == System.IntPtr.Zero)
{
return new Vector2();
}
Vector2 parameters = (Vector2)Marshal.PtrToStructure(p, typeof(Vector2));
return parameters;
}
///
/// Compute the focal length of the HMD.
///
/// Resolution of the headset's eye textures.
///
public float ComputeFocal(sl.Resolution targetSize)
{
float focal_hmd = dllz_compute_hmd_focal((int)targetSize.width, (int)targetSize.height, finalCenterEye.projectionMatrix.m00, finalCenterEye.projectionMatrix.m11);
return focal_hmd;
}
///
/// Called once the ZED is finished initializing. Subscribed to ZEDManager.OnZEDReady in OnEnable.
/// Uses the newly-available ZED parameters to scale the final planes (quadLeft and quadRight) to appear
/// properly in the currently-connected headset.
///
void ZEDReady()
{
Vector2 scaleFromZED;
halfBaselineOffset.x = zedCamera.Baseline / 2.0f;
float perception_distance = 1.0f;
float zed2eye_distance = 0.1f; //Estimating 10cm between your eye and physical location of the ZED Mini.
hasVRDevice = hasXRDevice();
if (hasVRDevice) {
sl.CalibrationParameters parameters = zedCamera.CalibrationParametersRectified;
scaleFromZED = ComputeSizePlaneWithGamma (new sl.Resolution ((uint)zedCamera.ImageWidth, (uint)zedCamera.ImageHeight),
perception_distance, zed2eye_distance, offset.z,
ComputeFocal (new sl.Resolution ((uint)XRSettings.eyeTextureWidth, (uint)XRSettings.eyeTextureHeight)),
parameters.leftCam.fx);
scale(quadCenter.gameObject, scaleFromZED);
}
ready = false;
// If using Vive, change ZED's settings to compensate for different screen.
if (getXRModelName().ToLower().Contains("vive"))
{
zedCamera.SetCameraSettings(sl.CAMERA_SETTINGS.CONTRAST, 3);
zedCamera.SetCameraSettings(sl.CAMERA_SETTINGS.SATURATION, 3);
}
//Set eye layers to respective eyes.
finalCenterEye.stereoTargetEye = StereoTargetEyeMask.Both;
/// AR Passtrough is recommended in 1280x720 at 60, due to FoV, FPS, etc.
/// If not set to this resolution, warn the user.
if (zedCamera.ImageWidth != 1280 && zedCamera.ImageHeight != 720)
Debug.LogWarning("[ZED AR Passthrough] This resolution is not ideal for a proper AR passthrough experience. Recommended resolution is 1280x720.");
zedReady = true;
}
public void OnEnable()
{
latencyCorrectionReady = false;
if (manager != null)
manager.OnZEDReady += ZEDReady;
}
public void OnDisable()
{
latencyCorrectionReady = false;
if (manager != null)
manager.OnZEDReady -= ZEDReady;
}
void OnGrab()
{
grabSucceeded = true;
}
///
/// Collects the position of the HMD with a timestamp, to be looked up later to correct for latency.
///
public void CollectPose()
{
if (manager == null)
return;
KeyPose k = new KeyPose();
InputTracking.GetNodeStates(nodeStates);
XRNodeState nodeState = nodeStates.Find(node => node.nodeType == XRNode.Head);
nodeState.TryGetRotation(out k.Orientation);
nodeState.TryGetPosition(out k.Translation);
if (manager.zedCamera.IsCameraReady)
{
k.Timestamp = manager.zedCamera.GetCurrentTimeStamp();
if (k.Timestamp >= 0)
{
dllz_latency_corrector_add_key_pose(ref k.Translation, ref k.Orientation, k.Timestamp); //Poses are handled by the wrapper.
}
}
}
///
/// Returns a pose at a specific time.
///
/// Rotation of the latency pose.
/// Translation/position of the latency pose.
/// Timestamp for looking up the pose.
/// Whether to use latency.
public int LatencyCorrector(out Quaternion r, out Vector3 t, ulong cameraTimeStamp, bool useLatency)
{
return dllz_latency_corrector_get_transform(cameraTimeStamp, useLatency, out t, out r);
}
///
/// Sets the GameObject's 3D local scale based on a 2D resolution (Z scale is unchanged).
/// Used for scaling quadLeft/quadRight.
///
/// Target GameObject to scale.
/// 2D scale factor.
public void scale(GameObject screen, Vector2 s)
{
screen.transform.localScale = new Vector3(s.x, s.y, 1);
}
///
/// Set the planes/canvases to the proper position after accounting for latency.
///
public void UpdateRenderPlane()
{
if (manager == null)
return;
if (!manager.IsStereoRig)
return; //Make sure we're in pass-through AR mode.
#if UNITY_2019_3_OR_NEWER
List eyes = new List();
InputDevices.GetDevicesWithCharacteristics(InputDeviceCharacteristics.HeadMounted, eyes);
if (eyes.Count > 0) // if a headset is detected
{
var eye = eyes[0];
eye.TryGetFeatureValue(CommonUsages.centerEyePosition, out Vector3 centerEyePosition);
eye.TryGetFeatureValue(CommonUsages.centerEyeRotation, out Quaternion centerEyeRotation);
finalCenterEye.transform.localPosition = centerEyePosition;
finalCenterEye.transform.localRotation = centerEyeRotation;
}
#endif
Quaternion r;
//Modified code to ensure view in HMD does not play like a movie screen
if (manager.inputType == sl.INPUT_TYPE.INPUT_TYPE_SVO || manager.inputType == sl.INPUT_TYPE.INPUT_TYPE_STREAM)
{
r = finalCameraCenter.transform.localRotation;
}
else
{
r = latencyPose.rotation;
}
// End of modified code
//Plane's distance from the final camera never changes, but it's rotated around it based on the latency pose.
quadCenter.localRotation = r;
quadCenter.localPosition = finalCenterEye.transform.localPosition + r * (offset);
}
///
/// Initialize the ZED's tracking with the current HMD position and HMD-ZED calibration.
/// This causes the ZED's internal tracking to start where the HMD is, despite being initialized later than the HMD.
///
/// Initial offset for the ZED's tracking.
public Pose InitTrackingAR()
{
if (manager == null)
return new Pose();
Transform tmpHMD = transform;
InputTracking.GetNodeStates(nodeStates);
XRNodeState nodeState = nodeStates.Find(node => node.nodeType == XRNode.Head);
nodeState.TryGetRotation(out Quaternion rot);
nodeState.TryGetPosition(out Vector3 pos);
Pose hmdTransform = new Pose(pos, rot);
Quaternion r = Quaternion.identity;
Vector3 t = Vector3.zero;
Pose const_offset = new Pose(t, r);
dllz_drift_corrector_set_calibration_const_offset_transform(ref const_offset);
zedCamera.ResetTrackingWithOffset(tmpHMD.rotation, tmpHMD.position, HmdToZEDCalibration.rotation, HmdToZEDCalibration.translation);
return new Pose(tmpHMD.position, tmpHMD.rotation);
}
///
/// Sets latencyPose to the pose of the headset at a given timestamp and flags whether or not it's valid for use.
///
/// Timestamp for looking up the pose.
public void ExtractLatencyPose(ulong cameraTimeStamp)
{
Quaternion latency_rot;
Vector3 latency_pos;
if (LatencyCorrector(out latency_rot, out latency_pos, cameraTimeStamp, true) == 1)
{
latencyPose = new Pose(latency_pos, latency_rot);
latencyCorrectionReady = true;
}
else
latencyCorrectionReady = false;
}
///
/// Returns the most recently retrieved latency pose.
///
///
/// Last retrieved latency pose.
public Pose LatencyPose()
{
return latencyPose;
}
///
/// Gets the proper position of the ZED virtual camera, factoring in HMD offset, latency, and anti-drift.
/// Used by ZEDManager to set the pose of Camera_eyes in the 'intermediate' rig (ZED_Rig_Stereo).
///
/// Current position as returned by the ZED's tracking.
/// Current rotation as returned by the ZED's tracking.
/// Final rotation.
/// Final translation/position.
public void AdjustTrackingAR(Vector3 position, Quaternion orientation, out Quaternion r, out Vector3 t, bool setimuprior)
{
hasVRDevice = hasXRDevice();
InputTracking.GetNodeStates(nodeStates);
XRNodeState nodeState = nodeStates.Find(node => node.nodeType == XRNode.Head);
nodeState.TryGetRotation(out Quaternion rot);
nodeState.TryGetPosition(out Vector3 pos);
Pose hmdTransform = new Pose(pos, rot);
trackingData.trackingState = (int)manager.ZEDTrackingState; //Whether the ZED's tracking is currently valid (not off or unable to localize).
trackingData.zedPathTransform = new Pose(position, orientation);
if (zedReady && latencyCorrectionReady && setimuprior == true)
{
zedCamera.SetIMUOrientationPrior(ref latencyPose.rotation);
}
dllz_drift_corrector_get_tracking_data(ref trackingData, ref hmdTransform, ref latencyPose, 0, true);
r = trackingData.offsetZedWorldTransform.rotation;
t = trackingData.offsetZedWorldTransform.translation;
}
///
/// Close related ZED processes when the application ends.
///
private void OnApplicationQuit()
{
dllz_latency_corrector_shutdown();
dllz_drift_corrector_shutdown();
}
///
/// Collects poses for latency correction, and updates the position of the rendering plane.
/// Also assigns textures from 'intermediate' cameras to the final quads' materials if ready and not done yet.
/// Called from ZEDManager.LateUpdate() so that it happens each frame after other tracking processes have finished.
///
public void LateUpdateHmdRendering()
{
if (!ready) //Make sure intermediate cameras are rendering to the quad's materials.
{
if (leftScreen.target != null && leftScreen.target.IsCreated())
{
centerMaterial.SetTexture(mainTexLeftID, leftScreen.target);
ready = true;
}
else ready = false;
if (rightScreen.target != null && rightScreen.target.IsCreated())
{
centerMaterial.SetTexture(mainTexRightID, rightScreen.target);
ready = true;
}
else ready = false;
}
if (hasVRDevice) //Do nothing if we no longer have a HMD connected.
{
CollectPose (); //File the current HMD pose into the latency poses to reference later.
UpdateRenderPlane(); //Reposition the final quads based on the latency pose.
}
}
#if ZED_HDRP || ZED_URP
private void SRPStartFrame(ScriptableRenderContext context, Camera[] cams)
{
foreach(Camera cam in cams) {
if (cam == finalCenterEye)
{
if ((!manager.IsZEDReady && manager.IsStereoRig))
{
System.Collections.Generic.List nodeStates = new System.Collections.Generic.List();
InputTracking.GetNodeStates(nodeStates);
XRNodeState nodeState = nodeStates.Find(node => node.nodeType == XRNode.Head);
nodeState.TryGetRotation(out Quaternion rot);
nodeState.TryGetPosition(out Vector3 pos);
quadCenter.localRotation = rot;
quadCenter.localPosition = pos + quadCenter.localRotation * offset;
}
}
}
}
#else
///
/// Before the ZED is ready, lock the quads in front of the cameras as latency correction isn't available yet.
/// This allows us to see the loading messages (and other virtual objects if desired) while the ZED is still loading.
/// Called by Camera.OnPreRender anytime any camera renders.
///
/// Cam.
public void PreRender(Camera cam)
{
if (cam == finalCenterEye)
{
if ((!manager.IsZEDReady && manager.IsStereoRig))
{
System.Collections.Generic.List nodeStates = new System.Collections.Generic.List();
InputTracking.GetNodeStates(nodeStates);
XRNodeState nodeState = nodeStates.Find(node => node.nodeType == XRNode.Head);
nodeState.TryGetRotation(out Quaternion rot);
nodeState.TryGetPosition(out Vector3 pos);
quadCenter.localRotation = rot;
quadCenter.localPosition = pos + quadCenter.localRotation * offset;
}
}
}
#endif
///
/// Loads the HMD to ZED calibration file and applies it to the hmdtozedCalibration offset.
/// Note that the file it loads is created using hard-coded values
/// and the ZED plugin doesn't ever change it. See CreateDefaultCalibrationFile().
///
public void LoadHmdToZEDCalibration()
{
if (hasVRDevice) {
/// Default calibration (may be changed)
hmdtozedCalibration.rotation = Quaternion.identity;
hmdtozedCalibration.translation.x = 0.0315f;//-zedCamera.Baseline/2;
hmdtozedCalibration.translation.y = 0.0f;
hmdtozedCalibration.translation.z = 0.11f;
//if a calibration exists then load it
//should be in ProgramData/stereolabs/mr/calibration.ini
string folder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.CommonApplicationData);
string specificFolder = Path.Combine(folder, @"Stereolabs\mr");
calibrationFilePath = Path.Combine(specificFolder, calibrationFile);
// Check if folder exists and if not, create it
if (!Directory.Exists(specificFolder))
{
Directory.CreateDirectory(specificFolder);
}
// Check if file exist and if not, create a default one
if (!ParseCalibrationFile(calibrationFilePath))
CreateDefaultCalibrationFile(calibrationFilePath);
// Set the calibration in mr processing
dllz_drift_corrector_set_calibration_transform(ref hmdtozedCalibration);
// Create a file system watcher for online modifications
CreateFileWatcher(specificFolder);
}
}
///
/// Creates a FileSystemEventHandler to watch the HMD-ZED calibration file and update settings if
/// it changes during runtime. If it does, calls OnChanged to fix tracking.
///
///
public void CreateFileWatcher(string folder)
{
// Create a new FileSystemWatcher and set its properties.
FileSystemWatcher watcher = new FileSystemWatcher();
watcher.Path = folder;
/* Watch for changes in LastAccess and LastWrite times, and
the renaming of files or directories. */
watcher.NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite
| NotifyFilters.FileName | NotifyFilters.DirectoryName;
// Only watch text files.
watcher.Filter = calibrationFile;
// Add event handlers.
watcher.Changed += new FileSystemEventHandler(OnChanged);
// Begin watching.
watcher.EnableRaisingEvents = true;
}
///
/// Reloads ZED-HMD offset calibration file and resets calibration accordintly.
/// Also calls OnHmdCalibChanged() which ZEDManager uses to run additional reset logic.
///
///
///
private void OnChanged(object source, FileSystemEventArgs e)
{
if (hasVRDevice)
{
ParseCalibrationFile(calibrationFilePath);
dllz_drift_corrector_set_calibration_transform(ref hmdtozedCalibration);
OnHmdCalibChanged();
}
}
///
/// Creates and saves a text file with the default ZED-HMD offset calibration parameters, to be loaded anytime this class runs in the future.
/// Values correspond to the distance from the center of the user's head to the ZED's left sensor.
///
/// Path to save the file.
private void CreateDefaultCalibrationFile(string path)
{
//Default Calibration: DO NOT CHANGE.
hmdtozedCalibration.rotation = Quaternion.identity;
hmdtozedCalibration.translation.x = -0.0315f;
hmdtozedCalibration.translation.y = 0.0f;
hmdtozedCalibration.translation.z = 0.115f;
//Write calibration file using default calibration.
using (System.IO.StreamWriter file = new System.IO.StreamWriter(path))
{
string node = "[HMD]";
string tx = "tx=" + hmdtozedCalibration.translation.x.ToString(System.Globalization.CultureInfo.InvariantCulture) + " //Translation x";
string ty = "ty=" + hmdtozedCalibration.translation.y.ToString(System.Globalization.CultureInfo.InvariantCulture) + " //Translation y";
string tz = "tz=" + hmdtozedCalibration.translation.z.ToString(System.Globalization.CultureInfo.InvariantCulture) + " //Translation z";
string rx = "rx=" + hmdtozedCalibration.rotation.x.ToString(System.Globalization.CultureInfo.InvariantCulture) + " //Quaternion x";
string ry = "ry=" + hmdtozedCalibration.rotation.y.ToString(System.Globalization.CultureInfo.InvariantCulture) + " //Quaternion y";
string rz = "rz=" + hmdtozedCalibration.rotation.z.ToString(System.Globalization.CultureInfo.InvariantCulture) + " //Quaternion z";
string rw = "rw=" + hmdtozedCalibration.rotation.w.ToString(System.Globalization.CultureInfo.InvariantCulture) + " //Quaternion w";
file.WriteLine(node);
file.WriteLine(tx);
file.WriteLine(ty);
file.WriteLine(tz);
file.WriteLine(rx);
file.WriteLine(ry);
file.WriteLine(rz);
file.WriteLine(rw);
file.Close();
}
}
///
/// Reads the ZED-HMD offset calibration file, if it exists, and loads calibration values to be applied to the final cameras.
/// Values correspond to the distance from the center of the user's head to the ZED's left sensor.
///
/// Path to save the file.
/// False if the file couldn't be loaded, whether empty, non-existant, etc.
private bool ParseCalibrationFile(string path)
{
if (!System.IO.File.Exists(path)) return false;
string[] lines = null;
try
{
lines = System.IO.File.ReadAllLines(path);
}
catch (System.Exception)
{
return false;
}
if (lines.Length == 0)
return false;
//Default to these values (which are the same ones put in the calibration file by default).
hmdtozedCalibration.rotation = Quaternion.identity;
hmdtozedCalibration.translation.x = -0.0315f;
hmdtozedCalibration.translation.y = 0.0f;
hmdtozedCalibration.translation.z = 0.115f;
foreach (string line in lines)
{
string[] splittedLine = line.Split('=');
if (splittedLine != null && splittedLine.Length >= 2)
{
string key = splittedLine[0];
string field = splittedLine[1].Split(' ')[0];
if (key == "tx")
{
hmdtozedCalibration.translation.x = float.Parse(field, System.Globalization.CultureInfo.InvariantCulture);
}
else if (key == "ty")
{
hmdtozedCalibration.translation.y = float.Parse(field, System.Globalization.CultureInfo.InvariantCulture);
}
else if (key == "tz")
{
hmdtozedCalibration.translation.z = float.Parse(field, System.Globalization.CultureInfo.InvariantCulture);
}
else if (key == "rx")
{
hmdtozedCalibration.rotation.x = float.Parse(field, System.Globalization.CultureInfo.InvariantCulture);
}
else if (key == "ry")
{
hmdtozedCalibration.rotation.y = float.Parse(field, System.Globalization.CultureInfo.InvariantCulture);
}
else if (key == "rz")
{
hmdtozedCalibration.rotation.z = float.Parse(field, System.Globalization.CultureInfo.InvariantCulture);
}
else if (key == "rw")
{
hmdtozedCalibration.rotation.w = float.Parse(field, System.Globalization.CultureInfo.InvariantCulture);
}
}
}
//Check if the calibration has values but they're all zeros.
if (hmdtozedCalibration.translation.x == 0.0f && hmdtozedCalibration.translation.y == 0.0f && hmdtozedCalibration.translation.z == 0.0f)
{
CreateDefaultCalibrationFile(path);
}
return true;
}
}