611 lines
22 KiB
C++
611 lines
22 KiB
C++
|
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
||
|
|
|
||
|
|
#include "ViveOpenXREyeTracker.h"
|
||
|
|
#include "EyeTrackerTypes.h"
|
||
|
|
#include "IXRTrackingSystem.h"
|
||
|
|
#include "GameFramework/HUD.h"
|
||
|
|
#include "OpenXRCore.h"
|
||
|
|
#include "DrawDebugHelpers.h"
|
||
|
|
#include "Engine/Engine.h"
|
||
|
|
#include "Interfaces/IPluginManager.h"
|
||
|
|
|
||
|
|
#if WITH_EDITOR
|
||
|
|
#endif
|
||
|
|
|
||
|
|
IMPLEMENT_MODULE(FViveOpenXREyeTrackerModule, ViveOpenXREyeTracker);
|
||
|
|
|
||
|
|
DEFINE_LOG_CATEGORY(LogViveOpenXREyeTracker);
|
||
|
|
|
||
|
|
static TAutoConsoleVariable<int32> CVarEnableOpenXREyetrackingDebug(TEXT("OpenXR.debug.EnableEyetrackingDebug"), 1, TEXT("0 - Eyetracking debug visualizations are disabled. 1 - Eyetracking debug visualizations are enabled."));
|
||
|
|
|
||
|
|
FViveOpenXREyeTracker::FViveOpenXREyeTracker()
|
||
|
|
{
|
||
|
|
RegisterOpenXRExtensionModularFeature();
|
||
|
|
}
|
||
|
|
|
||
|
|
FViveOpenXREyeTracker::~FViveOpenXREyeTracker()
|
||
|
|
{
|
||
|
|
Destroy();
|
||
|
|
}
|
||
|
|
|
||
|
|
void FViveOpenXREyeTracker::Destroy()
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
bool FViveOpenXREyeTracker::GetRequiredExtensions(TArray<const ANSICHAR*>& OutExtensions)
|
||
|
|
{
|
||
|
|
TArray<XrApiLayerProperties> Properties;
|
||
|
|
uint32_t Count = 0;
|
||
|
|
XR_ENSURE(xrEnumerateApiLayerProperties(0, &Count, nullptr));
|
||
|
|
Properties.SetNum(Count);
|
||
|
|
for (auto& Prop : Properties)
|
||
|
|
{
|
||
|
|
Prop = XrApiLayerProperties{ XR_TYPE_API_LAYER_PROPERTIES };
|
||
|
|
}
|
||
|
|
XR_ENSURE(xrEnumerateApiLayerProperties(Count, &Count, Properties.GetData()));
|
||
|
|
|
||
|
|
// Some API layers can crash the loader when enabled, if they're present we shouldn't enable the extension
|
||
|
|
for (const XrApiLayerProperties& Layer : Properties)
|
||
|
|
{
|
||
|
|
if (FCStringAnsi::Strstr(Layer.layerName, "XR_APILAYER_VIVE_eye_tracking") &&
|
||
|
|
Layer.layerVersion <= 1)
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
OutExtensions.Add("XR_EXT_eye_gaze_interaction");
|
||
|
|
OutExtensions.Add("XR_HTC_eye_tracker");
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
void FViveOpenXREyeTracker::PostCreateInstance(XrInstance InInstance)
|
||
|
|
{
|
||
|
|
Instance = InInstance;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool FViveOpenXREyeTracker::GetInteractionProfile(XrInstance InInstance, FString& OutKeyPrefix, XrPath& OutPath, bool& OutHasHaptics)
|
||
|
|
{
|
||
|
|
OutKeyPrefix = "EyeTracker";
|
||
|
|
OutHasHaptics = false;
|
||
|
|
return xrStringToPath(InInstance, "/interaction_profiles/ext/eye_gaze_interaction", &OutPath) == XR_SUCCESS;
|
||
|
|
}
|
||
|
|
|
||
|
|
void FViveOpenXREyeTracker::PostGetSystem(XrInstance InInstance, XrSystemId InSystem)
|
||
|
|
{
|
||
|
|
SystemId = InSystem;
|
||
|
|
XR_ENSURE(xrGetInstanceProcAddr(InInstance, "xrGetSystemProperties", (PFN_xrVoidFunction*)&FHTCEyeTracker_ext.xrGetSystemProperties));
|
||
|
|
|
||
|
|
XrSystemEyeTrackingPropertiesHTC eyeTrackingSystemProperties{ XR_TYPE_SYSTEM_EYE_TRACKING_PROPERTIES_HTC };
|
||
|
|
eyeTrackingSystemProperties.next = NULL;
|
||
|
|
XrSystemProperties systemProperties{ XR_TYPE_SYSTEM_PROPERTIES, &eyeTrackingSystemProperties };
|
||
|
|
|
||
|
|
XrResult result = FHTCEyeTracker_ext.xrGetSystemProperties(Instance, SystemId, &systemProperties);
|
||
|
|
XR_ENSURE(result);
|
||
|
|
|
||
|
|
if (!eyeTrackingSystemProperties.supportsEyeTracking) {
|
||
|
|
// The system does not support eye tracking.
|
||
|
|
isHTCEyeTrackerSupported = false;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
isHTCEyeTrackerSupported = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
void FViveOpenXREyeTracker::PostCreateSession(XrSession InSession)
|
||
|
|
{
|
||
|
|
Session = InSession;
|
||
|
|
}
|
||
|
|
|
||
|
|
const void* FViveOpenXREyeTracker::OnCreateSession(XrInstance InInstance, XrSystemId InSystem, const void* InNext)
|
||
|
|
{
|
||
|
|
if (!isHTCEyeTrackerSupported) return InNext;
|
||
|
|
XR_ENSURE(xrGetInstanceProcAddr(InInstance, "xrCreateEyeTrackerHTC", (PFN_xrVoidFunction*)&FHTCEyeTracker_ext.xrCreateEyeTrackerHTC));
|
||
|
|
XR_ENSURE(xrGetInstanceProcAddr(InInstance, "xrDestroyEyeTrackerHTC", (PFN_xrVoidFunction*)&FHTCEyeTracker_ext.xrDestroyEyeTrackerHTC));
|
||
|
|
XR_ENSURE(xrGetInstanceProcAddr(InInstance, "xrGetEyeGazeDataHTC", (PFN_xrVoidFunction*)&FHTCEyeTracker_ext.xrGetEyeGazeDataHTC));
|
||
|
|
XR_ENSURE(xrGetInstanceProcAddr(InInstance, "xrGetEyePupilDataHTC", (PFN_xrVoidFunction*)&FHTCEyeTracker_ext.xrGetEyePupilDataHTC));
|
||
|
|
XR_ENSURE(xrGetInstanceProcAddr(InInstance, "xrGetEyeGeometricDataHTC", (PFN_xrVoidFunction*)&FHTCEyeTracker_ext.xrGetEyeGeometricDataHTC));
|
||
|
|
return InNext;
|
||
|
|
}
|
||
|
|
|
||
|
|
void FViveOpenXREyeTracker::AttachActionSets(TSet<XrActionSet>& OutActionSets)
|
||
|
|
{
|
||
|
|
check(Instance != XR_NULL_HANDLE);
|
||
|
|
|
||
|
|
// This is a bit of a pain right now. Hopefully future refactors will make it better.
|
||
|
|
// We are creating an action set for our eye tracking pose. An action set can have session create/destroy
|
||
|
|
// lifetime. However currently the OpenXRInput module is loaded well after the OpenXRHMDModule creates
|
||
|
|
// the session, so we can't just setup all this input system right then. OpenXRInput instead checks if it is live and if
|
||
|
|
// not destroys any existing sets and then creates new ones near xrBeginSession (where the session starts running).
|
||
|
|
// It marks them as dead near xrDestroySession so that they will be destroyed on the BeginSession of the next created session,
|
||
|
|
// if any.
|
||
|
|
//
|
||
|
|
// To mirror that lifetime we are destroying and creating in AttachActionSets and marking as dead in OnDestroySession.
|
||
|
|
//
|
||
|
|
// Note the ActionSpace is easier because it is never used outside of this ExtensionPlugin. We are creating it, if necessary,
|
||
|
|
// in OnBeginSession and destroying it in OnDestroySession. If we had a good CreateSession hook we could create it then, along with
|
||
|
|
// the ActionSet and Action.
|
||
|
|
|
||
|
|
// We could have an action set from a previous session. If so it needs to go away.
|
||
|
|
if (EyeTrackerActionSet != XR_NULL_HANDLE)
|
||
|
|
{
|
||
|
|
xrDestroyActionSet(EyeTrackerActionSet);
|
||
|
|
EyeTrackerActionSet = XR_NULL_HANDLE;
|
||
|
|
EyeTrackerAction = XR_NULL_HANDLE;
|
||
|
|
}
|
||
|
|
|
||
|
|
{
|
||
|
|
XrActionSetCreateInfo Info;
|
||
|
|
Info.type = XR_TYPE_ACTION_SET_CREATE_INFO;
|
||
|
|
Info.next = nullptr;
|
||
|
|
FCStringAnsi::Strcpy(Info.actionSetName, XR_MAX_ACTION_SET_NAME_SIZE, "openxreyetrackeractionset");
|
||
|
|
FCStringAnsi::Strcpy(Info.localizedActionSetName, XR_MAX_LOCALIZED_ACTION_SET_NAME_SIZE, "OpenXR Eye Tracker Action Set");
|
||
|
|
Info.priority = 0;
|
||
|
|
XR_ENSURE(xrCreateActionSet(Instance, &Info, &EyeTrackerActionSet));
|
||
|
|
}
|
||
|
|
|
||
|
|
{
|
||
|
|
check(EyeTrackerAction == XR_NULL_HANDLE);
|
||
|
|
XrActionCreateInfo Info;
|
||
|
|
Info.type = XR_TYPE_ACTION_CREATE_INFO;
|
||
|
|
Info.next = nullptr;
|
||
|
|
Info.countSubactionPaths = 0;
|
||
|
|
Info.subactionPaths = nullptr;
|
||
|
|
FCStringAnsi::Strcpy(Info.actionName, XR_MAX_ACTION_NAME_SIZE, "openxreyetrackeraction");
|
||
|
|
FCStringAnsi::Strcpy(Info.localizedActionName, XR_MAX_LOCALIZED_ACTION_NAME_SIZE, "OpenXR Eye Tracker Action");
|
||
|
|
Info.actionType = XR_ACTION_TYPE_POSE_INPUT;
|
||
|
|
XR_ENSURE(xrCreateAction(EyeTrackerActionSet, &Info, &EyeTrackerAction));
|
||
|
|
}
|
||
|
|
|
||
|
|
{
|
||
|
|
XrPath EyeGazeInteractionProfilePath = XR_NULL_PATH;
|
||
|
|
XR_ENSURE(xrStringToPath(Instance, "/interaction_profiles/ext/eye_gaze_interaction", &EyeGazeInteractionProfilePath));
|
||
|
|
|
||
|
|
XrPath GazePosePath = XR_NULL_PATH;
|
||
|
|
XR_ENSURE(xrStringToPath(Instance, "/user/eyes_ext/input/gaze_ext/pose", &GazePosePath));
|
||
|
|
|
||
|
|
XrActionSuggestedBinding Bindings;
|
||
|
|
Bindings.action = EyeTrackerAction;
|
||
|
|
Bindings.binding = GazePosePath;
|
||
|
|
|
||
|
|
XrInteractionProfileSuggestedBinding SuggestedBindings{ XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING };
|
||
|
|
SuggestedBindings.interactionProfile = EyeGazeInteractionProfilePath;
|
||
|
|
SuggestedBindings.suggestedBindings = &Bindings;
|
||
|
|
SuggestedBindings.countSuggestedBindings = 1;
|
||
|
|
XR_ENSURE(xrSuggestInteractionProfileBindings(Instance, &SuggestedBindings));
|
||
|
|
}
|
||
|
|
|
||
|
|
OutActionSets.Add(EyeTrackerActionSet);
|
||
|
|
}
|
||
|
|
|
||
|
|
const void* FViveOpenXREyeTracker::OnBeginSession(XrSession InSession, const void* InNext)
|
||
|
|
{
|
||
|
|
static FName SystemName(TEXT("OpenXR"));
|
||
|
|
if (GEngine->XRSystem.IsValid() && (GEngine->XRSystem->GetSystemName() == SystemName))
|
||
|
|
{
|
||
|
|
XRTrackingSystem = GEngine->XRSystem.Get();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (GazeActionSpace == XR_NULL_HANDLE)
|
||
|
|
{
|
||
|
|
GazeActionSpace = XR_NULL_HANDLE;
|
||
|
|
XrActionSpaceCreateInfo CreateActionSpaceInfo{ XR_TYPE_ACTION_SPACE_CREATE_INFO };
|
||
|
|
check(EyeTrackerAction != XR_NULL_HANDLE);
|
||
|
|
CreateActionSpaceInfo.action = EyeTrackerAction;
|
||
|
|
CreateActionSpaceInfo.poseInActionSpace = ToXrPose(FTransform::Identity);
|
||
|
|
XR_ENSURE(xrCreateActionSpace(InSession, &CreateActionSpaceInfo, &GazeActionSpace));
|
||
|
|
|
||
|
|
SyncInfo.countActiveActionSets = 0;
|
||
|
|
SyncInfo.activeActionSets = XR_NULL_HANDLE;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isHTCEyeTrackerSupported && EyeTrackerHTC == XR_NULL_HANDLE)
|
||
|
|
{
|
||
|
|
XrResult result = XrResult::XR_ERROR_HANDLE_INVALID;
|
||
|
|
|
||
|
|
XrEyeTrackerCreateInfoHTC createInfo{ XR_TYPE_EYE_TRACKER_CREATE_INFO_HTC };
|
||
|
|
result = FHTCEyeTracker_ext.xrCreateEyeTrackerHTC(Session, &createInfo, &EyeTrackerHTC);
|
||
|
|
|
||
|
|
XR_ENSURE(result);
|
||
|
|
}
|
||
|
|
|
||
|
|
bSessionStarted = true;
|
||
|
|
|
||
|
|
return InNext;
|
||
|
|
}
|
||
|
|
|
||
|
|
void FViveOpenXREyeTracker::OnDestroySession(XrSession InSession)
|
||
|
|
{
|
||
|
|
if (GazeActionSpace)
|
||
|
|
{
|
||
|
|
XR_ENSURE(xrDestroySpace(GazeActionSpace));
|
||
|
|
}
|
||
|
|
GazeActionSpace = XR_NULL_HANDLE;
|
||
|
|
|
||
|
|
XrResult result = XrResult::XR_ERROR_HANDLE_INVALID;
|
||
|
|
if (isHTCEyeTrackerSupported)
|
||
|
|
{
|
||
|
|
if (EyeTrackerHTC == XR_NULL_HANDLE)
|
||
|
|
{
|
||
|
|
UE_LOG(LogViveOpenXREyeTracker, Error, TEXT("[DestroyEyeTrackerHTC] eyeTracker is XR_NULL_HANDLE."));
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
result = FHTCEyeTracker_ext.xrDestroyEyeTrackerHTC(EyeTrackerHTC);
|
||
|
|
EyeTrackerHTC = XR_NULL_HANDLE;
|
||
|
|
}
|
||
|
|
XR_ENSURE(result);
|
||
|
|
if (result != XR_SUCCESS)
|
||
|
|
{
|
||
|
|
UE_LOG(LogViveOpenXREyeTracker, Error, TEXT("[DestroyEyeTrackerHTC] DestroyEyeTrackerHTC failed: %s"), OpenXRResultToString(result));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void FViveOpenXREyeTracker::GetActiveActionSetsForSync(TArray<XrActiveActionSet>& OutActiveSets)
|
||
|
|
{
|
||
|
|
check(EyeTrackerActionSet != XR_NULL_HANDLE);
|
||
|
|
|
||
|
|
OutActiveSets.Add(XrActiveActionSet{ EyeTrackerActionSet, XR_NULL_PATH });
|
||
|
|
}
|
||
|
|
|
||
|
|
void FViveOpenXREyeTracker::PostSyncActions(XrSession InSession)
|
||
|
|
{
|
||
|
|
check(EyeTrackerAction != XR_NULL_HANDLE);
|
||
|
|
|
||
|
|
XrActionStateGetInfo GetActionStateInfo{ XR_TYPE_ACTION_STATE_GET_INFO };
|
||
|
|
GetActionStateInfo.action = EyeTrackerAction;
|
||
|
|
XR_ENSURE(xrGetActionStatePose(InSession, &GetActionStateInfo, &ActionStatePose));
|
||
|
|
}
|
||
|
|
|
||
|
|
void FViveOpenXREyeTracker::UpdateDeviceLocations(XrSession InSession, XrTime DisplayTime, XrSpace TrackingSpace)
|
||
|
|
{
|
||
|
|
if (ActionStatePose.isActive)
|
||
|
|
{
|
||
|
|
check(GazeActionSpace != XR_NULL_HANDLE);
|
||
|
|
XR_ENSURE(xrLocateSpace(GazeActionSpace, TrackingSpace, DisplayTime, &EyeTrackerSpaceLocation));
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// Clear the tracked bits if the action is not active
|
||
|
|
const XrSpaceLocationFlags TrackedFlags = XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT | XR_SPACE_LOCATION_POSITION_TRACKED_BIT;
|
||
|
|
EyeTrackerSpaceLocation.locationFlags &= ~TrackedFlags;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isHTCEyeTrackerSupported)
|
||
|
|
{
|
||
|
|
const float WorldToMetersScale = XRTrackingSystem->GetWorldToMetersScale();
|
||
|
|
XrResult result = XrResult::XR_ERROR_HANDLE_INVALID;
|
||
|
|
|
||
|
|
// Query Gaze information
|
||
|
|
XrEyeGazeDataInfoHTC gazesInfo{ XR_TYPE_EYE_GAZE_DATA_INFO_HTC };
|
||
|
|
gazesInfo.baseSpace = TrackingSpace;
|
||
|
|
gazesInfo.time = DisplayTime;
|
||
|
|
result = FHTCEyeTracker_ext.xrGetEyeGazeDataHTC(EyeTrackerHTC, &gazesInfo, &EyeGazesHTC);
|
||
|
|
XR_ENSURE(result);
|
||
|
|
if (result != XR_SUCCESS)
|
||
|
|
{
|
||
|
|
UE_LOG(LogViveOpenXREyeTracker, Error, TEXT("[GetEyeGazesHTC] xrGetEyeGazesHTC failed: %s"), OpenXRResultToString(result));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Query Pupil information
|
||
|
|
XrEyePupilDataInfoHTC pupilDatasInfo{ XR_TYPE_EYE_PUPIL_DATA_INFO_HTC };
|
||
|
|
result = FHTCEyeTracker_ext.xrGetEyePupilDataHTC(EyeTrackerHTC, &pupilDatasInfo, &PupilDatasHTC);
|
||
|
|
XR_ENSURE(result);
|
||
|
|
if (result != XR_SUCCESS)
|
||
|
|
{
|
||
|
|
UE_LOG(LogViveOpenXREyeTracker, Error, TEXT("[GetPupilDatasHTC] xrGetPupilDatasHTC failed: %s"), OpenXRResultToString(result));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Query Geometric information
|
||
|
|
XrEyeGeometricDataInfoHTC eyeGeometricDatasInfo{ XR_TYPE_EYE_GEOMETRIC_DATA_INFO_HTC };
|
||
|
|
result = FHTCEyeTracker_ext.xrGetEyeGeometricDataHTC(EyeTrackerHTC, &eyeGeometricDatasInfo, &EyeGeometricDatasHTC);
|
||
|
|
XR_ENSURE(result);
|
||
|
|
if (result != XR_SUCCESS)
|
||
|
|
{
|
||
|
|
UE_LOG(LogViveOpenXREyeTracker, Error, TEXT("[GetEyeGeometricDatasHTC] xrGetEyeGeometricDatasHTC failed: %s"), OpenXRResultToString(result));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool FViveOpenXREyeTracker::GetEyeTrackerGazeData(FEyeTrackerGazeData& OutGazeData) const
|
||
|
|
{
|
||
|
|
if (!bSessionStarted)
|
||
|
|
{
|
||
|
|
OutGazeData = FEyeTrackerGazeData();
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
const XrSpaceLocationFlags ValidFlags = XR_SPACE_LOCATION_ORIENTATION_VALID_BIT | XR_SPACE_LOCATION_POSITION_VALID_BIT;
|
||
|
|
const XrSpaceLocationFlags TrackedFlags = XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT | XR_SPACE_LOCATION_POSITION_TRACKED_BIT;
|
||
|
|
|
||
|
|
if ((EyeTrackerSpaceLocation.locationFlags & ValidFlags) != ValidFlags)
|
||
|
|
{
|
||
|
|
// Either Orientation or position are invalid
|
||
|
|
OutGazeData = FEyeTrackerGazeData();
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
else if ((EyeTrackerSpaceLocation.locationFlags & TrackedFlags) != TrackedFlags)
|
||
|
|
{
|
||
|
|
// Orientation and/or position are old or an estimate of some kind, confidence is low.
|
||
|
|
OutGazeData.ConfidenceValue = 0.0f;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// Both orientation and position are fully tracked now, confidence is high.
|
||
|
|
OutGazeData.ConfidenceValue = 1.0f;
|
||
|
|
}
|
||
|
|
|
||
|
|
const float WorldToMetersScale = XRTrackingSystem->GetWorldToMetersScale();
|
||
|
|
const XrPosef& Pose = EyeTrackerSpaceLocation.pose;
|
||
|
|
const FTransform EyeTrackerTransform = ToFTransform(Pose, WorldToMetersScale);
|
||
|
|
|
||
|
|
const FTransform& TrackingToWoldTransform = XRTrackingSystem->GetTrackingToWorldTransform();
|
||
|
|
const FTransform EyeTransform = EyeTrackerTransform * TrackingToWoldTransform;
|
||
|
|
|
||
|
|
OutGazeData.GazeDirection = EyeTransform.TransformVector(FVector::ForwardVector);
|
||
|
|
OutGazeData.GazeOrigin = EyeTransform.GetLocation();
|
||
|
|
OutGazeData.FixationPoint = FVector::ZeroVector; //not supported
|
||
|
|
|
||
|
|
if (isHTCEyeTrackerSupported)
|
||
|
|
{
|
||
|
|
OutGazeData.LeftPupilDiameter = PupilDatasHTC.pupils[XR_EYE_POSITION_LEFT_HTC].pupilDiameter;
|
||
|
|
OutGazeData.RightPupilDiameter = PupilDatasHTC.pupils[XR_EYE_POSITION_RIGHT_HTC].pupilDiameter;
|
||
|
|
|
||
|
|
if (EyeGeometricDatasHTC.geometrics[XR_EYE_POSITION_LEFT_HTC].eyeOpenness <= 0.0f)
|
||
|
|
OutGazeData.bIsLeftEyeBlink = true;
|
||
|
|
else
|
||
|
|
OutGazeData.bIsLeftEyeBlink = false;
|
||
|
|
|
||
|
|
if (EyeGeometricDatasHTC.geometrics[XR_EYE_POSITION_RIGHT_HTC].eyeOpenness <= 0.0f)
|
||
|
|
OutGazeData.bIsRightEyeBlink = true;
|
||
|
|
else
|
||
|
|
OutGazeData.bIsRightEyeBlink = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Integrate with HTC EyeTracker
|
||
|
|
bool FViveOpenXREyeTracker::GetEyeTrackerStereoGazeData(FEyeTrackerStereoGazeData& OutStereoGazeData) const
|
||
|
|
{
|
||
|
|
if (!isHTCEyeTrackerSupported) return false;
|
||
|
|
FEyeTrackerStereoGazeData OutGazeDataHTC;
|
||
|
|
const float WorldToMetersScale = XRTrackingSystem->GetWorldToMetersScale();
|
||
|
|
const FTransform& TrackingToWoldTransform = XRTrackingSystem->GetTrackingToWorldTransform();
|
||
|
|
|
||
|
|
if ((bool)EyeGazesHTC.gazes[XR_EYE_POSITION_RIGHT_HTC].isValid && (bool)EyeGazesHTC.gazes[XR_EYE_POSITION_LEFT_HTC].isValid)
|
||
|
|
OutGazeDataHTC.ConfidenceValue = 1.0f;
|
||
|
|
else if ((bool)EyeGazesHTC.gazes[XR_EYE_POSITION_RIGHT_HTC].isValid || (bool)EyeGazesHTC.gazes[XR_EYE_POSITION_LEFT_HTC].isValid)
|
||
|
|
OutGazeDataHTC.ConfidenceValue = 0.5f;
|
||
|
|
else
|
||
|
|
OutGazeDataHTC.ConfidenceValue = 0.0f;
|
||
|
|
|
||
|
|
XrPosef RightPose = EyeGazesHTC.gazes[XR_EYE_POSITION_RIGHT_HTC].gazePose;
|
||
|
|
const FTransform RightEyeTrackerTransform = ToFTransform(RightPose, WorldToMetersScale);
|
||
|
|
const FTransform RightEyeTransform = RightEyeTrackerTransform * TrackingToWoldTransform;
|
||
|
|
|
||
|
|
OutGazeDataHTC.RightEyeDirection = RightEyeTransform.TransformVector(FVector::ForwardVector);
|
||
|
|
OutGazeDataHTC.RightEyeOrigin = RightEyeTransform.GetLocation();
|
||
|
|
|
||
|
|
XrPosef LeftPose = EyeGazesHTC.gazes[XR_EYE_POSITION_LEFT_HTC].gazePose;
|
||
|
|
const FTransform LeftEyeTrackerTransform = ToFTransform(LeftPose, WorldToMetersScale);
|
||
|
|
const FTransform LeftEyeTransform = LeftEyeTrackerTransform * TrackingToWoldTransform;
|
||
|
|
|
||
|
|
OutGazeDataHTC.LeftEyeDirection = LeftEyeTransform.TransformVector(FVector::ForwardVector);
|
||
|
|
OutGazeDataHTC.LeftEyeOrigin = LeftEyeTransform.GetLocation();
|
||
|
|
|
||
|
|
OutGazeDataHTC.FixationPoint = FVector::ZeroVector; //not supported
|
||
|
|
|
||
|
|
OutStereoGazeData = OutGazeDataHTC;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
EEyeTrackerStatus FViveOpenXREyeTracker::GetEyeTrackerStatus() const
|
||
|
|
{
|
||
|
|
if (!bSessionStarted)
|
||
|
|
{
|
||
|
|
return EEyeTrackerStatus::NotConnected;
|
||
|
|
}
|
||
|
|
|
||
|
|
const XrSpaceLocationFlags ValidFlags = XR_SPACE_LOCATION_ORIENTATION_VALID_BIT | XR_SPACE_LOCATION_POSITION_VALID_BIT;
|
||
|
|
const XrSpaceLocationFlags TrackedFlags = XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT | XR_SPACE_LOCATION_POSITION_TRACKED_BIT;
|
||
|
|
|
||
|
|
if ((EyeTrackerSpaceLocation.locationFlags & ValidFlags) != ValidFlags)
|
||
|
|
{
|
||
|
|
return EEyeTrackerStatus::NotTracking;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ((EyeTrackerSpaceLocation.locationFlags & TrackedFlags) != TrackedFlags)
|
||
|
|
{
|
||
|
|
return EEyeTrackerStatus::NotTracking;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
return EEyeTrackerStatus::Tracking;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool FViveOpenXREyeTracker::IsStereoGazeDataAvailable() const
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
void FViveOpenXREyeTracker::DrawDebug(AHUD* HUD, UCanvas* Canvas, const FDebugDisplayInfo& DisplayInfo, float& YL, float& YPos)
|
||
|
|
{
|
||
|
|
if (!bSessionStarted)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const XrSpaceLocationFlags ValidFlags = XR_SPACE_LOCATION_ORIENTATION_VALID_BIT | XR_SPACE_LOCATION_POSITION_VALID_BIT;
|
||
|
|
const XrSpaceLocationFlags TrackedFlags = XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT | XR_SPACE_LOCATION_POSITION_TRACKED_BIT;
|
||
|
|
|
||
|
|
if ((EyeTrackerSpaceLocation.locationFlags & ValidFlags) != ValidFlags)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
FColor DrawColor = FColor::Yellow;
|
||
|
|
if ((EyeTrackerSpaceLocation.locationFlags & TrackedFlags) == TrackedFlags)
|
||
|
|
{
|
||
|
|
DrawColor = FColor::Green;
|
||
|
|
}
|
||
|
|
|
||
|
|
const float WorldToMetersScale = XRTrackingSystem->GetWorldToMetersScale();
|
||
|
|
const XrPosef& Pose = EyeTrackerSpaceLocation.pose;
|
||
|
|
FTransform EyeTrackerTransform = ToFTransform(Pose, WorldToMetersScale);
|
||
|
|
|
||
|
|
FVector GazeDirection = EyeTrackerTransform.TransformVector(FVector::ForwardVector);
|
||
|
|
FVector GazeOrigin = EyeTrackerTransform.GetLocation();
|
||
|
|
FVector DebugPos = GazeOrigin + (GazeDirection * 100.0f);
|
||
|
|
DrawDebugSphere(HUD->GetWorld(), DebugPos, 20.0f, 16, DrawColor);
|
||
|
|
}
|
||
|
|
|
||
|
|
/************************************************************************/
|
||
|
|
/* FViveOpenXREyeTrackerModule */
|
||
|
|
/************************************************************************/
|
||
|
|
|
||
|
|
FViveOpenXREyeTrackerModule::FViveOpenXREyeTrackerModule()
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
void FViveOpenXREyeTrackerModule::StartupModule()
|
||
|
|
{
|
||
|
|
IEyeTrackerModule::StartupModule();
|
||
|
|
|
||
|
|
check(GConfig && GConfig->IsReadyForUse());
|
||
|
|
FString modeName;
|
||
|
|
if (GConfig->GetString(TEXT("/Script/ViveOpenXRRuntimeSettings.ViveOpenXRRuntimeSettings"), TEXT("bEnableHTCEyeTracker"), modeName, GEngineIni))
|
||
|
|
{
|
||
|
|
if (modeName.Equals("False"))
|
||
|
|
{
|
||
|
|
m_bEnableHTCEyeTracker = false;
|
||
|
|
}
|
||
|
|
else if (modeName.Equals("True"))
|
||
|
|
{
|
||
|
|
m_bEnableHTCEyeTracker = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (m_bEnableHTCEyeTracker)
|
||
|
|
{
|
||
|
|
UE_LOG(LogViveOpenXREyeTracker, Log, TEXT("Enable HTCEyeTracker."));
|
||
|
|
|
||
|
|
FString ConflictingPluginName = TEXT("OpenXREyeTracker");
|
||
|
|
|
||
|
|
// Check if the plugin is enabled
|
||
|
|
#if WITH_EDITOR
|
||
|
|
if (IPluginManager::Get().FindPlugin(ConflictingPluginName)->IsEnabled())
|
||
|
|
{
|
||
|
|
UE_LOG(LogViveOpenXREyeTracker, Warning, TEXT("The plugin '%s' is enabled, which conflicts with HTC Eye Tracker feature. Please disable '%s' plugin to ensure proper functionality."), *ConflictingPluginName, *ConflictingPluginName);
|
||
|
|
}
|
||
|
|
#endif
|
||
|
|
IEyeTrackerModule::StartupModule();
|
||
|
|
EyeTracker = TSharedPtr<FViveOpenXREyeTracker, ESPMode::ThreadSafe>(new FViveOpenXREyeTracker());
|
||
|
|
OnDrawDebugHandle = AHUD::OnShowDebugInfo.AddRaw(this, &FViveOpenXREyeTrackerModule::OnDrawDebug);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
UE_LOG(LogViveOpenXREyeTracker, Log, TEXT("Disable HTCEyeTracker."));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void FViveOpenXREyeTrackerModule::ShutdownModule()
|
||
|
|
{
|
||
|
|
AHUD::OnShowDebugInfo.Remove(OnDrawDebugHandle);
|
||
|
|
}
|
||
|
|
|
||
|
|
TSharedPtr<class IEyeTracker, ESPMode::ThreadSafe> FViveOpenXREyeTrackerModule::CreateEyeTracker()
|
||
|
|
{
|
||
|
|
return EyeTracker;
|
||
|
|
}
|
||
|
|
|
||
|
|
void FViveOpenXREyeTrackerModule::OnDrawDebug(AHUD* HUD, UCanvas* Canvas, const FDebugDisplayInfo& DisplayInfo, float& YL, float& YPos)
|
||
|
|
{
|
||
|
|
if (CVarEnableOpenXREyetrackingDebug.GetValueOnGameThread())
|
||
|
|
{
|
||
|
|
if (EyeTracker.IsValid())
|
||
|
|
{
|
||
|
|
EyeTracker->DrawDebug(HUD, Canvas, DisplayInfo, YL, YPos);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool FViveOpenXREyeTrackerModule::IsEyeTrackerConnected() const
|
||
|
|
{
|
||
|
|
if (EyeTracker.IsValid())
|
||
|
|
{
|
||
|
|
EEyeTrackerStatus Status = EyeTracker->GetEyeTrackerStatus();
|
||
|
|
if ((Status != EEyeTrackerStatus::NotTracking) && (Status != EEyeTrackerStatus::NotConnected))
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/************************************************************************/
|
||
|
|
/* BlueprintFunctionLibray */
|
||
|
|
/************************************************************************/
|
||
|
|
|
||
|
|
FXrGazeDataHTC FViveOpenXREyeTracker::GetEyeGazeValidDatas()
|
||
|
|
{
|
||
|
|
FXrGazeDataHTC OutEyeGazeValidDataHTC;
|
||
|
|
if (!isHTCEyeTrackerSupported) return OutEyeGazeValidDataHTC;
|
||
|
|
|
||
|
|
const float WorldToMetersScale = XRTrackingSystem->GetWorldToMetersScale();
|
||
|
|
|
||
|
|
OutEyeGazeValidDataHTC.rightEyeGazeValid = (bool)EyeGazesHTC.gazes[XR_EYE_POSITION_RIGHT_HTC].isValid;
|
||
|
|
OutEyeGazeValidDataHTC.leftEyeGazeValid = (bool)EyeGazesHTC.gazes[XR_EYE_POSITION_LEFT_HTC].isValid;
|
||
|
|
|
||
|
|
return OutEyeGazeValidDataHTC;
|
||
|
|
}
|
||
|
|
|
||
|
|
FXrPupilDataHTC FViveOpenXREyeTracker::GetPupilDatas()
|
||
|
|
{
|
||
|
|
FXrPupilDataHTC OutPupilDataHTC;
|
||
|
|
if (!isHTCEyeTrackerSupported) return OutPupilDataHTC;
|
||
|
|
|
||
|
|
const float WorldToMetersScale = XRTrackingSystem->GetWorldToMetersScale();
|
||
|
|
|
||
|
|
OutPupilDataHTC.rightEyeDiameterValid = (bool)PupilDatasHTC.pupils[XR_EYE_POSITION_RIGHT_HTC].isDiameterValid;
|
||
|
|
OutPupilDataHTC.leftEyeDiameterValid = (bool)PupilDatasHTC.pupils[XR_EYE_POSITION_LEFT_HTC].isDiameterValid;
|
||
|
|
|
||
|
|
OutPupilDataHTC.rightEyePositionValid = (bool)PupilDatasHTC.pupils[XR_EYE_POSITION_RIGHT_HTC].isPositionValid;
|
||
|
|
OutPupilDataHTC.leftEyePositionValid = (bool)PupilDatasHTC.pupils[XR_EYE_POSITION_LEFT_HTC].isPositionValid;
|
||
|
|
|
||
|
|
OutPupilDataHTC.rightEyePupilDiameter = PupilDatasHTC.pupils[XR_EYE_POSITION_RIGHT_HTC].pupilDiameter;
|
||
|
|
OutPupilDataHTC.leftEyePupilDiameter = PupilDatasHTC.pupils[XR_EYE_POSITION_LEFT_HTC].pupilDiameter;
|
||
|
|
|
||
|
|
OutPupilDataHTC.rightEyePupilPosition.X = PupilDatasHTC.pupils[XR_EYE_POSITION_RIGHT_HTC].pupilPosition.x * WorldToMetersScale;
|
||
|
|
OutPupilDataHTC.rightEyePupilPosition.Y = PupilDatasHTC.pupils[XR_EYE_POSITION_RIGHT_HTC].pupilPosition.y * WorldToMetersScale;
|
||
|
|
|
||
|
|
OutPupilDataHTC.leftEyePupilPosition.X = PupilDatasHTC.pupils[XR_EYE_POSITION_LEFT_HTC].pupilPosition.x * WorldToMetersScale;
|
||
|
|
OutPupilDataHTC.leftEyePupilPosition.Y = PupilDatasHTC.pupils[XR_EYE_POSITION_LEFT_HTC].pupilPosition.y * WorldToMetersScale;
|
||
|
|
|
||
|
|
return OutPupilDataHTC;
|
||
|
|
}
|
||
|
|
|
||
|
|
FXrEyeGeometricDataHTC FViveOpenXREyeTracker::GetEyeGeometricDatas()
|
||
|
|
{
|
||
|
|
FXrEyeGeometricDataHTC OutEyeGeometricDataHTC;
|
||
|
|
|
||
|
|
if (!isHTCEyeTrackerSupported) return OutEyeGeometricDataHTC;
|
||
|
|
|
||
|
|
OutEyeGeometricDataHTC.rightEyeIsValid = (bool)EyeGeometricDatasHTC.geometrics[XR_EYE_POSITION_RIGHT_HTC].isValid;
|
||
|
|
OutEyeGeometricDataHTC.leftEyeIsValid = (bool)EyeGeometricDatasHTC.geometrics[XR_EYE_POSITION_LEFT_HTC].isValid;
|
||
|
|
|
||
|
|
OutEyeGeometricDataHTC.rightEyeOpenness = EyeGeometricDatasHTC.geometrics[XR_EYE_POSITION_RIGHT_HTC].eyeOpenness;
|
||
|
|
OutEyeGeometricDataHTC.leftEyeOpenness = EyeGeometricDatasHTC.geometrics[XR_EYE_POSITION_LEFT_HTC].eyeOpenness;
|
||
|
|
|
||
|
|
OutEyeGeometricDataHTC.rightEyeSqueeze = EyeGeometricDatasHTC.geometrics[XR_EYE_POSITION_RIGHT_HTC].eyeSqueeze;
|
||
|
|
OutEyeGeometricDataHTC.leftEyeSqueeze = EyeGeometricDatasHTC.geometrics[XR_EYE_POSITION_LEFT_HTC].eyeSqueeze;
|
||
|
|
|
||
|
|
OutEyeGeometricDataHTC.rightEyeWide = EyeGeometricDatasHTC.geometrics[XR_EYE_POSITION_RIGHT_HTC].eyeWide;
|
||
|
|
OutEyeGeometricDataHTC.leftEyeWide = EyeGeometricDatasHTC.geometrics[XR_EYE_POSITION_LEFT_HTC].eyeWide;
|
||
|
|
|
||
|
|
return OutEyeGeometricDataHTC;
|
||
|
|
}
|