2024-11-12 17:24:59 +01:00

437 lines
16 KiB
C#

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<InputDevice> devices = new List<InputDevice>();
private InputDevice device;
private Eyes eyes;
private VarjoEyeTracking.GazeData gazeData;
private List<VarjoEyeTracking.GazeData> dataSinceLastUpdate;
private List<VarjoEyeTracking.EyeMeasurements> 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<MeshRenderer>().enabled = !gazeTarget.GetComponentInChildren<MeshRenderer>().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<RotateWithGaze>();
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();
}
}