using System; using System.IO; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.XR; using Varjo.XR; public enum GazeDataSource { InputSubsystem, GazeAPI } public class EyeTrackingExample : MonoBehaviour { [Header("Gaze data")] public GazeDataSource gazeDataSource = GazeDataSource.InputSubsystem; [Header("Gaze calibration settings")] public VarjoEyeTracking.GazeCalibrationMode gazeCalibrationMode = VarjoEyeTracking.GazeCalibrationMode.Fast; public KeyCode calibrationRequestKey = KeyCode.Space; [Header("Gaze output filter settings")] public VarjoEyeTracking.GazeOutputFilterType gazeOutputFilterType = VarjoEyeTracking.GazeOutputFilterType.Standard; public KeyCode setOutputFilterTypeKey = KeyCode.RightShift; [Header("Gaze data output frequency")] public VarjoEyeTracking.GazeOutputFrequency frequency; [Header("Toggle gaze target visibility")] public KeyCode toggleGazeTarget = KeyCode.Return; [Header("Debug Gaze")] public KeyCode checkGazeAllowed = KeyCode.PageUp; public KeyCode checkGazeCalibrated = KeyCode.PageDown; [Header("Toggle fixation point indicator visibility")] public bool showFixationPoint = true; [Header("Visualization Transforms")] public Transform fixationPointTransform; public Transform leftEyeTransform; public Transform rightEyeTransform; [Header("XR camera")] public Camera xrCamera; [Header("Gaze point indicator")] public GameObject gazeTarget; [Header("Gaze ray radius")] public float gazeRadius = 0.01f; [Header("Gaze point distance if not hit anything")] public float floatingGazeTargetDistance = 5f; [Header("Gaze target offset towards viewer")] public float targetOffset = 0.2f; [Header("Amout of force give to freerotating objects at point where user is looking")] public float hitForce = 5f; [Header("Gaze data logging")] public KeyCode loggingToggleKey = KeyCode.RightControl; [Header("Default path is Logs under application data path.")] public bool useCustomLogPath = false; public string customLogPath = ""; [Header("Print gaze data framerate while logging.")] public bool printFramerate = false; private List devices = new List(); private InputDevice device; private Eyes eyes; private VarjoEyeTracking.GazeData gazeData; private List dataSinceLastUpdate; private List eyeMeasurementsSinceLastUpdate; private Vector3 leftEyePosition; private Vector3 rightEyePosition; private Quaternion leftEyeRotation; private Quaternion rightEyeRotation; private Vector3 fixationPoint; private Vector3 direction; private Vector3 rayOrigin; private RaycastHit hit; private float distance; private StreamWriter writer = null; private bool logging = false; private static readonly string[] ColumnNames = { "Frame", "CaptureTime", "LogTime", "HMDPosition", "HMDRotation", "GazeStatus", "CombinedGazeForward", "CombinedGazePosition", "InterPupillaryDistanceInMM", "LeftEyeStatus", "LeftEyeForward", "LeftEyePosition", "LeftPupilIrisDiameterRatio", "LeftPupilDiameterInMM", "LeftIrisDiameterInMM", "RightEyeStatus", "RightEyeForward", "RightEyePosition", "RightPupilIrisDiameterRatio", "RightPupilDiameterInMM", "RightIrisDiameterInMM", "FocusDistance", "FocusStability" }; private const string ValidString = "VALID"; private const string InvalidString = "INVALID"; int gazeDataCount = 0; float gazeTimer = 0f; void GetDevice() { InputDevices.GetDevicesAtXRNode(XRNode.CenterEye, devices); device = devices.FirstOrDefault(); } void OnEnable() { if (!device.isValid) { GetDevice(); } } private void Start() { VarjoEyeTracking.SetGazeOutputFrequency(frequency); //Hiding the gazetarget if gaze is not available or if the gaze calibration is not done if (VarjoEyeTracking.IsGazeAllowed() && VarjoEyeTracking.IsGazeCalibrated()) { gazeTarget.SetActive(true); } else { gazeTarget.SetActive(false); } if (showFixationPoint) { fixationPointTransform.gameObject.SetActive(true); } else { fixationPointTransform.gameObject.SetActive(false); } } void Update() { if (logging && printFramerate) { gazeTimer += Time.deltaTime; if (gazeTimer >= 1.0f) { Debug.Log("Gaze data rows per second: " + gazeDataCount); gazeDataCount = 0; gazeTimer = 0f; } } // Request gaze calibration if (Input.GetKeyDown(calibrationRequestKey)) { VarjoEyeTracking.RequestGazeCalibration(gazeCalibrationMode); } // Set output filter type if (Input.GetKeyDown(setOutputFilterTypeKey)) { VarjoEyeTracking.SetGazeOutputFilterType(gazeOutputFilterType); Debug.Log("Gaze output filter type is now: " + VarjoEyeTracking.GetGazeOutputFilterType()); } // Check if gaze is allowed if (Input.GetKeyDown(checkGazeAllowed)) { Debug.Log("Gaze allowed: " + VarjoEyeTracking.IsGazeAllowed()); } // Check if gaze is calibrated if (Input.GetKeyDown(checkGazeCalibrated)) { Debug.Log("Gaze calibrated: " + VarjoEyeTracking.IsGazeCalibrated()); } // Toggle gaze target visibility if (Input.GetKeyDown(toggleGazeTarget)) { gazeTarget.GetComponentInChildren().enabled = !gazeTarget.GetComponentInChildren().enabled; } // Get gaze data if gaze is allowed and calibrated if (VarjoEyeTracking.IsGazeAllowed() && VarjoEyeTracking.IsGazeCalibrated()) { //Get device if not valid if (!device.isValid) { GetDevice(); } // Show gaze target gazeTarget.SetActive(true); if (gazeDataSource == GazeDataSource.InputSubsystem) { // Get data for eye positions, rotations and the fixation point if (device.TryGetFeatureValue(CommonUsages.eyesData, out eyes)) { if (eyes.TryGetLeftEyePosition(out leftEyePosition)) { leftEyeTransform.localPosition = leftEyePosition; } if (eyes.TryGetLeftEyeRotation(out leftEyeRotation)) { leftEyeTransform.localRotation = leftEyeRotation; } if (eyes.TryGetRightEyePosition(out rightEyePosition)) { rightEyeTransform.localPosition = rightEyePosition; } if (eyes.TryGetRightEyeRotation(out rightEyeRotation)) { rightEyeTransform.localRotation = rightEyeRotation; } if (eyes.TryGetFixationPoint(out fixationPoint)) { fixationPointTransform.localPosition = fixationPoint; } } // Set raycast origin point to VR camera position rayOrigin = xrCamera.transform.position; // Direction from VR camera towards fixation point direction = (fixationPointTransform.position - xrCamera.transform.position).normalized; } else { gazeData = VarjoEyeTracking.GetGaze(); if (gazeData.status != VarjoEyeTracking.GazeStatus.Invalid) { // GazeRay vectors are relative to the HMD pose so they need to be transformed to world space if (gazeData.leftStatus != VarjoEyeTracking.GazeEyeStatus.Invalid) { leftEyeTransform.position = xrCamera.transform.TransformPoint(gazeData.left.origin); leftEyeTransform.rotation = Quaternion.LookRotation(xrCamera.transform.TransformDirection(gazeData.left.forward)); } if (gazeData.rightStatus != VarjoEyeTracking.GazeEyeStatus.Invalid) { rightEyeTransform.position = xrCamera.transform.TransformPoint(gazeData.right.origin); rightEyeTransform.rotation = Quaternion.LookRotation(xrCamera.transform.TransformDirection(gazeData.right.forward)); } // Set gaze origin as raycast origin rayOrigin = xrCamera.transform.TransformPoint(gazeData.gaze.origin); // Set gaze direction as raycast direction direction = xrCamera.transform.TransformDirection(gazeData.gaze.forward); // Fixation point can be calculated using ray origin, direction and focus distance fixationPointTransform.position = rayOrigin + direction * gazeData.focusDistance; } } } // Raycast to world from VR Camera position towards fixation point if (Physics.SphereCast(rayOrigin, gazeRadius, direction, out hit)) { // Put target on gaze raycast position with offset towards user gazeTarget.transform.position = hit.point - direction * targetOffset; // Make gaze target point towards user gazeTarget.transform.LookAt(rayOrigin, Vector3.up); // Scale gazetarget with distance so it apperas to be always same size distance = hit.distance; gazeTarget.transform.localScale = Vector3.one * distance; // Prefer layers or tags to identify looked objects in your application // This is done here using GetComponent for the sake of clarity as an example RotateWithGaze rotateWithGaze = hit.collider.gameObject.GetComponent(); if (rotateWithGaze != null) { rotateWithGaze.RayHit(); } // Alternative way to check if you hit object with tag if (hit.transform.CompareTag("FreeRotating")) { AddForceAtHitPosition(); } } else { // If gaze ray didn't hit anything, the gaze target is shown at fixed distance gazeTarget.transform.position = rayOrigin + direction * floatingGazeTargetDistance; gazeTarget.transform.LookAt(rayOrigin, Vector3.up); gazeTarget.transform.localScale = Vector3.one * floatingGazeTargetDistance; } if (Input.GetKeyDown(loggingToggleKey)) { if (!logging) { StartLogging(); } else { StopLogging(); } return; } if (logging) { int dataCount = VarjoEyeTracking.GetGazeList(out dataSinceLastUpdate, out eyeMeasurementsSinceLastUpdate); if (printFramerate) gazeDataCount += dataCount; for (int i = 0; i < dataCount; i++) { LogGazeData(dataSinceLastUpdate[i], eyeMeasurementsSinceLastUpdate[i]); } } } void AddForceAtHitPosition() { //Get Rigidbody form hit object and add force on hit position Rigidbody rb = hit.rigidbody; if (rb != null) { rb.AddForceAtPosition(direction * hitForce, hit.point, ForceMode.Force); } } void LogGazeData(VarjoEyeTracking.GazeData data, VarjoEyeTracking.EyeMeasurements eyeMeasurements) { string[] logData = new string[23]; // Gaze data frame number logData[0] = data.frameNumber.ToString(); // Gaze data capture time (nanoseconds) logData[1] = data.captureTime.ToString(); // Log time (milliseconds) logData[2] = (DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond).ToString(); // HMD logData[3] = xrCamera.transform.localPosition.ToString("F3"); logData[4] = xrCamera.transform.localRotation.ToString("F3"); // Combined gaze bool invalid = data.status == VarjoEyeTracking.GazeStatus.Invalid; logData[5] = invalid ? InvalidString : ValidString; logData[6] = invalid ? "" : data.gaze.forward.ToString("F3"); logData[7] = invalid ? "" : data.gaze.origin.ToString("F3"); // IPD logData[8] = invalid ? "" : eyeMeasurements.interPupillaryDistanceInMM.ToString("F3"); // Left eye bool leftInvalid = data.leftStatus == VarjoEyeTracking.GazeEyeStatus.Invalid; logData[9] = leftInvalid ? InvalidString : ValidString; logData[10] = leftInvalid ? "" : data.left.forward.ToString("F3"); logData[11] = leftInvalid ? "" : data.left.origin.ToString("F3"); logData[12] = leftInvalid ? "" : eyeMeasurements.leftPupilIrisDiameterRatio.ToString("F3"); logData[13] = leftInvalid ? "" : eyeMeasurements.leftPupilDiameterInMM.ToString("F3"); logData[14] = leftInvalid ? "" : eyeMeasurements.leftIrisDiameterInMM.ToString("F3"); // Right eye bool rightInvalid = data.rightStatus == VarjoEyeTracking.GazeEyeStatus.Invalid; logData[15] = rightInvalid ? InvalidString : ValidString; logData[16] = rightInvalid ? "" : data.right.forward.ToString("F3"); logData[17] = rightInvalid ? "" : data.right.origin.ToString("F3"); logData[18] = rightInvalid ? "" : eyeMeasurements.rightPupilIrisDiameterRatio.ToString("F3"); logData[19] = rightInvalid ? "" : eyeMeasurements.rightPupilDiameterInMM.ToString("F3"); logData[20] = rightInvalid ? "" : eyeMeasurements.rightIrisDiameterInMM.ToString("F3"); // Focus logData[21] = invalid ? "" : data.focusDistance.ToString(); logData[22] = invalid ? "" : data.focusStability.ToString(); Log(logData); } // Write given values in the log file void Log(string[] values) { if (!logging || writer == null) return; string line = ""; for (int i = 0; i < values.Length; ++i) { values[i] = values[i].Replace("\r", "").Replace("\n", ""); // Remove new lines so they don't break csv line += values[i] + (i == (values.Length - 1) ? "" : ";"); // Do not add semicolon to last data string } writer.WriteLine(line); } public void StartLogging() { if (logging) { Debug.LogWarning("Logging was on when StartLogging was called. No new log was started."); return; } logging = true; string logPath = useCustomLogPath ? customLogPath : Application.dataPath + "/Logs/"; Directory.CreateDirectory(logPath); DateTime now = DateTime.Now; string fileName = string.Format("{0}-{1:00}-{2:00}-{3:00}-{4:00}", now.Year, now.Month, now.Day, now.Hour, now.Minute); string path = logPath + fileName + ".csv"; writer = new StreamWriter(path); Log(ColumnNames); Debug.Log("Log file started at: " + path); } void StopLogging() { if (!logging) return; if (writer != null) { writer.Flush(); writer.Close(); writer = null; } logging = false; Debug.Log("Logging ended"); } void OnApplicationQuit() { StopLogging(); } }