Added Tobii from 4.23 + updated

This commit is contained in:
2023-03-04 04:19:09 +11:00
parent 2d5b9008e5
commit 1cde391516
521 changed files with 11735 additions and 0 deletions

View File

@@ -0,0 +1,322 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#include "TobiiBlueprintLibrary.h"
#include "TobiiCoreModule.h"
#include "TobiiPlatformSpecific.h"
#include "ITobiiCore.h"
#include "IXRTrackingSystem.h"
#include "Engine/LocalPlayer.h"
#include "Engine/Engine.h"
#include "GameFramework/PlayerController.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/Paths.h"
#include "Slate/SceneViewport.h"
UTobiiBlueprintLibrary::UTobiiBlueprintLibrary(const class FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
void UTobiiBlueprintLibrary::SetTobiiEyetrackingEnabled(bool bEyetrackingEnabled)
{
SetTobiiInt("tobii.EnableEyetracking", bEyetrackingEnabled ? 1 : 0);
}
bool UTobiiBlueprintLibrary::GetTobiiEyetrackingEnabled()
{
return GetTobiiInt("tobii.EnableEyetracking") != 0;
}
void UTobiiBlueprintLibrary::SetTobiiEyetrackingFrozen(bool bEyetrackingFrozen)
{
SetTobiiInt("tobii.FreezeGazeData", bEyetrackingFrozen ? 1 : 0);
}
bool UTobiiBlueprintLibrary::GetTobiiEyetrackingFrozen()
{
return GetTobiiInt("tobii.FreezeGazeData") != 0;
}
FTobiiGazeData UTobiiBlueprintLibrary::GetTobiiCombinedGazeData()
{
static FTobiiGazeData DummyData;
return FTobiiCoreModule::IsAvailable() ? FTobiiCoreModule::GetEyeTracker()->GetCombinedGazeData() : DummyData;
}
FTobiiGazeData UTobiiBlueprintLibrary::GetTobiiLeftGazeData()
{
static FTobiiGazeData DummyData;
return FTobiiCoreModule::IsAvailable() ? FTobiiCoreModule::GetEyeTracker()->GetLeftGazeData() : DummyData;
}
FTobiiGazeData UTobiiBlueprintLibrary::GetTobiiRightGazeData()
{
static FTobiiGazeData DummyData;
return FTobiiCoreModule::IsAvailable() ? FTobiiCoreModule::GetEyeTracker()->GetRightGazeData() : DummyData;
}
FHitResult UTobiiBlueprintLibrary::GetTobiiCombinedWorldGazeHitData()
{
static FHitResult DummyData;
DummyData.Actor = nullptr;
return FTobiiCoreModule::IsAvailable() ? FTobiiCoreModule::GetEyeTracker()->GetCombinedWorldGazeHitData() : DummyData;
}
ETobiiGazeTrackerStatus UTobiiBlueprintLibrary::GetTobiiGazeTrackerStatus()
{
return FTobiiCoreModule::IsAvailable() ? FTobiiCoreModule::GetEyeTracker()->GetGazeTrackerStatus() : ETobiiGazeTrackerStatus::NotConnected;
}
FTobiiDisplayInfo UTobiiBlueprintLibrary::GetTobiiDisplayInformation()
{
return FTobiiCoreModule::IsAvailable() ? FTobiiCoreModule::GetEyeTracker()->GetDisplayInformation() : FTobiiDisplayInfo();
}
const FTobiiHeadPoseData& UTobiiBlueprintLibrary::GetTobiiHeadPoseData()
{
static FTobiiHeadPoseData DummyData;
return FTobiiCoreModule::IsAvailable() ? FTobiiCoreModule::GetEyeTracker()->GetHeadPoseData() : DummyData;
}
const FTobiiDesktopTrackBox& UTobiiBlueprintLibrary::GetTobiiDesktopTrackBox()
{
static FTobiiDesktopTrackBox DummyData;
return FTobiiCoreModule::IsAvailable() ? FTobiiCoreModule::GetEyeTracker()->GetDesktopTrackBox() : DummyData;
}
FRotator UTobiiBlueprintLibrary::GetTobiiInfiniteScreenAngles()
{
if (FTobiiCoreModule::IsAvailable())
{
return ITobiiCore::GetEyeTracker()->GetInfiniteScreenAngles();
}
else
{
return FRotator::ZeroRotator;
}
}
/************************************************************************/
/* Utils */
/************************************************************************/
bool UTobiiBlueprintLibrary::VirtualDesktopPixelToViewportCoordinateUNorm(const FVector2D& VirtualDesktopPixel, FVector2D& OutViewportCoordinateUNorm)
{
if (!FTobiiCoreModule::IsAvailable() || GEngine == nullptr || GEngine->GameViewport == nullptr || GEngine->GameViewport->GetGameViewport() == nullptr)
{
return false;
}
OutViewportCoordinateUNorm = GEngine->GameViewport->GetGameViewport()->VirtualDesktopPixelToViewport(FIntPoint(VirtualDesktopPixel.X, VirtualDesktopPixel.Y));
return true;
}
bool UTobiiBlueprintLibrary::ViewportCoordinateUNormToVirtualDesktopPixel(const FVector2D& ViewportCoordinateUNorm, FVector2D& OutVirtualDesktopPixel)
{
if (!FTobiiCoreModule::IsAvailable() || GEngine == nullptr || GEngine->GameViewport == nullptr || GEngine->GameViewport->GetGameViewport() == nullptr)
{
return false;
}
const FTobiiDisplayInfo& DisplayInfo = FTobiiCoreModule::GetEyeTracker()->GetDisplayInformation();
FVector2D ScaledInPoint(ViewportCoordinateUNorm.X / DisplayInfo.DpiScale, ViewportCoordinateUNorm.Y / DisplayInfo.DpiScale);
OutVirtualDesktopPixel = GEngine->GameViewport->GetGameViewport()->ViewportToVirtualDesktopPixel(ScaledInPoint);
return true;
}
bool UTobiiBlueprintLibrary::ViewportPixelCoordToCmCoord(const FVector2D& InCoordinatePx, FVector2D& OutCoordinateCm)
{
const FTobiiDisplayInfo& DisplayInfo = FTobiiCoreModule::GetEyeTracker()->GetDisplayInformation();
if (DisplayInfo.MainViewportWidthPx > 0.0f && DisplayInfo.MainViewportWidthCm > 0.0f && DisplayInfo.MainViewportHeightPx > 0.0f && DisplayInfo.MainViewportHeightCm > 0.0f)
{
OutCoordinateCm.Set(InCoordinatePx.X / DisplayInfo.MainViewportWidthPx * DisplayInfo.MainViewportWidthCm, InCoordinatePx.Y / DisplayInfo.MainViewportHeightPx * DisplayInfo.MainViewportHeightCm);
return true;
}
OutCoordinateCm = FVector2D::ZeroVector;
return false;
}
bool UTobiiBlueprintLibrary::ViewportCmCoordToPixelCoord(const FVector2D& InCoordinateCm, FVector2D& OutCoordinatePx)
{
const FTobiiDisplayInfo& DisplayInfo = FTobiiCoreModule::GetEyeTracker()->GetDisplayInformation();
if (DisplayInfo.MainViewportWidthPx > 0.0f && DisplayInfo.MainViewportWidthCm > 0.0f && DisplayInfo.MainViewportHeightPx > 0.0f && DisplayInfo.MainViewportHeightCm > 0.0f)
{
OutCoordinatePx.Set(InCoordinateCm.X / DisplayInfo.MainViewportWidthCm * DisplayInfo.MainViewportWidthPx, InCoordinateCm.Y / DisplayInfo.MainViewportHeightCm * DisplayInfo.MainViewportHeightPx);
return true;
}
OutCoordinatePx = FVector2D::ZeroVector;
return false;
}
bool UTobiiBlueprintLibrary::ViewportPixelCoordToUNormCoord(const FVector2D& InCoordinatePx, FVector2D& OutCoordinateUNorm)
{
const FTobiiDisplayInfo& DisplayInfo = FTobiiCoreModule::GetEyeTracker()->GetDisplayInformation();
if (DisplayInfo.MainViewportWidthPx > 0.0f && DisplayInfo.MainViewportHeightPx > 0.0f)
{
OutCoordinateUNorm.Set(InCoordinatePx.X / DisplayInfo.MainViewportWidthPx, InCoordinatePx.Y / DisplayInfo.MainViewportHeightPx);
return true;
}
OutCoordinateUNorm = FVector2D::ZeroVector;
return false;
}
bool UTobiiBlueprintLibrary::ViewportUNormCoordToPixelCoord(const FVector2D& InCoordinateUNorm, FVector2D& OutCoordinatePx)
{
const FTobiiDisplayInfo& DisplayInfo = FTobiiCoreModule::GetEyeTracker()->GetDisplayInformation();
if (DisplayInfo.MainViewportWidthPx > 0.0f && DisplayInfo.MainViewportHeightPx > 0.0f)
{
OutCoordinatePx.Set(InCoordinateUNorm.X * DisplayInfo.MainViewportWidthPx, InCoordinateUNorm.Y * DisplayInfo.MainViewportHeightPx);
return true;
}
OutCoordinatePx = FVector2D::ZeroVector;
return false;
}
// internal helper
FQuat GetHMDToWorldRotation(APlayerController* PlayerController)
{
FQuat ToWorld = FQuat::Identity;
FVector HMDPosition;
bool valid = GEngine->XRSystem->GetCurrentPose(IXRTrackingSystem::HMDDeviceId, ToWorld, HMDPosition);
const bool IsEmulated = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.emulation.EnableEyetrackingEmulation"))->GetInt() != 0;
const bool IsApplyControlRotation = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.xr.ApplyControlRotation"))->GetInt() != 0;
const bool IsApplyActorRotation = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.xr.ApplyActorRotation"))->GetInt() != 0;
if (!IsEmulated)
{
if (IsApplyControlRotation)
{
FVector RealWorldUp = ToWorld.Inverse().RotateVector(FVector::UpVector);
FQuat YawRotation = FQuat(RealWorldUp, FMath::DegreesToRadians(PlayerController->GetControlRotation().Yaw));
ToWorld *= YawRotation;
}
}
if (IsApplyActorRotation && PlayerController != nullptr && PlayerController->GetPawn() != nullptr)
{
ToWorld *= PlayerController->GetPawn()->GetActorRotation().Quaternion();
}
return ToWorld;
}
int32 UTobiiBlueprintLibrary::GetTobiiInt(FString CVarName)
{
const auto CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName);
return (CVar != nullptr) ? CVar->GetInt() : 0;
}
void UTobiiBlueprintLibrary::SetTobiiInt(FString CVarName, const int32 NewValue)
{
const auto CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName);
if (CVar != nullptr)
{
CVar->Set(NewValue);
}
}
float UTobiiBlueprintLibrary::GetTobiiFloat(FString CVarName)
{
const auto CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName);
return (CVar != nullptr) ? CVar->GetFloat() : 0.0f;
}
void UTobiiBlueprintLibrary::SetTobiiFloat(FString CVarName, const float NewValue)
{
const auto CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName);
if (CVar != nullptr)
{
CVar->Set(NewValue);
}
}
FString GetTobiiSettingsFilePath()
{
return FPaths::Combine(FPaths::GeneratedConfigDir() + ANSI_TO_TCHAR(FPlatformProperties::PlatformName()), "TobiiEyetracking.ini");
}
void UTobiiBlueprintLibrary::LoadTobiiSettings()
{
ApplyCVarSettingsFromIni(TEXT("TobiiEyetracking"), *GetTobiiSettingsFilePath(), ECVF_SetByConstructor);
}
void UTobiiBlueprintLibrary::SaveTobiiSetting(FString CVarSettingToSave)
{
auto CVar = IConsoleManager::Get().FindConsoleVariable(*CVarSettingToSave);
if (CVar != nullptr)
{
GConfig->EmptySection(TEXT("TobiiEyetracking"), *GetTobiiSettingsFilePath());
GConfig->SetString(TEXT("TobiiEyetracking"), *CVarSettingToSave, *CVar->GetString(), *GetTobiiSettingsFilePath());
GConfig->Flush(false, *GetTobiiSettingsFilePath());
}
}
bool UTobiiBlueprintLibrary::IsXRPlayerController(const APlayerController* PlayerController)
{
if (GEngine != nullptr)
{
if (PlayerController != nullptr)
{
ULocalPlayer* LocalPlayer = Cast<ULocalPlayer>(PlayerController->Player);
if (LocalPlayer != nullptr && LocalPlayer->ViewportClient != nullptr)
{
//@TODO: Bugged in Shipping builds 4.18? Returns false even though running through VIVE.
return GEngine->IsStereoscopic3D(LocalPlayer->ViewportClient->Viewport);
}
}
return GEngine->IsStereoscopic3D();
}
return false;
}
FVector UTobiiBlueprintLibrary::ConvertGazeToUserSpace(APlayerController* PlayerController, const FVector& WorldSpaceGazeDirection)
{
FVector LocalSpaceDirection = WorldSpaceGazeDirection;
if (GEngine == nullptr || PlayerController == nullptr)
{
return LocalSpaceDirection;
}
if (UTobiiBlueprintLibrary::IsXRPlayerController(PlayerController))
{
static const auto HMDOrientationCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.xr.ApplyHMDOrientation"));
static const auto ActorRotationCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.xr.ApplyActorRotation"));
if (ActorRotationCVar->GetInt() != 0
&& PlayerController->GetPawn() != nullptr)
{
FQuat ActorRotation = PlayerController->GetPawn()->GetActorRotation().Quaternion();
LocalSpaceDirection = ActorRotation.UnrotateVector(LocalSpaceDirection);
}
if (HMDOrientationCVar->GetInt() != 0)
{
FQuat HMDOrientation;
FVector HMDPosition;
GEngine->XRSystem->GetCurrentPose(IXRTrackingSystem::HMDDeviceId, HMDOrientation, HMDPosition);
LocalSpaceDirection = HMDOrientation.UnrotateVector(LocalSpaceDirection);
}
}
else
{
if (PlayerController->PlayerCameraManager != nullptr)
{
FRotator CameraRotation = PlayerController->PlayerCameraManager->GetCameraRotation();
LocalSpaceDirection = CameraRotation.UnrotateVector(LocalSpaceDirection);
}
}
return LocalSpaceDirection;
}

View File

@@ -0,0 +1,84 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#include "TobiiCoreModule.h"
#include "UserGuide/TobiiEditorExtension.h"
#include "IEyeTrackerModule.h"
#include "GameFramework/HUD.h"
#include "Misc/Paths.h"
IMPLEMENT_MODULE(FTobiiCoreModule, TobiiCore)
/************************************************************************/
/* FTobiiCoreModule */
/************************************************************************/
void FTobiiCoreModule::StartupModule()
{
EyeTracker.Reset();
#if TOBII_EYETRACKING_ACTIVE
FString RelativeGICDllPath = FString(TEXT(TOBII_GIC_RELATIVE_DLL_PATH));
#if TOBII_COMPILE_AS_ENGINE_PLUGIN
FString FullGICDllPath = FPaths::Combine(FPaths::EngineDir(), RelativeGICDllPath);
#else
FString FullGICDllPath = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectPluginsDir(), RelativeGICDllPath));
#endif //TOBII_COMPILE_AS_ENGINE_PLUGIN
GICDllHandle = FPlatformProcess::GetDllHandle(*FullGICDllPath);
if (GICDllHandle != nullptr)
{
FTobiiEyeTracker* NewEyeTracker = new FTobiiEyeTracker();
if (NewEyeTracker != nullptr && NewEyeTracker->IsConnectedToEyeTracker())
{
IModularFeatures::Get().RegisterModularFeature(GetModularFeatureName(), this);
EyeTracker = TSharedPtr<ITobiiEyeTracker, ESPMode::ThreadSafe>(NewEyeTracker);
}
#if WITH_EDITOR
EditorExtensions = new FTobiiEditorExtension(this);
#endif //WITH_EDITOR
}
#endif //TOBII_EYETRACKING_ACTIVE
}
void FTobiiCoreModule::ShutdownModule()
{
#if WITH_EDITOR
if (EditorExtensions != nullptr)
{
delete EditorExtensions;
}
#endif
if (EyeTracker.IsValid())
{
IModularFeatures::Get().UnregisterModularFeature(GetModularFeatureName(), this);
FTobiiEyeTracker* EyetrackerPtr = (FTobiiEyeTracker*)EyeTracker.Get();
EyetrackerPtr->Shutdown();
EyeTracker.Reset();
}
}
TSharedPtr<IEyeTracker, ESPMode::ThreadSafe> FTobiiCoreModule::CreateEyeTracker()
{
return StaticCastSharedPtr<IEyeTracker, ITobiiEyeTracker, ESPMode::ThreadSafe>(EyeTracker);
}
bool FTobiiCoreModule::IsEyeTrackerConnected() const
{
bool bSRanipalRunning = FModuleManager::Get().IsModuleLoaded("SRanipalCore") && FModuleManager::Get().GetModuleChecked<IEyeTrackerModule>("SRanipalCore").IsEyeTrackerConnected();
return EyeTracker.IsValid() && !bSRanipalRunning;
}
TSharedPtr<ITobiiEyeTracker, ESPMode::ThreadSafe> FTobiiCoreModule::GetEyeTrackerInternal()
{
return EyeTracker;
}

View File

@@ -0,0 +1,41 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "IInputDevice.h"
#include "ITobiiCore.h"
#include "TobiiEyetracker.h"
#include "CoreMinimal.h"
#include "DisplayDebugHelpers.h"
class AHUD;
class UCanvas;
class FTobiiCoreModule : public ITobiiCore
{
/************************************************************************/
/* ITobiiCore */
/************************************************************************/
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
virtual TSharedPtr<class IEyeTracker, ESPMode::ThreadSafe> CreateEyeTracker() override;
virtual bool IsEyeTrackerConnected() const override;
protected:
virtual TSharedPtr<ITobiiEyeTracker, ESPMode::ThreadSafe> GetEyeTrackerInternal() override;
private:
TSharedPtr<ITobiiEyeTracker, ESPMode::ThreadSafe> EyeTracker;
void* GICDllHandle;
#if WITH_EDITOR
class FTobiiEditorExtension* EditorExtensions;
#endif
};

View File

@@ -0,0 +1,929 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#include "TobiiEyetracker.h"
#include "TobiiBlueprintLibrary.h"
#include "DrawDebugHelpers.h"
#include "IXRTrackingSystem.h"
#include "Engine/Engine.h"
#include "Engine/Canvas.h"
#include "Engine/LocalPlayer.h"
#include "GameFramework/HUD.h"
#include "GameFramework/PlayerController.h"
#include "Slate/SceneViewport.h"
#include "Widgets/SWindow.h"
#include "Misc/App.h"
#include "tobii_gameintegration.h"
/************************************************************************/
/* Configuration CVars */
/************************************************************************/
static TAutoConsoleVariable<int32> CVarTobiiEnableEyetracking(TEXT("tobii.EnableEyetracking"), 1, TEXT("0 - Eyetracking is disabled. 1 - Eyetracking is enabled."));
static TAutoConsoleVariable<int32> CVarTobiiFreezingGazeData(TEXT("tobii.FreezeGazeData"), 0, TEXT("0 - Eyetracking data is not frozen. 1 - Eyetracking data is frozen. This is really useful for visualizing gaze and debugging."));
static TAutoConsoleVariable<int32> CVarTobiiFocusTraceChannel(TEXT("tobii.FocusTraceChannel"), (int32)ECC_Visibility, TEXT("This is the trace channel that will be used in all eye tracking queries."));
static TAutoConsoleVariable<float> CVarTobiiFovealConeAngleDegrees(TEXT("tobii.FovealConeAngleDegrees"), 5.0f, TEXT("A larger value here will lead to the GTOM system considering a larger area around the gaze point. Refer to this link to see what values are reasonable: https://en.wikipedia.org/wiki/Fovea_centralis#/media/File:Macula.svg. Eventually, we will hopefully select good values for you so you don't have to care about this."));
static TAutoConsoleVariable<float> CVarTobiiMaximumTraceDistance(TEXT("tobii.MaximumTraceDistance"), 5000.0f, TEXT("This is how far in front of the player we can detect objects. This could impact game play so be careful. Shorter range can also positively impact performance."));
static TAutoConsoleVariable<float> CVarTobiiMaximumAcceptableStableGazeAverageAngularSpeedDegPerMicroSecs(TEXT("tobii.MaximumAcceptableStableGazeAverageAngularSpeedDegPerMicroSecs"), 0.00001f, TEXT("Raising this value will make the system more generous when determining if a gaze point is currently stable or not."));
static TAutoConsoleVariable<float> CVarTobiiStablePointInterpolationBias(TEXT("tobii.StablePointInterpolationBias"), 0.6f, TEXT("Gaze point stability is based on a linear interpolation filter. This scalar determines the bias given to the new point."));
static TAutoConsoleVariable<float> CVarTobiiWatchingToNotWatchingInertiaSecs(TEXT("tobii.desktop.WatchingToNotWatchingInertiaSecs"), 0.6f, TEXT("To make the experience more stable we introduce inertia for transitioning from the UserPresentAndWatchingWindow eyetracking status state. This is the length of that inertia period in seconds. Please note that the eyetracker also has innate inertia, this is just a way to further control it."));
static TAutoConsoleVariable<float> CVarTobiiAdditionalWindowMarginCm(TEXT("tobii.desktop.AdditionalWindowMarginCm"), 1.3f, TEXT("We add an artificial margin around the game window to avoid precision and accuracy problems when the gaze point is close to the edge of the screen. The value is the size of this extra border in centimeters (same as unreal units)."));
static TAutoConsoleVariable<float> CVarTobiiHeadPosePositionInterpolationBias(TEXT("tobii.desktop.HeadPosePositionInterpolationBias"), 0.6f, TEXT("Head pose position filtration is done using a linear interpolation filter. This scalar determines the bias given to the new position data."));
static TAutoConsoleVariable<float> CVarTobiiHeadPoseRotationInterpolationBias(TEXT("tobii.desktop.HeadPoseRotationInterpolationBias"), 0.1f, TEXT("Head pose rotation filtration is done using a spherical linear interpolation filter. This scalar is used as the interpolation constant."));
static TAutoConsoleVariable<int32> CVarInfiniteScreenEnabled(TEXT("tobii.desktop.InfiniteScreenEnabled"), 1, TEXT("0 - Extended View is off. 1 - Extended View is on."));
static TAutoConsoleVariable<float> CVarExtendedViewMaxGazeAngleDeg(TEXT("tobii.desktop.ExtendedViewMaxGazeAngleDeg"), 30, TEXT("Extended View angles will not rotate the camera more than this amount of degrees due to the user's gaze."));
static TAutoConsoleVariable<float> CVarExtendedViewHeadSensitivity(TEXT("tobii.desktop.ExtendedViewHeadSensitivity"), 0.5f, TEXT("This is how sensitive the head component of extended view will be."));
static TAutoConsoleVariable<float> CVarExtendedViewGazeSensitivity(TEXT("tobii.desktop.ExtendedViewGazeSensitivity"), 0.5f, TEXT("This is how sensitive the gaze component of extended view will be."));
static TAutoConsoleVariable<float> CVarExtendedViewGazeOnlyScalar(TEXT("tobii.desktop.ExtendedViewGazeOnlyScalar"), 1.3f, TEXT("This is how we will scale the gaze extended view if only gaze is available."));
static TAutoConsoleVariable<float> CVarExtendedViewHeadOnlyScalar(TEXT("tobii.desktop.ExtendedViewHeadOnlyScalar"), 1.3f, TEXT("This is how we will scale the head extended view if only head is available."));
static TAutoConsoleVariable<float> CVarHMDScreenDistanceToEyeCm(TEXT("tobii.xr.HMDScreenDistanceToEyeCm"), 3.0f, TEXT("Since the foveal cone on the screen depends on the distance of the eye from the screen, we need this. Since UE4 doesn't provide APIs to poll it dynamically, you have to set it manually here instead."));
static TAutoConsoleVariable<int32> CVarTobiiApplyHMDOrientation(TEXT("tobii.xr.ApplyHMDOrientation"), 1, TEXT("Apply the HMD orientation to your gaze direction"));
static TAutoConsoleVariable<int32> CVarTobiiApplyActorRotation(TEXT("tobii.xr.ApplyActorRotation"), 1, TEXT("Apply your actor rotation to your gaze direction"));
static TAutoConsoleVariable<int32> CVarEnableEyetrackingEmulation(TEXT("tobii.emulation.EnableEyetrackingEmulation"), 0, TEXT("0 - Don't emulate eye tracking. 1 - Emulate eye tracking."));
static TAutoConsoleVariable<float> CVarEyetrackingEmulationGazeSpeed(TEXT("tobii.emulation.DesktopEyetrackingEmulationGazeSpeed"), 0.4f, TEXT("When emulating gaze on desktop, this is the speed scalar used for the gaze point."));
static TAutoConsoleVariable<int32> CVarEnableEyetrackingDebug(TEXT("tobii.debug"), 0, TEXT("0 - Eyetracking debug visualizations are disabled. 1 - Eyetracking debug visualizations are enabled."));
static TAutoConsoleVariable<int32> CVarEnableGazePointDebug(TEXT("tobii.debug.EnableGazePointDebug"), 1, TEXT("0 - Gaze point debug visualizations are disabled. 1 - Gaze point debug visualizations are enabled."));
static TAutoConsoleVariable<int32> CVarEnableHeadPoseDebug(TEXT("tobii.debug.EnableHeadPoseDebug"), 0, TEXT("0 - Head pose debug visualizations are disabled. 1 - Head pose debug visualizations are enabled."));
#if TOBII_EYETRACKING_ACTIVE
using namespace TobiiGameIntegration;
FTobiiEyeTracker::FTobiiEyeTracker()
: TgiApi(nullptr)
, ActivePlayerController(nullptr)
, bIsXR(false)
{
TgiApi = GetApi(TCHAR_TO_ANSI(FApp::GetProjectName()));
StartTime = FDateTime::UtcNow();
ResetData();
}
FTobiiEyeTracker::~FTobiiEyeTracker()
{
Shutdown();
}
void FTobiiEyeTracker::Shutdown()
{
if (TgiApi != nullptr)
{
TgiApi->Shutdown();
TgiApi = nullptr;
}
GazeTrackerStatus = ETobiiGazeTrackerStatus::NotConnected;
}
bool FTobiiEyeTracker::IsConnectedToEyeTracker()
{
if (TgiApi != nullptr)
{
IStreamsProvider* StreamsProvider = TgiApi->GetStreamsProvider();
ITrackerController* TrackerController = TgiApi->GetTrackerController();
if (TrackerController != nullptr && StreamsProvider != nullptr)
{
return true;
}
}
return false;
}
void FTobiiEyeTracker::ResetData()
{
ActivePlayerController.Reset();
CombinedWorldGazeHitData = FHitResult();
LeftWorldGazeHitData= FHitResult();
RightWorldGazeHitData = FHitResult();
LeftGazeData = FTobiiGazeData();
RightGazeData = FTobiiGazeData();
CombinedGazeData = FTobiiGazeData();
GazeTrackerStatus = ETobiiGazeTrackerStatus::NotConnected;
HeadPoseData = FTobiiHeadPoseData();
DisplayInfo = FTobiiDisplayInfo();
GazePointDeltaTimeMicroSecs = 0;
CurrentAverageGazeAngularSpeedDegPerMicroSecs = 0.0;
}
bool FTobiiEyeTracker::Tick(float DeltaTime)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_TobiiEyetracking_Tick);
if (GEngine->EyeTrackingDevice.Get() != this)
{
Shutdown();
return true;
}
//////////////////////////////////////////////////////////////////////////
// Update status
//////////////////////////////////////////////////////////////////////////
if (!CVarTobiiEnableEyetracking.GetValueOnGameThread())
{
if (GazeTrackerStatus != ETobiiGazeTrackerStatus::Disabled)
{
ResetData();
GazeTrackerStatus = ETobiiGazeTrackerStatus::Disabled;
}
return true;
}
if (GEngine == nullptr
|| GEngine->GameViewport == nullptr
|| GEngine->GameViewport->GetGameViewport() == nullptr
|| TgiApi == nullptr)
{
ResetData();
return true;
}
if (!UpdateLowLevelResources())
{
ResetData();
return true;
}
//If we have no active PC, set default
if (!ActivePlayerController.IsValid())
{
SetEyeTrackedPlayer(nullptr);
}
if (!CVarTobiiFreezingGazeData.GetValueOnGameThread())
{
if (bIsXR)
{
TickXR(DeltaTime);
}
else
{
TickDesktop(DeltaTime);
}
}
UpdateWorldSpaceData(DeltaTime);
UpdateStabilityData(DeltaTime);
if (ActivePlayerController.IsValid()
&& ActivePlayerController->GetWorld() != nullptr
&& CVarEnableEyetrackingDebug.GetValueOnGameThread())
{
if (CVarEnableGazePointDebug.GetValueOnGameThread())
{
UE_LOG(LogTemp, Warning, TEXT("Gaze Direction: (%.2f, %.2f, %.2f)"), CombinedGazeData.WorldGazeDirection.X, CombinedGazeData.WorldGazeDirection.Y, CombinedGazeData.WorldGazeDirection.Z);
const float DrawSize = FVector::Dist(CombinedGazeData.WorldGazeOrigin, CombinedWorldGazeHitData.Location) * FMath::Tan(FMath::DegreesToRadians(CombinedGazeData.WorldGazeConeAngleDegrees));
DrawDebugSphere(ActivePlayerController->GetWorld(), CombinedWorldGazeHitData.Location, DrawSize, 16, CombinedGazeData.bIsStable ? FColor::Green : FColor::Red, false, 0.0f);
}
if (CVarEnableHeadPoseDebug.GetValueOnGameThread())
{
UE_LOG(LogTemp, Warning, TEXT("Head location: (%.2f, %.2f, %.2f) ~~ Head orientation: (%.2f, %.2f, %.2f)")
, HeadPoseData.HeadLocation.X, HeadPoseData.HeadLocation.Y, HeadPoseData.HeadLocation.Z
, HeadPoseData.HeadOrientation.Yaw, HeadPoseData.HeadOrientation.Pitch, HeadPoseData.HeadOrientation.Roll);
}
}
return true;
}
bool FTobiiEyeTracker::UpdateLowLevelResources()
{
//Get game window and monitor handles
if (DisplayInfo.GameMonitorHandle == nullptr || PlatformNotifications.bShouldUpdateGameMonitorHandle)
{
PlatformNotifications.bShouldUpdateGameMonitorHandle = false;
if (!GEngine->GameViewport->GetWindow().IsValid())
{
return false;
}
SWindow* GameWindow = GEngine->GameViewport->GetWindow().Get();
if (!GameWindow->GetNativeWindow().IsValid())
{
return false;
}
DisplayInfo.DpiScale = GameWindow->GetCachedGeometry().Scale;
DisplayInfo.GameWindowHandle = GameWindow->GetNativeWindow().Get()->GetOSWindowHandle();
if (DisplayInfo.GameWindowHandle == nullptr)
{
return false;
}
DisplayInfo.GameMonitorHandle = FTobiiPlatformSpecific::GetMonitorInformation(DisplayInfo.GameWindowHandle, DisplayInfo.MonitorWidthPx, DisplayInfo.MonitorHeightPx);
}
//Since the viewport size can change every frame, we cannot treat this data as constant.
IStreamsProvider* StreamsProvider = TgiApi->GetStreamsProvider();
if (StreamsProvider != nullptr)
{
GazePoint MaxGazeUNorm, MaxGazeMm;
MaxGazeUNorm.X = MaxGazeUNorm.Y = 1.0f;
StreamsProvider->ConvertGazePoint(MaxGazeUNorm, MaxGazeMm, Normalized, Mm);
DisplayInfo.MonitorWidthCm = MaxGazeMm.X / 10.0f;
DisplayInfo.MonitorHeightCm = MaxGazeMm.Y / 10.0f;
FVector2D ViewportSize;
GEngine->GameViewport->GetViewportSize(ViewportSize);
DisplayInfo.MainViewportWidthPx = ViewportSize.X;
DisplayInfo.MainViewportHeightPx = ViewportSize.Y;
DisplayInfo.MainViewportWidthCm = (((float)DisplayInfo.MainViewportWidthPx / (float)DisplayInfo.MonitorWidthPx) * DisplayInfo.MonitorWidthCm);
DisplayInfo.MainViewportHeightCm = (((float)DisplayInfo.MainViewportHeightPx / (float)DisplayInfo.MonitorHeightPx) * DisplayInfo.MonitorHeightCm);
}
return true;
}
FVector2D& FTobiiEyeTracker::TickEmulatedGazePointUNorm(float DeltaTime)
{
static FVector2D EmulatedGazeNorm(0.5f, 0.5f);
if (ActivePlayerController.IsValid())
{
const float EmulatedGazeSpeed = CVarEyetrackingEmulationGazeSpeed.GetValueOnGameThread();
if (ActivePlayerController->IsInputKeyDown(FKey("Up")))
{
EmulatedGazeNorm.Y -= DeltaTime * EmulatedGazeSpeed;
}
else if (ActivePlayerController->IsInputKeyDown(FKey("Down")))
{
EmulatedGazeNorm.Y += DeltaTime * EmulatedGazeSpeed;
}
if (ActivePlayerController->IsInputKeyDown(FKey("Left")))
{
EmulatedGazeNorm.X -= DeltaTime * EmulatedGazeSpeed;
}
else if (ActivePlayerController->IsInputKeyDown(FKey("Right")))
{
EmulatedGazeNorm.X += DeltaTime * EmulatedGazeSpeed;
}
}
EmulatedGazeNorm.X = FMath::Clamp(EmulatedGazeNorm.X, 0.0f, 1.0f);
EmulatedGazeNorm.Y = FMath::Clamp(EmulatedGazeNorm.Y, 0.0f, 1.0f);
return EmulatedGazeNorm;
}
void FTobiiEyeTracker::TickDesktop(float DeltaTime)
{
//Pump API
ITrackerController* TrackerController = TgiApi->GetTrackerController();
if (TrackerController != nullptr)
{
TrackerController->TrackWindow(DisplayInfo.GameWindowHandle);
}
TgiApi->Update();
FDateTime Now = FDateTime::UtcNow();
IStreamsProvider* StreamsProvider = nullptr;
bool bIsEmulating = false;
if (CVarEnableEyetrackingEmulation.GetValueOnGameThread())
{
bIsEmulating = true;
GazeTrackerStatus = ETobiiGazeTrackerStatus::UserPresent;
}
else
{
//Test for status
StreamsProvider = TgiApi->GetStreamsProvider();
if (StreamsProvider != nullptr && TrackerController != nullptr && TrackerController->IsConnected())
{
if (GazeTrackerStatus < ETobiiGazeTrackerStatus::UserNotPresent)
{
GazeTrackerStatus = ETobiiGazeTrackerStatus::UserNotPresent;
}
}
else
{
GazeTrackerStatus = ETobiiGazeTrackerStatus::NotConnected;
}
}
if (GazeTrackerStatus >= ETobiiGazeTrackerStatus::UserNotPresent)
{
if (bIsEmulating)
{
RawGazePoint.TimeStamp = Now;
RawGazePoint.GazePointNormalized = TickEmulatedGazePointUNorm(DeltaTime);
HeadPoseData.HeadLocation = FVector::ZeroVector;
HeadPoseData.HeadOrientation = FRotator::ZeroRotator;
InfiniteScreenAngles = FRotator::ZeroRotator;
}
else
{
GazeTrackerStatus = StreamsProvider->IsPresent() ? ETobiiGazeTrackerStatus::UserPresent : ETobiiGazeTrackerStatus::UserNotPresent;
//Get new gaze data
const GazePoint* GazePointsSinceLastUpdateSNorm;
int NumGazePointsSinceLastUpdate = StreamsProvider->GetGazePoints(GazePointsSinceLastUpdateSNorm);
if (NumGazePointsSinceLastUpdate > 0)
{
RawGazePoint.GazePointNormalized.Set(0.0f, 0.0f);
for (int32 GazeIdx = 0; GazeIdx < NumGazePointsSinceLastUpdate; GazeIdx++)
{
RawGazePoint.GazePointNormalized += FVector2D(GazePointsSinceLastUpdateSNorm[GazeIdx].X, GazePointsSinceLastUpdateSNorm[GazeIdx].Y);
}
RawGazePoint.TimeStamp = Now;
RawGazePoint.GazePointNormalized /= (float)NumGazePointsSinceLastUpdate;
//Convert to UNorm
RawGazePoint.GazePointNormalized.Set((RawGazePoint.GazePointNormalized.X + 1.0f) / 2.0f
, (-RawGazePoint.GazePointNormalized.Y + 1.0f) / 2.0f);
}
//Get new head pose data
const HeadPose* HeadPosesSinceLastUpdate;
int NumHeadPosesSinceLastUpdate = StreamsProvider->GetHeadPoses(HeadPosesSinceLastUpdate);
if (NumHeadPosesSinceLastUpdate > 0)
{
RawHeadPose.HeadPositionCm.Set(0.0f, 0.0f, 0.0f);
float RawPitchRad = 0.0f;
float RawYawRad = 0.0f;
float RawRollRad = 0.0f;
for (int32 HeadPoseIdx = 0; HeadPoseIdx < NumHeadPosesSinceLastUpdate; HeadPoseIdx++)
{
RawHeadPose.HeadPositionCm += FVector(HeadPosesSinceLastUpdate[HeadPoseIdx].Position.X, HeadPosesSinceLastUpdate[HeadPoseIdx].Position.Y, HeadPosesSinceLastUpdate[HeadPoseIdx].Position.Z);
RawPitchRad += HeadPosesSinceLastUpdate[HeadPoseIdx].Rotation.Pitch;
RawYawRad += HeadPosesSinceLastUpdate[HeadPoseIdx].Rotation.Yaw;
RawRollRad += HeadPosesSinceLastUpdate[HeadPoseIdx].Rotation.Roll;
}
RawHeadPose.HeadPositionCm /= (float)NumHeadPosesSinceLastUpdate;
RawPitchRad /= (float)NumHeadPosesSinceLastUpdate;
RawYawRad /= (float)NumHeadPosesSinceLastUpdate;
RawRollRad /= (float)NumHeadPosesSinceLastUpdate;
RawHeadPose.TimeStamp = Now;
RawHeadPose.HeadPositionCm /= 10.0f; //Unit conversion
RawHeadPose.HeadOrientation = FRotator(FMath::RadiansToDegrees(RawPitchRad), FMath::RadiansToDegrees(RawYawRad), FMath::RadiansToDegrees(RawRollRad));
}
//Calculate derived data
const float HeadPosePositionInterpolationBias = FMath::Clamp(CVarTobiiHeadPosePositionInterpolationBias.GetValueOnGameThread(), 0.3f, 1.0f);
const float HeadPoseRotationInterpolationBias = FMath::Clamp(CVarTobiiHeadPoseRotationInterpolationBias.GetValueOnGameThread(), 0.01f, 1.0f);
HeadPoseData.HeadLocation = FMath::LerpStable(HeadPoseData.HeadLocation, RawHeadPose.HeadPositionCm, HeadPosePositionInterpolationBias);
HeadPoseData.HeadOrientation = FQuat::Slerp(HeadPoseData.HeadOrientation.Quaternion(), RawHeadPose.HeadOrientation.Quaternion(), HeadPoseRotationInterpolationBias).Rotator();
//Infinite screen
bool bExtendedViewUpdateSuccessful = false;
IFeatures* Features = TgiApi->GetFeatures();
if (CVarInfiniteScreenEnabled.GetValueOnGameThread() && !bIsXR && Features != nullptr)
{
IExtendedView* ExtendedView = Features->GetExtendedView();
if (ExtendedView != nullptr)
{
const float BaseHeadViewResponsiveness = FMath::Clamp(CVarExtendedViewHeadSensitivity.GetValueOnGameThread(), 0.0f, 1.0f);
const float BaseGazeViewResponsiveness = FMath::Clamp(CVarExtendedViewGazeSensitivity.GetValueOnGameThread(), 0.0f, 1.0f);
ExtendedViewSettings Settings;
Settings.NormalizedGazeViewMinimumExtensionAngle = Settings.NormalizedGazeViewExtensionAngle = CVarExtendedViewMaxGazeAngleDeg.GetValueOnGameThread() / 360.0f;
Settings.HeadViewResponsiveness = BaseHeadViewResponsiveness;
Settings.GazeViewResponsiveness = BaseGazeViewResponsiveness;
Settings.HeadViewAutoCenter.IsEnabled = false;
//Update default settings
ExtendedView->UpdateSettings(Settings);
//Update specific settings
Settings.GazeViewResponsiveness = BaseGazeViewResponsiveness * CVarExtendedViewGazeOnlyScalar.GetValueOnGameThread();
ExtendedView->UpdateGazeOnlySettings(Settings);
Settings.HeadViewResponsiveness = BaseHeadViewResponsiveness * CVarExtendedViewHeadOnlyScalar.GetValueOnGameThread();
ExtendedView->UpdateHeadOnlySettings(Settings);
Transformation ExtendedViewData = ExtendedView->GetTransformation();
InfiniteScreenAngles.Roll = 0.0f;
InfiniteScreenAngles.Yaw = ExtendedViewData.Rotation.Yaw;
InfiniteScreenAngles.Pitch = ExtendedViewData.Rotation.Pitch;
bExtendedViewUpdateSuccessful = true;
}
}
if (!bExtendedViewUpdateSuccessful)
{
InfiniteScreenAngles = FRotator::ZeroRotator;
}
}
//Since these are not time based, only run when necessary.
static FDateTime PreviousRawGazePointTime;
if (PreviousRawGazePointTime != RawGazePoint.TimeStamp)
{
GazePointDeltaTimeMicroSecs = (uint64)FMath::Max((RawGazePoint.TimeStamp - PreviousRawGazePointTime).GetTotalMicroseconds(), 0.0);
CombinedGazeData.TimeStamp = RawGazePoint.TimeStamp;
FVector2D ScreenSpaceGazePointUNorm = ConvertRawGazePointUNormToGameViewportCoordinateUNorm(GEngine->GameViewport->GetGameViewport(), RawGazePoint.GazePointNormalized);
CombinedGazeData.ScreenGazePointPx.Set(ScreenSpaceGazePointUNorm.X * DisplayInfo.MainViewportWidthPx, ScreenSpaceGazePointUNorm.Y * DisplayInfo.MainViewportHeightPx);
PreviousRawGazePointTime = RawGazePoint.TimeStamp;
}
GazeDataTimeStampMicroSecs = (PreviousRawGazePointTime - StartTime).GetTotalMicroseconds();
const float AspectRatio = DisplayInfo.MainViewportWidthPx / (float)DisplayInfo.MainViewportHeightPx;
const float FovealRegionSizeDeg = FMath::Max(CVarTobiiFovealConeAngleDegrees.GetValueOnGameThread(), 0.0f);
CombinedGazeData.ScreenGazeCircleRadiiPx.Y = FEyetrackingUtils::CalculateFovealRegionHeightPx(DisplayInfo.MainViewportHeightCm, DisplayInfo.MainViewportHeightPx, HeadPoseData.HeadLocation.Z, FovealRegionSizeDeg);
CombinedGazeData.ScreenGazeCircleRadiiPx.X = CombinedGazeData.ScreenGazeCircleRadiiPx.Y * AspectRatio;
CombinedGazeData.WorldGazeConeAngleDegrees = FovealRegionSizeDeg;
CombinedGazeData.EyeOpenness = 1.0f;
}
}
void FTobiiEyeTracker::TickXR(float DeltaTime)
{
//Pump API
ITrackerController* TrackerController = TgiApi->GetTrackerController();
if (TrackerController != nullptr)
{
TrackerController->TrackHMD();
}
TgiApi->Update();
//Test for status
FDateTime Now = FDateTime::UtcNow();
IStreamsProvider* StreamsProvider = nullptr;
bool bIsEmulating = false;
if (CVarEnableEyetrackingEmulation.GetValueOnGameThread())
{
bIsEmulating = true;
GazeTrackerStatus = ETobiiGazeTrackerStatus::UserPresent;
}
else
{
StreamsProvider = TgiApi->GetStreamsProvider();
GazeTrackerStatus = StreamsProvider != nullptr && TrackerController != nullptr && TrackerController->IsConnected()
? ETobiiGazeTrackerStatus::UserPresent
: ETobiiGazeTrackerStatus::NotConnected;
}
if (GazeTrackerStatus == ETobiiGazeTrackerStatus::UserPresent && ActivePlayerController.IsValid())
{
bool bNewGazeData = false;
if (bIsEmulating)
{
FQuat HMDOrientation;
FVector HMDPosition;
GEngine->XRSystem->GetCurrentPose(IXRTrackingSystem::HMDDeviceId, HMDOrientation, HMDPosition);
FVector2D& GazeModifierUNorm = TickEmulatedGazePointUNorm(DeltaTime);
FVector2D GazeModifierSNorm((GazeModifierUNorm.X - 0.5f) * 2.0f, (GazeModifierUNorm.Y - 0.5f) * 2.0f);
HMDOrientation *= FQuat::MakeFromEuler(FVector(0.0f, -GazeModifierSNorm.Y * 90.0f, GazeModifierSNorm.X * 90.0f));
LeftGazeData.WorldGazeDirection = RightGazeData.WorldGazeDirection = HMDOrientation.GetForwardVector();
LeftGazeData.bIsGazeDataValid = RightGazeData.bIsGazeDataValid = true;
LeftGazeData.EyeOpenness = RightGazeData.EyeOpenness = 1.0f;
bNewGazeData = true;
}
else
{
//Find direction and openness
//const HMDGaze* RawTobiiGazeData;
HMDGaze RawTobiiGazeData;
//int NumDataSinceLastUpdate = StreamsProvider->GetHMDGaze(RawTobiiGazeData);
int NumDataSinceLastUpdate = StreamsProvider->GetLatestHMDGaze(RawTobiiGazeData) ? 1 : 0;
if (NumDataSinceLastUpdate > 0)
{
const HMDGaze& LatestGazeData = RawTobiiGazeData;//[NumDataSinceLastUpdate - 1];
LeftGazeData.bIsGazeDataValid = (LatestGazeData.Validity & HMDValidityFlags::LeftEyeIsValid) == HMDValidityFlags::LeftEyeIsValid;
RightGazeData.bIsGazeDataValid = (LatestGazeData.Validity & HMDValidityFlags::RightEyeIsValid) == HMDValidityFlags::RightEyeIsValid;
LeftGazeData.WorldGazeDirection.Set(0.0f, 0.0f, 0.0f);
RightGazeData.WorldGazeDirection.Set(0.0f, 0.0f, 0.0f);
for (int32 GazeIdx = 0; GazeIdx < NumDataSinceLastUpdate; GazeIdx++)
{
const HMDGaze& GazeData = RawTobiiGazeData;// [GazeIdx];
LeftGazeData.WorldGazeDirection += FVector(GazeData.LeftEyeInfo.GazeDirection.Z, -GazeData.LeftEyeInfo.GazeDirection.X, GazeData.LeftEyeInfo.GazeDirection.Y);
RightGazeData.WorldGazeDirection += FVector(GazeData.RightEyeInfo.GazeDirection.Z, -GazeData.RightEyeInfo.GazeDirection.X, GazeData.RightEyeInfo.GazeDirection.Y);
}
LeftGazeData.bIsGazeDataValid = LeftGazeData.bIsGazeDataValid && LeftGazeData.WorldGazeDirection.Normalize();
RightGazeData.bIsGazeDataValid = RightGazeData.bIsGazeDataValid && RightGazeData.WorldGazeDirection.Normalize();
LeftGazeData.EyeOpenness = LatestGazeData.LeftEyeInfo.EyeOpenness;
RightGazeData.EyeOpenness = LatestGazeData.RightEyeInfo.EyeOpenness;
GazePointDeltaTimeMicroSecs = (int64)LatestGazeData.Timestamp - GazeDataTimeStampMicroSecs;
GazeDataTimeStampMicroSecs = (int64)LatestGazeData.Timestamp;
bNewGazeData = true;
}
//Optionally correct orientation depending on project settings
{
if (CVarTobiiApplyHMDOrientation.GetValueOnGameThread())
{
FQuat HMDOrientation;
FVector HMDPosition;
GEngine->XRSystem->GetCurrentPose(IXRTrackingSystem::HMDDeviceId, HMDOrientation, HMDPosition);
LeftGazeData.WorldGazeDirection = HMDOrientation.RotateVector(LeftGazeData.WorldGazeDirection);
RightGazeData.WorldGazeDirection = HMDOrientation.RotateVector(RightGazeData.WorldGazeDirection);
}
}
}
if (CVarTobiiApplyActorRotation.GetValueOnGameThread()
&& ActivePlayerController.IsValid()
&& ActivePlayerController->GetPawn() != nullptr)
{
FQuat ActorRotation = ActivePlayerController->GetPawn()->GetActorRotation().Quaternion();
LeftGazeData.WorldGazeDirection = ActorRotation.RotateVector(LeftGazeData.WorldGazeDirection);
RightGazeData.WorldGazeDirection = ActorRotation.RotateVector(RightGazeData.WorldGazeDirection);
}
//Calculate combined data
CombinedGazeData.bIsGazeDataValid = LeftGazeData.bIsGazeDataValid || RightGazeData.bIsGazeDataValid;
if (LeftGazeData.bIsGazeDataValid && RightGazeData.bIsGazeDataValid)
{
CombinedGazeData.WorldGazeDirection = (LeftGazeData.WorldGazeDirection + RightGazeData.WorldGazeDirection);
CombinedGazeData.bIsGazeDataValid = CombinedGazeData.bIsGazeDataValid && CombinedGazeData.WorldGazeDirection.Normalize();
}
else if (LeftGazeData.bIsGazeDataValid)
{
CombinedGazeData.WorldGazeDirection = LeftGazeData.WorldGazeDirection;
}
else if (RightGazeData.bIsGazeDataValid)
{
CombinedGazeData.WorldGazeDirection = RightGazeData.WorldGazeDirection;
}
if (bNewGazeData)
{
CombinedGazeData.TimeStamp = Now;
}
const float AspectRatio = DisplayInfo.MainViewportWidthPx / (float)DisplayInfo.MainViewportHeightPx;
const float FovealRegionSizeDeg = FMath::Max(CVarTobiiFovealConeAngleDegrees.GetValueOnGameThread(), 0.0f);
CombinedGazeData.ScreenGazeCircleRadiiPx.Y = FEyetrackingUtils::CalculateFovealRegionHeightPx(DisplayInfo.MainViewportHeightCm, DisplayInfo.MainViewportHeightPx, CVarHMDScreenDistanceToEyeCm.GetValueOnGameThread(), FovealRegionSizeDeg);
CombinedGazeData.ScreenGazeCircleRadiiPx.X = CombinedGazeData.ScreenGazeCircleRadiiPx.Y * AspectRatio;
LeftGazeData.WorldGazeConeAngleDegrees = RightGazeData.WorldGazeConeAngleDegrees = CombinedGazeData.WorldGazeConeAngleDegrees = FovealRegionSizeDeg;
LeftGazeData.ScreenGazeCircleRadiiPx = RightGazeData.ScreenGazeCircleRadiiPx = CombinedGazeData.ScreenGazeCircleRadiiPx;
}
}
void FTobiiEyeTracker::UpdateWorldSpaceData(float DeltaTime)
{
if (ActivePlayerController.IsValid() && ActivePlayerController->GetWorld() != nullptr)
{
if (bIsXR)
{
ULocalPlayer* const LocalPlayer = ActivePlayerController->GetLocalPlayer();
if (LocalPlayer != nullptr)
{
//Find origin and screen point. This also calculates screen data for VR as it is expressed in world space data.
//@TODO: We need to make it obvious that if you want to line trace here, you must move the origin forward to the near plane.
const float ProjectionDistanceCm = 1000.0f;
FSceneViewProjectionData LeftProjectionData, RightProjectionData;
if (LocalPlayer->GetProjectionData(LocalPlayer->ViewportClient->Viewport, eSSP_LEFT_EYE, /*out*/ LeftProjectionData))
{
LeftGazeData.WorldGazeOrigin = LeftProjectionData.ViewOrigin;
const FVector ProjectionPoint = LeftGazeData.WorldGazeOrigin + (LeftGazeData.WorldGazeDirection * ProjectionDistanceCm);
ActivePlayerController->ProjectWorldLocationToScreen(ProjectionPoint, LeftGazeData.ScreenGazePointPx);
}
else if (ActivePlayerController->PlayerCameraManager != nullptr)
{
LeftGazeData.WorldGazeOrigin = ActivePlayerController->PlayerCameraManager->GetCameraLocation();
}
if (LocalPlayer->GetProjectionData(LocalPlayer->ViewportClient->Viewport, eSSP_RIGHT_EYE, /*out*/ RightProjectionData))
{
RightGazeData.WorldGazeOrigin = RightProjectionData.ViewOrigin;
const FVector ProjectionPoint = RightGazeData.WorldGazeOrigin + (RightGazeData.WorldGazeDirection * ProjectionDistanceCm);
ActivePlayerController->ProjectWorldLocationToScreen(ProjectionPoint, RightGazeData.ScreenGazePointPx);
}
else if (ActivePlayerController->PlayerCameraManager != nullptr)
{
RightGazeData.WorldGazeOrigin = ActivePlayerController->PlayerCameraManager->GetCameraLocation();
}
CombinedGazeData.WorldGazeOrigin = (LeftGazeData.WorldGazeOrigin + RightGazeData.WorldGazeOrigin) / 2.0f;
CombinedGazeData.ScreenGazePointPx = (LeftGazeData.ScreenGazePointPx + RightGazeData.ScreenGazePointPx) / 2.0f;
}
}
else
{
CombinedGazeData.bIsGazeDataValid = ActivePlayerController->DeprojectScreenPositionToWorld(CombinedGazeData.ScreenGazePointPx.X, CombinedGazeData.ScreenGazePointPx.Y, CombinedGazeData.WorldGazeOrigin, CombinedGazeData.WorldGazeDirection);
CombinedGazeData.WorldGazeOrigin = ActivePlayerController->PlayerCameraManager->GetCameraLocation();
LeftGazeData = RightGazeData = CombinedGazeData;
}
//Verify direction validity
if (!CombinedGazeData.WorldGazeDirection.IsNormalized())
{
CombinedGazeData.bIsGazeDataValid = false;
}
if (!LeftGazeData.WorldGazeDirection.IsNormalized())
{
LeftGazeData.bIsGazeDataValid = false;
}
if (!RightGazeData.WorldGazeDirection.IsNormalized())
{
RightGazeData.bIsGazeDataValid = false;
}
FCollisionQueryParams CollisionQueryParams;
CollisionQueryParams.AddIgnoredActor(ActivePlayerController.Get());
CollisionQueryParams.AddIgnoredActor(ActivePlayerController->GetPawn());
const float MaximumTraceDistance = FMath::Max(CVarTobiiMaximumTraceDistance.GetValueOnGameThread(), 0.0f);
const FVector CombinedGazeFarLocation = CombinedGazeData.WorldGazeOrigin + (CombinedGazeData.WorldGazeDirection * MaximumTraceDistance);
if (!CombinedGazeData.bIsGazeDataValid ||
!ActivePlayerController->GetWorld()->LineTraceSingleByChannel(CombinedWorldGazeHitData, CombinedGazeData.WorldGazeOrigin
, CombinedGazeFarLocation, (ECollisionChannel)CVarTobiiFocusTraceChannel.GetValueOnGameThread(), CollisionQueryParams))
{
CombinedWorldGazeHitData.Actor = nullptr;
CombinedWorldGazeHitData.Component = nullptr;
CombinedWorldGazeHitData.Distance = MaximumTraceDistance;
CombinedWorldGazeHitData.Location = CombinedGazeFarLocation;
CombinedWorldGazeHitData.bBlockingHit = false;
}
const FVector LeftGazeFarLocation = LeftGazeData.WorldGazeOrigin + (LeftGazeData.WorldGazeDirection * MaximumTraceDistance);
if (!LeftGazeData.bIsGazeDataValid ||
!ActivePlayerController->GetWorld()->LineTraceSingleByChannel(LeftWorldGazeHitData, LeftGazeData.WorldGazeOrigin
, LeftGazeFarLocation, (ECollisionChannel)CVarTobiiFocusTraceChannel.GetValueOnGameThread(), CollisionQueryParams))
{
LeftWorldGazeHitData.Actor = nullptr;
LeftWorldGazeHitData.Component = nullptr;
LeftWorldGazeHitData.Distance = MaximumTraceDistance;
LeftWorldGazeHitData.Location = CombinedGazeFarLocation;
LeftWorldGazeHitData.bBlockingHit = false;
}
const FVector RightGazeFarLocation = RightGazeData.WorldGazeOrigin + (RightGazeData.WorldGazeDirection * MaximumTraceDistance);
if (!RightGazeData.bIsGazeDataValid ||
!ActivePlayerController->GetWorld()->LineTraceSingleByChannel(RightWorldGazeHitData, RightGazeData.WorldGazeOrigin
, RightGazeFarLocation, (ECollisionChannel)CVarTobiiFocusTraceChannel.GetValueOnGameThread(), CollisionQueryParams))
{
RightWorldGazeHitData.Actor = nullptr;
RightWorldGazeHitData.Component = nullptr;
RightWorldGazeHitData.Distance = MaximumTraceDistance;
RightWorldGazeHitData.Location = CombinedGazeFarLocation;
RightWorldGazeHitData.bBlockingHit = false;
}
}
}
void FTobiiEyeTracker::UpdateStabilityData(float DeltaTime)
{
if (CVarEnableEyetrackingEmulation.GetValueOnGameThread() && bIsXR)
{
CombinedGazeData.bIsStable = true;
}
else
{
//We will calculate the average gaze point velocity using a very simple moving cumulative average.
//If the average velocity of these points is under some predetermined threshold, we will consider it stable.
if (GazePointDeltaTimeMicroSecs > 0)
{
const float GazeAngleDiffDeg = FMath::RadiansToDegrees(FMath::Acos(FVector::DotProduct(CombinedGazeData.WorldGazeDirection, PrevCombinedGazeDirection)));
const double AngularSpeedDegPerMicroSecs = GazeAngleDiffDeg / (double)GazePointDeltaTimeMicroSecs;
const float StablePointInterpolationBias = FMath::Clamp(CVarTobiiStablePointInterpolationBias.GetValueOnGameThread(), 0.3f, 1.0f);
const float NewAvgSpeed = FMath::LerpStable(CurrentAverageGazeAngularSpeedDegPerMicroSecs, AngularSpeedDegPerMicroSecs, StablePointInterpolationBias);
CurrentAverageGazeAngularSpeedDegPerMicroSecs = FMath::IsFinite(NewAvgSpeed) ? NewAvgSpeed : 0.0f; //Protect from overflow
PrevCombinedGazeDirection = CombinedGazeData.WorldGazeDirection;
const float MaxAcceptableStablePointAverageSpeed = FMath::Max(CVarTobiiMaximumAcceptableStableGazeAverageAngularSpeedDegPerMicroSecs.GetValueOnGameThread(), 0.0f);
CombinedGazeData.bIsStable = CurrentAverageGazeAngularSpeedDegPerMicroSecs <= MaxAcceptableStablePointAverageSpeed;
}
}
LeftGazeData.bIsStable = RightGazeData.bIsStable = CombinedGazeData.bIsStable;
}
void FTobiiEyeTracker::SetEyeTrackedPlayer(APlayerController* PlayerController)
{
if (PlayerController != nullptr)
{
ActivePlayerController = PlayerController;
}
else if (GEngine != nullptr && GEngine->GameViewport != nullptr)
{
ActivePlayerController = GEngine->GetFirstLocalPlayerController(GEngine->GameViewport->GetWorld());
}
bIsXR = UTobiiBlueprintLibrary::IsXRPlayerController(ActivePlayerController.Get());
}
/************************************************************************/
/* IGazeTracker */
/************************************************************************/
const FTobiiGazeData& FTobiiEyeTracker::GetCombinedGazeData() const
{
return CombinedGazeData;
}
const FTobiiGazeData& FTobiiEyeTracker::GetLeftGazeData() const
{
return LeftGazeData;
}
const FTobiiGazeData& FTobiiEyeTracker::GetRightGazeData() const
{
return RightGazeData;
}
ETobiiGazeTrackerStatus FTobiiEyeTracker::GetGazeTrackerStatus() const
{
return GazeTrackerStatus;
}
bool FTobiiEyeTracker::GetGazeTrackerCapability(ETobiiGazeTrackerCapability Capability) const
{
//Desktop trackers only support CombinedGazeData
return bIsXR || Capability == ETobiiGazeTrackerCapability::CombinedGazeData;
}
/************************************************************************/
/* IHeadTracker */
/************************************************************************/
const FTobiiHeadPoseData& FTobiiEyeTracker::GetHeadPoseData() const
{
return HeadPoseData;
}
/************************************************************************/
/* ITobiiEyetracker */
/************************************************************************/
const FHitResult& FTobiiEyeTracker::GetCombinedWorldGazeHitData() const
{
return CombinedWorldGazeHitData;
}
const FHitResult& FTobiiEyeTracker::GetLeftWorldGazeHitData() const
{
return LeftWorldGazeHitData;
}
const FHitResult& FTobiiEyeTracker::GetRightWorldGazeHitData() const
{
return RightWorldGazeHitData;
}
const FTobiiDisplayInfo& FTobiiEyeTracker::GetDisplayInformation() const
{
return DisplayInfo;
}
const FTobiiDesktopTrackBox& FTobiiEyeTracker::GetDesktopTrackBox() const
{
return DesktopTrackBox;
}
const FRotator& FTobiiEyeTracker::GetInfiniteScreenAngles() const
{
return InfiniteScreenAngles;
}
/************************************************************************/
/* IEyetracker */
/************************************************************************/
bool FTobiiEyeTracker::GetEyeTrackerGazeData(FEyeTrackerGazeData& OutGazeData) const
{
OutGazeData.ConfidenceValue = CombinedGazeData.bIsGazeDataValid ? 1.0f : 0.0f;
OutGazeData.FixationPoint = FVector::ZeroVector; //This data is not useful for a game designer as it is not reliable enough, so we don't supply it.
OutGazeData.GazeOrigin = CombinedGazeData.WorldGazeOrigin;
OutGazeData.GazeDirection = CombinedGazeData.WorldGazeDirection;
return true;
}
bool FTobiiEyeTracker::GetEyeTrackerStereoGazeData(FEyeTrackerStereoGazeData& OutGazeData) const
{
OutGazeData.ConfidenceValue = CombinedGazeData.bIsGazeDataValid ? 1.0f : 0.0f;
OutGazeData.FixationPoint = FVector::ZeroVector; //This data is not useful for a game designer as it is not reliable enough, so we don't supply it.
OutGazeData.LeftEyeOrigin = LeftGazeData.WorldGazeOrigin;
OutGazeData.LeftEyeDirection = LeftGazeData.WorldGazeDirection;
OutGazeData.RightEyeOrigin = RightGazeData.WorldGazeOrigin;
OutGazeData.RightEyeOrigin = RightGazeData.WorldGazeDirection;
return true;
}
EEyeTrackerStatus FTobiiEyeTracker::GetEyeTrackerStatus() const
{
switch (GazeTrackerStatus)
{
case ETobiiGazeTrackerStatus::NotConnected: return EEyeTrackerStatus::NotConnected;
case ETobiiGazeTrackerStatus::NotConfigured: return EEyeTrackerStatus::NotConnected;
case ETobiiGazeTrackerStatus::Disabled: return EEyeTrackerStatus::NotConnected;
case ETobiiGazeTrackerStatus::UserNotPresent: return EEyeTrackerStatus::NotTracking;
case ETobiiGazeTrackerStatus::UserPresent: return EEyeTrackerStatus::Tracking;
default: return EEyeTrackerStatus::NotConnected;
}
}
FVector2D FTobiiEyeTracker::ConvertRawGazePointUNormToGameViewportCoordinateUNorm(FSceneViewport* GameViewport, const FVector2D& InNormalizedPoint)
{
#if WITH_EDITOR
//We need this to make sure we get a correct gaze point if the editor has tool windows attached to the window itself.
FIntPoint VirtualDesktopPixelCoord;
if (FTobiiPlatformSpecific::ConvertGazeCoordinateToVirtualDesktopPixel(DisplayInfo.GameWindowHandle, InNormalizedPoint, VirtualDesktopPixelCoord))
{
FVector2D ViewportPixelCoordUNorm;
if (UTobiiBlueprintLibrary::VirtualDesktopPixelToViewportCoordinateUNorm(VirtualDesktopPixelCoord, ViewportPixelCoordUNorm))
{
return ViewportPixelCoordUNorm;
}
}
#endif
return InNormalizedPoint;
}
// void FTobiiEyeTracker::DrawDebug(AHUD* HUD, UCanvas* Canvas, const FDebugDisplayInfo& DebugDisplayInfo, float& YL, float& YPos)
// {
// if (bIsXR)
// {
// return;
// }
//
// UWorld* World = Canvas->GetWorld();
// UFont* RenderFont = GEngine->GetSmallFont();
// EGazeTrackerStatus Status = GetGazeTrackerStatus();
//
// //First draw our status string
// FString StatusText;
// switch (Status)
// {
// case EGazeTrackerStatus::Disabled:
// StatusText = "Eyetracking disabled";
// Canvas->DrawColor = FColor::Magenta;
// break;
// case EGazeTrackerStatus::UserNotPresent:
// StatusText = "User not present";
// Canvas->DrawColor = FColor::Purple;
// break;
// case EGazeTrackerStatus::UserPresent:
// StatusText = "User is present";
// Canvas->DrawColor = FColor::Blue;
// break;
// default:
// StatusText = "Eyetracking not available";
// Canvas->DrawColor = FColor::Red;
// break;
// }
//
// TArray<FStringFormatArg> Args;
// Args.Emplace(StatusText);
//
// //Can't use SizeX/Y since it wont work on other DPI settings
// float CanvasDrawWidth = Canvas->ClipX - Canvas->OrgX;
// float CanvasDrawHeight = Canvas->ClipY - Canvas->OrgY;
//
// Canvas->DrawText(RenderFont, FString::Format(TEXT("Status: {0}"), Args), 0.5f * CanvasDrawWidth - 100, 0.05f * CanvasDrawHeight);
//
// if (CVarEnableHeadPoseDebug.GetValueOnGameThread())
// {
// const float PoseDebugScale = 1.0f;
//
// FVector WorldPosition, WorldDirection;
// HUD->Deproject(CanvasDrawWidth * 0.5f, CanvasDrawHeight * 0.8f, WorldPosition, WorldDirection);
//
// //First we figure our origin data
// const FVector PoseDebugLocation = WorldPosition + WorldDirection * 200.0f;
// const FQuat PoseDebugRotation = HUD->GetOwner()->GetActorRotation().Quaternion();
//
// FCanvasLineItem LineItem;
// const FHeadPoseData& HeadPose = GetHeadPoseData();
// const float BasisScale = PoseDebugScale * 7.0f;
// LineItem.LineThickness = 3.0f;
//
// FQuat OriginRotation = PoseDebugRotation * HeadPose.HeadOrientation.Quaternion();
// FVector ForwardBasis = OriginRotation.RotateVector(FVector::ForwardVector) * BasisScale;
// FVector RightBasis = OriginRotation.RotateVector(FVector::RightVector) * BasisScale * 0.5f;
// FVector UpBasis = OriginRotation.RotateVector(FVector::UpVector) * BasisScale * 0.5f;
//
// FVector OriginWorldPosition = PoseDebugLocation;// + PoseDebugRotation.RotateVector((HeadPose.HeadPositionCm - TrackBoxCenter) * PoseDebugScale);
// FVector ProjectedOrigin = HUD->Project(OriginWorldPosition);
// FVector ProjectedForward = HUD->Project(OriginWorldPosition + ForwardBasis);
// FVector ProjectedRight = HUD->Project(OriginWorldPosition + RightBasis);
// FVector ProjectedUp = HUD->Project(OriginWorldPosition + UpBasis);
//
// LineItem.SetColor(FLinearColor::Red);
// LineItem.Origin = ProjectedOrigin;
// LineItem.EndPos = ProjectedForward;
// Canvas->DrawItem(LineItem);
// LineItem.SetColor(FLinearColor::Green);
// LineItem.Origin = ProjectedOrigin;
// LineItem.EndPos = ProjectedRight;
// Canvas->DrawItem(LineItem);
// LineItem.SetColor(FLinearColor::Blue);
// LineItem.Origin = ProjectedOrigin;
// LineItem.EndPos = ProjectedUp;
// Canvas->DrawItem(LineItem);
// }
// }
#endif //TOBII_EYETRACKING_ACTIVE

View File

@@ -0,0 +1,104 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#if TOBII_EYETRACKING_ACTIVE
#include "ITobiiEyeTracker.h"
#include "TobiiPlatformSpecific.h"
#include "TobiiInternalTypes.h"
#include "tobii_gameintegration.h"
#include "CoreMinimal.h"
#include "Containers/Ticker.h"
#include "GameFramework/PlayerController.h"
#include "Slate/SceneViewport.h"
/*
* FTickerObjectBase here is ticked almost last in a frame.
* The order seems to go 'IInputDevice', then shortly after that 'Actor/Components' and very late after that the 'TickerCore' and thereby also FTickerObjectBase.
* No matter if we choose IInputDevice or FTickerObjectBase we will be working with complete physics data from the last frame.
* Of course we would rather be working with current frame physics data, but doing that would make our data more TickGroup dependent.
* Maybe that's fine, but seeing as our traces are not really precise anyways, I think the simplicity of design of using FTickerObjectBase wins out.
*
* The big alternative would be to move all GTOM related objects to an actor that belongs to the PostPhysics tick group together with the focus managers and introduce a tick dependency.
* See FEngineLoop::Tick() for more detailed info.
*/
class FTobiiEyeTracker : public ITobiiEyeTracker, public FTickerObjectBase
{
public:
FTobiiEyeTracker();
virtual ~FTobiiEyeTracker();
virtual bool Tick(float DeltaTime) override;
void Shutdown();
bool IsConnectedToEyeTracker();
/************************************************************************/
/* ITobiiEyetracker */
/************************************************************************/
public:
virtual const FTobiiGazeData& GetCombinedGazeData() const override;
virtual const FTobiiGazeData& GetLeftGazeData() const override;
virtual const FTobiiGazeData& GetRightGazeData() const override;
virtual ETobiiGazeTrackerStatus GetGazeTrackerStatus() const override;
virtual bool GetGazeTrackerCapability(ETobiiGazeTrackerCapability Capability) const override;
virtual const FHitResult& GetCombinedWorldGazeHitData() const override;
virtual const FHitResult& GetLeftWorldGazeHitData() const override;
virtual const FHitResult& GetRightWorldGazeHitData() const override;
virtual const FTobiiDisplayInfo& GetDisplayInformation() const override;
virtual const FTobiiHeadPoseData& GetHeadPoseData() const override;
virtual const FTobiiDesktopTrackBox& GetDesktopTrackBox() const override;
virtual const FRotator& GetInfiniteScreenAngles() const override;
public:
virtual void SetEyeTrackedPlayer(APlayerController* PlayerController) override;
virtual bool IsStereoGazeDataAvailable() const override { return bIsXR; }
virtual bool GetEyeTrackerGazeData(FEyeTrackerGazeData& OutGazeData) const override;
virtual bool GetEyeTrackerStereoGazeData(FEyeTrackerStereoGazeData& OutGazeData) const override;
virtual EEyeTrackerStatus GetEyeTrackerStatus() const override;
private:
TobiiGameIntegration::ITobiiGameIntegrationApi* TgiApi;
FTobiiPlatformNotifications PlatformNotifications;
TWeakObjectPtr<APlayerController> ActivePlayerController;
FHitResult CombinedWorldGazeHitData;
FHitResult LeftWorldGazeHitData;
FHitResult RightWorldGazeHitData;
FTobiiGazeData LeftGazeData;
FTobiiGazeData RightGazeData;
FTobiiGazeData CombinedGazeData;
ETobiiGazeTrackerStatus GazeTrackerStatus;
FTobiiHeadPoseData HeadPoseData;
FTobiiDisplayInfo DisplayInfo;
FRotator InfiniteScreenAngles;
FTobiiDesktopTrackBox DesktopTrackBox;
FTobiiRawGazePoint RawGazePoint;
FTobiiRawHeadPose RawHeadPose;
FVector PrevCombinedGazeDirection;
bool bIsXR;
int64 GazeDataTimeStampMicroSecs;
uint64 GazePointDeltaTimeMicroSecs;
double CurrentAverageGazeAngularSpeedDegPerMicroSecs;
FDateTime StartTime;
void ResetData();
bool UpdateLowLevelResources();
void TickDesktop(float DeltaTime);
void TickXR(float DeltaTime);
void UpdateWorldSpaceData(float DeltaTime);
void UpdateStabilityData(float DeltaTime);
FVector2D ConvertRawGazePointUNormToGameViewportCoordinateUNorm(FSceneViewport* GameViewport, const FVector2D& InNormalizedPoint);
FVector2D& TickEmulatedGazePointUNorm(float DeltaTime);
};
#endif //TOBII_EYETRACKING_ACTIVE

View File

@@ -0,0 +1,76 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "TobiiTypes.h"
#include "CoreMinimal.h"
class FEyetrackingUtils
{
public:
static float CalculateFovealRegionHeightPx(const float MainViewportHeightCm, const float MainViewportHeightPx, const float HeadDistanceCm, const float FovealRegionAngleDeg)
{
if (MainViewportHeightCm > FLT_EPSILON && MainViewportHeightPx > FLT_EPSILON && HeadDistanceCm > FLT_EPSILON)
{
const float FovealRegionSizeCm = HeadDistanceCm * FMath::Tan(FMath::DegreesToRadians(FovealRegionAngleDeg));
return FovealRegionSizeCm / MainViewportHeightCm * MainViewportHeightPx;
}
return 100.0f;
}
};
struct FTobiiRawGazePoint
{
public:
FVector2D GazePointNormalized;
FDateTime TimeStamp;
FTobiiRawGazePoint()
: GazePointNormalized(0.0f, 0.0f)
, TimeStamp()
{ }
};
struct FTobiiRawHeadPose
{
public:
FVector HeadPositionCm;
FRotator HeadOrientation;
FDateTime TimeStamp;
public:
FTobiiRawHeadPose()
: HeadPositionCm(0.0f, 0.0f, 0.0f)
, HeadOrientation(0.0f, 0.0f, 0.0f)
, TimeStamp()
{}
};
struct FTobiiRawXREyetrackingData
{
public:
FVector RawLeftEyeDirection;
FVector RawRightEyeDirection;
FDateTime EngineTimeStamp;
int64 EyetrackerTimeStamp;
bool bLeftEyeValid;
bool bRightEyeValid;
FTobiiRawXREyetrackingData()
: RawLeftEyeDirection()
, RawRightEyeDirection()
, EngineTimeStamp()
, EyetrackerTimeStamp(-1)
, bLeftEyeValid(false)
, bRightEyeValid(false)
{
}
};
DEFINE_LOG_CATEGORY_STATIC(LogTobiiEyetracking, All, All);

View File

@@ -0,0 +1,135 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#include "TobiiPlatformSpecific.h"
#include "Framework/Application/SlateApplication.h"
#if PLATFORM_WINDOWS
#include "Windows/AllowWindowsPlatformTypes.h"
#include "dbt.h"
struct MonitorHandleContext
{
FString DeviceName;
HMONITOR hMonitorHandle;
};
BOOL CALLBACK EnumDisplayMonitorsProc(HMONITOR hMonitorHandle, HDC, LPRECT, LPARAM Data)
{
MonitorHandleContext* Context = (MonitorHandleContext*)Data;
MONITORINFOEXA MonitorInfo;
MonitorInfo.cbSize = sizeof(MonitorInfo);
int res = GetMonitorInfoA(hMonitorHandle, &MonitorInfo);
if (res && _stricmp(MonitorInfo.szDevice, TCHAR_TO_ANSI(*Context->DeviceName)) == 0)
{
Context->hMonitorHandle = hMonitorHandle;
return FALSE;
}
return TRUE;
}
void* FTobiiPlatformSpecific::GetMonitorInformation(const void* GameWindowHandle, int& MonitorWidthPx, int& MonitorHeightPx)
{
HWND hGameWindowHandle = (HWND)GameWindowHandle;
HMONITOR hGameMonitor = MonitorFromWindow(hGameWindowHandle, MONITOR_DEFAULTTONEAREST);
MONITORINFO MonitorInfo;
MonitorInfo.cbSize = sizeof(MONITORINFO);
if (GetMonitorInfo(hGameMonitor, &MonitorInfo))
{
MonitorWidthPx = MonitorInfo.rcMonitor.right - MonitorInfo.rcMonitor.left;
MonitorHeightPx = MonitorInfo.rcMonitor.bottom - MonitorInfo.rcMonitor.top;
return (void*)hGameMonitor;
}
MonitorWidthPx = -1;
MonitorHeightPx = -1;
return nullptr;
}
void* FTobiiPlatformSpecific::MonitorHandleFromDeviceName(FString DeviceName)
{
MonitorHandleContext Context;
Context.DeviceName = DeviceName;
Context.hMonitorHandle = nullptr;
EnumDisplayMonitors(nullptr, nullptr, EnumDisplayMonitorsProc, (LPARAM)&Context);
return (void*)Context.hMonitorHandle;
}
bool FTobiiPlatformSpecific::ConvertGazeCoordinateToVirtualDesktopPixel(void* GameWindowHandle, const FVector2D& ClientCoordsUNorm, FIntPoint& OutVirtualDesktopPixel)
{
//Now we can try to convert our point
RECT ClientRect;
if (GetClientRect((HWND)GameWindowHandle, &ClientRect))
{
POINT ClientPoint;
ClientPoint.x = (int32)(ClientCoordsUNorm.X * (ClientRect.right - ClientRect.left));
ClientPoint.y = (int32)(ClientCoordsUNorm.Y * (ClientRect.bottom - ClientRect.top));
if (ClientToScreen((HWND)GameWindowHandle, &ClientPoint))
{
OutVirtualDesktopPixel.X = ClientPoint.x;
OutVirtualDesktopPixel.Y = ClientPoint.y;
return true;
}
}
return false;
}
FTobiiPlatformNotifications::FTobiiPlatformNotifications()
: bShouldForceEyetrackerReconnect(true)
, bShouldUpdateGameMonitorHandle(true)
{
if (FSlateApplication::IsInitialized())
{
const TSharedPtr<GenericApplication> Application = FSlateApplication::Get().GetPlatformApplication();
if (Application.IsValid())
{
FWindowsApplication* WindowsApplication = (FWindowsApplication*)Application.Get();
WindowsApplication->AddMessageHandler(*this);
}
}
}
FTobiiPlatformNotifications::~FTobiiPlatformNotifications()
{
if (FSlateApplication::IsInitialized())
{
const TSharedPtr<GenericApplication> Application = FSlateApplication::Get().GetPlatformApplication();
if (Application.IsValid())
{
FWindowsApplication* WindowsApplication = (FWindowsApplication*)Application.Get();
WindowsApplication->RemoveMessageHandler(*this);
}
}
}
bool FTobiiPlatformNotifications::ProcessMessage(HWND hwnd, uint32 msg, WPARAM wParam, LPARAM lParam, int32& OutResult)
{
switch (msg)
{
case WM_DEVICECHANGE:
{
if (wParam == DBT_DEVICEARRIVAL)
{
//This is narrow enough for us. We just want to short-circuit the reconnection timer if someone plugged in an eyetracker so they don't have to wait the full reconnection time.
bShouldForceEyetrackerReconnect = true;
}
break;
}
case WM_DISPLAYCHANGE:
{
bShouldUpdateGameMonitorHandle = true;
break;
}
}
return false;
}
#include "Runtime/Core/Public/Windows/HideWindowsPlatformTypes.h"
#endif

View File

@@ -0,0 +1,39 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "CoreMinimal.h"
class FTobiiPlatformSpecific
{
public:
static void* GetMonitorInformation(const void* GameWindowHandle, int& MonitorWidthPx, int& MonitorHeightPx);
static void* MonitorHandleFromDeviceName(FString DeviceName);
static bool ConvertGazeCoordinateToVirtualDesktopPixel(void* GameWindowHandle, const FVector2D& ClientCoordsUNorm, FIntPoint& OutVirtualDesktopPixel);
};
#if PLATFORM_WINDOWS
#include "Windows/WindowsApplication.h"
#include "HAL/ThreadSafeBool.h"
class FTobiiPlatformNotifications : public IWindowsMessageHandler
{
public:
FTobiiPlatformNotifications();
virtual ~FTobiiPlatformNotifications();
virtual bool ProcessMessage(HWND hwnd, uint32 msg, WPARAM wParam, LPARAM lParam, int32& OutResult) override;
public:
FThreadSafeBool bShouldForceEyetrackerReconnect;
FThreadSafeBool bShouldUpdateGameMonitorHandle;
};
#elif PLATFORM_MAC
#else
#endif

View File

@@ -0,0 +1,154 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#if WITH_EDITOR
#include "STobiiLicenseWindow.h"
#include "STobiiWelcomeWindow.h"
#include "CoreGlobals.h"
#include "Editor.h"
#include "UnrealClient.h"
#include "EditorStyleSet.h"
#include "Framework/Application/SlateApplication.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/FileHelper.h"
#include "Widgets/Layout/SGridPanel.h"
#include "Widgets/Layout/SScrollBox.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Input/SButton.h"
#define LOCTEXT_NAMESPACE "TobiiLicenseWindow"
void STobiiLicenseWindow::Construct(const FArguments& InArgs, FTobiiCoreModule* InCoreModule)
{
if (GEditor == nullptr)
{
return;
}
CoreModule = InCoreModule;
#if TOBII_COMPILE_AS_ENGINE_PLUGIN
FString LicenseFilePath = FPaths::EnginePluginsDir() / TEXT("Runtime/TobiiEyetracking/Resources/License.txt");
#else
FString LicenseFilePath = FPaths::ProjectPluginsDir() / TEXT("TobiiEyetracking/Resources/License.txt");
#endif
FString LicenseText;
bool bCanAccept = true;
if (!FFileHelper::LoadFileToString(LicenseText, *LicenseFilePath))
{
LicenseText = "License file not found in Resources folder!\n\nAccept button disabled.\nIf you cannot repair/replace the Tobii SDK, please remove it from your project.";
bCanAccept = false;
}
SWindow::Construct(SWindow::FArguments()
.SupportsMaximize(false)
.SupportsMinimize(false)
.IsPopupWindow(true)
.CreateTitleBar(false)
.SizingRule(ESizingRule::FixedSize)
.SupportsTransparency(EWindowTransparency::None)
.InitialOpacity(1.0f)
.FocusWhenFirstShown(true)
.bDragAnywhere(false)
.ActivationPolicy(EWindowActivationPolicy::FirstShown)
.ClientSize(FVector2D(1024, 768))
.ScreenPosition(FVector2D((float)(GEditor->GetActiveViewport()->GetSizeXY().X) / 2.0,
(float)(GEditor->GetActiveViewport()->GetSizeXY().Y) / 2.0))
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.FillHeight(0.9f)
.Padding(5)
[
SNew(SScrollBox)
+SScrollBox::Slot()
[
SNew(SGridPanel)
.FillColumn(0, 1.0f)
.FillRow(0, 1.0f)
+ SGridPanel::Slot(0, 0)
[
SNew(SImage)
.ColorAndOpacity(FSlateColor(FLinearColor(FColor::Black)))
]
+ SGridPanel::Slot(0, 0)
[
SNew(STextBlock)
.AutoWrapText(true)
.Margin(10.0f)
.Text(FText::FromString(LicenseText))
]
]
]
+ SVerticalBox::Slot()
.FillHeight(0.05f)
[
SNew(SButton)
.VAlign(VAlign_Center)
.HAlign(HAlign_Fill)
.IsEnabled(bCanAccept)
.OnClicked(this, &STobiiLicenseWindow::AcceptLicense)
[
SNew(STextBlock)
.Text(LOCTEXT("OkButton", "I Accept the terms of this agreement"))
.Justification(ETextJustify::Center)
]
]
+ SVerticalBox::Slot()
.FillHeight(0.05f)
[
SNew(SButton)
.VAlign(VAlign_Center)
.HAlign(HAlign_Fill)
.OnClicked(this, &STobiiLicenseWindow::ShutdownEditor)
[
SNew(STextBlock)
.Text(LOCTEXT("ExitButton", "I do not accept the terms. Shut down editor so I can remove the plugin."))
.Justification(ETextJustify::Center)
]
]
]);
bIsTopmostWindow = true;
FlashWindow();
}
FReply STobiiLicenseWindow::AcceptLicense()
{
RequestDestroyWindow();
if (GConfig != nullptr)
{
GConfig->SetBool(TEXT("Tobii"), TEXT("TobiiLicenseAccepted"), true, GGameIni);
}
bool bTutorialShown = false;
GConfig->GetBool(TEXT("Tobii"), TEXT("TobiiTutorialShown"), bTutorialShown, GGameIni);
if (!bTutorialShown && GEditor != nullptr)
{
FSlateApplication::Get().AddWindow(SNew(STobiiWelcomeWindow));
}
return FReply::Handled();
}
FReply STobiiLicenseWindow::ShutdownEditor()
{
RequestDestroyWindow();
FGenericPlatformMisc::RequestExit(false);
return FReply::Handled();
}
#undef LOCTEXT_NAMESPACE
#endif //WITH_EDITOR

View File

@@ -0,0 +1,36 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#if WITH_EDITOR
#include "../TobiiCoreModule.h"
#include "CoreMinimal.h"
#include "SWebBrowser.h"
#include "Widgets/SWindow.h"
class STobiiLicenseWindow : public SWindow
{
public:
SLATE_BEGIN_ARGS(STobiiLicenseWindow) { }
SLATE_END_ARGS()
STobiiLicenseWindow() {}
/** Widget constructor */
void Construct(const FArguments& Args, FTobiiCoreModule* InCoreModule);
private:
TSharedPtr<SWebBrowser> MainBrowser;
FTobiiCoreModule* CoreModule;
FReply AcceptLicense();
FReply ShutdownEditor();
};
#endif //WITH_EDITOR

View File

@@ -0,0 +1,214 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#if WITH_EDITOR
#include "STobiiWelcomeWindow.h"
#include "Editor.h"
#include "EditorStyleSet.h"
#include "ContentBrowserModule.h"
#include "IContentBrowserSingleton.h"
#include "CoreGlobals.h"
#include "UnrealClient.h"
#include "Widgets/Layout/SGridPanel.h"
#include "Widgets/Layout/SScrollBox.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SCheckBox.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/FileHelper.h"
#include "Modules/ModuleManager.h"
#define LOCTEXT_NAMESPACE "STobiiWelcomeWindow"
void STobiiWelcomeWindow::Construct(const FArguments& InArgs)
{
if (GEditor == nullptr)
{
return;
}
bDontShowAgain = false;
SWindow::Construct(SWindow::FArguments()
.SupportsMaximize(false)
.SupportsMinimize(false)
.IsPopupWindow(true)
.CreateTitleBar(false)
.SizingRule(ESizingRule::FixedSize)
.SupportsTransparency(EWindowTransparency::None)
.InitialOpacity(1.0f)
.FocusWhenFirstShown(true)
.bDragAnywhere(false)
.ActivationPolicy(EWindowActivationPolicy::FirstShown)
.ClientSize(FVector2D(600, 400))
.ScreenPosition(FVector2D((float)(GEditor->GetActiveViewport()->GetSizeXY().X) / 2.0,
(float)(GEditor->GetActiveViewport()->GetSizeXY().Y) / 2.0))
[
SNew(SGridPanel)
.FillColumn(0, 1.0f)
.FillRow(0, 1.0f)
+ SGridPanel::Slot(0, 0)
[
SNew(SImage)
.ColorAndOpacity(FSlateColor(FLinearColor(FColor::Black)))
]
+ SGridPanel::Slot(0, 0)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.Padding(10.0f)
[
SNew(STextBlock)
.Justification(ETextJustify::Center)
.Margin(5.0f)
.Font(FEditorStyle::GetFontStyle(FName("ToggleButton.LabelFont")))
.Text(FText::FromString("Tobii Sample Content"))
]
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(STextBlock)
.AutoWrapText(true)
.Margin(5.0f)
.Text(FText::FromString("Thank you again for downloading the UE4 Tobii eyetracking plugin!.\nThis plugin comes loaded with sample content as well as the core functionality to enable your development environment to leverage the eyetracker hardware.\nThe sample content is packaged can be accessed via the content browser under the 'TobiiEyetracking' plugin section. As plugin content is not visible by default however, you need to first click the small eye button at the bottom right of your content browser and check the 'show plugin content' checkbox. After that you should be able to scroll your content browser down to see the category."))
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(15.0f)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SCheckBox)
.IsChecked(bDontShowAgain ? ECheckBoxState::Checked : ECheckBoxState::Unchecked)
.OnCheckStateChanged_Lambda([this](ECheckBoxState State) { bDontShowAgain = State == ECheckBoxState::Checked; })
]
+SHorizontalBox::Slot()
.AutoWidth()
[
SNew(STextBlock)
.Text(FText::FromString("Don't show this window again."))
]
]
+ SVerticalBox::Slot()
.Padding(0.0f, 50.0f, 0.0f, 0.0f)
[
SNew(STextBlock)
.Text(FText::FromString("Would you like to navigate to the samples now?"))
]
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(SButton)
.VAlign(VAlign_Center)
.HAlign(HAlign_Fill)
.OnClicked(this, &STobiiWelcomeWindow::ShowDesktopSamples)
[
SNew(STextBlock)
.Text(LOCTEXT("OkButton", "Yes, please show me the desktop samples"))
.Justification(ETextJustify::Center)
]
]
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(SButton)
.VAlign(VAlign_Center)
.HAlign(HAlign_Fill)
.OnClicked(this, &STobiiWelcomeWindow::ShowXRSamples)
[
SNew(STextBlock)
.Text(LOCTEXT("OkButtonXR", "Yes, please show me the XR samples"))
.Justification(ETextJustify::Center)
]
]
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(SButton)
.VAlign(VAlign_Center)
.HAlign(HAlign_Fill)
.OnClicked(this, &STobiiWelcomeWindow::AbortTutorial)
[
SNew(STextBlock)
.Text(LOCTEXT("ExitButton", "No thank you."))
.Justification(ETextJustify::Center)
]
]
]
]);
bIsTopmostWindow = true;
FlashWindow();
}
void STobiiWelcomeWindow::UpdateFlag()
{
if (GConfig != nullptr && bDontShowAgain)
{
GConfig->SetBool(TEXT("Tobii"), TEXT("TobiiTutorialShown"), true, GGameIni);
}
}
FReply STobiiWelcomeWindow::ShowDesktopSamples()
{
RequestDestroyWindow();
UpdateFlag();
//Actually turn on plugin content viewing and navigate the content browser to the sample
IContentBrowserSingleton& ContentBrowserInterface = FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser")).Get();
ContentBrowserInterface.ForceShowPluginContent(false);
TArray<FAssetData> MapAsset;
MapAsset.Add(FAssetData(FName("/TobiiEyetracking/Sample/Desktop/DesktopExampleMap")
, FName("/TobiiEyetracking/Sample/Desktop")
, FName("DesktopExampleMap")
, FName("World")));
ContentBrowserInterface.SyncBrowserToAssets(MapAsset);
return FReply::Handled();
}
FReply STobiiWelcomeWindow::ShowXRSamples()
{
RequestDestroyWindow();
UpdateFlag();
//Actually turn on plugin content viewing and navigate the content browser to the sample
IContentBrowserSingleton& ContentBrowserInterface = FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser")).Get();
ContentBrowserInterface.ForceShowPluginContent(false);
TArray<FAssetData> MapAsset;
MapAsset.Add(FAssetData(FName("/TobiiEyetracking/Sample/XR/XRExampleMap")
, FName("/TobiiEyetracking/Sample/XR")
, FName("XRExampleMap")
, FName("World")));
ContentBrowserInterface.SyncBrowserToAssets(MapAsset);
return FReply::Handled();
}
FReply STobiiWelcomeWindow::AbortTutorial()
{
RequestDestroyWindow();
UpdateFlag();
return FReply::Handled();
}
#undef LOCTEXT_NAMESPACE
#endif //WITH_EDITOR

View File

@@ -0,0 +1,38 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#if WITH_EDITOR
#include "CoreMinimal.h"
#include "SWebBrowser.h"
#include "Widgets/SWindow.h"
class STobiiWelcomeWindow : public SWindow
{
public:
SLATE_BEGIN_ARGS(STobiiWelcomeWindow)
{
}
SLATE_END_ARGS()
STobiiWelcomeWindow() {}
/** Widget constructor */
void Construct(const FArguments& Args);
private:
TSharedPtr<SWebBrowser> MainBrowser;
bool bDontShowAgain;
void UpdateFlag();
FReply ShowDesktopSamples();
FReply ShowXRSamples();
FReply AbortTutorial();
};
#endif //WITH_EDITOR

View File

@@ -0,0 +1,88 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#if WITH_EDITOR
#include "TobiiEditorExtension.h"
#include "STobiiLicenseWindow.h"
#include "RHI.h"
#include "Editor.h"
#include "Modules/ModuleManager.h"
#include "Framework/Application/SlateApplication.h"
FTobiiEditorExtension::FTobiiEditorExtension(FTobiiCoreModule* InCoreModule)
: CoreModule(InCoreModule)
, bIsEditorInitialized(false)
{
// Defer Level Editor UI extensions until Level Editor has been loaded:
if (FModuleManager::Get().IsModuleLoaded("LevelEditor"))
{
Initialize();
}
else
{
FModuleManager::Get().OnModulesChanged().AddLambda([this](FName name, EModuleChangeReason reason)
{
if ((name == "LevelEditor") && (reason == EModuleChangeReason::ModuleLoaded))
{
Initialize();
}
});
}
}
FTobiiEditorExtension::~FTobiiEditorExtension()
{
}
void FTobiiEditorExtension::Initialize()
{
if (GUsingNullRHI)
{
return;
}
FSlateRenderer* SlateRenderer = FSlateApplication::Get().GetRenderer();
if (SlateRenderer != nullptr)
{
LoadedDelegateHandle = SlateRenderer->OnSlateWindowRendered().AddRaw(this, &FTobiiEditorExtension::OnEditorLoaded);
}
}
void FTobiiEditorExtension::OnEditorLoaded(SWindow& SlateWindow, void* ViewportRHIPtr)
{
// would be nice to use the preprocessor definition WITH_EDITOR instead,
// but the user may launch a standalone game through the editor...
if (GEditor == nullptr || CoreModule == nullptr)
{
return;
}
if (IsInGameThread())
{
FSlateRenderer* SlateRenderer = FSlateApplication::Get().GetRenderer();
SlateRenderer->OnSlateWindowRendered().Remove(LoadedDelegateHandle);
}
if (bIsEditorInitialized)
{
return;
}
bIsEditorInitialized = true;
if (GConfig != nullptr)
{
bool bTobiiLicenseAccepted(false);
GConfig->GetBool(TEXT("Tobii"), TEXT("TobiiLicenseAccepted"), bTobiiLicenseAccepted, GGameIni);
if (!bTobiiLicenseAccepted && GEditor != nullptr)
{
FSlateApplication::Get().AddWindow(SNew(STobiiLicenseWindow, CoreModule));
}
}
}
#endif //WITH_EDITOR

View File

@@ -0,0 +1,30 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#if WITH_EDITOR
#include "../TobiiCoreModule.h"
#include "CoreMinimal.h"
class FTobiiEditorExtension
{
public:
FTobiiEditorExtension(FTobiiCoreModule* InCoreModule);
~FTobiiEditorExtension();
void Initialize();
private:
FTobiiCoreModule* CoreModule;
FDelegateHandle LoadedDelegateHandle;
bool bIsEditorInitialized;
void OnEditorLoaded(SWindow& SlateWindow, void* ViewportRHIPtr);
};
#endif //WITH_EDITOR

View File

@@ -0,0 +1,59 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "ITobiiEyetracker.h"
#include "CoreMinimal.h"
#include "IEyeTrackerModule.h"
#include "Modules/ModuleInterface.h"
#include "Modules/ModuleManager.h"
DECLARE_DELEGATE(FPostLoadedTobiiEyetrackingDelegate);
/**
* The public interface of the TobiiEyeX Module.
*/
class ITobiiCore : public IEyeTrackerModule
{
public:
/**
* Singleton-like access to this module's interface. This is just for convenience!
* Beware of calling this during the shutdown phase, though. Your module might have been
* unloaded already.
*
* @return Returns singleton instance, loading the module on demand if needed
*/
static inline ITobiiCore& Get()
{
return FModuleManager::LoadModuleChecked<ITobiiCore>("TobiiCore");
}
/**
* Checks to see if this module is loaded and ready. It is only valid to call Get() if
* IsAvailable() returns true.
*
* @return True if the module is loaded and ready to use
*/
static inline bool IsAvailable()
{
return FModuleManager::Get().IsModuleLoaded("TobiiCore") && Get().GetEyeTrackerInternal().IsValid();
}
virtual FString GetModuleKeyName() const override
{
return FString("TobiiCore");
}
static TSharedPtr<ITobiiEyeTracker, ESPMode::ThreadSafe> GetEyeTracker()
{
return IsAvailable() ? Get().GetEyeTrackerInternal() : TSharedPtr<ITobiiEyeTracker, ESPMode::ThreadSafe>(nullptr);
}
protected:
virtual TSharedPtr<ITobiiEyeTracker, ESPMode::ThreadSafe> GetEyeTrackerInternal() = 0;
};

View File

@@ -0,0 +1,118 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "TobiiTypes.h"
#include "CoreMinimal.h"
#include "IEyeTracker.h"
class ITobiiEyeTracker : public IEyeTracker
{
/************************************************************************/
/* Common Data */
/************************************************************************/
public:
/**
* Will return gaze data for a combination of both eyes.
* For mono trackers this might mean sending the data from the active eye.
* For a more advanced system, it might be some combination of both eyes.
* It contains all kinds of data you might need for low level interaction.
*
* @returns Combined gaze data.
*/
virtual const FTobiiGazeData& GetCombinedGazeData() const = 0;
/**
* Will return the gaze data for the left eye.
* It contains all kinds of data you might need for low level interaction.
*
* @returns Gaze data for the right eye.
*/
virtual const FTobiiGazeData& GetLeftGazeData() const = 0;
/**
* Will return the gaze data for the right eye.
* It contains all kinds of data you might need for low level interaction.
*
* @returns Gaze data for the left eye.
*/
virtual const FTobiiGazeData& GetRightGazeData() const = 0;
/**
* Returns information about the status of the current device.
*
* @return The status of the device.
*/
virtual ETobiiGazeTrackerStatus GetGazeTrackerStatus() const = 0;
/**
* Some gaze trackers might only be able to deliver data for one eye, and others might be able to serve data for both.
* If you are planning to use other functions than GetCombinedGazeData, checking that the gaze tracker you are working with supports them is recommended.
*
* @param Capability The capability to poll.
* @returns Whether the gaze tracker supports the capability polled.
*/
virtual bool GetGazeTrackerCapability(ETobiiGazeTrackerCapability Capability) const = 0;
/**
* This is information about where the combined gaze ray hits the world. As this is needed by many different gaze related features, we do this raycast for the developer.
*
* @returns Relevant hit result.
*/
virtual const FHitResult& GetCombinedWorldGazeHitData() const = 0;
/**
* This is information about where the combined gaze ray hits the world. As this is needed by many different gaze related features, we do this raycast for the developer.
*
* @returns Relevant hit result.
*/
virtual const FHitResult& GetLeftWorldGazeHitData() const = 0;
/**
* This is information about where the combined gaze ray hits the world. As this is needed by many different gaze related features, we do this raycast for the developer.
*
* @returns Relevant hit result.
*/
virtual const FHitResult& GetRightWorldGazeHitData() const = 0;
/**
* Knowledge about the monitor size when designing gaze interactions on desktop can be very useful since working in SI units can make code screen size invariant. Display information is also useful for foveation techniques both in desktop and XR.
*
* @returns Information regarding the display associated with the currently active tracker.
*/
virtual const FTobiiDisplayInfo& GetDisplayInformation() const = 0;
/************************************************************************/
/* Head Tracker */
/************************************************************************/
public:
/**
* This gives you all the relevant data for head tracking
*
* @returns Information regarding head position and orientation
*/
virtual const FTobiiHeadPoseData& GetHeadPoseData() const = 0;
/************************************************************************/
/* Desktop */
/************************************************************************/
public:
/**
* On desktop, the tracker can only track the user within a limited frustum.
*
* @returns The vertices defining the tracking frustum in the Tobii User Coordinate Space.
*/
virtual const FTobiiDesktopTrackBox& GetDesktopTrackBox() const = 0;
/**
* This is output from the Tobii Infinite screen algorithm. You ideally shouldn't use these angles by yourself, but instead use the camera modifier found in the interaction module.
*
* @returns Infinite screen angles.
*/
virtual const FRotator& GetInfiniteScreenAngles() const = 0;
};

View File

@@ -0,0 +1,153 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "CoreMinimal.h"
#include "TobiiTypes.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Components/WidgetComponent.h"
#include "TobiiBlueprintLibrary.generated.h"
/**
* Simplified interface for blueprint use. Only exposes the features most likely to be consumed from BP.
*/
UCLASS()
class TOBIICORE_API UTobiiBlueprintLibrary : public UBlueprintFunctionLibrary
{
GENERATED_UCLASS_BODY()
/************************************************************************/
/* Common functions */
/************************************************************************/
public:
/**
* This is just a helper function to set the eyetracking enabled CVar for blueprint convenience.
* Set eyetracking disabled if you don't need tracking and don't want to pay the CPU cost in some parts of your program.
*/
UFUNCTION(BlueprintCallable, Category = "Tobii Eyetracking")
static void SetTobiiEyetrackingEnabled(bool bEyetrackingEnabled);
UFUNCTION(BlueprintPure, Category = "Tobii Eyetracking")
static bool GetTobiiEyetrackingEnabled();
/**
* This is just a helper function to set the eyetracking frozen CVar for blueprint convenience.
* Freezing the eyetracking can be very useful for debugging as well as when trying to show someone what effect eyetracking has.
*/
UFUNCTION(BlueprintCallable, Category = "Tobii Eyetracking")
static void SetTobiiEyetrackingFrozen(bool bEyetrackingFrozen);
UFUNCTION(BlueprintPure, Category = "Tobii Eyetracking")
static bool GetTobiiEyetrackingFrozen();
/**
* This is the main method to get basic eye tracking data. You should not be using this though for most things. Use the focus system instead if you can.
*/
UFUNCTION(BlueprintPure, Category = "Tobii Eyetracking")
static FTobiiGazeData GetTobiiCombinedGazeData();
UFUNCTION(BlueprintPure, Category = "Tobii Eyetracking")
static FTobiiGazeData GetTobiiLeftGazeData();
UFUNCTION(BlueprintPure, Category = "Tobii Eyetracking")
static FTobiiGazeData GetTobiiRightGazeData();
UFUNCTION(BlueprintPure, Category = "Tobii Eyetracking")
static FHitResult GetTobiiCombinedWorldGazeHitData();
/**
* This will indicate the readiness of the gaze tracking subsystem. For most applications this will also indicate head tracking readiness.
*/
UFUNCTION(BlueprintPure, Category = "Tobii Eyetracking")
static ETobiiGazeTrackerStatus GetTobiiGazeTrackerStatus();
/**
* Gets information about the display device and window used for the application.
*/
UFUNCTION(BlueprintPure, Category = "Tobii Eyetracking")
static FTobiiDisplayInfo GetTobiiDisplayInformation();
UFUNCTION(BlueprintPure, Category = "Tobii Eyetracking")
static FRotator GetTobiiInfiniteScreenAngles();
/************************************************************************/
/* Desktop functions */
/************************************************************************/
public:
/**
* This is the main method to get basic head pose data.
*/
UFUNCTION(BlueprintPure, Category = "Tobii Desktop Eyetracking")
static const FTobiiHeadPoseData& GetTobiiHeadPoseData();
/**
* Track box information.
*/
UFUNCTION(BlueprintPure, Category = "Tobii Desktop Eyetracking")
static const FTobiiDesktopTrackBox& GetTobiiDesktopTrackBox();
/************************************************************************/
/* Settings functions */
/************************************************************************/
public:
//Use these functions to get and set CVars in runtime.
UFUNCTION(BlueprintPure, Category = "Tobii Settings")
static int32 GetTobiiInt(FString CVarName);
UFUNCTION(BlueprintCallable, Category = "Tobii Settings")
static void SetTobiiInt(FString CVarName, const int32 NewValue);
UFUNCTION(BlueprintPure, Category = "Tobii Settings")
static float GetTobiiFloat(FString CVarName);
UFUNCTION(BlueprintCallable, Category = "Tobii Settings")
static void SetTobiiFloat(FString CVarName, const float NewValue);
//If you are persisting your custom CVar values, you should call this function when you start your application.
UFUNCTION(BlueprintCallable, Category = "Tobii Settings")
static void LoadTobiiSettings();
//If you want to persist a CVar to .ini, use this function.
UFUNCTION(BlueprintCallable, Category = "Tobii Settings")
static void SaveTobiiSetting(FString CVarSettingToSave);
/************************************************************************/
/* Math Util functions */
/************************************************************************/
public:
/**
* Use this function if you want to project slate information to viewport space. Includes DPI adjustments
*/
UFUNCTION(BlueprintPure, Category = "Tobii Math Utils")
static bool VirtualDesktopPixelToViewportCoordinateUNorm(const FVector2D& VirtualDesktopPixel, FVector2D& OutViewportCoordinateUNorm);
UFUNCTION(BlueprintPure, Category = "Tobii Math Utils")
static bool ViewportCoordinateUNormToVirtualDesktopPixel(const FVector2D& ViewportCoordinateUNorm, FVector2D& OutVirtualDesktopPixel);
UFUNCTION(BlueprintPure, Category = "Tobii Math Utils")
static bool ViewportPixelCoordToCmCoord(const FVector2D& InCoordinatePx, FVector2D& OutCoordinateCm);
UFUNCTION(BlueprintPure, Category = "Tobii Math Utils")
static bool ViewportCmCoordToPixelCoord(const FVector2D& InCoordinateCm, FVector2D& OutCoordinatePx);
UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils")
static bool ViewportPixelCoordToUNormCoord(const FVector2D& InCoordinatePx, FVector2D& OutCoordinateUNorm);
UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils")
static bool ViewportUNormCoordToPixelCoord(const FVector2D& InCoordinateUNorm, FVector2D& OutCoordinatePx);
/**
* This function lets you move from world space to the local space of the user. In XR this would be head space, and for desktop it would be relative to the scene camera.
*/
UFUNCTION(BlueprintPure, Category = "Tobii Math Utils")
static FVector ConvertGazeToUserSpace(APlayerController* PlayerController, const FVector& WorldSpaceGazeDirection);
/************************************************************************/
/* Util functions */
/************************************************************************/
public:
/**
* Tests if a given player controller is XR enabled
*/
UFUNCTION(BlueprintPure, Category = "Tobii XR Utils")
static bool IsXRPlayerController(const APlayerController* PlayerController);
};

View File

@@ -0,0 +1,225 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "CoreMinimal.h"
#include "Engine/EngineTypes.h"
#include "TobiiTypes.generated.h"
/**
* Structure that contains gaze information from one eye in both screen and world space.
*/
USTRUCT(BlueprintType)
struct FTobiiGazeData
{
GENERATED_USTRUCT_BODY()
public:
//Origin of the eye's gaze ray in Unreal world space.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Space Gaze Data")
FVector WorldGazeOrigin;
//Forward direction of the eye's gaze ray this frame in Unreal world space.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Space Gaze Data")
FVector WorldGazeDirection;
//Due to how the eye works and imperfections in eye tracking technology, it makes more sense to express the world gaze field as a cone rather than a ray. This angle is the angle between the Gaze Direction and the side of the cone expressed in degrees.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Space Data")
float WorldGazeConeAngleDegrees;
//The gaze point in screen space in pixels this frame.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Screen Space Gaze Data")
FVector2D ScreenGazePointPx;
//Due to how the eye works and imperfections in eye tracking technology, it makes more sense to express the screen space gaze field as an ellipse rather than a point. This is the extent of that ellipse expressed in pixels for the eye.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Screen Space Data")
FVector2D ScreenGazeCircleRadiiPx;
//Time when the gaze point was created.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data")
FDateTime TimeStamp;
//This is how open the eye is. 0 means closed, 1 means open.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data")
float EyeOpenness;
//If the gaze point is moving slowly enough, it is considered stable. This is useful when trying to prevent false positives when the gaze point is moving past objects.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data")
bool bIsStable;
//If this is true, all other properties related to the eye should be safe to use. This might be false because the tracking system is a mono tracker, or if the data is bad for this frame.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data")
bool bIsGazeDataValid;
FTobiiGazeData()
: bIsGazeDataValid(false)
{
}
};
/**
* Location and orientation of the head.
*/
USTRUCT(BlueprintType)
struct FTobiiHeadPoseData
{
GENERATED_USTRUCT_BODY()
public:
FTobiiHeadPoseData()
: HeadLocation(0.0f, 0.0f, 0.0f)
, HeadOrientation(0.0f, 0.0f, 0.0f)
{}
//This is the local space location of the head relative to the device specified origin in centimeters.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Pose Data")
FVector HeadLocation;
//This is the local space orientation of the head.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Pose Data")
FRotator HeadOrientation;
};
/**
* Information regarding the display associated with the currently active eye tracker.
*/
USTRUCT(BlueprintType)
struct FTobiiDisplayInfo
{
GENERATED_USTRUCT_BODY()
public:
//This is the width in pixels of the monitor the game is running on as reported by windows.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Display Info")
int32 MonitorWidthPx;
//This is the height in pixels of the monitor the game is running on as reported by windows.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Display Info")
int32 MonitorHeightPx;
//This is the width in centimeters of the monitor the active eyetracker is attached to as reported by the eyetracker. Centimeters is also the measurement used for unreal units.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Display Info")
float MonitorWidthCm;
//This is the height in centimeters of the monitor the active eyetracker is attached to as reported by the eyetracker. Centimeters is also the measurement used for unreal units.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Display Info")
float MonitorHeightCm;
//This is the width in pixels of the main game viewport as reported by the game.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Display Info")
int32 MainViewportWidthPx;
//This is the height in pixels of the main game viewport as reported by the game.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Display Info")
int32 MainViewportHeightPx;
//This is the width of the main game viewport in centimeters as calculated using data from the game and the eyetracker. Centimeters is also the measurement used for unreal units.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Display Info")
float MainViewportWidthCm;
//This is the height of the main game viewport in centimeters as calculated using data from the game and the eyetracker. Centimeters is also the measurement used for unreal units.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Display Info")
float MainViewportHeightCm;
//This is the dpi scale of the monitor the game is running on
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Display Info")
float DpiScale;
public:
void* GameWindowHandle;
void* GameMonitorHandle;
public:
FTobiiDisplayInfo()
: MonitorWidthPx(0)
, MonitorHeightPx(0)
, MonitorWidthCm(0.0f)
, MonitorHeightCm(0.0f)
, MainViewportWidthPx(0)
, MainViewportHeightPx(0)
, MainViewportWidthCm(0.0f)
, MainViewportHeightCm(0.0f)
, DpiScale(1.0f)
, GameWindowHandle(nullptr)
, GameMonitorHandle(nullptr)
{}
};
/**
* This is the vertices for the desktop only track box expressed in Tobii User Coordinate System coordinates.
* This coordinate system has its origin in the middle of the active display. X+ is towards the user's right. Y+ is up. Z+ is away from the screen towards the user.
* See general API documentation for details.
*/
USTRUCT(BlueprintType)
struct FTobiiDesktopTrackBox
{
GENERATED_USTRUCT_BODY()
public:
//These are the extreme points of the eye tracker tracking frustum where we get optimal tracking quality in centimeters. These are in the Tobii User Coordinate System.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Track Box")
FVector FrontUpperRightPointCm;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Track Box")
FVector FrontUpperLeftPointCm;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Track Box")
FVector FrontLowerLeftPointCm;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Track Box")
FVector FrontLowerRightPointCm;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Track Box")
FVector BackUpperRightPointCm;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Track Box")
FVector BackUpperLeftPointCm;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Track Box")
FVector BackLowerLeftPointCm;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Track Box")
FVector BackLowerRightPointCm;
public:
FTobiiDesktopTrackBox()
: FrontUpperRightPointCm(0.0f, 0.0f, 0.0f)
, FrontUpperLeftPointCm(0.0f, 0.0f, 0.0f)
, FrontLowerLeftPointCm(0.0f, 0.0f, 0.0f)
, FrontLowerRightPointCm(0.0f, 0.0f, 0.0f)
, BackUpperRightPointCm(0.0f, 0.0f, 0.0f)
, BackUpperLeftPointCm(0.0f, 0.0f, 0.0f)
, BackLowerLeftPointCm(0.0f, 0.0f, 0.0f)
, BackLowerRightPointCm(0.0f, 0.0f, 0.0f)
{}
};
/**
* Describes the current state of the gaze tracker. Is designed in such a way as later values indicates a higher level of readiness to make it easy to use in interactions.
*/
UENUM(BlueprintType)
enum class ETobiiGazeTrackerStatus : uint8
{
/** The gaze tracker is not connected to UE4 for some reason. The tracker might not be plugged in, the game window is currently running on a screen without a gaze tracker or is otherwise not available. */
NotConnected,
/** The gaze tracker is connected, but cannot track as it is missing some important configuration step. */
NotConfigured,
/** Gaze Tracking has been disabled by the user or developer or some other eventuality, like for example the game being minimized which might suspend the tracking. */
Disabled,
/** The gaze tracker is running but has not yet detected a user. Not relevant in XR. */
UserNotPresent,
/** The gaze tracker has detected a user and is actively tracking them. They appear not to be focusing on the game window at the moment however, so be careful about how you are using the data. */
UserPresent
};
/**
* Some gaze trackers might only be able to deliver data for one eye, and others might be able to serve data for both.
* If you are planning to use the left or right gaze data functions, checking that the gaze tracker you are working with supports them is recommended.
*/
UENUM(BlueprintType)
enum class ETobiiGazeTrackerCapability : uint8
{
/** This gaze tracker can deliver combined gaze data. */
None = 0 UMETA(DisplayName = "No Support"),
/** This gaze tracker can deliver combined gaze data. */
CombinedGazeData = 1 UMETA(DisplayName = "Supports Combined Gaze Data"),
/** This gaze tracker can deliver individual gaze data for the left eye. */
LeftGazeData = 2 UMETA(DisplayName = "Supports Left Eye Gaze Data"),
/** This gaze tracker can deliver individual gaze data for the right eye. */
RightGazeData = 4 UMETA(DisplayName = "Supports Right Eye Gaze Data")
};

View File

@@ -0,0 +1,112 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
using System;
using System.IO;
using System.Diagnostics;
using Tools.DotNETCommon;
namespace UnrealBuildTool.Rules
{
public class TobiiCore : ModuleRules
{
//If you want to remove eyetracking functionality from your builds without modifying your game code / project you can do so easily by setting this to false.
private bool IsEyetrackingActive { get { return true; } }
public TobiiCore(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PrivateDependencyModuleNames.AddRange(new string[]
{
"Core"
, "CoreUObject"
, "Engine"
, "UMG"
, "InputCore"
, "InputDevice"
, "HeadMountedDisplay"
});
PublicDependencyModuleNames.AddRange(new string[]
{
"EyeTracker"
});
if (Target.bBuildEditor)
{
DynamicallyLoadedModuleNames.AddRange(new string[] { "LevelEditor" });
PublicDependencyModuleNames.AddRange(new string[]
{
"Slate"
, "SlateCore"
, "EditorStyle"
, "UnrealEd"
, "MainFrame"
, "GameProjectGeneration"
, "WebBrowser"
, "RHI"
});
}
string AssemblyLocation = Path.GetDirectoryName(new Uri(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).LocalPath);
DirectoryReference RootDirectory = new DirectoryReference(Path.Combine(AssemblyLocation, "..", "..", ".."));
bool IsEnginePlugin = RootDirectory.GetDirectoryName() == "Engine";
PublicDefinitions.Add("TOBII_COMPILE_AS_ENGINE_PLUGIN=" + (IsEnginePlugin ? 1 : 0));
//Platform specific
if (IsEyetrackingActive && (Target.Platform == UnrealTargetPlatform.Win64 || Target.Platform == UnrealTargetPlatform.Win32))
{
string PlatformString = (Target.Platform == UnrealTargetPlatform.Win64) ? "Win64" : "Win32";
string PluginsPath = Path.Combine(ModuleDirectory, "../../../");
string TobiiRelativeAPIPath = "TobiiEyetracking/ThirdParty/GameIntegration";
string TobiiRelativeIncludePath = Path.Combine(TobiiRelativeAPIPath, "include");
string TobiiRelativeLibraryBasePath = Path.Combine(TobiiRelativeAPIPath, "lib");
//Includes
PrivateIncludePaths.Add(Path.Combine(PluginsPath, TobiiRelativeIncludePath));
//Add libraries
AddLibrary(Path.Combine(PluginsPath, TobiiRelativeLibraryBasePath, PlatformString, (Target.Platform == UnrealTargetPlatform.Win32) ? "tobii_gameintegration_x86.lib" : "tobii_gameintegration_x64.lib"));
//Add DLL
string RelativeGICDllPath = "";
string GICDllName = (Target.Platform == UnrealTargetPlatform.Win32) ? "tobii_gameintegration_x86.dll" : "tobii_gameintegration_x64.dll";
if (IsEnginePlugin)
{
RelativeGICDllPath = Path.Combine("Binaries/ThirdParty/TobiiEyetracking", PlatformString, GICDllName);
RuntimeDependencies.Add("$(EngineDir)/" + RelativeGICDllPath);
}
else
{
RelativeGICDllPath = Path.Combine(TobiiRelativeLibraryBasePath, PlatformString, GICDllName);
RuntimeDependencies.Add(Path.Combine(PluginsPath, RelativeGICDllPath));
}
PublicDefinitions.Add("TOBII_EYETRACKING_ACTIVE=1");
PublicDefinitions.Add("TOBII_GIC_RELATIVE_DLL_PATH=R\"(" + RelativeGICDllPath + ")\"");
PublicDelayLoadDLLs.Add(GICDllName);
}
else
{
PublicDefinitions.Add("TOBII_EYETRACKING_ACTIVE=0");
}
}
private void AddLibrary(string libraryPath)
{
if (File.Exists(libraryPath))
{
PublicAdditionalLibraries.Add(libraryPath);
}
else
{
Debug.WriteLine("Cannot find Tobii API Lib file. Path does not exist! " + libraryPath);
}
}
}
}

View File

@@ -0,0 +1,161 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#include "STobiiGazeFocusableWidget.h"
#include "TobiiGTOMBlueprintLibrary.h"
#include "Engine/Canvas.h"
#include "Engine/Engine.h"
#include "Components/WidgetComponent.h"
#include "HAL/IConsoleManager.h"
#include "Slate/SceneViewport.h"
static TAutoConsoleVariable<int32> CVarEnableCleanUI(TEXT("tobii.EnableCleanUI"), 1, TEXT("0 - CleanUI is disabled. 1 - CleanUI is enabled."));
static TAutoConsoleVariable<float> CVarCleanUIFadeInTimeSecs(TEXT("tobii.CleanUIFadeInTimeSecs"), 0.0f, TEXT("We want the fade in time to be fairly fast so that the information is instantly available to the user."));
static TAutoConsoleVariable<float> CVarCleanUIFadeOutTimeSecs(TEXT("tobii.CleanUIFadeOutTimeSecs"), 1.8f, TEXT("The fade out time should be fairly slow however to make sure that it doesn't draw peripheral vision attention."));
static TAutoConsoleVariable<float> CVarCleanUIMinAlpha(TEXT("tobii.CleanUIMinAlpha"), 0.3f, TEXT("The clean UI won't fade something out beyond this minimum alpha."));
static TAutoConsoleVariable<float> CVarCleanUIMaxAlpha(TEXT("tobii.CleanUIMaxAlpha"), 1.0f, TEXT("Change this if you don't want the clean UI to increase alpha beyond this point"));
STobiiGazeFocusableWidget::STobiiGazeFocusableWidget()
: CleanUIAlpha(1.0f)
, CleanUIContainersToPollHitsFrom()
{
bCanSupportFocus = false;
SetCanTick(true);
}
void STobiiGazeFocusableWidget::Construct(const FArguments& InArgs)
{
SBox::FArguments ParentArgs;
ParentArgs._HAlign = InArgs._HAlign;
ParentArgs._VAlign = InArgs._VAlign;
ParentArgs._Padding = InArgs._Padding;
ParentArgs._Content = InArgs._Content;
SBox::Construct(ParentArgs);
OnHovered = InArgs._OnHovered;
OnUnhovered = InArgs._OnUnhovered;
}
bool STobiiGazeFocusableWidget::HitByGaze()
{
return UMGWidget.IsValid() && (UMGWidget->CleanUIMode == ETobiiCleanUIMode::FocusExclusive ?
UMGWidget->HasFocus() : UMGWidget->IsInFocusCollection());
}
void STobiiGazeFocusableWidget::AddExtraCleanUIContainerToPollHitsFrom(STobiiGazeFocusableWidget* CleanUIContainerToPoll, bool PollGazeHits /*= true*/, bool PollMouseHits /*= true*/)
{
if (CleanUIContainerToPoll != nullptr)
{
CleanUIPollingInfo PollingInfo;
PollingInfo.CleanUIToPollFrom = TWeakPtr<SWidget>(CleanUIContainerToPoll->AsShared());
PollingInfo.PollGaze = PollGazeHits;
PollingInfo.PollPointer = PollMouseHits;
CleanUIContainersToPollHitsFrom.Add(PollingInfo);
}
}
void STobiiGazeFocusableWidget::RemoveExtraCleanUIContainerToPollHitsFrom(STobiiGazeFocusableWidget* CleanUIContainerToStopPolling)
{
if (CleanUIContainerToStopPolling != nullptr)
{
CleanUIContainersToPollHitsFrom.RemoveAll([=](CleanUIPollingInfo& CurCleanUIPollingInfo)
{
return !CurCleanUIPollingInfo.CleanUIToPollFrom.IsValid() || CurCleanUIPollingInfo.CleanUIToPollFrom.HasSameObject(CleanUIContainerToStopPolling);
});
}
}
void STobiiGazeFocusableWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
SWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime);
if (!UMGWidget.IsValid()
|| UMGWidget->CleanUIMode == ETobiiCleanUIMode::Disabled
|| GEngine == nullptr
|| GEngine->GameViewport == nullptr
|| GEngine->GameViewport->GetGameViewport() == nullptr
|| GetChildren()->Num() == 0)
{
CleanUIAlpha = 1.0f;
return;
}
bool bTriggerCleanUIOnMouseOver = UMGWidget.IsValid() && UMGWidget->bTriggerCleanUIOnMouseOver;
bool bHitByGaze = HitByGaze();
bool bShouldFadeIn = bHitByGaze || (bTriggerCleanUIOnMouseOver && bIsHovered);
// Check if we have any hits on our CleanUIContainersToPollHitsFrom to see if we should still fade in even though we still think we should fade out
if (!bShouldFadeIn)
{
for (CleanUIPollingInfo& PollingInfo : CleanUIContainersToPollHitsFrom)
{
if (PollingInfo.CleanUIToPollFrom.IsValid())
{
TSharedPtr<SWidget> CleanUIWidget = PollingInfo.CleanUIToPollFrom.Pin();
STobiiGazeFocusableWidget* CleanUIToPollFrom = (STobiiGazeFocusableWidget*)CleanUIWidget.Get();
if ((PollingInfo.PollGaze && CleanUIToPollFrom->HitByGaze())
|| PollingInfo.PollPointer && bTriggerCleanUIOnMouseOver && CleanUIToPollFrom->bIsHovered)
{
bShouldFadeIn = true;
break;
}
}
}
}
UMGWidget->TimeCleanUIIsSuppressedForSecs = FMath::Max(UMGWidget->TimeCleanUIIsSuppressedForSecs -= InDeltaTime, 0.0f);
bool bCleanUISuppressed = UMGWidget->TimeCleanUIIsSuppressedForSecs > FLT_EPSILON;
bShouldFadeIn = bShouldFadeIn || bCleanUISuppressed;
float MinAlpha = UMGWidget->CleanUIMinAlphaOverride >= 0.0f ? UMGWidget->CleanUIMinAlphaOverride : CVarCleanUIMinAlpha.GetValueOnAnyThread();
float MaxAlpha = UMGWidget->CleanUIMaxAlphaOverride >= 0.0f ? UMGWidget->CleanUIMaxAlphaOverride : CVarCleanUIMaxAlpha.GetValueOnAnyThread();
if (bShouldFadeIn)
{
float FadeInTime = UMGWidget->CleanUIFadeInTimeSecsOverride >= 0.0f ? UMGWidget->CleanUIFadeInTimeSecsOverride : CVarCleanUIFadeInTimeSecs.GetValueOnAnyThread();
CleanUIAlpha = FadeInTime > 0.0f ? FMath::Clamp(CleanUIAlpha + InDeltaTime / FadeInTime, MinAlpha, MaxAlpha) : MaxAlpha;
}
else
{
float FadeOutTime = UMGWidget->CleanUIFadeOutTimeSecsOverride >= 0.0f ? UMGWidget->CleanUIFadeOutTimeSecsOverride : CVarCleanUIFadeOutTimeSecs.GetValueOnAnyThread();
CleanUIAlpha = FadeOutTime > 0.0f ? FMath::Clamp(CleanUIAlpha - InDeltaTime / FadeOutTime, MinAlpha, MaxAlpha) : MinAlpha;
}
}
int32 STobiiGazeFocusableWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
const float Alpha = UMGWidget.IsValid() ? (UMGWidget->CleanUIMode == ETobiiCleanUIMode::Silent ? 1.0f : CleanUIAlpha) : 1.0f;
FWidgetStyle CompoundedWidgetStyle = FWidgetStyle(InWidgetStyle).BlendColorAndOpacityTint(FLinearColor(1.0f, 1.0f, 1.0f, Alpha));
return SBox::OnPaint(Args, AllottedGeometry, MyClippingRect, OutDrawElements, LayerId, CompoundedWidgetStyle, bParentEnabled);
}
void STobiiGazeFocusableWidget::OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
SWidget::OnMouseEnter(MyGeometry, MouseEvent);
OnHovered.ExecuteIfBound();
Invalidate(EInvalidateWidget::Layout);
}
void STobiiGazeFocusableWidget::OnMouseLeave(const FPointerEvent& MouseEvent)
{
// Call parent implementation
SWidget::OnMouseLeave(MouseEvent);
OnUnhovered.ExecuteIfBound();
Invalidate(EInvalidateWidget::Layout);
}
STobiiGazeFocusableWidget* STobiiGazeFocusableWidget::TryCastToTobiiGazeFocusable(SWidget* Widget)
{
if (Widget != nullptr && Widget->GetType() == FName("STobiiGazeFocusableWidget"))
{
return (STobiiGazeFocusableWidget*)Widget;
}
return nullptr;
}

View File

@@ -0,0 +1,318 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#include "STobiiRadialMenuWidget.h"
#include "TobiiGTOMBlueprintLibrary.h"
#include "Engine/Canvas.h"
#include "Engine/Engine.h"
#include "Components/WidgetComponent.h"
#include "Framework/Application/SlateApplication.h"
#include "Slate/SceneViewport.h"
#define TWO_PI (6.28318530718)
#define PI_OVER_TWO (1.57079632679)
STobiiRadialMenuWidget::STobiiRadialMenuWidget()
: Children(this)
{
SetCanTick(false);
bCanSupportFocus = false;
}
void STobiiRadialMenuWidget::Construct(const STobiiRadialMenuWidget::FArguments& InArgs)
{
const int32 NumSlots = InArgs.Slots.Num();
for (int32 SlotIndex = 0; SlotIndex < NumSlots; ++SlotIndex)
{
Children.Add(InArgs.Slots[SlotIndex]);
}
bUseHardBorder = InArgs._UseHardBorder;
BorderColor = InArgs._BorderColor;
PanelColor = InArgs._PanelColor;
BorderThicknessPx = InArgs._BorderThicknessPx;
SegmentSeparationPx = InArgs._SegmentSeparationPx;
AngularDisplacementDeg = InArgs._AngularDisplacementDeg;
RadiusPx = InArgs._RadiusPx;
VertexCount = InArgs._VertexCount;
BrushResourceHandle = FSlateApplication::Get().GetRenderer()->GetResourceHandle(*FCoreStyle::Get().GetDefaultBrush());
}
void STobiiRadialMenuWidget::ClearChildren()
{
if (Children.Num())
{
Invalidate(EInvalidateWidget::Layout);
Children.Empty();
}
}
int32 STobiiRadialMenuWidget::RemoveSlot(const TSharedRef<SWidget>& SlotWidget)
{
Invalidate(EInvalidateWidget::Layout);
for (int32 SlotIdx = 0; SlotIdx < Children.Num(); ++SlotIdx)
{
if (SlotWidget == Children[SlotIdx].GetWidget())
{
Children.RemoveAt(SlotIdx);
return SlotIdx;
}
}
return -1;
}
void STobiiRadialMenuWidget::GeneratePanelRenderData(const FGeometry& AllottedGeometry, int32 NrChildren, TArray<FTobiiRadialMenuPanelRenderData>& OutChildRenderData) const
{
OutChildRenderData.Empty(NrChildren);
const FVector2D WidgetCenter = AllottedGeometry.GetLocalSize() / 2.0f;
const int32 VerticesPerSegment = FMath::Max(VertexCount / Children.Num(), 3);
const float ActualRadius = FMath::Min(WidgetCenter.X, WidgetCenter.Y);
const float AngleStep = TWO_PI / Children.Num();
const float AngleHalfStep = AngleStep / 2.0f;
const float AngleMinorStep = AngleStep / VerticesPerSegment;
const float HalfSeparation = SegmentSeparationPx / 2.0f;
const float BorderCenterOffsetDist = HalfSeparation / FMath::Sin(AngleHalfStep);
const float BorderSeparationAngle = FMath::Asin(HalfSeparation / ActualRadius);
const float NoBorderDistOffset = HalfSeparation + BorderThicknessPx;
const float NoBorderRadius = ActualRadius - BorderThicknessPx;
const float NoBorderCenterOffsetDist = NoBorderDistOffset / FMath::Sin(AngleHalfStep);
const float NoBorderSeparationAngle = FMath::Asin(NoBorderDistOffset / NoBorderRadius);
float CurrentChildAngle = PI_OVER_TWO + FMath::DegreesToRadians(AngularDisplacementDeg); //Start at the top
for (int32 ChildIndex = 0; ChildIndex < NrChildren; ++ChildIndex)
{
FTobiiRadialMenuPanelRenderData NewRenderData;
const STobiiRadialMenuWidget::FSlot& CurChild = Children[ChildIndex];
const float ChildAlpha = CurChild.AlphaAttr.Get();
FColor AdjustedBorderColor = BorderColor;
AdjustedBorderColor.A *= ChildAlpha;
FColor AdjustedPanelColor = PanelColor;
AdjustedPanelColor.A *= ChildAlpha;
//Panel polygon
{
float ChildSine, ChildCosine;
FMath::SinCos(&ChildSine, &ChildCosine, -CurrentChildAngle);
FVector2D WidgetCenterToChildCenterDir(ChildCosine, ChildSine);
//First generate the nexus point
const FVector2D CenterVertexPosition = WidgetCenter + WidgetCenterToChildCenterDir * NoBorderCenterOffsetDist;
FSlateVertex CenterVertex = FSlateVertex::Make<ESlateVertexRounding::Enabled>(AllottedGeometry.GetAccumulatedRenderTransform(), CenterVertexPosition, FVector2D::ZeroVector, AdjustedPanelColor);
NewRenderData.PanelVertexData.Add(CenterVertex);
//Next we need to find our segment extreme points
const float SegmentStartVertexAngle = CurrentChildAngle - AngleHalfStep + NoBorderSeparationAngle;
const float SegmentEndVertexAngle = CurrentChildAngle + AngleHalfStep - NoBorderSeparationAngle;
//Generate all edge vertices
for (int32 VertexIdx = 0; VertexIdx < VerticesPerSegment; VertexIdx++)
{
const float AngleAlpha = (float)VertexIdx / (VerticesPerSegment - 1);
const float CurrentAngle = FMath::Lerp(SegmentStartVertexAngle, SegmentEndVertexAngle, AngleAlpha);
float VertexSine, VertexCosine;
FMath::SinCos(&VertexSine, &VertexCosine, -CurrentAngle);
const FVector2D SegmentVertexPosition = WidgetCenter + FVector2D(NoBorderRadius * VertexCosine, NoBorderRadius * VertexSine);
FSlateVertex SegmentVertex = FSlateVertex::Make<ESlateVertexRounding::Enabled>(AllottedGeometry.GetAccumulatedRenderTransform(), SegmentVertexPosition, FVector2D::ZeroVector, AdjustedPanelColor);
NewRenderData.PanelVertexData.Add(SegmentVertex);
}
//Build indices
for (int32 VertexIdx = 1; VertexIdx < VerticesPerSegment; VertexIdx++)
{
NewRenderData.PanelIndexData.Add(0);
NewRenderData.PanelIndexData.Add(VertexIdx);
NewRenderData.PanelIndexData.Add(VertexIdx + 1);
}
}
//Border polygon
{
float ChildSine, ChildCosine;
FMath::SinCos(&ChildSine, &ChildCosine, -CurrentChildAngle);
FVector2D WidgetCenterToChildCenterDir(ChildCosine, ChildSine);
//First generate the nexus point
const FVector2D CenterVertexPosition = WidgetCenter + WidgetCenterToChildCenterDir * BorderCenterOffsetDist;
FSlateVertex CenterVertex = FSlateVertex::Make<ESlateVertexRounding::Enabled>(AllottedGeometry.GetAccumulatedRenderTransform(), CenterVertexPosition, FVector2D::ZeroVector, AdjustedBorderColor);
NewRenderData.BorderVertexData.Add(CenterVertex);
//Next we need to find our segment extreme points
const float SegmentStartVertexAngle = CurrentChildAngle - AngleHalfStep + BorderSeparationAngle;
const float SegmentEndVertexAngle = CurrentChildAngle + AngleHalfStep - BorderSeparationAngle;
//Generate all edge vertices
for (int32 VertexIdx = 0; VertexIdx < VerticesPerSegment; VertexIdx++)
{
const float AngleAlpha = (float)VertexIdx / (VerticesPerSegment - 1);
const float CurrentAngle = FMath::Lerp(SegmentStartVertexAngle, SegmentEndVertexAngle, AngleAlpha);
float VertexSine, VertexCosine;
FMath::SinCos(&VertexSine, &VertexCosine, -CurrentAngle);
const FVector2D SegmentVertexPosition = WidgetCenter + FVector2D(ActualRadius * VertexCosine, ActualRadius * VertexSine);
FSlateVertex SegmentVertex = FSlateVertex::Make<ESlateVertexRounding::Enabled>(AllottedGeometry.GetAccumulatedRenderTransform(), SegmentVertexPosition, FVector2D::ZeroVector, AdjustedBorderColor);
NewRenderData.BorderVertexData.Add(SegmentVertex);
}
//Add the panel vertices at the end of the array
const int32 FirstPanelVertexIdx = NewRenderData.BorderVertexData.Num();
for (FSlateVertex SlateVertex : NewRenderData.PanelVertexData)
{
if (bUseHardBorder)
{
SlateVertex.Color = AdjustedBorderColor;
}
NewRenderData.BorderVertexData.Add(SlateVertex);
}
//Build indices, building quads from the inner panel outwards
for (int32 VertexIdx = 0; VertexIdx < VerticesPerSegment; VertexIdx++)
{
//First tri
NewRenderData.BorderIndexData.Add(FirstPanelVertexIdx + VertexIdx);
NewRenderData.BorderIndexData.Add(VertexIdx);
NewRenderData.BorderIndexData.Add(VertexIdx + 1);
//Second tri
NewRenderData.BorderIndexData.Add(FirstPanelVertexIdx + VertexIdx);
NewRenderData.BorderIndexData.Add(VertexIdx + 1);
NewRenderData.BorderIndexData.Add(FirstPanelVertexIdx + VertexIdx + 1);
}
//Final two tris
NewRenderData.BorderIndexData.Add(NewRenderData.BorderVertexData.Num() - 1);
NewRenderData.BorderIndexData.Add(FirstPanelVertexIdx - 1);
NewRenderData.BorderIndexData.Add(0);
NewRenderData.BorderIndexData.Add(NewRenderData.BorderVertexData.Num() - 1);
NewRenderData.BorderIndexData.Add(0);
NewRenderData.BorderIndexData.Add(FirstPanelVertexIdx);
}
OutChildRenderData.Add(MoveTemp(NewRenderData));
CurrentChildAngle += AngleStep;
}
}
void STobiiRadialMenuWidget::OnArrangeChildren(const FGeometry& AllottedGeometry, FArrangedChildren& ArrangedChildren) const
{
if (Children.Num() > 0)
{
const FVector2D WidgetCenter = AllottedGeometry.GetLocalSize() / 2.0f;
const float ActualRadius = FMath::Min(WidgetCenter.X, WidgetCenter.Y);
const float AngleStep = TWO_PI / Children.Num();
FVector2D LargestInscribedRect;
float DistanceToRectCenter;
UTobiiGTOMBlueprintLibrary::FindLargestInscribedAlignedRect(AngleStep, ActualRadius, LargestInscribedRect, DistanceToRectCenter);
const float ChildElementSideLength = FMath::Min(LargestInscribedRect.X, LargestInscribedRect.Y);
const float SlightlyScaledDownSize = ChildElementSideLength * 0.8f;
float CurrentAngle = PI_OVER_TWO + FMath::DegreesToRadians(AngularDisplacementDeg); //Start at the top
for (int32 ChildIndex = 0; ChildIndex < Children.Num(); ++ChildIndex)
{
const STobiiRadialMenuWidget::FSlot& CurChild = Children[ChildIndex];
const TSharedRef<SWidget>& CurWidget = CurChild.GetWidget();
const EVisibility ChildVisibility = CurWidget->GetVisibility();
if (ArrangedChildren.Accepts(ChildVisibility))
{
const FVector2D Offset = CurChild.OffsetAttr.Get();
float ChildSine, ChildCosine;
FMath::SinCos(&ChildSine, &ChildCosine, -CurrentAngle);
const FVector2D ChildCenter(DistanceToRectCenter * ChildCosine, DistanceToRectCenter * ChildSine);
const float ChildScale = CurChild.ScaleAttr.Get();
const FVector2D ChildSize(SlightlyScaledDownSize * ChildScale, SlightlyScaledDownSize * ChildScale);
const FVector2D BaseChildOffset = -ChildSize / 2.0f;
const FVector2D LocalPosition = WidgetCenter + ChildCenter + BaseChildOffset + Offset;
CurrentAngle += AngleStep;
ArrangedChildren.AddWidget(ChildVisibility, AllottedGeometry.MakeChild(CurWidget, LocalPosition, ChildSize));
}
}
}
}
int32 STobiiRadialMenuWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
SCOPED_NAMED_EVENT_TEXT("SConstraintCanvas", FColor::Orange);
if (Children.Num() < 2)
{
return LayerId;
}
FArrangedChildren ArrangedChildren(EVisibility::Visible);
OnArrangeChildren(AllottedGeometry, ArrangedChildren);
TArray<FTobiiRadialMenuPanelRenderData> ChildRenderData;
GeneratePanelRenderData(AllottedGeometry, Children.Num(), ChildRenderData);
const bool bForwardedEnabled = ShouldBeEnabled(bParentEnabled);
const float AngleStep = TWO_PI / Children.Num();
float CurrentChildAngle = PI_OVER_TWO + FMath::DegreesToRadians(AngularDisplacementDeg); //Start at the top
// Because we paint multiple children, we must track the maximum layer id that they produced in case one of our parents
// wants to an overlay for all of its contents.
int32 MaxLayerId = LayerId;
const FPaintArgs NewArgs = Args.WithNewParent(this);
for (int32 ChildIndex = 0; ChildIndex < ArrangedChildren.Num(); ++ChildIndex)
{
FArrangedWidget& CurWidget = ArrangedChildren[ChildIndex];
if (!IsChildWidgetCulled(MyCullingRect, CurWidget))
{
MaxLayerId++;
//Draw underlying panel if our render data is valid
if (ChildIndex < ChildRenderData.Num())
{
const FTobiiRadialMenuPanelRenderData& CurrentRenderData = ChildRenderData[ChildIndex];
FSlateDrawElement::MakeCustomVerts(OutDrawElements, LayerId
, BrushResourceHandle
, CurrentRenderData.PanelVertexData
, CurrentRenderData.PanelIndexData
, nullptr, 0, 0, ESlateDrawEffect::NoPixelSnapping);
FSlateDrawElement::MakeCustomVerts(OutDrawElements, LayerId
, BrushResourceHandle
, CurrentRenderData.BorderVertexData
, CurrentRenderData.BorderIndexData
, nullptr, 0, 0, ESlateDrawEffect::NoPixelSnapping);
}
const STobiiRadialMenuWidget::FSlot& CurChild = Children[ChildIndex];
FWidgetStyle CompoundedWidgetStyle = FWidgetStyle(InWidgetStyle).BlendColorAndOpacityTint(FLinearColor(1.0f, 1.0f, 1.0f, CurChild.AlphaAttr.Get()));
CurWidget.Widget->Paint(NewArgs, CurWidget.Geometry, MyCullingRect, OutDrawElements, MaxLayerId, CompoundedWidgetStyle, bForwardedEnabled);
CurrentChildAngle += AngleStep;
}
}
return MaxLayerId;
}
FVector2D STobiiRadialMenuWidget::ComputeDesiredSize(float LayoutScaleMultiplier) const
{
const float DiameterPx = RadiusPx * 2.0f;
return FVector2D(DiameterPx, DiameterPx);
}
FChildren* STobiiRadialMenuWidget::GetChildren()
{
return &Children;
}

View File

@@ -0,0 +1,223 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#include "TobiiGazeFocusableWidget.h"
#include "TobiiGTOMBlueprintLibrary.h"
#include "Components/SizeBoxSlot.h"
#define LOCTEXT_NAMESPACE "UMG"
static TMap<FTobiiFocusableUID, TWeakObjectPtr<UTobiiGazeFocusableWidget>> GRegisteredTobiiScreenSpaceFocusableWidgets;
UTobiiGazeFocusableWidget::UTobiiGazeFocusableWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
, OnHovered()
, OnUnhovered()
, bHasGazeFocus(false)
, bIsGazeFocusable(true)
, GazeFocusPriority(0.0f)
, MaxFocusDistance(-1.0f)
, FocusLayer("")
, ReceivedGazeFocus()
, LostGazeFocus()
, CleanUIMode(ETobiiCleanUIMode::Disabled)
, TimeCleanUIIsSuppressedForSecs(0.0f)
, CleanUIFadeInTimeSecsOverride(-1.0f)
, CleanUIFadeOutTimeSecsOverride(-1.0f)
, CleanUIMinAlphaOverride(-1.0f)
, CleanUIMaxAlphaOverride(-1.0f)
, bTriggerCleanUIOnMouseOver(false)
, WorldSpaceHostWidgetComponent()
, MySlateWidget()
{
Visibility = ESlateVisibility::Visible;
bIsVariable = true;
}
void UTobiiGazeFocusableWidget::ReleaseSlateResources(bool bReleaseChildren)
{
Super::ReleaseSlateResources(bReleaseChildren);
MySlateWidget.Reset();
}
TSharedRef<SWidget> UTobiiGazeFocusableWidget::RebuildWidget()
{
MySizeBox = MySlateWidget = SNew(STobiiGazeFocusableWidget)
.OnHovered_UObject(this, &ThisClass::SlateHandleHovered)
.OnUnhovered_UObject(this, &ThisClass::SlateHandleUnhovered);
MySlateWidget->UMGWidget = this;
if (GetChildrenCount() > 0)
{
Cast<USizeBoxSlot>(GetContentSlot())->BuildSlot(MySizeBox.ToSharedRef());
}
return MySizeBox.ToSharedRef();
}
bool UTobiiGazeFocusableWidget::IsHoveredByPointer()
{
if (MySlateWidget.IsValid())
{
return MySlateWidget->IsHoveredByPointer();
}
return false;
}
void UTobiiGazeFocusableWidget::RegisterWidgetToGTOM(UWidgetComponent* HostWidget)
{
if (HostWidget != nullptr)
{
WorldSpaceHostWidgetComponent = HostWidget;
}
else
{
uint32 Uid = GetUniqueID();
if (GRegisteredTobiiScreenSpaceFocusableWidgets.Contains(Uid))
{
//Overwrite
GRegisteredTobiiScreenSpaceFocusableWidgets[Uid] = this;
}
else
{
GRegisteredTobiiScreenSpaceFocusableWidgets.Add(Uid, this);
}
}
}
bool UTobiiGazeFocusableWidget::IsWorldSpaceWidget()
{
return WorldSpaceHostWidgetComponent.IsValid();
}
UWidgetComponent* UTobiiGazeFocusableWidget::GetWorldSpaceHostWidgetComponent()
{
return WorldSpaceHostWidgetComponent.IsValid() ? WorldSpaceHostWidgetComponent.Get() : nullptr;
}
bool UTobiiGazeFocusableWidget::HasFocus()
{
FTobiiGazeFocusData FocusData;
return UTobiiGTOMBlueprintLibrary::GetGazeFocusData(FocusData) && FocusData.FocusedWidget == this;
}
bool UTobiiGazeFocusableWidget::IsInFocusCollection()
{
TArray<FTobiiGazeFocusData> AllFocusData;
if (UTobiiGTOMBlueprintLibrary::GetAllGazeFocusData(AllFocusData))
{
for (const FTobiiGazeFocusData& FocusData : AllFocusData)
{
if (FocusData.FocusedWidget == this)
{
return true;
}
}
}
return false;
}
float UTobiiGazeFocusableWidget::GetCleanUIAlpha()
{
if (MySlateWidget.IsValid())
{
return MySlateWidget->GetCleanUIAlpha();
}
return 1.0f;
}
void UTobiiGazeFocusableWidget::AddGazeFocusableWidgetToPollHitsFrom(UTobiiGazeFocusableWidget* GazeFocusableWidgetToPoll, bool PollGazeHits /*= true*/, bool PollMouseHits /*= true*/)
{
if (GazeFocusableWidgetToPoll != nullptr)
{
STobiiGazeFocusableWidget* SlateCleanUIContainerToPoll = GazeFocusableWidgetToPoll->GetSlateGazeFocusableWidget();
if (SlateCleanUIContainerToPoll != nullptr && MySlateWidget.IsValid())
{
MySlateWidget->AddExtraCleanUIContainerToPollHitsFrom(SlateCleanUIContainerToPoll, PollGazeHits, PollMouseHits);
}
}
}
void UTobiiGazeFocusableWidget::RemoveGazeFocusableWidgetToPollHitsFrom(UTobiiGazeFocusableWidget* GazeFocusableWidgetToStopPolling)
{
if (GazeFocusableWidgetToStopPolling != nullptr)
{
STobiiGazeFocusableWidget* SlateCleanUIContainerToStopPolling = GazeFocusableWidgetToStopPolling->GetSlateGazeFocusableWidget();
if (SlateCleanUIContainerToStopPolling != nullptr && MySlateWidget.IsValid())
{
MySlateWidget->RemoveExtraCleanUIContainerToPollHitsFrom(SlateCleanUIContainerToStopPolling);
}
}
}
void UTobiiGazeFocusableWidget::ReceiveFocus()
{
ReceivedGazeFocus.Broadcast(this);
}
void UTobiiGazeFocusableWidget::LoseFocus()
{
LostGazeFocus.Broadcast(this);
}
void UTobiiGazeFocusableWidget::SlateHandleHovered()
{
OnHovered.Broadcast(this);
}
void UTobiiGazeFocusableWidget::SlateHandleUnhovered()
{
OnUnhovered.Broadcast(this);
}
STobiiGazeFocusableWidget* UTobiiGazeFocusableWidget::GetSlateGazeFocusableWidget()
{
if (MySlateWidget.IsValid())
{
return MySlateWidget.Get();
}
return nullptr;
}
TMap<FTobiiFocusableUID, TWeakObjectPtr<UTobiiGazeFocusableWidget>>& UTobiiGazeFocusableWidget::GetTobiiScreenSpaceFocusableWidgets()
{
TArray<FTobiiFocusableUID> IdsToPrune;
for (auto& WidgetPair : GRegisteredTobiiScreenSpaceFocusableWidgets)
{
if (!WidgetPair.Value.IsValid())
{
IdsToPrune.Add(WidgetPair.Key);
}
}
for (FTobiiFocusableUID IdToPrune : IdsToPrune)
{
GRegisteredTobiiScreenSpaceFocusableWidgets.Remove(IdToPrune);
}
return GRegisteredTobiiScreenSpaceFocusableWidgets;
}
#if WITH_EDITOR
const FText UTobiiGazeFocusableWidget::GetPaletteCategory()
{
return LOCTEXT("Tobii", "Tobii");
}
#endif
///////////////////////////////////////////////////
#undef LOCTEXT_NAMESPACE

View File

@@ -0,0 +1,334 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#include "TobiiRadialMenuWidget.h"
#include "TobiiGazeFocusableWidget.h"
#include "STobiiGazeFocusableWidget.h"
#define LOCTEXT_NAMESPACE "UMG"
UTobiiRadialMenuSlot::UTobiiRadialMenuSlot(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
, Offset(0.0f, 0.0f)
, Scale(1.0f)
, Alpha(1.0f)
, Slot(nullptr)
{
}
void UTobiiRadialMenuSlot::ReleaseSlateResources(bool bReleaseChildren)
{
Super::ReleaseSlateResources(bReleaseChildren);
Slot = nullptr;
}
void UTobiiRadialMenuSlot::BuildSlot(TSharedRef<STobiiRadialMenuWidget> RadialMenu)
{
Slot = &RadialMenu->AddSlot()
[
Content == nullptr ? SNullWidget::NullWidget : Content->TakeWidget()
];
SynchronizeProperties();
}
#if WITH_EDITOR
bool UTobiiRadialMenuSlot::NudgeByDesigner(const FVector2D& NudgeDirection, const TOptional<int32>& GridSnapSize)
{
return false;
}
bool UTobiiRadialMenuSlot::DragDropPreviewByDesigner(const FVector2D& LocalCursorPosition, const TOptional<int32>& XGridSnapSize, const TOptional<int32>& YGridSnapSize)
{
return false;
}
void UTobiiRadialMenuSlot::SynchronizeFromTemplate(const UPanelSlot* const TemplateSlot)
{
}
#endif //WITH_EDITOR
void UTobiiRadialMenuSlot::SetOffset(FVector2D InOffset)
{
Offset = InOffset;
if (Slot)
{
Slot->Offset(InOffset);
}
}
FVector2D UTobiiRadialMenuSlot::GetOffset() const
{
if (Slot)
{
return Slot->OffsetAttr.Get();
}
return FVector2D::ZeroVector;
}
void UTobiiRadialMenuSlot::SetScale(float InScale)
{
Scale = InScale;
if (Slot)
{
Slot->Scale(InScale);
}
}
float UTobiiRadialMenuSlot::GetScale() const
{
if (Slot)
{
return Slot->ScaleAttr.Get();
}
return 1.0f;
}
void UTobiiRadialMenuSlot::SetAlpha(float InAlpha)
{
Alpha = InAlpha;
if (Slot)
{
Slot->Alpha(InAlpha);
}
}
float UTobiiRadialMenuSlot::GetAlpha() const
{
if (Slot)
{
return Slot->AlphaAttr.Get();
}
return 1.0f;
}
void UTobiiRadialMenuSlot::SynchronizeProperties()
{
SetOffset(Offset);
SetScale(Scale);
SetAlpha(Alpha);
}
#if WITH_EDITOR
void UTobiiRadialMenuSlot::PreEditChange(class FEditPropertyChain& PropertyAboutToChange)
{
Super::PreEditChange(PropertyAboutToChange);
}
void UTobiiRadialMenuSlot::PostEditChangeChainProperty(struct FPropertyChangedChainEvent& PropertyChangedEvent)
{
SynchronizeProperties();
Super::PostEditChangeProperty(PropertyChangedEvent);
}
#endif
///////////////////////////////////////////////////////////////////////////////////////////////////////////
UTobiiRadialMenuWidget::UTobiiRadialMenuWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
, bUseHardBorder(true)
, BorderColor(FColor(230, 230, 230))
, PanelColor(FColor(70, 70, 70))
, BorderThicknessPx(5.0f)
, SegmentSeparationPx(10.0f)
, AngularDisplacementDeg(0.0f)
, RadiusPx(500.0f)
, VertexCount(360)
{
bIsVariable = false;
STobiiRadialMenuWidget::FArguments Defaults;
Visibility = UWidget::ConvertRuntimeToSerializedVisibility(Defaults._Visibility.Get());
}
void UTobiiRadialMenuWidget::ReleaseSlateResources(bool bReleaseChildren)
{
Super::ReleaseSlateResources(bReleaseChildren);
MyRadialMenu.Reset();
}
TSharedRef<SWidget> UTobiiRadialMenuWidget::RebuildWidget()
{
MyRadialMenu = SNew(STobiiRadialMenuWidget)
.UseHardBorder(bUseHardBorder)
.BorderColor(BorderColor)
.PanelColor(PanelColor)
.BorderThicknessPx(BorderThicknessPx)
.SegmentSeparationPx(SegmentSeparationPx)
.AngularDisplacementDeg(AngularDisplacementDeg)
.RadiusPx(RadiusPx)
.VertexCount(VertexCount);
for (UPanelSlot* PanelSlot : Slots)
{
if (UTobiiRadialMenuSlot* TypedSlot = Cast<UTobiiRadialMenuSlot>(PanelSlot))
{
TypedSlot->Parent = this;
TypedSlot->BuildSlot(MyRadialMenu.ToSharedRef());
UTobiiGazeFocusableWidget* GazeFocusableWidget = Cast<UTobiiGazeFocusableWidget>(TypedSlot->Content);
if (GazeFocusableWidget != nullptr)
{
STobiiGazeFocusableWidget* SlateWidget = GazeFocusableWidget->GetSlateGazeFocusableWidget();
//SlateWidget->ParentRadialMenuSlot = TypedSlot->GetSlateSlot();
}
}
}
return MyRadialMenu.ToSharedRef();
}
UClass* UTobiiRadialMenuWidget::GetSlotClass() const
{
return UTobiiRadialMenuSlot::StaticClass();
}
void UTobiiRadialMenuWidget::SetUseHardBorder(bool bNewUseHardBorder)
{
bUseHardBorder = bNewUseHardBorder;
if (MyRadialMenu.IsValid())
{
MyRadialMenu->bUseHardBorder = bUseHardBorder;
}
}
void UTobiiRadialMenuWidget::SetBorderColor(FColor NewBorderColor)
{
BorderColor = NewBorderColor;
if (MyRadialMenu.IsValid())
{
MyRadialMenu->BorderColor = BorderColor;
}
}
void UTobiiRadialMenuWidget::SetPanelColor(FColor NewPanelColor)
{
PanelColor = NewPanelColor;
if (MyRadialMenu.IsValid())
{
MyRadialMenu->PanelColor = PanelColor;
}
}
void UTobiiRadialMenuWidget::SetBorderThicknessPx(float NewBorderThicknessPx)
{
BorderThicknessPx = NewBorderThicknessPx;
if (MyRadialMenu.IsValid())
{
MyRadialMenu->BorderThicknessPx = BorderThicknessPx;
}
}
void UTobiiRadialMenuWidget::SetSegmentSeparationPx(float NewSegmentSeparationPx)
{
SegmentSeparationPx = NewSegmentSeparationPx;
if (MyRadialMenu.IsValid())
{
MyRadialMenu->SegmentSeparationPx = SegmentSeparationPx;
}
}
void UTobiiRadialMenuWidget::SetAngularDisplacementDeg(float NewAngularDisplacementDeg)
{
AngularDisplacementDeg = NewAngularDisplacementDeg;
if (MyRadialMenu.IsValid())
{
MyRadialMenu->AngularDisplacementDeg = AngularDisplacementDeg;
}
}
void UTobiiRadialMenuWidget::SetRadiusPx(float NewRadiusPx)
{
RadiusPx = NewRadiusPx;
if (MyRadialMenu.IsValid())
{
MyRadialMenu->RadiusPx = RadiusPx;
}
}
void UTobiiRadialMenuWidget::SetVertexCount(int32 NewVertexCount)
{
VertexCount = NewVertexCount;
if (MyRadialMenu.IsValid())
{
MyRadialMenu->VertexCount = VertexCount;
}
}
void UTobiiRadialMenuWidget::OnSlotAdded(UPanelSlot* InSlot)
{
// Add the child to the live canvas if it already exists
if (MyRadialMenu.IsValid())
{
CastChecked<UTobiiRadialMenuSlot>(InSlot)->BuildSlot(MyRadialMenu.ToSharedRef());
}
}
void UTobiiRadialMenuWidget::OnSlotRemoved(UPanelSlot* InSlot)
{
// Remove the widget from the live slot if it exists.
if (MyRadialMenu.IsValid())
{
TSharedPtr<SWidget> Widget = InSlot->Content->GetCachedWidget();
if (Widget.IsValid())
{
MyRadialMenu->RemoveSlot(Widget.ToSharedRef());
}
}
}
UTobiiRadialMenuSlot* UTobiiRadialMenuWidget::AddRadialMenuChild(UWidget* Content)
{
return Cast<UTobiiRadialMenuSlot>(Super::AddChild(Content));
}
bool UTobiiRadialMenuWidget::GetGeometryForSlot(int32 SlotIndex, FGeometry& ArrangedGeometry) const
{
UTobiiRadialMenuSlot* PanelSlot = CastChecked<UTobiiRadialMenuSlot>(Slots[SlotIndex]);
return GetGeometryForSlot(PanelSlot, ArrangedGeometry);
}
bool UTobiiRadialMenuWidget::GetGeometryForSlot(UTobiiRadialMenuSlot* InSlot, FGeometry& ArrangedGeometry) const
{
if (InSlot->Content == nullptr)
{
return false;
}
if (MyRadialMenu.IsValid())
{
FArrangedChildren ArrangedChildren(EVisibility::All);
MyRadialMenu->ArrangeChildren(MyRadialMenu->GetCachedGeometry(), ArrangedChildren);
for (int32 ChildIndex = 0; ChildIndex < ArrangedChildren.Num(); ChildIndex++)
{
if (ArrangedChildren[ChildIndex].Widget == InSlot->Content->GetCachedWidget())
{
ArrangedGeometry = ArrangedChildren[ChildIndex].Geometry;
return true;
}
}
}
return false;
}
#if WITH_EDITOR
const FText UTobiiRadialMenuWidget::GetPaletteCategory()
{
return LOCTEXT("Tobii", "Tobii");
}
#endif
/////////////////////////////////////////////////////
#undef LOCTEXT_NAMESPACE

View File

@@ -0,0 +1,508 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#include "TobiiGTOMBlueprintLibrary.h"
#include "TobiiGTOMModule.h"
#include "TobiiGazeFocusableComponent.h"
#include "TobiiGTOMInternalTypes.h"
#include "TobiiGTOMEngine.h"
#include "IEyeTracker.h"
#include "Engine/Engine.h"
#include "Engine/LocalPlayer.h"
#include "GameFramework/PlayerController.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/Paths.h"
#include "Slate/SceneViewport.h"
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
#define TWO_PI (6.28318530718)
UTobiiGTOMBlueprintLibrary::UTobiiGTOMBlueprintLibrary(const class FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
void UTobiiGTOMBlueprintLibrary::SetGTOMPlayerController(APlayerController* NewGTOMPlayerController)
{
if (FTobiiGTOMModule::IsAvailable())
{
if (FTobiiGTOMModule::Get().GTOMInputDevice.IsValid())
{
FTobiiGTOMModule::Get().GTOMInputDevice->GTOMPlayerController = NewGTOMPlayerController;
}
if (GEngine && GEngine->EyeTrackingDevice.IsValid())
{
GEngine->EyeTrackingDevice->SetEyeTrackedPlayer(NewGTOMPlayerController);
}
}
}
bool UTobiiGTOMBlueprintLibrary::GetGazeFocusData(FTobiiGazeFocusData& OutFocusData)
{
if (FTobiiGTOMModule::IsAvailable() && FTobiiGTOMModule::Get().GTOMInputDevice.IsValid())
{
const TArray<FTobiiGazeFocusData>& FocusData = FTobiiGTOMModule::Get().GTOMInputDevice->GetFocusData();
if (FocusData.Num() > 0)
{
OutFocusData = FocusData[0];
return true;
}
}
OutFocusData.FocusedActor = nullptr;
OutFocusData.FocusedPrimitiveComponent = nullptr;
OutFocusData.FocusedWidget = nullptr;
return false;
}
bool UTobiiGTOMBlueprintLibrary::GetAllGazeFocusData(TArray<FTobiiGazeFocusData>& OutFocusData)
{
OutFocusData.Empty();
if (FTobiiGTOMModule::IsAvailable() && FTobiiGTOMModule::Get().GTOMInputDevice.IsValid())
{
OutFocusData = FTobiiGTOMModule::Get().GTOMInputDevice->GetFocusData();
return OutFocusData.Num() > 0;
}
return false;
}
bool UTobiiGTOMBlueprintLibrary::GetFilteredGazeFocusData(const TArray<FName>& FocusLayerFilterList, const bool bIsWhiteList, const bool bWantPrimitives, const bool bWantWidgets, FTobiiGazeFocusData& OutFocusData)
{
if (FTobiiGTOMModule::IsAvailable() && FTobiiGTOMModule::Get().GTOMInputDevice.IsValid())
{
const TArray<FTobiiGazeFocusData>& AllFocusData = FTobiiGTOMModule::Get().GTOMInputDevice->GetFocusData();
for (const FTobiiGazeFocusData& FocusData : AllFocusData)
{
if (bWantWidgets && FocusData.FocusedWidget.IsValid())
{
if (FTobiiGTOMUtils::ValidateFocusLayers(FocusLayerFilterList, bIsWhiteList, FocusData.FocusedWidget.Get()))
{
OutFocusData = FocusData;
return true;
}
}
else if(bWantPrimitives && FocusData.FocusedPrimitiveComponent.IsValid() && !FocusData.FocusedWidget.IsValid())
{
if (FTobiiGTOMUtils::ValidateFocusLayers(FocusLayerFilterList, bIsWhiteList, FocusData.FocusedPrimitiveComponent.Get()))
{
OutFocusData = FocusData;
return true;
}
}
}
}
OutFocusData.FocusedActor = nullptr;
OutFocusData.FocusedPrimitiveComponent = nullptr;
OutFocusData.FocusedWidget = nullptr;
return false;
}
bool UTobiiGTOMBlueprintLibrary::GetAllFilteredGazeFocusData(const TArray<FName>& FocusLayerFilterList, const bool bIsWhiteList, const bool bWantPrimitives, const bool bWantWidgets, TArray<FTobiiGazeFocusData>& OutFocusData)
{
OutFocusData.Empty();
if (FTobiiGTOMModule::IsAvailable() && FTobiiGTOMModule::Get().GTOMInputDevice.IsValid())
{
const TArray<FTobiiGazeFocusData>& AllFocusData = FTobiiGTOMModule::Get().GTOMInputDevice->GetFocusData();
for (const FTobiiGazeFocusData& FocusData : AllFocusData)
{
if (bWantWidgets && FocusData.FocusedWidget.IsValid())
{
if (FTobiiGTOMUtils::ValidateFocusLayers(FocusLayerFilterList, bIsWhiteList, FocusData.FocusedWidget.Get()))
{
OutFocusData.Add(FocusData);
}
}
else if (bWantPrimitives && FocusData.FocusedPrimitiveComponent.IsValid() && !FocusData.FocusedWidget.IsValid())
{
if (FTobiiGTOMUtils::ValidateFocusLayers(FocusLayerFilterList, bIsWhiteList, FocusData.FocusedPrimitiveComponent.Get()))
{
OutFocusData.Add(FocusData);
}
}
}
}
return OutFocusData.Num() > 0;
}
void UTobiiGTOMBlueprintLibrary::RegisterScreenSpaceGazeFocusableWidgets(UWidget* Root)
{
UUserWidget* UserWidget = Cast<UUserWidget>(Root);
if (UserWidget != nullptr)
{
RegisterScreenSpaceGazeFocusableWidgets(UserWidget->GetRootWidget());
}
else
{
UTobiiGazeFocusableWidget* GazeFocusableWidget = Cast<UTobiiGazeFocusableWidget>(Root);
if (GazeFocusableWidget != nullptr)
{
GazeFocusableWidget->RegisterWidgetToGTOM(nullptr);
}
UPanelWidget* Panel = Cast<UPanelWidget>(Root);
if (Panel != nullptr)
{
for (int32 ChildIdx = 0; ChildIdx < Panel->GetChildrenCount(); ChildIdx++)
{
RegisterScreenSpaceGazeFocusableWidgets(Panel->GetChildAt(ChildIdx));
}
}
}
}
const FHitResult& UTobiiGTOMBlueprintLibrary::GetNaiveGazeHit()
{
static FHitResult Dummy;
if (FTobiiGTOMModule::IsAvailable() && FTobiiGTOMModule::Get().GTOMInputDevice.IsValid())
{
return FTobiiGTOMModule::Get().GTOMInputDevice->CombinedWorldGazeHitData;
}
else
{
return Dummy;
}
}
bool UTobiiGTOMBlueprintLibrary::IsPrimitiveComponentGazeFocusable(UPrimitiveComponent* PrimitiveComponent)
{
return UTobiiGazeFocusableComponent::IsPrimitiveFocusable(PrimitiveComponent);
}
bool UTobiiGTOMBlueprintLibrary::IsWidgetGazeFocusable(UWidget* Widget)
{
return UTobiiGazeFocusableComponent::IsWidgetFocusable(Cast<UTobiiGazeFocusableWidget>(Widget));
}
bool UTobiiGTOMBlueprintLibrary::GetPrimitiveComponentFocusOffset(USceneComponent* Component, FVector& OutFocusOffset)
{
OutFocusOffset = FVector::ZeroVector;
if (Component == nullptr)
{
return false;
}
for (const FName& CurrentTag : Component->ComponentTags)
{
FString CurrentTagString = CurrentTag.ToString();
if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetXTag))
{
FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetXTag.Len());
OutFocusOffset.X += FCString::Atof(*Argument);
}
if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetYTag))
{
FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetYTag.Len());
OutFocusOffset.Y += FCString::Atof(*Argument);
}
if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetZTag))
{
FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetZTag.Len());
OutFocusOffset.Z += FCString::Atof(*Argument);
}
}
return true;
}
bool UTobiiGTOMBlueprintLibrary::GetPrimitiveComponentFocusLocation(USceneComponent* Component, FVector& OutFocusLocation)
{
OutFocusLocation = FVector::ZeroVector;
if (Component == nullptr)
{
return false;
}
OutFocusLocation = Component->GetComponentLocation();
FVector Offset = FVector::ZeroVector;
for (const FName& CurrentTag : Component->ComponentTags)
{
FString CurrentTagString = CurrentTag.ToString();
if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetXTag))
{
FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetXTag.Len());
Offset.X += FCString::Atof(*Argument);
}
if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetYTag))
{
FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetYTag.Len());
Offset.Y += FCString::Atof(*Argument);
}
if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetZTag))
{
FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetZTag.Len());
Offset.Z += FCString::Atof(*Argument);
}
}
OutFocusLocation += Component->GetComponentTransform().TransformVector(Offset);
return true;
}
bool UTobiiGTOMBlueprintLibrary::MakeGazeFocusDataForWidgetInteractionComponent(UWidgetInteractionComponent* WidgetInteraction, ECollisionChannel CollisionChannel, FTobiiGazeFocusData& OutGazeFocusData)
{
OutGazeFocusData = FTobiiGazeFocusData();
if (WidgetInteraction == nullptr || !WidgetInteraction->bEnableHitTesting)
{
return false;
}
UWorld* CurrentWorld = WidgetInteraction->GetWorld();
if (CurrentWorld == nullptr)
{
return false;
}
FHitResult HitResult;
FVector StartPoint = WidgetInteraction->GetComponentLocation();
FVector EndPoint = StartPoint + WidgetInteraction->GetForwardVector() * WidgetInteraction->InteractionDistance;
FCollisionQueryParams CollisionParams;
CollisionParams.AddIgnoredActor(WidgetInteraction->GetOwner());
if (CurrentWorld->LineTraceSingleByChannel(HitResult, StartPoint, EndPoint, CollisionChannel, CollisionParams))
{
UPrimitiveComponent* HitComponent = HitResult.GetComponent();
if (UTobiiGTOMBlueprintLibrary::IsPrimitiveComponentGazeFocusable(HitComponent))
{
OutGazeFocusData.FocusedActor = HitResult.GetActor();
OutGazeFocusData.FocusedPrimitiveComponent = HitComponent;
OutGazeFocusData.LastVisibleWorldLocation = HitResult.Location;
OutGazeFocusData.FocusConfidence = 1.0f;
for (const TWeakPtr<SWidget>& WidgetMaybe : WidgetInteraction->GetHoveredWidgetPath().Widgets)
{
if (WidgetMaybe.IsValid())
{
SWidget* HoveredWidgetPtr = WidgetMaybe.Pin().Get();
STobiiGazeFocusableWidget* TobiiSlateWidget = STobiiGazeFocusableWidget::TryCastToTobiiGazeFocusable(HoveredWidgetPtr);
if (TobiiSlateWidget != nullptr && TobiiSlateWidget->UMGWidget.IsValid())
{
OutGazeFocusData.FocusedWidget = TobiiSlateWidget->UMGWidget;
break;
}
}
}
}
}
return OutGazeFocusData.FocusedActor != nullptr;
}
void UTobiiGTOMBlueprintLibrary::EmulateGazeFocusUsingWidgetInteractionComponent(UWidgetInteractionComponent* WidgetInteraction, ECollisionChannel CollisionChannel)
{
if (FTobiiGTOMModule::IsAvailable())
{
FTobiiGazeFocusData GazeFocusData;
MakeGazeFocusDataForWidgetInteractionComponent(WidgetInteraction, CollisionChannel, GazeFocusData);
TArray<FTobiiGazeFocusData> SingleFocusData;
SingleFocusData.Add(GazeFocusData);
FTobiiGTOMModule::Get().GTOMInputDevice->EmulateGazeFocus(SingleFocusData);
}
}
bool UTobiiGTOMBlueprintLibrary::FindLargestInscribedAlignedRect(float CircleSegmentAngleRad, float CircleRadius, FVector2D& LargestInscribedRectSize, float& DistanceToCenter)
{
//https://math.stackexchange.com/questions/2829710/largest-inscribed-rectangle-in-sector
if (CircleSegmentAngleRad > FLT_EPSILON && CircleRadius > FLT_EPSILON)
{
if (CircleSegmentAngleRad >= PI - FLT_EPSILON)
{
const float SqrtTwo = FMath::Sqrt(2.0f);
const float SqrtTwoOverTwo = SqrtTwo / 2.0f;
LargestInscribedRectSize.Set(SqrtTwo * CircleRadius, SqrtTwoOverTwo * CircleRadius);
DistanceToCenter = CircleRadius / 2.0f;
}
else
{
const float Theta = CircleSegmentAngleRad / 4.0f;
float ThetaSine, ThetaCosine;
FMath::SinCos(&ThetaSine, &ThetaCosine, Theta);
const float Height = CircleRadius * ThetaSine * 2.0f;
const float AlongX = CircleRadius * ThetaCosine;
const float HalfAlpha = CircleSegmentAngleRad / 2.0f;
float HalfAlphaSine, HalfAlphaCosine;
FMath::SinCos(&HalfAlphaSine, &HalfAlphaCosine, HalfAlpha);
const float B = CircleRadius * ThetaSine * HalfAlphaCosine / HalfAlphaSine;
LargestInscribedRectSize.Set(AlongX - B, Height);
DistanceToCenter = B + FMath::Min(LargestInscribedRectSize.X, LargestInscribedRectSize.Y) / 2.0f;
}
return true;
}
LargestInscribedRectSize.Set(0.0f, 0.0f);
DistanceToCenter = 0.0f;
return false;
}
bool UTobiiGTOMBlueprintLibrary::TransformWidgetLocalPointToWorld(UWidgetComponent* Component, const FVector2D& LocalWidgetLocation, FVector& OutWorldLocation)
{
if (Component == nullptr)
{
return false;
}
EWidgetGeometryMode GeometryMode = Component->GetGeometryMode();
switch (GeometryMode)
{
case EWidgetGeometryMode::Plane:
{
//First let's calculate some measurements we will need
const FVector2D DrawSize = Component->GetDrawSize();
const FVector2D Pivot = Component->GetPivot();
//Undo the pivot and transform to world space
OutWorldLocation = Component->GetComponentTransform().TransformPosition(FVector(0.0f, DrawSize.X * Pivot.X - LocalWidgetLocation.X, DrawSize.Y * Pivot.Y - LocalWidgetLocation.Y));
return true;
}
case EWidgetGeometryMode::Cylinder:
{
//First let's calculate some measurements we will need
const FVector2D DrawSize = Component->GetDrawSize();
const FVector2D Pivot = Component->GetPivot();
const float NormalizedLocationX = LocalWidgetLocation.X / DrawSize.X;
const float ArcAngleRadians = FMath::DegreesToRadians(Component->GetCylinderArcAngle());
const float Radius = DrawSize.X / ArcAngleRadians;
const float Apothem = Radius * FMath::Cos(0.5f * ArcAngleRadians);
const float ChordLength = 2.0f * Radius * FMath::Sin(0.5f * ArcAngleRadians);
const float PivotOffsetX = ChordLength * (0.5 - Pivot.X);
//Determine the endpoints of the UI surface in UI space radians. The projection is "reversed" here so the UI surface segment is actually centered around X-.
const float Endpoint1 = FMath::Fmod(FMath::Atan2(-0.5f * ChordLength, -Apothem) + 2 * PI, 2 * PI);
const float Endpoint2 = FMath::Fmod(FMath::Atan2(+0.5f * ChordLength, -Apothem) + 2 * PI, 2 * PI);
//Figure out where on the circle segment our X is using our normalized coordinate
const float InterpolatedAngle = FMath::Lerp(Endpoint1, Endpoint2, NormalizedLocationX);
//We can now determine our coordinate in circle space (the space where the origin is the center of the cylinder circle Y-segment the X-coordinate we are seeking is on) as well as the distance to the cylinder surface.
const float CircleSpaceWidgetX = Radius * FMath::Cos(InterpolatedAngle);
const float CircleSpaceWidgetDistanceY = Radius * FMath::Sin(InterpolatedAngle);
//Figure out our Z coordinate in widget space. This is easy since it's a cylinder.
const float WidgetSpaceZ = DrawSize.Y * Pivot.Y - LocalWidgetLocation.Y;
//We now convert the circle space coordinates to widget space (The origin is located at the center of the corda, in the middle of the widget component plane) and insert the widget space Z.
const FVector WidgetSpaceCoord(Apothem + CircleSpaceWidgetX, -CircleSpaceWidgetDistanceY + PivotOffsetX, WidgetSpaceZ);
//Finally we can simply transform this location to world space and we're done.
OutWorldLocation = Component->GetComponentTransform().TransformPosition(WidgetSpaceCoord);
return true;
}
default:
break;
}
return false;
}
bool UTobiiGTOMBlueprintLibrary::TransformWorldPointToWidgetLocal(UWidgetComponent* Component, const FVector& WorldLocation, const FVector& WorldDirection, FVector2D& OutLocalWidgetLocation)
{
if (Component == nullptr)
{
return false;
}
EWidgetGeometryMode GeometryMode = Component->GetGeometryMode();
switch (GeometryMode)
{
case EWidgetGeometryMode::Plane:
{
Component->GetLocalHitLocation(WorldLocation, OutLocalWidgetLocation);
return true;
}
case EWidgetGeometryMode::Cylinder:
{
TTuple<FVector, FVector2D> Output = Component->GetCylinderHitLocation(WorldLocation, WorldDirection);
OutLocalWidgetLocation = Output.Value;
return true;
}
default:
break;
}
return false;
}
bool UTobiiGTOMBlueprintLibrary::TestConeSphereIntersection(const FVector& ConeApex, const FVector& ConeDirection, const float ConeAngleDeg, const FVector& SphereCenter, const float SphereRadius)
{
//http://blog.julien.cayzac.name/2009/12/frustum-culling-sphere-cone-test-with.html
float ConeAngleSine, ConeAngleCosine;
FMath::SinCos(&ConeAngleSine, &ConeAngleCosine, FMath::DegreesToRadians(ConeAngleDeg));
const FVector DirectionTowardsSphere = SphereCenter - ConeApex;
const float SelfDot = FVector::DotProduct(DirectionTowardsSphere, DirectionTowardsSphere);
const float a = FVector::DotProduct(DirectionTowardsSphere, ConeDirection);
const float p(a*ConeAngleSine);
const float q(ConeAngleCosine * ConeAngleCosine * SelfDot - a * a);
const bool tmp(q < 0);
float lhs[2];
lhs[0] = (SphereRadius*SphereRadius - q);
lhs[1] = -lhs[0];
int result = (lhs[(p < SphereRadius) || !tmp] < 2.0f * SphereRadius * p) ? -1 : tmp;
return result != 0; // -1 means partially included. 1 means fully included
}
bool UTobiiGTOMBlueprintLibrary::TestRectEllipseIntersection(const FVector2D& RectangleCenter, const FVector2D& RectangleRightAxis, const FVector2D& RectangleUpAxis, const FVector2D& RectangleExtents, const FVector2D& EllipseCenter, const FVector2D& EllipseRadii, const float& EllipseRotationDeg)
{
//https://www.geometrictools.com/Documentation/IntersectionRectangleEllipse.pdf :: Minkowski sum
//Compute the increase in extents for R'.
float EllipseSine, EllipseCosine;
FMath::SinCos(&EllipseSine, &EllipseCosine, FMath::DegreesToRadians(EllipseRotationDeg));
FMatrix2x2 V(EllipseCosine, -EllipseSine, EllipseSine, EllipseCosine);
FMatrix2x2 VT(EllipseCosine, EllipseSine, -EllipseSine, EllipseCosine);
FMatrix2x2 D(1.0f / EllipseRadii.X, 0.0f, 0.0f, 1.0f / EllipseRadii.Y);
FMatrix2x2 M = V.Concatenate(D).Concatenate(D).Concatenate(VT);
FMatrix2x2 MInv = M.Inverse();
const float LRight = FMath::Sqrt(FVector2D::DotProduct(RectangleRightAxis, MInv.TransformVector(RectangleRightAxis)));
const float LUp = FMath::Sqrt(FVector2D::DotProduct(RectangleUpAxis, MInv.TransformVector(RectangleUpAxis)));
//Transform the ellipse center to the rectangle coordinate system.
FVector2D KmC = EllipseCenter - RectangleCenter;
float xi[2] = { FVector2D::DotProduct(RectangleRightAxis, KmC), FVector2D::DotProduct(RectangleUpAxis, KmC) };
if (FMath::Abs(xi[0]) <= RectangleExtents.X + LRight && FMath::Abs(xi[1]) <= RectangleExtents.Y + LUp)
{
float s[2] = { (xi[0] >= 0.0f ? 1.0f : -1.0f), (xi[1] >= 0.0f ? 1.0f : -1.0f) };
FVector2D PmC = s[0] * RectangleExtents.X * RectangleRightAxis + s[1] * RectangleExtents.X * RectangleUpAxis;
FVector2D MDelta = M.TransformVector(KmC - PmC);
if (s[0] * FVector2D::DotProduct(RectangleRightAxis, MDelta) <= 0.0f
|| s[1] * FVector2D::DotProduct(RectangleUpAxis, MDelta) <= 0.0f)
{
return true;
}
return FVector2D::DotProduct(MDelta, M.TransformVector(MDelta)) <= 1.0f;
}
return false;
}
FVector2D UTobiiGTOMBlueprintLibrary::GetUniformlyDistributedRandomCirclePoint(float CircleRadius)
{
float Angle = FMath::RandRange(0.0f, TWO_PI);
float Radius = CircleRadius * FMath::Sqrt(FMath::RandRange(0.0f, 1.0f));
return FVector2D(Radius * FMath::Cos(Angle), Radius * FMath::Sin(Angle));
}

View File

@@ -0,0 +1,631 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#include "TobiiGTOMEngine.h"
#include "TobiiGazeFocusableComponent.h"
#include "TobiiGazeFocusableWidget.h"
#include "TobiiGTOMBlueprintLibrary.h"
#include "TobiiGTOMInternalTypes.h"
#include "Engine/Engine.h"
#include "IEyeTracker.h"
#include "DrawDebugHelpers.h"
#include "GameFramework/PlayerController.h"
#include "Runtime/Engine/Public/Slate/SceneViewport.h"
#include "Misc/Paths.h"
static TAutoConsoleVariable<int32> CVarDebugDisplayGTOMVisibility(TEXT("tobii.debug.DisplayGTOMVisibility"), 0, TEXT("1 means we visualize which objects are visible to G2OM"));
static TAutoConsoleVariable<int32> CVarDebugDisplayG2OMCandidateSet(TEXT("tobii.debug.DisplayG2OMCandidateSet"), 1, TEXT("1 will visualize all bounds calculated in G2OM. This is useful to test for math errors."));
FTobiiGTOMEngine::FTobiiGTOMEngine()
{
g2om_context_create(&G2OMContext);
}
FTobiiGTOMEngine::~FTobiiGTOMEngine()
{
if (G2OMContext != nullptr)
{
g2om_context_destroy(&G2OMContext);
G2OMContext = nullptr;
}
}
void FTobiiGTOMEngine::Tick(float DeltaTimeSecs)
{
static const auto DrawDebugCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.debug"));
static const auto MaximumTraceDistanceCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.MaximumTraceDistance"));
static const auto FocusTraceChannelCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.FocusTraceChannel"));
if (GEngine == nullptr
|| GEngine->GameViewport == nullptr
|| GEngine->GameViewport->GetWorld() == nullptr
|| GEngine->GameViewport->GetGameViewport() == nullptr
|| !GEngine->EyeTrackingDevice.IsValid())
{
return;
}
//Gaze data
FEyeTrackerGazeData CombinedGazeData;
GEngine->EyeTrackingDevice->GetEyeTrackerGazeData(CombinedGazeData);
FVector2D CombinedGazePtUNorm = FVector2D::ZeroVector;
FVector2D ScreenGazePointPx;
FVector2D ViewportSize;
GEngine->GameViewport->GetViewportSize(ViewportSize);
GTOMPlayerController->ProjectWorldLocationToScreen(CombinedGazeData.GazeOrigin + CombinedGazeData.GazeDirection * 10.0f, ScreenGazePointPx);
if (ViewportSize.X > 0.0f && ViewportSize.Y > 0.0f)
{
CombinedGazePtUNorm = FVector2D(ScreenGazePointPx.X / ViewportSize.X, ScreenGazePointPx.Y / ViewportSize.Y);
}
if (!GTOMPlayerController.IsValid())
{
GTOMPlayerController = GEngine->GameViewport->GetWorld()->GetFirstPlayerController();
GEngine->EyeTrackingDevice->SetEyeTrackedPlayer(GTOMPlayerController.Get());
}
//Main G2OM processing
if (G2OMContext == nullptr
|| !GTOMPlayerController.IsValid()
|| GTOMPlayerController->PlayerCameraManager == nullptr)
{
return;
}
QUICK_SCOPE_CYCLE_COUNTER(STAT_TobiiEyetracking_GTOM);
FRotator CameraRotation = GTOMPlayerController->PlayerCameraManager->GetCameraRotation();
G2OMGazeData.timestamp_in_s = GTOMPlayerController->GetWorld()->GetTimeSeconds();
G2OMGazeData.camera_up_direction_world_space = FTobiiGTOMUtils::UE4VectorToG2OMVector(CameraRotation.RotateVector(FVector::UpVector));
G2OMGazeData.camera_right_direction_world_space = FTobiiGTOMUtils::UE4VectorToG2OMVector(CameraRotation.RotateVector(FVector::RightVector));
G2OMGazeData.gaze_ray_world_space.is_valid = CombinedGazeData.ConfidenceValue > 0.5f; // Sigh. Why is this still a thing?
G2OMGazeData.gaze_ray_world_space.ray.origin.x = CombinedGazeData.GazeOrigin.X;
G2OMGazeData.gaze_ray_world_space.ray.origin.y = CombinedGazeData.GazeOrigin.Y;
G2OMGazeData.gaze_ray_world_space.ray.origin.z = CombinedGazeData.GazeOrigin.Z;
G2OMGazeData.gaze_ray_world_space.ray.direction.x = CombinedGazeData.GazeDirection.X;
G2OMGazeData.gaze_ray_world_space.ray.direction.y = CombinedGazeData.GazeDirection.Y;
G2OMGazeData.gaze_ray_world_space.ray.direction.z = CombinedGazeData.GazeDirection.Z;
//Raycasts
FCollisionQueryParams CollisionQueryParams;
CollisionQueryParams.AddIgnoredActor(GTOMPlayerController.Get());
CollisionQueryParams.AddIgnoredActor(GTOMPlayerController->GetPawn());
const float MaximumTraceDistance = FMath::Max(MaximumTraceDistanceCVar->GetFloat(), 0.0f);
const FVector CombinedGazeFarLocation = CombinedGazeData.GazeOrigin + (CombinedGazeData.GazeDirection * MaximumTraceDistance);
if (CombinedGazeData.ConfidenceValue < 0.5f ||
!GTOMPlayerController->GetWorld()->LineTraceSingleByChannel(CombinedWorldGazeHitData, CombinedGazeData.GazeOrigin
, CombinedGazeFarLocation, (ECollisionChannel)FocusTraceChannelCVar->GetInt(), CollisionQueryParams))
{
CombinedWorldGazeHitData.Actor = nullptr;
CombinedWorldGazeHitData.Component = nullptr;
CombinedWorldGazeHitData.Distance = MaximumTraceDistance;
CombinedWorldGazeHitData.Location = CombinedGazeFarLocation;
CombinedWorldGazeHitData.bBlockingHit = false;
}
FillG2OMRaycast(CombinedWorldGazeHitData, CombinedGazePtUNorm, CombinedGazeData.GazeDirection, G2OMRaycastResults.raycast);
//Candidate generation
TArray<g2om_candidate> Candidates;
TArray<g2om_candidate_result> CandidateResults;
TMap<FEngineFocusableUID, TWeakObjectPtr<UTobiiGazeFocusableComponent>> FocusableComponentsWithWidgets;
const TMap<FEngineFocusableUID, FTobiiGTOMOcclusionData>& VisibleSet = OcclusionTester.Tick(DeltaTimeSecs, GTOMPlayerController.Get(), CombinedGazeData, G2OMGazeData, G2OMContext);
TMap<FTobiiFocusableUID, TWeakObjectPtr<UTobiiGazeFocusableWidget>>& ScreenSpaceWidgets = UTobiiGazeFocusableWidget::GetTobiiScreenSpaceFocusableWidgets();
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_TobiiEyetracking_GTOM_BuildCandidates);
//We must do this every frame since any of these properties might have changed since the last tick.
for (auto OcclusionDataIterator = VisibleSet.CreateConstIterator(); OcclusionDataIterator; ++OcclusionDataIterator)
{
const FTobiiGTOMOcclusionData& OcclusionData = OcclusionDataIterator.Value();
const FEngineFocusableUID FocusableID = OcclusionDataIterator.Key();
if (OcclusionData.PrimitiveComponent.IsValid() && OcclusionData.PrimitiveComponent->IsVisible())
{
bool bShouldAddPrimitive = true;
if (OcclusionData.FocusableComponent.IsValid())
{
const TArray<TWeakObjectPtr<UTobiiGazeFocusableWidget>>& FocusableWidgets = OcclusionData.FocusableComponent->GetFocusableWidgetsForPrimitiveComponent(OcclusionData.PrimitiveComponent.Get());
if (FocusableWidgets.Num() > 0)
{
//If our primitive contains widgets, then only add the widgets
bShouldAddPrimitive = false;
g2om_candidate NewCandidate;
FTransform LocalToWorldTranform = OcclusionData.PrimitiveComponent->GetComponentTransform();
FMatrix LocalToWorldMatrix = LocalToWorldTranform.ToMatrixWithScale();
FMatrix WorldToLocalMatrix = LocalToWorldMatrix.InverseFast();
FMemory::Memcpy(NewCandidate.local_to_world_matrix.data, LocalToWorldMatrix.M, 16 * sizeof(float));
FMemory::Memcpy(NewCandidate.world_to_local_matrix.data, WorldToLocalMatrix.M, 16 * sizeof(float));
for (const auto& Widget : FocusableWidgets)
{
if (Widget.IsValid() && !FocusableComponentsWithWidgets.Contains(Widget->GetUniqueID())
&& UTobiiGazeFocusableComponent::IsWidgetFocusable(Widget.Get())
&& Widget->GetWorldSpaceHostWidgetComponent() != nullptr)
{
STobiiGazeFocusableWidget* SlateContainer = (STobiiGazeFocusableWidget*)&Widget->TakeWidget().Get();
FSlateRect WidgetRenderBounds = SlateContainer->GetCachedGeometry().GetRenderBoundingRect();
FVector WidgetTopLeft, WidgetBottomRight;
UTobiiGTOMBlueprintLibrary::TransformWidgetLocalPointToWorld(Widget->GetWorldSpaceHostWidgetComponent(), WidgetRenderBounds.GetTopLeft(), WidgetTopLeft);
UTobiiGTOMBlueprintLibrary::TransformWidgetLocalPointToWorld(Widget->GetWorldSpaceHostWidgetComponent(), WidgetRenderBounds.GetBottomRight(), WidgetBottomRight);
//Range test
bool bIsInRange = true;
float MaxDistance;
const float DistanceToWidget = FVector::Distance(CombinedGazeData.GazeOrigin, (WidgetTopLeft + WidgetBottomRight) / 2.0f);
if (UTobiiGazeFocusableComponent::GetMaxFocusDistanceForWidget(Widget.Get(), MaxDistance))
{
bIsInRange = DistanceToWidget <= MaxDistance;
}
if (bIsInRange)
{
WidgetTopLeft = WorldToLocalMatrix.TransformPosition(WidgetTopLeft);
WidgetBottomRight = WorldToLocalMatrix.TransformPosition(WidgetBottomRight);
NewCandidate.id = Widget->GetUniqueID();
NewCandidate.min_local_space.x = WidgetTopLeft.X;
NewCandidate.min_local_space.y = WidgetTopLeft.Y;
NewCandidate.min_local_space.z = WidgetTopLeft.Z;
NewCandidate.max_local_space.x = WidgetBottomRight.X;
NewCandidate.max_local_space.y = WidgetBottomRight.Y;
NewCandidate.max_local_space.z = WidgetBottomRight.Z;
Candidates.Add(NewCandidate);
CandidateResults.Add(g2om_candidate_result());
FocusableComponentsWithWidgets.Add(Widget->GetUniqueID(), OcclusionData.FocusableComponent);
}
}
}
}
}
if(bShouldAddPrimitive)
{
//If there were no widgets, just add the primitive.
g2om_candidate NewCandidate;
NewCandidate.id = FocusableID;
FTransform LocalToWorldTranform = OcclusionData.PrimitiveComponent->GetComponentTransform();
FMatrix LocalToWorldMatrix = LocalToWorldTranform.ToMatrixWithScale();
FMatrix WorldToLocalMatrix = LocalToWorldMatrix.InverseFast();
FMemory::Memcpy(NewCandidate.local_to_world_matrix.data, LocalToWorldMatrix.M, 16 * sizeof(float));
FMemory::Memcpy(NewCandidate.world_to_local_matrix.data, WorldToLocalMatrix.M, 16 * sizeof(float));
FBox BoxBounds = OcclusionData.PrimitiveComponent->CalcBounds(FTransform::Identity).GetBox();
NewCandidate.min_local_space.x = BoxBounds.Min.X;
NewCandidate.min_local_space.y = BoxBounds.Min.Y;
NewCandidate.min_local_space.z = BoxBounds.Min.Z;
NewCandidate.max_local_space.x = BoxBounds.Max.X;
NewCandidate.max_local_space.y = BoxBounds.Max.Y;
NewCandidate.max_local_space.z = BoxBounds.Max.Z;
Candidates.Add(MoveTemp(NewCandidate));
CandidateResults.Add(g2om_candidate_result());
}
}
}
//Finally add registered screen space widgets as candidates.
TArray<FTobiiFocusableUID> StaleWidgetIds;
for (auto& WidgetPair : ScreenSpaceWidgets)
{
TWeakObjectPtr<UTobiiGazeFocusableWidget> Widget = WidgetPair.Value;
if (Widget.IsValid())
{
if (!FocusableComponentsWithWidgets.Contains(Widget->GetUniqueID()) && UTobiiGazeFocusableComponent::IsWidgetFocusable(Widget.Get()))
{
STobiiGazeFocusableWidget* SlateContainer = (STobiiGazeFocusableWidget*)&Widget->TakeWidget().Get();
FSlateRect WidgetRenderBounds = SlateContainer->GetCachedGeometry().GetRenderBoundingRect();
FVector2D ScreenTopLeft = GEngine->GameViewport->GetGameViewport()->VirtualDesktopPixelToViewport(FIntPoint(WidgetRenderBounds.GetTopLeft().X, WidgetRenderBounds.GetTopLeft().Y));
FVector2D ScreenBottomRight = GEngine->GameViewport->GetGameViewport()->VirtualDesktopPixelToViewport(FIntPoint(WidgetRenderBounds.GetBottomRight().X, WidgetRenderBounds.GetBottomRight().Y));
ScreenTopLeft.X *= ViewportSize.X;
ScreenTopLeft.Y *= ViewportSize.Y;
ScreenBottomRight.X *= ViewportSize.X;
ScreenBottomRight.Y *= ViewportSize.Y;
FVector2D ScreenBottomLeft(ScreenTopLeft.X, ScreenBottomRight.Y);
FVector WorldTopLeftLocation, WorldBottomRightLocation, WorldBottomLeftLocation;
FVector WorldTopLeftDir, WorldBottomRightDir, WorldBottomLeftDir;
GTOMPlayerController->DeprojectScreenPositionToWorld(ScreenTopLeft.X, ScreenTopLeft.Y, WorldTopLeftLocation, WorldTopLeftDir);
GTOMPlayerController->DeprojectScreenPositionToWorld(ScreenBottomRight.X, ScreenBottomRight.Y, WorldBottomRightLocation, WorldBottomRightDir);
GTOMPlayerController->DeprojectScreenPositionToWorld(ScreenBottomLeft.X, ScreenBottomLeft.Y, WorldBottomLeftLocation, WorldBottomLeftDir);
WorldTopLeftLocation += WorldTopLeftDir;
WorldBottomRightLocation += WorldBottomRightDir;
WorldBottomLeftLocation += WorldBottomLeftDir;
FVector WorldTranslation = (WorldTopLeftLocation + WorldBottomRightLocation) / 2.0f;
FVector WorldRightY = WorldBottomRightLocation - WorldBottomLeftLocation;
FVector WorldUpZ = WorldTopLeftLocation - WorldBottomLeftLocation;
FVector WorldForwardX = FVector::CrossProduct(WorldRightY, WorldUpZ);
bool bCouldBuildOrthoNormalBasis = WorldForwardX.Normalize();
bCouldBuildOrthoNormalBasis = bCouldBuildOrthoNormalBasis && WorldRightY.Normalize();
bCouldBuildOrthoNormalBasis = bCouldBuildOrthoNormalBasis && WorldUpZ.Normalize();
if (bCouldBuildOrthoNormalBasis)
{
FMatrix LocalToWorldMatrix = FMatrix(WorldForwardX, WorldRightY, WorldUpZ, WorldTranslation);
FMatrix WorldToLocalMatrix = LocalToWorldMatrix.InverseFast();
WorldTopLeftLocation = WorldToLocalMatrix.TransformPosition(WorldTopLeftLocation + WorldTopLeftDir);
WorldBottomRightLocation = WorldToLocalMatrix.TransformPosition(WorldBottomRightLocation + WorldBottomRightDir);
//Screen space widgets are always in range, so we can skip the distance test
g2om_candidate NewCandidate;
NewCandidate.id = Widget->GetUniqueID();
FMemory::Memcpy(NewCandidate.local_to_world_matrix.data, LocalToWorldMatrix.M, 16 * sizeof(float));
FMemory::Memcpy(NewCandidate.world_to_local_matrix.data, WorldToLocalMatrix.M, 16 * sizeof(float));
NewCandidate.min_local_space.x = WorldTopLeftLocation.X;
NewCandidate.min_local_space.y = WorldTopLeftLocation.Y;
NewCandidate.min_local_space.z = WorldTopLeftLocation.Z;
NewCandidate.max_local_space.x = WorldBottomRightLocation.X;
NewCandidate.max_local_space.y = WorldBottomRightLocation.Y;
NewCandidate.max_local_space.z = WorldBottomRightLocation.Z;
Candidates.Add(NewCandidate);
CandidateResults.Add(g2om_candidate_result());
}
}
}
else
{
StaleWidgetIds.Add(WidgetPair.Key);
}
for (FTobiiFocusableUID Id : StaleWidgetIds)
{
ScreenSpaceWidgets.Remove(Id);
}
}
}
UPrimitiveComponent* NewTopFocusPrimitive = nullptr;
UTobiiGazeFocusableWidget* NewTopFocusWidget = nullptr;
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_TobiiEyetracking_GTOM_G2OMProcess);
g2om_process(G2OMContext, &G2OMGazeData, &G2OMRaycastResults, Candidates.Num(), Candidates.GetData(), CandidateResults.GetData());
G2OMFocusResults.Empty();
for (const g2om_candidate_result& ResultCandidate : CandidateResults)
{
if (ResultCandidate.score > FLT_EPSILON)
{
if(ScreenSpaceWidgets.Contains(ResultCandidate.id))
{
//Screen space widget
TWeakObjectPtr<UTobiiGazeFocusableWidget> Widget = ScreenSpaceWidgets[ResultCandidate.id];
if (Widget.IsValid())
{
FTobiiGazeFocusData NewFocusData;
NewFocusData.FocusedWidget = Widget;
NewFocusData.FocusConfidence = ResultCandidate.score;
NewFocusData.FocusedActor = nullptr;
NewFocusData.FocusedPrimitiveComponent = nullptr;
NewFocusData.LastVisibleWorldLocation = FVector::ZeroVector;
G2OMFocusResults.Add(MoveTemp(NewFocusData));
if (NewTopFocusWidget == nullptr)
{
NewTopFocusWidget = Widget.Get();
}
}
}
else if (FocusableComponentsWithWidgets.Contains(ResultCandidate.id))
{
//World space widget
TWeakObjectPtr<UTobiiGazeFocusableComponent> Focusable = FocusableComponentsWithWidgets[ResultCandidate.id];
if (Focusable.IsValid())
{
const TMap<uint32, TWeakObjectPtr<UTobiiGazeFocusableWidget>>& FocusableWidgets = Focusable->GetAllFocusableWidgets();
if (FocusableWidgets.Contains(ResultCandidate.id))
{
TWeakObjectPtr<UTobiiGazeFocusableWidget> Widget = FocusableWidgets[ResultCandidate.id];
if (Widget.IsValid())
{
FTobiiGazeFocusData NewFocusData;
NewFocusData.FocusedWidget = Widget;
NewFocusData.FocusConfidence = ResultCandidate.score;
if (Widget->GetWorldSpaceHostWidgetComponent() != nullptr)
{
NewFocusData.FocusedActor = Widget->GetWorldSpaceHostWidgetComponent()->GetOwner();
NewFocusData.FocusedPrimitiveComponent = Widget->GetWorldSpaceHostWidgetComponent();
NewFocusData.LastVisibleWorldLocation = Widget->GetWorldSpaceHostWidgetComponent()->GetComponentLocation();
}
G2OMFocusResults.Add(MoveTemp(NewFocusData));
if (NewTopFocusWidget == nullptr)
{
NewTopFocusWidget = Widget.Get();
}
}
}
}
}
else if(VisibleSet.Contains(ResultCandidate.id))
{
//Primitive Component
TWeakObjectPtr<UPrimitiveComponent> FocusedPrimitivePtr = VisibleSet[ResultCandidate.id].PrimitiveComponent;
if (FocusedPrimitivePtr.IsValid())
{
FTobiiGazeFocusData NewFocusData;
NewFocusData.FocusedActor = FocusedPrimitivePtr->GetOwner();
NewFocusData.FocusedPrimitiveComponent = FocusedPrimitivePtr;
NewFocusData.FocusedWidget = nullptr;
UTobiiGTOMBlueprintLibrary::GetPrimitiveComponentFocusLocation(FocusedPrimitivePtr.Get(), NewFocusData.LastVisibleWorldLocation); // We want this to be taken care of by GXOM, but the current system does not support it.
NewFocusData.FocusConfidence = ResultCandidate.score;
G2OMFocusResults.Add(MoveTemp(NewFocusData));
if (NewTopFocusPrimitive == nullptr)
{
NewTopFocusPrimitive = FocusedPrimitivePtr.Get();
}
}
}
}
}
}
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_TobiiEyetracking_GTOM_G2OMNotify);
UpdateWinners(NewTopFocusPrimitive, NewTopFocusWidget);
}
if (DrawDebugCVar->GetInt() && CVarDebugDisplayG2OMCandidateSet.GetValueOnGameThread() != 0)
{
for (g2om_candidate& Candidate : Candidates)
{
bool bIsFocused = false;
float FocusAlpha = 1.0f;
for (const g2om_candidate_result& ResultCandidate : CandidateResults)
{
if (ResultCandidate.id == Candidate.id && ResultCandidate.score > FLT_EPSILON)
{
bIsFocused = true;
FocusAlpha = ResultCandidate.score;
break;
}
}
g2om_vector Corners[G2OM_CORNERS_COUNT];
g2om_get_worldspace_corner_of_candidate(&Candidate, G2OM_CORNERS_COUNT, Corners);
TArray<FVector> UCorners;
for (int32 Idx = 0; Idx < G2OM_CORNERS_COUNT; Idx++)
{
UCorners.Add(FTobiiGTOMUtils::G2OMVectorToUE4Vector(Corners[Idx]));
}
UWorld* World = GTOMPlayerController->GetWorld();
const float DistanceToFirstCorner = FVector::Distance(UCorners[G2OM_CORNERS_FLL], GTOMPlayerController->GetPawn()->GetActorLocation());
const float Thickness = (bIsFocused && DistanceToFirstCorner > 100.0f) ? 3.0f : 0.0f;
const FColor BoxColor = bIsFocused ? FColor(0, 255, 0, FocusAlpha * 255)
: (FocusableComponentsWithWidgets.Contains(Candidate.id) ? FColor::Cyan : FColor::Orange);
//FRONT
DrawDebugLine(World, UCorners[G2OM_CORNERS_FLL], UCorners[G2OM_CORNERS_FLR], BoxColor, false, 0.0f, 0, Thickness);
DrawDebugLine(World, UCorners[G2OM_CORNERS_FLR], UCorners[G2OM_CORNERS_FUR], BoxColor, false, 0.0f, 0, Thickness);
DrawDebugLine(World, UCorners[G2OM_CORNERS_FUR], UCorners[G2OM_CORNERS_FUL], BoxColor, false, 0.0f, 0, Thickness);
DrawDebugLine(World, UCorners[G2OM_CORNERS_FUL], UCorners[G2OM_CORNERS_FLL], BoxColor, false, 0.0f, 0, Thickness);
//BACK
DrawDebugLine(World, UCorners[G2OM_CORNERS_BLL], UCorners[G2OM_CORNERS_BLR], BoxColor, false, 0.0f, 0, Thickness);
DrawDebugLine(World, UCorners[G2OM_CORNERS_BLR], UCorners[G2OM_CORNERS_BUR], BoxColor, false, 0.0f, 0, Thickness);
DrawDebugLine(World, UCorners[G2OM_CORNERS_BUR], UCorners[G2OM_CORNERS_BUL], BoxColor, false, 0.0f, 0, Thickness);
DrawDebugLine(World, UCorners[G2OM_CORNERS_BUL], UCorners[G2OM_CORNERS_BLL], BoxColor, false, 0.0f, 0, Thickness);
//SPOKES
DrawDebugLine(World, UCorners[G2OM_CORNERS_FLL], UCorners[G2OM_CORNERS_BLL], BoxColor, false, 0.0f, 0, Thickness);
DrawDebugLine(World, UCorners[G2OM_CORNERS_FLR], UCorners[G2OM_CORNERS_BLR], BoxColor, false, 0.0f, 0, Thickness);
DrawDebugLine(World, UCorners[G2OM_CORNERS_FUR], UCorners[G2OM_CORNERS_BUR], BoxColor, false, 0.0f, 0, Thickness);
DrawDebugLine(World, UCorners[G2OM_CORNERS_FUL], UCorners[G2OM_CORNERS_BUL], BoxColor, false, 0.0f, 0, Thickness);
}
}
}
void FTobiiGTOMEngine::UpdateWinners(UPrimitiveComponent* NewTopFocusPrimitive, UTobiiGazeFocusableWidget* NewTopFocusWidget)
{
//Primitive components
{
if (NewTopFocusPrimitive == nullptr && PreviouslyFocusedPrimitiveComponent.IsValid())
{
//We lost focus completely
NotifyPrimitiveComponentGazeFocusLost(*PreviouslyFocusedPrimitiveComponent.Get());
}
else if (NewTopFocusPrimitive != nullptr && !PreviouslyFocusedPrimitiveComponent.IsValid())
{
//We gained focus from no focus
NotifyPrimitiveComponentGazeFocusReceived(*NewTopFocusPrimitive);
}
else if (NewTopFocusPrimitive != nullptr && PreviouslyFocusedPrimitiveComponent.IsValid() && NewTopFocusPrimitive != PreviouslyFocusedPrimitiveComponent.Get())
{
//Focus shifted from one object to another
NotifyPrimitiveComponentGazeFocusLost(*PreviouslyFocusedPrimitiveComponent.Get());
NotifyPrimitiveComponentGazeFocusReceived(*NewTopFocusPrimitive);
}
PreviouslyFocusedPrimitiveComponent = TWeakObjectPtr<UPrimitiveComponent>(NewTopFocusPrimitive);
}
//Widgets
{
if (NewTopFocusWidget == nullptr && PreviouslyFocusedWidget.IsValid())
{
//We lost focus completely
NotifyWidgetGazeFocusLost(*PreviouslyFocusedWidget.Get());
}
else if (NewTopFocusWidget != nullptr && !PreviouslyFocusedWidget.IsValid())
{
//We gained focus from no focus
NotifyWidgetGazeFocusReceived(*NewTopFocusWidget);
}
else if (NewTopFocusWidget != nullptr && PreviouslyFocusedWidget.IsValid() && NewTopFocusWidget != PreviouslyFocusedWidget.Get())
{
//Focus shifted from one object to another
NotifyWidgetGazeFocusLost(*PreviouslyFocusedWidget.Get());
NotifyWidgetGazeFocusReceived(*NewTopFocusWidget);
}
PreviouslyFocusedWidget = TWeakObjectPtr<UTobiiGazeFocusableWidget>(NewTopFocusWidget);
}
}
void FTobiiGTOMEngine::FillG2OMRaycast(const FHitResult& HitResult, const FVector2D& ScreenGazePointUNorm, const FVector& GazeDirection, g2om_raycast& RaycastToModify)
{
RaycastToModify.is_valid = false;
RaycastToModify.id = -1;
if (GEngine == nullptr
|| GEngine->GameViewport == nullptr
|| GEngine->GameViewport->GetWorld() == nullptr
|| GEngine->GameViewport->GetGameViewport() == nullptr
|| !GEngine->EyeTrackingDevice.IsValid())
{
return;
}
//First test screen space widgets
FVector2D VirtualDesktopGazePoint = GEngine->GameViewport->GetGameViewport()->ViewportToVirtualDesktopPixel(ScreenGazePointUNorm);
TMap<FTobiiFocusableUID, TWeakObjectPtr<UTobiiGazeFocusableWidget>>& ScreenSpaceWidgets = UTobiiGazeFocusableWidget::GetTobiiScreenSpaceFocusableWidgets();
for (auto& WidgetPair : ScreenSpaceWidgets)
{
TWeakObjectPtr<UTobiiGazeFocusableWidget> Widget = WidgetPair.Value;
if (Widget.IsValid())
{
if (UTobiiGazeFocusableComponent::IsWidgetFocusable(Widget.Get()))
{
STobiiGazeFocusableWidget* SlateContainer = (STobiiGazeFocusableWidget*)&Widget->TakeWidget().Get();
FSlateRect WidgetRenderBounds = SlateContainer->GetCachedGeometry().GetRenderBoundingRect();
if (WidgetRenderBounds.ContainsPoint(VirtualDesktopGazePoint))
{
RaycastToModify.is_valid = true;
RaycastToModify.id = Widget->GetUniqueID();
return;
}
}
}
}
UPrimitiveComponent* Primitive = HitResult.GetComponent();
if (Primitive != nullptr)
{
RaycastToModify.is_valid = UTobiiGazeFocusableComponent::IsPrimitiveFocusable(Primitive);
if (Primitive->GetOwner())
{
UTobiiGazeFocusableComponent* FocusableComponent = (UTobiiGazeFocusableComponent*)Primitive->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass());
if (FocusableComponent != nullptr)
{
const TArray<TWeakObjectPtr<UTobiiGazeFocusableWidget>>& FocusableWidgets = FocusableComponent->GetFocusableWidgetsForPrimitiveComponent(Primitive);
for (const auto& Widget : FocusableWidgets)
{
if (Widget.IsValid() && UTobiiGazeFocusableComponent::IsWidgetFocusable(Widget.Get()))
{
FVector2D WidgetLocation;
if (UTobiiGTOMBlueprintLibrary::TransformWorldPointToWidgetLocal(Widget->GetWorldSpaceHostWidgetComponent(), HitResult.Location, GazeDirection, WidgetLocation))
{
STobiiGazeFocusableWidget* SlateContainer = (STobiiGazeFocusableWidget*)&Widget->TakeWidget().Get();
FSlateRect WidgetRenderBounds = SlateContainer->GetCachedGeometry().GetRenderBoundingRect();
if (WidgetRenderBounds.ContainsPoint(WidgetLocation))
{
//We found a hit!
RaycastToModify.id = Widget->GetUniqueID();
return;
}
}
}
}
}
}
//Default
RaycastToModify.id = Primitive->GetUniqueID();
}
}
void FTobiiGTOMEngine::NotifyPrimitiveComponentGazeFocusReceived(UPrimitiveComponent& PrimitiveComponentToReceiveFocus)
{
PrimitiveComponentToReceiveFocus.ComponentTags.AddUnique(FTobiiPrimitiveComponentGazeFocusTags::HasGazeFocusTag);
if (PrimitiveComponentToReceiveFocus.GetOwner() != nullptr)
{
UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)PrimitiveComponentToReceiveFocus.GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass());
if (GazeFocusableComponent != nullptr)
{
GazeFocusableComponent->PrimitiveReceivedGazeFocus(&PrimitiveComponentToReceiveFocus);
}
}
}
void FTobiiGTOMEngine::NotifyPrimitiveComponentGazeFocusLost(UPrimitiveComponent& PrimitiveComponentToLoseFocus)
{
PrimitiveComponentToLoseFocus.ComponentTags.Remove(FTobiiPrimitiveComponentGazeFocusTags::HasGazeFocusTag);
if (PrimitiveComponentToLoseFocus.GetOwner() != nullptr)
{
UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)PrimitiveComponentToLoseFocus.GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass());
if (GazeFocusableComponent != nullptr)
{
GazeFocusableComponent->PrimitiveLostGazeFocus(&PrimitiveComponentToLoseFocus);
}
}
}
void FTobiiGTOMEngine::NotifyWidgetGazeFocusReceived(UTobiiGazeFocusableWidget& WidgetToReceiveFocus)
{
WidgetToReceiveFocus.bHasGazeFocus = true;
WidgetToReceiveFocus.ReceiveFocus();
if (WidgetToReceiveFocus.GetWorldSpaceHostWidgetComponent() != nullptr && WidgetToReceiveFocus.GetWorldSpaceHostWidgetComponent()->GetOwner() != nullptr)
{
UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)WidgetToReceiveFocus.GetWorldSpaceHostWidgetComponent()->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass());
if (GazeFocusableComponent != nullptr)
{
GazeFocusableComponent->WidgetReceivedGazeFocus(&WidgetToReceiveFocus);
}
}
}
void FTobiiGTOMEngine::NotifyWidgetGazeFocusLost(UTobiiGazeFocusableWidget& WidgetToLoseFocus)
{
WidgetToLoseFocus.bHasGazeFocus = false;
WidgetToLoseFocus.LoseFocus();
if (WidgetToLoseFocus.GetWorldSpaceHostWidgetComponent() != nullptr && WidgetToLoseFocus.GetWorldSpaceHostWidgetComponent()->GetOwner() != nullptr)
{
UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)WidgetToLoseFocus.GetWorldSpaceHostWidgetComponent()->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass());
if (GazeFocusableComponent != nullptr)
{
GazeFocusableComponent->WidgetLostGazeFocus(&WidgetToLoseFocus);
}
}
}
void FTobiiGTOMEngine::EmulateGazeFocus(TArray<FTobiiGazeFocusData>& EmulatedFocusData)
{
G2OMFocusResults.Empty();
G2OMFocusResults = EmulatedFocusData;
UPrimitiveComponent* TopPrimitive = nullptr;
UTobiiGazeFocusableWidget* TopWidget = nullptr;
if (EmulatedFocusData.Num() > 0)
{
TopPrimitive = EmulatedFocusData[0].FocusedPrimitiveComponent.IsValid() ? EmulatedFocusData[0].FocusedPrimitiveComponent.Get() : nullptr;
TopWidget = EmulatedFocusData[0].FocusedWidget.IsValid() ? EmulatedFocusData[0].FocusedWidget.Get() : nullptr;
}
UpdateWinners(TopPrimitive, TopWidget);
}

View File

@@ -0,0 +1,55 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "TobiiGTOMOcclusionTester.h"
#include "TobiiGazeFocusableWidget.h"
#include "TobiiGTOMTypes.h"
#include "tobii_g2om.h"
#include "CoreMinimal.h"
#include "Runtime/InputDevice/Public/IInputDevice.h"
class TOBIIGTOM_API FTobiiGTOMEngine : public IInputDevice
{
public:
FTobiiGTOMEngine();
virtual ~FTobiiGTOMEngine();
FHitResult CombinedWorldGazeHitData;
TWeakObjectPtr<APlayerController> GTOMPlayerController;
const TArray<FTobiiGazeFocusData>& GetFocusData() { return G2OMFocusResults; }
void EmulateGazeFocus(TArray<FTobiiGazeFocusData>& EmulatedFocusData);
// IInputDevice
public:
virtual void Tick(float DeltaTime) override;
virtual void SendControllerEvents() override { }
virtual void SetMessageHandler(const TSharedRef<FGenericApplicationMessageHandler>& InMessageHandler) override { }
virtual bool Exec(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar) override { return true; }
virtual void SetChannelValue(int32 ControllerId, FForceFeedbackChannelType ChannelType, float Value) override { }
virtual void SetChannelValues(int32 ControllerId, const FForceFeedbackValues &values) override { }
private:
g2om_context* G2OMContext;
g2om_raycast_result G2OMRaycastResults;
g2om_gaze_data G2OMGazeData;
FTobiiGTOMOcclusionTester OcclusionTester;
TArray<FTobiiGazeFocusData> G2OMFocusResults;
TWeakObjectPtr<UPrimitiveComponent> PreviouslyFocusedPrimitiveComponent;
TWeakObjectPtr<UTobiiGazeFocusableWidget> PreviouslyFocusedWidget;
void UpdateWinners(UPrimitiveComponent* NewTopFocusPrimitive, UTobiiGazeFocusableWidget* NewTopFocusWidget);
void NotifyPrimitiveComponentGazeFocusReceived(UPrimitiveComponent& PrimitiveComponentToReceiveFocus);
void NotifyPrimitiveComponentGazeFocusLost(UPrimitiveComponent& PrimitiveComponentToLoseFocus);
void NotifyWidgetGazeFocusReceived(UTobiiGazeFocusableWidget& WidgetToReceiveFocus);
void NotifyWidgetGazeFocusLost(UTobiiGazeFocusableWidget& WidgetToLoseFocus);
void FillG2OMRaycast(const FHitResult& HitResult, const FVector2D& ScreenGazePointUNorm, const FVector& GazeDirection, g2om_raycast& RaycastToModify);
};

View File

@@ -0,0 +1,107 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "tobii_g2om.h"
#include "CoreMinimal.h"
static class FTobiiGTOMUtils
{
public:
static g2om_vector UE4VectorToG2OMVector(const FVector& Input)
{
return g2om_vector { Input.X, Input.Y, Input.Z };
}
static FVector G2OMVectorToUE4Vector(const g2om_vector& Input)
{
return FVector(Input.x, Input.y, Input.z);
}
static void UE4SpaceToUnitySpace(g2om_vector& Arg)
{
//Unity forward is Z, right is X, up is Y
//UE4 forward is X, right is Y, up is Z
const float Temp = Arg.z;
Arg.z = Arg.x;
Arg.x = Arg.y;
Arg.y = Temp;
}
static void UnitySpaceToUE4Space(g2om_vector& Arg)
{
//Unity forward is Z, right is X, up is Y
//UE4 forward is X, right is Y, up is Z
const float Temp = Arg.x;
Arg.x = Arg.z;
Arg.z = Arg.y;
Arg.y = Temp;
}
static bool QueryFocusableLayer(UPrimitiveComponent* Candidate, const FName& LayerToQuery)
{
FName FocusLayer = UTobiiGazeFocusableComponent::GetFocusLayerForPrimitive(Candidate);
if (FocusLayer == FName("Default"))
{
return true;
}
return FocusLayer == LayerToQuery;
}
static bool QueryFocusableLayer(UTobiiGazeFocusableWidget* Candidate, const FName& LayerToQuery)
{
FName FocusLayer = UTobiiGazeFocusableComponent::GetFocusLayerForWidget(Candidate);
if (FocusLayer == FName("Default"))
{
return true;
}
return FocusLayer == LayerToQuery;
}
template <class T>
static bool ValidateFocusLayers(const TArray<FName>& FocusLayerFilterList, const bool bIsWhiteList, T* Candidate)
{
if (Candidate == nullptr)
{
return false;
}
if (bIsWhiteList)
{
//A white list only allows focusables that have a layer that is in the list.
for (auto& Layer : FocusLayerFilterList)
{
if (QueryFocusableLayer(Candidate, Layer))
{
return true;
}
}
}
else
{
//A black list will block any focusables whos layer is in the list.
for (auto& Layer : FocusLayerFilterList)
{
if (QueryFocusableLayer(Candidate, Layer))
{
return false;
}
}
//No filters hit, this object is ok.
return true;
}
return false;
}
};
DEFINE_LOG_CATEGORY_STATIC(LogTobiiGTOM, All, All);

View File

@@ -0,0 +1,59 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#include "TobiiGTOMModule.h"
#include "TobiiGazeFocusableComponent.h"
#include "TobiiGazeFocusableWidget.h"
#include "TobiiGTOMBlueprintLibrary.h"
#include "TobiiGTOMInternalTypes.h"
#include "TobiiGTOMEngine.h"
#include "IEyeTracker.h"
#include "DrawDebugHelpers.h"
#include "GameFramework/PlayerController.h"
#include "Misc/Paths.h"
#include "Runtime/Engine/Public/Slate/SceneViewport.h"
IMPLEMENT_MODULE(FTobiiGTOMModule, TobiiGTOM)
void FTobiiGTOMModule::StartupModule()
{
G2OMDllHandle = nullptr;
#if TOBII_EYETRACKING_ACTIVE
FString RelativeG2OMDllPath = FString(TEXT(TOBII_G2OM_RELATIVE_DLL_PATH));
#if TOBII_COMPILE_AS_ENGINE_PLUGIN
FString FullG2OMDllPath = FPaths::Combine(FPaths::EngineDir(), RelativeG2OMDllPath);
#else
FString FullG2OMDllPath = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectPluginsDir(), RelativeG2OMDllPath));
#endif //TOBII_COMPILE_AS_ENGINE_PLUGIN
G2OMDllHandle = FPlatformProcess::GetDllHandle(*FullG2OMDllPath);
if (G2OMDllHandle != nullptr)
{
IModularFeatures::Get().RegisterModularFeature(IInputDeviceModule::GetModularFeatureName(), this);
}
#endif //TOBII_EYETRACKING_ACTIVE
}
void FTobiiGTOMModule::ShutdownModule()
{
#if TOBII_EYETRACKING_ACTIVE
if (G2OMDllHandle != nullptr)
{
IModularFeatures::Get().UnregisterModularFeature(IInputDeviceModule::GetModularFeatureName(), this);
}
G2OMDllHandle = nullptr;
#endif //TOBII_EYETRACKING_ACTIVE
}
TSharedPtr<class IInputDevice> FTobiiGTOMModule::CreateInputDevice(const TSharedRef<FGenericApplicationMessageHandler>& InMessageHandler)
{
GTOMInputDevice = MakeShareable(new FTobiiGTOMEngine());
return TSharedPtr<class IInputDevice>(GTOMInputDevice);
}

View File

@@ -0,0 +1,35 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "CoreMinimal.h"
#include "Runtime/InputDevice/Public/IInputDeviceModule.h"
#include "Modules/ModuleManager.h"
class TOBIIGTOM_API FTobiiGTOMModule : public IInputDeviceModule
{
public:
static inline FTobiiGTOMModule& Get()
{
return FModuleManager::LoadModuleChecked<FTobiiGTOMModule>("TobiiGTOM");
}
static inline bool IsAvailable()
{
return FModuleManager::Get().IsModuleLoaded("TobiiGTOM");
}
public:
TSharedPtr<class FTobiiGTOMEngine> GTOMInputDevice;
virtual void StartupModule() override;
virtual void ShutdownModule() override;
virtual TSharedPtr< class IInputDevice > CreateInputDevice(const TSharedRef< FGenericApplicationMessageHandler >& InMessageHandler);
private:
void* G2OMDllHandle;
};

View File

@@ -0,0 +1,203 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#if TOBII_EYETRACKING_ACTIVE
#include "TobiiGTOMOcclusionTester.h"
#include "TobiiGazeFocusableComponent.h"
#include "TobiiGTOMInternalTypes.h"
#include "tobii_g2om.h"
#include "DrawDebugHelpers.h"
#include "Components/PrimitiveComponent.h"
#include "Engine/Engine.h"
#include "Engine/LocalPlayer.h"
#define TOBII_MAX_RAYS_PER_FRAME (15)
static TAutoConsoleVariable<int32> CVarOcclusionTesterTrackingBlastMode(TEXT("tobii.gtom.OcclusionTesterTrackingBlastMode"), (int32)ETobiiTrackingBlastMode::OnlyTrackMissedObjects, TEXT("0 - Do not use tracking blasts. This will be very fast, but quite unstable. 1 - Only do tracking blasts when an object was not hit by a shotgun pellet to reduce hysteres somewhat. Prefer this mode if you have a lot of small objects constantly in the active set and mode two is working too slowly. 2 - Do tracking blasts for every focus component in the active group. This greatly helps reduce hysteres and leads to a LOT more stable last visible locations by trading off performance since it will require an additional extra line cast per active focus object and is such O(n)."));
static TAutoConsoleVariable<float> CVarOcclusionTesterTracesPerSecond(TEXT("tobii.gtom.OcclusionTesterTracesPerSecond"), 700.0f, TEXT("This is the approximate number of additional line casts (in addition to the center one) that will be performed per second. Setting this to zero will make the shotgun behave like a normal line scorer. A higher number will improve reliability at the cost of performance."));
static TAutoConsoleVariable<float> CVarOcclusionTesterTimeToLive(TEXT("tobii.gtom.OcclusionTesterTimeToLive"), 0.5f, TEXT("This is how long an object will be kept in the visibility set given it hasn't been detected. Having this too high can make the occlusion testing system output objects that are no longer visible. Having it too low may lead to hysteres."));
static TAutoConsoleVariable<int32> CVarDebugDisplayGTOMVisibilityRays(TEXT("tobii.debug.DisplayGTOMVisibilityRays"), 1, TEXT("1 means we visualize the rays from GTOM"));
g2om_gaze_ray RaysThisFrame[TOBII_MAX_RAYS_PER_FRAME];
FTobiiGTOMOcclusionTester::FTobiiGTOMOcclusionTester()
{
}
const TMap<FEngineFocusableUID, FTobiiGTOMOcclusionData>& FTobiiGTOMOcclusionTester::Tick(float DeltaTimeSecs, APlayerController* PlayerController, const FEyeTrackerGazeData& GazeData, g2om_gaze_data& G2OMGazeData, g2om_context* G2OMContext)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_TobiiEyetracking_GTOM_OcclusionTesting);
if (GEngine == nullptr
|| GEngine->GameViewport == nullptr
|| PlayerController == nullptr
|| PlayerController->GetWorld() == nullptr)
{
VisibleSet.Empty();
return VisibleSet;
}
static const auto FovealAngleDegCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.FovealConeAngleDegrees"));
const float FovealAngleDeg = FovealAngleDegCVar->GetFloat();
const ETobiiTrackingBlastMode TrackingBlastMode = (ETobiiTrackingBlastMode)CVarOcclusionTesterTrackingBlastMode.GetValueOnGameThread();
const int32 NrCastsThisFrame = FMath::Clamp(FMath::CeilToInt(CVarOcclusionTesterTracesPerSecond.GetValueOnGameThread() * DeltaTimeSecs), 3, TOBII_MAX_RAYS_PER_FRAME);
const FDateTime UtcNow = FDateTime::UtcNow();
FMemory::Memzero(RaysThisFrame, TOBII_MAX_RAYS_PER_FRAME * sizeof(g2om_gaze_ray));
g2om_get_candidate_search_pattern(G2OMContext, &G2OMGazeData, NrCastsThisFrame, RaysThisFrame);
FCollisionQueryParams CollisionQueryParams;
CollisionQueryParams.AddIgnoredActor(PlayerController);
CollisionQueryParams.AddIgnoredActor(PlayerController->GetPawn());
//Do shotgun blast
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_TobiiEyetracking_GTOM_OcclusionTesting_Shotgun);
for (int32 RayIdx = 0; RayIdx < NrCastsThisFrame; RayIdx++)
{
g2om_gaze_ray& G2OMRay = RaysThisFrame[RayIdx];
if (!G2OMRay.is_valid)
{
continue;
}
FVector CurrentDirection = FTobiiGTOMUtils::G2OMVectorToUE4Vector(G2OMRay.ray.direction);
if (CurrentDirection.Normalize())
{
TestRay(UtcNow, PlayerController, GazeData.GazeOrigin, CurrentDirection, CollisionQueryParams);
}
}
}
//Do tracking rays
if (TrackingBlastMode != ETobiiTrackingBlastMode::NoTrackingBlasts)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_TobiiEyetracking_GTOM_OcclusionTesting_TrackingBlasts);
//First we should check if our previous objects are still visible using so called "tracking blasts".
//This is both to provide more stable last known locations as well as to avoid hysteresis.
for (auto RecordIterator = VisibleSet.CreateIterator(); RecordIterator; ++RecordIterator)
{
const FTobiiGTOMOcclusionData& Record = RecordIterator.Value();
if (TrackingBlastMode == ETobiiTrackingBlastMode::OnlyTrackMissedObjects && Record.LastHitTime == UtcNow)
{
continue;
}
FVector TrackingBlastDirection = Record.LastKnownVisibleWorldSpaceLocation - GazeData.GazeOrigin;
if (TrackingBlastDirection.Normalize())
{
const float DotBetweenBlastDirAndGaze = FVector::DotProduct(GazeData.GazeDirection, TrackingBlastDirection);
const float AngleBetweenObjectAndGazeDeg = FMath::RadiansToDegrees(FMath::Acos(DotBetweenBlastDirAndGaze));
if (AngleBetweenObjectAndGazeDeg > FovealAngleDeg)
{
//We need to clamp the rotation amount.
FVector RotationAxis = FVector::CrossProduct(GazeData.GazeDirection, TrackingBlastDirection);
RotationAxis.Normalize();
TrackingBlastDirection = GazeData.GazeDirection.RotateAngleAxis(FovealAngleDeg, RotationAxis);
}
TestRay(UtcNow, PlayerController, GazeData.GazeOrigin, TrackingBlastDirection, CollisionQueryParams);
}
}
}
//Decay old objects
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_TobiiEyetracking_GTOM_OcclusionTesting_Decay);
TArray<FEngineFocusableUID> DecayedIds;
for (auto RecordIterator = VisibleSet.CreateIterator(); RecordIterator; ++RecordIterator)
{
const FTobiiGTOMOcclusionData& Record = RecordIterator.Value();
if ((UtcNow - Record.LastHitTime).GetTotalSeconds() > CVarOcclusionTesterTimeToLive.GetValueOnGameThread())
{
DecayedIds.Add(RecordIterator.Key());
}
}
for (FEngineFocusableUID DecayID : DecayedIds)
{
VisibleSet.Remove(DecayID);
}
}
return VisibleSet;
}
void FTobiiGTOMOcclusionTester::TestRay(const FDateTime& UtcNow, APlayerController* PlayerController, const FVector& Origin, const FVector& Direction, const FCollisionQueryParams& Params)
{
static const auto DrawDebugCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.debug"));
const float DebugBoxSize = 2.0f;
static const auto FocusTraceChannelCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.FocusTraceChannel"));
static const auto MaximumTraceDistanceCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.MaximumTraceDistance"));
const float MaxTraceDistance = MaximumTraceDistanceCVar->GetFloat();
const ECollisionChannel TraceChannel = (ECollisionChannel)FocusTraceChannelCVar->GetInt();
const FVector RayEndPoint = Origin + (Direction * MaxTraceDistance);
FHitResult RayHitResult;
if (PlayerController->GetWorld()->LineTraceSingleByChannel(RayHitResult, Origin, RayEndPoint, TraceChannel, Params))
{
if (DrawDebugCVar->GetInt() != 0 && CVarDebugDisplayGTOMVisibilityRays.GetValueOnGameThread())
{
DrawDebugBox(PlayerController->GetWorld(), RayHitResult.Location, FVector(DebugBoxSize, DebugBoxSize, DebugBoxSize), FColor::Green, false, 0.0f, 0, 2.0f);
}
if (UTobiiGazeFocusableComponent::IsPrimitiveFocusable(RayHitResult.GetComponent()))
{
//Range test
bool bIsInRange = true;
float MaxDistance;
const float DistanceToPrimitive = FVector::Distance(Origin, RayHitResult.Location);
if (UTobiiGazeFocusableComponent::GetMaxFocusDistanceForPrimitive(RayHitResult.GetComponent(), MaxDistance))
{
bIsInRange = DistanceToPrimitive <= MaxDistance;
}
if (bIsInRange)
{
FEngineFocusableUID Id = RayHitResult.GetComponent()->GetUniqueID();
if (VisibleSet.Contains(Id))
{
VisibleSet[Id].LastKnownVisibleWorldSpaceLocation = RayHitResult.Location;
VisibleSet[Id].LastHitTime = UtcNow;
}
else
{
FTobiiGTOMOcclusionData NewRecord;
NewRecord.ParentActor = RayHitResult.GetActor();
NewRecord.PrimitiveComponent = RayHitResult.GetComponent();
NewRecord.LastKnownVisibleWorldSpaceLocation = RayHitResult.Location;
NewRecord.LastHitTime = UtcNow;
if (NewRecord.ParentActor.IsValid())
{
NewRecord.FocusableComponent = (UTobiiGazeFocusableComponent*)NewRecord.ParentActor->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass());
}
VisibleSet.Add(Id, MoveTemp(NewRecord));
}
}
}
}
else if (DrawDebugCVar->GetInt() != 0 && CVarDebugDisplayGTOMVisibilityRays.GetValueOnGameThread())
{
DrawDebugBox(PlayerController->GetWorld(), RayEndPoint, FVector(DebugBoxSize, DebugBoxSize, DebugBoxSize), FColor::Red, false, 0.0f, 0, 2.0f);
}
}
#endif //TOBII_EYETRACKING_ACTIVE

View File

@@ -0,0 +1,50 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#if TOBII_EYETRACKING_ACTIVE
#include "TobiiGazeFocusableComponent.h"
#include "tobii_g2om.h"
#include "CoreMinimal.h"
#include "IEyeTracker.h"
#include "GameFramework/PlayerController.h"
enum class ETobiiTrackingBlastMode : uint8
{
NoTrackingBlasts = 0
, OnlyTrackMissedObjects = 1
, TrackAllObjects = 2
};
struct FTobiiGTOMOcclusionData
{
public:
TWeakObjectPtr<AActor> ParentActor;
TWeakObjectPtr<UPrimitiveComponent> PrimitiveComponent;
TWeakObjectPtr<UTobiiGazeFocusableComponent> FocusableComponent;
FVector LastKnownVisibleWorldSpaceLocation;
FDateTime LastHitTime;
};
class FTobiiGTOMOcclusionTester
{
public:
FTobiiGTOMOcclusionTester();
virtual ~FTobiiGTOMOcclusionTester() {}
//Returns the visible set
const TMap<FEngineFocusableUID, FTobiiGTOMOcclusionData>& Tick(float DeltaTimeSecs, APlayerController* PlayerController, const FEyeTrackerGazeData& GazeData, g2om_gaze_data& G2OMGazeData, g2om_context* G2OMContext);
private:
TMap<FEngineFocusableUID, FTobiiGTOMOcclusionData> VisibleSet;
void TestRay(const FDateTime& UtcNow, APlayerController* PlayerController, const FVector& Origin, const FVector& Direction, const FCollisionQueryParams& Params);
};
#endif //TOBII_EYETRACKING_ACTIVE

View File

@@ -0,0 +1,20 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "TobiiGTOMTypes.h"
FName FTobiiPrimitiveComponentGazeFocusTags::HasGazeFocusTag("HasGazeFocus");
FName FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableTag("GazeFocusable");
FName FTobiiPrimitiveComponentGazeFocusTags::NotGazeFocusableTag("NotGazeFocusable");
FString FTobiiPrimitiveComponentGazeFocusTags::GazeFocusablePriorityTag("GazeFocusPriority");
FString FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableMaximumDistanceTag("GazeFocusMaxDistance");
FString FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableLayerTag("GazeFocusLayer");
FString FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetXTag("PrimitiveFocusOffsetX");
FString FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetYTag("PrimitiveFocusOffsetY");
FString FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetZTag("PrimitiveFocusOffsetZ");

View File

@@ -0,0 +1,137 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#include "TobiiGazeFocusManagerComponent.h"
#include "TobiiGazeFocusableComponent.h"
#include "TobiiGTOMModule.h"
#include "TobiiGTOMBlueprintLibrary.h"
#include "DrawDebugHelpers.h"
#include "Components/PrimitiveComponent.h"
#include "Engine/Engine.h"
#include "HAL/IConsoleManager.h"
UTobiiGazeFocusManagerComponent::UTobiiGazeFocusManagerComponent()
: bWantPrimitives(true)
, bWantWidgets(true)
, bIsWhiteList(false)
, FocusLayerFilters()
, PreviouslyFocusedPrimitiveComponent()
{
PrimaryComponentTick.bCanEverTick = true;
ResetFocusData();
}
void UTobiiGazeFocusManagerComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
ResetFocusData();
UTobiiGTOMBlueprintLibrary::GetAllFilteredGazeFocusData(FocusLayerFilters, bIsWhiteList, bWantPrimitives, bWantWidgets, AllFocusData);
for (const FTobiiGazeFocusData& FocusData : AllFocusData)
{
if (bWantPrimitives
&& !BestPrimitiveComponentFocusData.FocusedPrimitiveComponent.IsValid()
&& (FocusData.FocusedPrimitiveComponent.IsValid() && !FocusData.FocusedWidget.IsValid()))
{
BestPrimitiveComponentFocusData = FocusData;
}
if (bWantWidgets
&& !BestWidgetFocusData.FocusedWidget.IsValid()
&& FocusData.FocusedWidget.IsValid())
{
BestWidgetFocusData = FocusData;
}
if((!bWantPrimitives || (BestPrimitiveComponentFocusData.FocusedPrimitiveComponent.IsValid() && !BestPrimitiveComponentFocusData.FocusedWidget.IsValid()))
&& (!bWantWidgets || BestWidgetFocusData.FocusedWidget.IsValid()))
{
//We found the best candidates we were after
break;
}
}
//Primitive components
if(bWantPrimitives)
{
TWeakObjectPtr<UPrimitiveComponent> NewTopFocusPrimitive = BestPrimitiveComponentFocusData.FocusedPrimitiveComponent;
if (!NewTopFocusPrimitive.IsValid() && PreviouslyFocusedPrimitiveComponent.IsValid())
{
//We lost focus completely
OnPrimitiveLostGazeFocus.Broadcast(PreviouslyFocusedPrimitiveComponent.Get());
}
else if (NewTopFocusPrimitive.IsValid() && !PreviouslyFocusedPrimitiveComponent.IsValid())
{
//We gained focus from no focus
OnPrimitiveReceivedGazeFocus.Broadcast(NewTopFocusPrimitive.Get());
}
else if (NewTopFocusPrimitive.IsValid() && PreviouslyFocusedPrimitiveComponent.IsValid() && NewTopFocusPrimitive.Get() != PreviouslyFocusedPrimitiveComponent.Get())
{
//Focus shifted from one object to another
OnPrimitiveLostGazeFocus.Broadcast(PreviouslyFocusedPrimitiveComponent.Get());
OnPrimitiveReceivedGazeFocus.Broadcast(NewTopFocusPrimitive.Get());
}
PreviouslyFocusedPrimitiveComponent = NewTopFocusPrimitive;
}
//Widgets
if(bWantWidgets)
{
TWeakObjectPtr<UTobiiGazeFocusableWidget> NewTopFocusWidget = BestPrimitiveComponentFocusData.FocusedWidget;
if (!NewTopFocusWidget.IsValid() && PreviouslyFocusedWidget.IsValid())
{
//We lost focus completely
OnWidgetLostGazeFocus.Broadcast(PreviouslyFocusedWidget.Get());
}
else if (NewTopFocusWidget.IsValid() && !PreviouslyFocusedWidget.IsValid())
{
//We gained focus from no focus
OnWidgetReceivedGazeFocus.Broadcast(NewTopFocusWidget.Get());
}
else if (NewTopFocusWidget.IsValid() && PreviouslyFocusedWidget.IsValid() && NewTopFocusWidget.Get() != PreviouslyFocusedWidget.Get())
{
//Focus shifted from one object to another
OnWidgetLostGazeFocus.Broadcast(PreviouslyFocusedWidget.Get());
OnWidgetReceivedGazeFocus.Broadcast(NewTopFocusWidget.Get());
}
PreviouslyFocusedWidget = NewTopFocusWidget;
}
}
void UTobiiGazeFocusManagerComponent::ResetFocusData()
{
AllFocusData.Empty();
BestPrimitiveComponentFocusData.FocusedPrimitiveComponent.Reset();
BestPrimitiveComponentFocusData.FocusedActor.Reset();
BestPrimitiveComponentFocusData.FocusedWidget.Reset();
BestWidgetFocusData.FocusedPrimitiveComponent.Reset();
BestWidgetFocusData.FocusedActor.Reset();
BestWidgetFocusData.FocusedWidget.Reset();
}
void UTobiiGazeFocusManagerComponent::GetBestFilteredPrimitiveComponentFocusData(FTobiiGazeFocusData& OutFocusData) const
{
OutFocusData = BestPrimitiveComponentFocusData;
}
void UTobiiGazeFocusManagerComponent::GetBestFilteredWidgetComponentFocusData(FTobiiGazeFocusData& OutFocusData) const
{
OutFocusData = BestWidgetFocusData;
}
void UTobiiGazeFocusManagerComponent::GetAllFilteredFocusData(TArray<FTobiiGazeFocusData>& OutFocusData) const
{
OutFocusData = AllFocusData;
}

View File

@@ -0,0 +1,504 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#include "TobiiGazeFocusableComponent.h"
#include "TobiiGTOMBlueprintLibrary.h"
#include "TobiiGTOMModule.h"
#include "DrawDebugHelpers.h"
#include "Engine/Engine.h"
#include "Components/PrimitiveComponent.h"
#include "Components/WidgetComponent.h"
#include "GameFramework/PlayerController.h"
#include "HAL/IConsoleManager.h"
static TAutoConsoleVariable<int32> CVarDrawCleanUIDebug(TEXT("tobii.debug.CleanUI"), 1, TEXT("0 - CleanUI debug visualizations are not displayed. 1 - CleanUI debug visualizations are displayed."));
static TMap<FTobiiFocusableUID, TWeakObjectPtr<UTobiiGazeFocusableComponent>> GRegisteredTobiiFocusableComponents;
static TSet<FPrimitiveComponentId>* GazeFocusPrioSetA = nullptr;
static TSet<FPrimitiveComponentId>* GazeFocusPrioSetB = nullptr;
static bool bUseGazeFocusPrioSetA = true;
void UTobiiGazeFocusableComponent::ClearFocusableComponents()
{
GRegisteredTobiiFocusableComponents.Empty();
}
const TMap<FTobiiFocusableUID, TWeakObjectPtr<UTobiiGazeFocusableComponent>>& UTobiiGazeFocusableComponent::GetFocusableComponents()
{
return GRegisteredTobiiFocusableComponents;
}
void UTobiiGazeFocusableComponent::UpdateGazeFocusPrio(FVector POVActorLocation, int32 MaxNrFocusables)
{
TMap<FPrimitiveComponentId, float> PrioMap;
TSet<FPrimitiveComponentId> NonPrioSet;
//Determine which primitive components should be considered for gaze focus.
for (auto& FocusableElement : GRegisteredTobiiFocusableComponents)
{
if (FocusableElement.Value.IsValid())
{
const UTobiiGazeFocusableComponent* FocusableComponent = FocusableElement.Value.Get();
const AActor* FocusableActor = FocusableComponent->GetOwner();
if (FocusableActor != nullptr)
{
TArray<UActorComponent*> PrimitiveComponents = FocusableActor->GetComponentsByClass(UPrimitiveComponent::StaticClass());
//TArray<UActorComponent*> PrimativeComponents = FocusableActor->GetComponents
for (UActorComponent* Comp : PrimitiveComponents)
{
UPrimitiveComponent* PrimitiveComponent = (UPrimitiveComponent*)Comp;
if (PrimitiveComponent == nullptr)
{
continue;
}
bool bSkipComponent = false;
float MaxDistance = 0.0f;
float Priority = 0.0f;
for (const FName& CurrentTag : PrimitiveComponent->ComponentTags)
{
if (CurrentTag == FTobiiPrimitiveComponentGazeFocusTags::NotGazeFocusableTag)
{
bSkipComponent = true;
break;
}
FString CurrentTagString = CurrentTag.ToString();
if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableMaximumDistanceTag))
{
FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableMaximumDistanceTag.Len());
MaxDistance = FCString::Atof(*Argument);
}
if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusablePriorityTag))
{
FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusablePriorityTag.Len());
Priority = FCString::Atof(*Argument);
}
}
if (bSkipComponent)
{
continue;
}
if (MaxDistance == 0.0f)
{
MaxDistance = FocusableComponent->DefaultMaxFocusDistance;
}
if (Priority == 0.0f)
{
Priority = FocusableComponent->DefaultFocusPriority;
}
bool bAcceptComponent = true;
if (MaxDistance != 0.0f)
{
float Distance = FVector::Distance(POVActorLocation, PrimitiveComponent->GetComponentLocation());
if (Distance > MaxDistance)
{
bAcceptComponent = false;
}
}
if (bAcceptComponent)
{
if (Priority == 0.0f)
{
NonPrioSet.Add(PrimitiveComponent->ComponentId);
}
else
{
PrioMap.Add(PrimitiveComponent->ComponentId, Priority);
}
}
}
}
}
}
PrioMap.ValueSort(TGreater<float>());
TSet<FPrimitiveComponentId>* NewGazeFocusPrioSet = new TSet<FPrimitiveComponentId>();
MaxNrFocusables -= 1; //Since we test before add, take that into account
for (auto& SortedElement : PrioMap)
{
if (NewGazeFocusPrioSet->Num() >= MaxNrFocusables)
{
break;
}
NewGazeFocusPrioSet->Add(SortedElement.Key);
}
for (auto NonPrioElement : NonPrioSet)
{
if (NewGazeFocusPrioSet->Num() >= MaxNrFocusables)
{
break;
}
NewGazeFocusPrioSet->Add(NonPrioElement);
}
//Destroy old set, set our new one and flip our record pointer so the one we swapped into becomes the active one.
TSet<FPrimitiveComponentId>*& ListPtr = bUseGazeFocusPrioSetA ? GazeFocusPrioSetB : GazeFocusPrioSetA;
if (ListPtr != nullptr)
{
delete ListPtr;
}
ListPtr = NewGazeFocusPrioSet;
bUseGazeFocusPrioSetA = !bUseGazeFocusPrioSetA;
}
const TSet<FPrimitiveComponentId>* UTobiiGazeFocusableComponent::GetGazeFocusPrioSet()
{
return bUseGazeFocusPrioSetA ? GazeFocusPrioSetA : GazeFocusPrioSetB;
}
bool UTobiiGazeFocusableComponent::IsPrimitiveFocusable(UPrimitiveComponent* Primitive)
{
if (Primitive == nullptr || !Primitive->IsVisible() || Primitive->ComponentHasTag(FTobiiPrimitiveComponentGazeFocusTags::NotGazeFocusableTag))
{
//Override disabled
return false;
}
else if (Primitive->ComponentHasTag(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableTag))
{
//Override enabled
return true;
}
else if (Primitive->GetOwner())
{
UTobiiGazeFocusableComponent* GazeFocusable = (UTobiiGazeFocusableComponent*)Primitive->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass());
if (GazeFocusable != nullptr && GazeFocusable->bDefaultIsFocusable)
{
return true;
}
}
return false;
}
bool UTobiiGazeFocusableComponent::GetMaxFocusDistanceForPrimitive(UPrimitiveComponent* Primitive, float& OutMaxDistance)
{
if (Primitive == nullptr)
{
return false;
}
bool bMaxDistanceFound = false;
for (const FName& CurrentTag : Primitive->ComponentTags)
{
FString CurrentTagString = CurrentTag.ToString();
if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableMaximumDistanceTag))
{
FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableMaximumDistanceTag.Len());
OutMaxDistance = FCString::Atof(*Argument);
bMaxDistanceFound = OutMaxDistance != 0.0f; //Atof returns 0.0 on failure
}
}
if (!bMaxDistanceFound && Primitive->GetOwner())
{
UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)Primitive->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass());
if (GazeFocusableComponent != nullptr && GazeFocusableComponent->DefaultMaxFocusDistance > FLT_EPSILON)
{
OutMaxDistance = GazeFocusableComponent->DefaultMaxFocusDistance;
bMaxDistanceFound = true;
}
}
return bMaxDistanceFound;
}
float UTobiiGazeFocusableComponent::GetFocusPriorityForPrimitive(UPrimitiveComponent* Primitive)
{
float Priority = 0.0f;
bool bPriorityFound = false;
if (Primitive == nullptr)
{
return Priority;
}
for (const FName& CurrentTag : Primitive->ComponentTags)
{
FString CurrentTagString = CurrentTag.ToString();
if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusablePriorityTag))
{
FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusablePriorityTag.Len());
Priority = FCString::Atof(*Argument);
bPriorityFound = Priority != 0.0f; //Atof returns 0.0 on failure
}
}
if (!bPriorityFound && Primitive->GetOwner())
{
UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)Primitive->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass());
if (GazeFocusableComponent != nullptr)
{
Priority = GazeFocusableComponent->DefaultFocusPriority;
}
}
return Priority;
}
FName UTobiiGazeFocusableComponent::GetFocusLayerForPrimitive(UPrimitiveComponent* Primitive)
{
FName FocusLayer = FName("Default");
bool bFocusLayerFound = false;
if (Primitive == nullptr)
{
return FocusLayer;
}
for (const FName& CurrentTag : Primitive->ComponentTags)
{
FString CurrentTagString = CurrentTag.ToString();
if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableLayerTag))
{
FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableLayerTag.Len());
Argument.TrimStartAndEndInline();
if (!Argument.IsEmpty())
{
FocusLayer = FName(*Argument);
bFocusLayerFound = true;
}
}
}
if (!bFocusLayerFound && Primitive->GetOwner())
{
UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)Primitive->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass());
if (GazeFocusableComponent != nullptr)
{
FocusLayer = GazeFocusableComponent->DefaultFocusLayer;
}
}
return FocusLayer;
}
bool UTobiiGazeFocusableComponent::IsWidgetFocusable(UTobiiGazeFocusableWidget* GazeFocusableWidget)
{
if (GazeFocusableWidget == nullptr || !GazeFocusableWidget->IsVisible() || !GazeFocusableWidget->bIsGazeFocusable)
{
return false;
}
else if (GazeFocusableWidget->GetWorldSpaceHostWidgetComponent() != nullptr && GazeFocusableWidget->GetWorldSpaceHostWidgetComponent()->GetOwner() != nullptr)
{
if (!GazeFocusableWidget->GetWorldSpaceHostWidgetComponent()->IsVisible())
{
return false;
}
UTobiiGazeFocusableComponent* GazeFocusable = (UTobiiGazeFocusableComponent*)GazeFocusableWidget->GetWorldSpaceHostWidgetComponent()->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass());
if (GazeFocusable != nullptr)
{
return GazeFocusable->bDefaultIsFocusable;
}
}
return true;
}
bool UTobiiGazeFocusableComponent::GetMaxFocusDistanceForWidget(UTobiiGazeFocusableWidget* GazeFocusableWidget, float& OutMaxDistance)
{
if (GazeFocusableWidget == nullptr)
{
return false;
}
bool bMaxDistanceFound = false;
if (GazeFocusableWidget->MaxFocusDistance >= FLT_EPSILON)
{
OutMaxDistance = GazeFocusableWidget->MaxFocusDistance;
bMaxDistanceFound = true;
}
else if (GazeFocusableWidget->GetWorldSpaceHostWidgetComponent() != nullptr && GazeFocusableWidget->GetWorldSpaceHostWidgetComponent()->GetOwner() != nullptr)
{
UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)GazeFocusableWidget->GetWorldSpaceHostWidgetComponent()->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass());
if (GazeFocusableComponent != nullptr && GazeFocusableComponent->DefaultMaxFocusDistance > FLT_EPSILON)
{
OutMaxDistance = GazeFocusableComponent->DefaultMaxFocusDistance;
bMaxDistanceFound = true;
}
}
return bMaxDistanceFound;
}
float UTobiiGazeFocusableComponent::GetFocusPriorityForWidget(UTobiiGazeFocusableWidget* GazeFocusableWidget)
{
if (GazeFocusableWidget == nullptr)
{
return 0.0f;
}
if (GazeFocusableWidget->GazeFocusPriority >= 0.0f)
{
return GazeFocusableWidget->GazeFocusPriority;
}
else if (GazeFocusableWidget->GetWorldSpaceHostWidgetComponent() != nullptr && GazeFocusableWidget->GetWorldSpaceHostWidgetComponent()->GetOwner() != nullptr)
{
UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)GazeFocusableWidget->GetWorldSpaceHostWidgetComponent()->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass());
if (GazeFocusableComponent)
{
return GazeFocusableComponent->DefaultFocusPriority;
}
}
return 0.0f;
}
FName UTobiiGazeFocusableComponent::GetFocusLayerForWidget(UTobiiGazeFocusableWidget* GazeFocusableWidget)
{
FName FocusLayer = FName("Default");
if (GazeFocusableWidget == nullptr)
{
return FocusLayer;
}
if (!GazeFocusableWidget->FocusLayer.IsEmpty())
{
FocusLayer = FName(*GazeFocusableWidget->FocusLayer);
}
else if (GazeFocusableWidget->GetWorldSpaceHostWidgetComponent() != nullptr && GazeFocusableWidget->GetWorldSpaceHostWidgetComponent()->GetOwner() != nullptr)
{
UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)GazeFocusableWidget->GetWorldSpaceHostWidgetComponent()->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass());
if (GazeFocusableComponent != nullptr)
{
FocusLayer = GazeFocusableComponent->DefaultFocusLayer;
}
}
return FocusLayer;
}
//////////////////////////////////////////////
UTobiiGazeFocusableComponent::UTobiiGazeFocusableComponent()
: bDefaultIsFocusable(true)
, DefaultFocusPriority(0.0f)
, DefaultMaxFocusDistance(0.0f)
, DefaultFocusLayer("Default")
, bWidgetsRefreshedOnce(false)
{
PrimaryComponentTick.bCanEverTick = true;
}
void UTobiiGazeFocusableComponent::PrimitiveReceivedGazeFocus(UPrimitiveComponent* FocusedComponent)
{
OnPrimitiveReceivedGazeFocus.Broadcast(FocusedComponent);
}
void UTobiiGazeFocusableComponent::PrimitiveLostGazeFocus(UPrimitiveComponent* FocusedComponent)
{
OnPrimitiveLostGazeFocus.Broadcast(FocusedComponent);
}
void UTobiiGazeFocusableComponent::WidgetReceivedGazeFocus( UTobiiGazeFocusableWidget* FocusedWidget)
{
OnWidgetReceivedGazeFocus.Broadcast(FocusedWidget);
}
void UTobiiGazeFocusableComponent::WidgetLostGazeFocus(UTobiiGazeFocusableWidget* FocusedWidget)
{
OnWidgetLostGazeFocus.Broadcast(FocusedWidget);
}
const TArray<TWeakObjectPtr<UTobiiGazeFocusableWidget>>& UTobiiGazeFocusableComponent::GetFocusableWidgetsForPrimitiveComponent(UPrimitiveComponent* PrimitiveComponent)
{
if (PrimitiveComponent == nullptr || !PrimitiveMap.Contains(PrimitiveComponent->GetUniqueID()))
{
static TArray<TWeakObjectPtr<UTobiiGazeFocusableWidget>> DefaultArray;
return DefaultArray;
}
return PrimitiveMap[PrimitiveComponent->GetUniqueID()];
}
void UTobiiGazeFocusableComponent::BeginPlay()
{
Super::BeginPlay();
bWidgetsRefreshedOnce = false;
GRegisteredTobiiFocusableComponents.Add(GetUniqueID(), this);
}
void UTobiiGazeFocusableComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
GRegisteredTobiiFocusableComponents.Remove(GetUniqueID());
Super::EndPlay(EndPlayReason);
}
void UTobiiGazeFocusableComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (GEngine == nullptr
|| GEngine->GameViewport == nullptr
|| GEngine->GameViewport->GetGameViewport() == nullptr)
{
return;
}
if (!bWidgetsRefreshedOnce)
{
bWidgetsRefreshedOnce = true;
RefreshOwnedWidgets();
}
}
void UTobiiGazeFocusableComponent::RefreshOwnedWidgets()
{
AllFocusableWidgets.Empty();
PrimitiveMap.Empty();
AActor* Owner = GetOwner();
if (Owner != nullptr)
{
TArray<UActorComponent*> WidgetComponents = Owner->GetComponentsByClass(UWidgetComponent::StaticClass());
for (UActorComponent* Component : WidgetComponents)
{
UWidgetComponent* WidgetComponent = (UWidgetComponent*)Component;
TArray<TWeakObjectPtr<UTobiiGazeFocusableWidget>>& NewFocusableArray = PrimitiveMap.Add(WidgetComponent->GetUniqueID(), TArray<TWeakObjectPtr<UTobiiGazeFocusableWidget>>());
GatherGazeFocusableWidgets(WidgetComponent->GetUserWidgetObject(), NewFocusableArray, WidgetComponent);
}
}
}
void UTobiiGazeFocusableComponent::GatherGazeFocusableWidgets(UWidget* Parent, TArray<TWeakObjectPtr<UTobiiGazeFocusableWidget>>& WidgetArray, UWidgetComponent* OptionalHostWidgetComponent)
{
UUserWidget* UserWidget = Cast<UUserWidget>(Parent);
if (UserWidget != nullptr)
{
GatherGazeFocusableWidgets(UserWidget->GetRootWidget(), WidgetArray, OptionalHostWidgetComponent);
}
else
{
UTobiiGazeFocusableWidget* CleanUIContainer = Cast<UTobiiGazeFocusableWidget>(Parent);
if (CleanUIContainer != nullptr)
{
CleanUIContainer->RegisterWidgetToGTOM(OptionalHostWidgetComponent);
AllFocusableWidgets.Add(CleanUIContainer->GetUniqueID(), CleanUIContainer);
WidgetArray.Add(CleanUIContainer);
}
UPanelWidget* Panel = Cast<UPanelWidget>(Parent);
if (Panel != nullptr)
{
for (int32 ChildIdx = 0; ChildIdx < Panel->GetChildrenCount(); ChildIdx++)
{
GatherGazeFocusableWidgets(Panel->GetChildAt(ChildIdx), WidgetArray, OptionalHostWidgetComponent);
}
}
}
}

View File

@@ -0,0 +1,90 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "Slate/SRetainerWidget.h"
#include "TobiiGTOMTypes.h"
#include "STobiiRadialMenuWidget.h"
#include "CoreMinimal.h"
#include "DisplayDebugHelpers.h"
#include "Widgets/Layout/SBox.h"
class AHUD;
class UCanvas;
class UTobiiGazeFocusableWidget;
//This is the underlying slate widget.
//WARNING: Please note that we do not support using these directly yet.
class TOBIIGTOM_API STobiiGazeFocusableWidget : public SBox
{
public:
SLATE_BEGIN_ARGS(STobiiGazeFocusableWidget)
{
_Visibility = EVisibility::SelfHitTestInvisible;
}
SLATE_EVENT(FSimpleDelegate, OnHovered)
SLATE_EVENT(FSimpleDelegate, OnUnhovered)
/************************************************************************/
/* From SBox widget */
/************************************************************************/
//Horizontal alignment of content in the area allotted to the SBox by its parent
SLATE_ARGUMENT(EHorizontalAlignment, HAlign)
//Vertical alignment of content in the area allotted to the SBox by its parent
SLATE_ARGUMENT(EVerticalAlignment, VAlign)
//Padding between the SBox and the content that it presents. Padding affects desired size.
SLATE_ATTRIBUTE(FMargin, Padding)
//The widget content presented by the SBox
SLATE_DEFAULT_SLOT(FArguments, Content)
SLATE_END_ARGS()
/************************************************************************/
/* Tobii */
/************************************************************************/
public:
TWeakObjectPtr<UTobiiGazeFocusableWidget> UMGWidget;
STobiiGazeFocusableWidget();
void Construct(const FArguments& InArgs);
float GetCleanUIAlpha() { return CleanUIAlpha; }
bool IsHoveredByPointer() { return bIsHovered; }
bool HitByGaze();
void AddExtraCleanUIContainerToPollHitsFrom(STobiiGazeFocusableWidget* CleanUIContainerToPoll, bool PollGazeHits = true, bool PollMouseHits = true);
void RemoveExtraCleanUIContainerToPollHitsFrom(STobiiGazeFocusableWidget* CleanUIContainerToStopPolling);
static STobiiGazeFocusableWidget* TryCastToTobiiGazeFocusable(SWidget* Widget);
/************************************************************************/
/* SBox */
/************************************************************************/
public:
FSimpleDelegate OnHovered;
FSimpleDelegate OnUnhovered;
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override;
virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override;
virtual void OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
virtual void OnMouseLeave(const FPointerEvent& MouseEvent) override;
private:
struct CleanUIPollingInfo
{
TWeakPtr<SWidget> CleanUIToPollFrom;
bool PollGaze;
bool PollPointer;
};
float CleanUIAlpha;
TArray<CleanUIPollingInfo> CleanUIContainersToPollHitsFrom;
};

View File

@@ -0,0 +1,159 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "CoreMinimal.h"
#include "DisplayDebugHelpers.h"
#include "Slate/SRetainerWidget.h"
#include "Widgets/Layout/SBox.h"
class FArrangedChildren;
class FPaintArgs;
class FSlateWindowElementList;
struct FTobiiRadialMenuPanelRenderData
{
TArray<FSlateVertex> PanelVertexData;
TArray<SlateIndex> PanelIndexData;
TArray<FSlateVertex> BorderVertexData; //This also contains the PanelVertexData in the end.
TArray<SlateIndex> BorderIndexData;
};
//This is the underlying slate widget.
class TOBIIGTOM_API STobiiRadialMenuWidget : public SPanel
{
public:
class FSlot : public TSlotBase<FSlot>
{
public:
FSlot& Offset(const TAttribute<FVector2D>& InOffset)
{
OffsetAttr = InOffset;
return *this;
}
FSlot& Scale(const TAttribute<float>& InScale)
{
ScaleAttr = InScale;
return *this;
}
FSlot& Alpha(const TAttribute<float>& InAlpha)
{
AlphaAttr = InAlpha;
return *this;
}
/** Offset */
TAttribute<FVector2D> OffsetAttr;
/** Scale */
TAttribute<float> ScaleAttr;
/** Alpha */
TAttribute<float> AlphaAttr;
/** Default values for a slot. */
FSlot()
: TSlotBase<FSlot>()
, OffsetAttr(FVector2D(0.0f, 0.0f))
, ScaleAttr(1.0f)
, AlphaAttr(1.0f)
{ }
};
SLATE_BEGIN_ARGS(STobiiRadialMenuWidget)
: _UseHardBorder(true)
, _BorderColor(FColor(230, 230, 230))
, _PanelColor(FColor(70, 70, 70))
, _BorderThicknessPx(5.0f)
, _SegmentSeparationPx(10.0f)
, _AngularDisplacementDeg(0.0f)
, _RadiusPx(500.0f)
, _VertexCount(360)
{
_Visibility = EVisibility::SelfHitTestInvisible;
}
SLATE_SUPPORTS_SLOT(STobiiRadialMenuWidget::FSlot)
SLATE_ARGUMENT(bool, UseHardBorder)
SLATE_ARGUMENT(FColor, BorderColor)
SLATE_ARGUMENT(FColor, PanelColor)
SLATE_ARGUMENT(float, BorderThicknessPx)
SLATE_ARGUMENT(float, SegmentSeparationPx)
SLATE_ARGUMENT(float, AngularDisplacementDeg)
SLATE_ARGUMENT(float, RadiusPx)
SLATE_ARGUMENT(int32, VertexCount)
SLATE_END_ARGS()
STobiiRadialMenuWidget();
void Construct(const FArguments& InArgs);
static FSlot& Slot()
{
return *(new FSlot());
}
/**
* Adds a content slot.
*
* @return The added slot.
*/
FSlot& AddSlot()
{
Invalidate(EInvalidateWidget::Layout);
STobiiRadialMenuWidget::FSlot& NewSlot = *(new FSlot());
this->Children.Add(&NewSlot);
return NewSlot;
}
/**
* Removes a particular content slot.
*
* @param SlotWidget The widget in the slot to remove.
*/
int32 RemoveSlot(const TSharedRef<SWidget>& SlotWidget);
/**
* Removes all slots from the panel.
*/
void ClearChildren();
public:
// Begin SWidget overrides
virtual void OnArrangeChildren(const FGeometry& AllottedGeometry, FArrangedChildren& ArrangedChildren) const override;
virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override;
virtual FChildren* GetChildren() override;
// End SWidget overrides
protected:
// Begin SWidget overrides.
virtual FVector2D ComputeDesiredSize(float LayoutScaleMultiplier) const override;
// End SWidget overrides.
protected:
/** The ConstraintCanvas widget's children. */
TPanelChildren<FSlot> Children;
public:
FSlateResourceHandle BrushResourceHandle;
bool bUseHardBorder;
FColor BorderColor;
FColor PanelColor;
float BorderThicknessPx;
float SegmentSeparationPx;
float AngularDisplacementDeg;
float RadiusPx;
int32 VertexCount;
private:
void GeneratePanelRenderData(const FGeometry& AllottedGeometry, int32 NrChildren, TArray<FTobiiRadialMenuPanelRenderData>& OutChildRenderData) const;
};

View File

@@ -0,0 +1,167 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "TobiiGTOMTypes.h"
#include "STobiiGazeFocusableWidget.h"
#include "CoreMinimal.h"
#include "ObjectEditorUtils.h"
#include "Components/WidgetComponent.h"
#include "Components/SizeBox.h"
#include "TobiiGazeFocusableWidget.generated.h"
class UPrimitiveComponent;
class UTobiiGazeFocusManagerComponent;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FWidgetGazeFocusSignature, UTobiiGazeFocusableWidget*, FocusedWidget);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FWidgetHoverSignature, UTobiiGazeFocusableWidget*, FocusedWidget);
/**
* Use this widget to wrap other widgets you want to mark as gaze focusable.
* In contrast to gaze focusable components, each widget you want to make focusable needs to be wrapped by its own UTobiiGazeFocusableWidget. There is more info below on why this is the case.
* Please note that while this wraps a raw slate widget, we currently do not officially support using the slate widgets directly. Please use the UMG wrapper.
*
* There are some major differences to how gaze focusable widgets versus components work:
* 1. Since widgets don't support tags, widgets can only participate in GTOM by being wrapped by this widget.
* 2. Actor component hierarchies can be very rigid since they can be partially constructed in cpp, both by the developer and the engine. This means that designating gaze focusability via component parenting wouldn't be reliable.
* 3. It is very common to want to make it so that every primitive component on an actor should contribute to focusing that actor. This is not true for widgets, here it is more common that each individual widget wants to know if it has been individually focused.
*
* All of these things should help to illustrate why using different models for gaze focus in slate/UMG and actors is to be preferred.
*/
UCLASS(BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent, DisplayName= "Tobii Gaze Focusable"))
class TOBIIGTOM_API UTobiiGazeFocusableWidget : public USizeBox
{
GENERATED_UCLASS_BODY()
/************************************************************************/
/* USizeBox */
/************************************************************************/
public:
// UVisual interface
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
protected:
// UWidget interface
virtual TSharedRef<SWidget> RebuildWidget() override;
public:
//These are for pointer hovering
UPROPERTY(BlueprintAssignable, Category = "Event")
FWidgetHoverSignature OnHovered;
UPROPERTY(BlueprintAssignable, Category = "Event")
FWidgetHoverSignature OnUnhovered;
//This function must be called to make the widget participate in GTOM.
//WARNING: For screen space widgets, you must do this manually by sending in a nullptr HostWidget! For worldspace widgets, the UTobiiGazeFocusableComponent will do this for you though.
UFUNCTION(BlueprintCallable, Category = "Gaze Focus")
void RegisterWidgetToGTOM(UWidgetComponent* HostWidget);
UFUNCTION(BlueprintPure, Category = "General")
UWidgetComponent* GetWorldSpaceHostWidgetComponent();
UFUNCTION(BlueprintPure, Category = "General")
bool IsHoveredByPointer();
UFUNCTION(BlueprintPure, Category = "General")
bool IsWorldSpaceWidget();
/************************************************************************/
/* Tobii */
/************************************************************************/
public:
//This will be true if this is the most likely widget to currently have focus irregardless of layer filters.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Gaze Focus")
bool bHasGazeFocus;
//Set this to false if you want to stop this widget participating in GTOM.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus")
bool bIsGazeFocusable;
//The gaze focus priority is optional input that you can provide to GTOM. Objects with higher focus priority are more "important" and are therefore more likely to be construed as being in focus. If this is negative, it will not be enforced.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus")
float GazeFocusPriority;
//If this object is at least this far away from the player, it will not participate in GTOM. If this is less or equal to zero, it will not be enforced.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus")
float MaxFocusDistance;
//A focus manager can opt to only query a subset of focusables by using a focus layer. If this is empty, the default layer will be used (Either from a gaze focusable component, or the global default).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus")
FString FocusLayer;
//This delegate will fire when this widget receives user focus
UPROPERTY(BlueprintAssignable, Category = "Gaze Focus|Event")
FWidgetGazeFocusSignature ReceivedGazeFocus;
//This delegate will fire when this widget loses user focus
UPROPERTY(BlueprintAssignable, Category = "Gaze Focus|Event")
FWidgetGazeFocusSignature LostGazeFocus;
//This will return true if this widget has user focus
UFUNCTION(BlueprintPure, Category = "Gaze Focus")
bool HasFocus();
//This will return true if this widget is close enough to the user's gaze to be considered in focus, even though it might not be the object the user is actively focusing on.
UFUNCTION(BlueprintPure, Category = "Gaze Focus")
bool IsInFocusCollection();
public:
//How should we apply CleanUI?
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "CleanUI")
ETobiiCleanUIMode CleanUIMode;
//If this is larger than 0, CleanUI is currently being suppressed.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "CleanUI")
float TimeCleanUIIsSuppressedForSecs;
//If you want this cleanUI panel to behave differently from the default governed by CVars you can override properties here. Any negative value will result in the CVar value being used.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "CleanUI")
float CleanUIFadeInTimeSecsOverride;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "CleanUI")
float CleanUIFadeOutTimeSecsOverride;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "CleanUI")
float CleanUIMinAlphaOverride;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "CleanUI")
float CleanUIMaxAlphaOverride;
//If you have a game where you want this cleanUI widget to fade in on mouse over, set this to true.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "CleanUI")
bool bTriggerCleanUIOnMouseOver;
UFUNCTION(BlueprintPure, Category = "CleanUI")
float GetCleanUIAlpha();
public:
//If this widget should fade out when another container is being looked at, use these functions to make that happen. This requires bUseFastHitTesting to be off.
UFUNCTION(BlueprintCallable, Category = "Dependent Widgets")
void AddGazeFocusableWidgetToPollHitsFrom(UTobiiGazeFocusableWidget* GazeFocusableWidgetToPoll, bool PollGazeHits = true, bool PollMouseHits = true);
UFUNCTION(BlueprintCallable, Category = "Dependent Widgets")
void RemoveGazeFocusableWidgetToPollHitsFrom(UTobiiGazeFocusableWidget* GazeFocusableWidgetToStopPolling);
public:
STobiiGazeFocusableWidget* GetSlateGazeFocusableWidget();
virtual void ReceiveFocus();
virtual void LoseFocus();
static TMap<FTobiiFocusableUID, TWeakObjectPtr<UTobiiGazeFocusableWidget>>& GetTobiiScreenSpaceFocusableWidgets();
public:
#if WITH_EDITOR
virtual const FText GetPaletteCategory() override;
#endif
protected:
//This is set during run time by a UTobiiGazeFocusableComponent if this is a world space widget.
TWeakObjectPtr<UWidgetComponent> WorldSpaceHostWidgetComponent;
TSharedPtr<class STobiiGazeFocusableWidget> MySlateWidget;
void SlateHandleHovered();
void SlateHandleUnhovered();
};

View File

@@ -0,0 +1,166 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "TobiiGTOMTypes.h"
#include "STobiiRadialMenuWidget.h"
#include "CoreMinimal.h"
#include "ObjectEditorUtils.h"
#include "Components/SizeBox.h"
#include "Components/WidgetComponent.h"
#include "TobiiRadialMenuWidget.generated.h"
UCLASS()
class TOBIIGTOM_API UTobiiRadialMenuSlot : public UPanelSlot
{
GENERATED_UCLASS_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Layout|Radial Menu Slot")
FVector2D Offset;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Layout|Radial Menu Slot")
float Scale;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Layout|Radial Menu Slot")
float Alpha;
public:
#if WITH_EDITOR
virtual bool NudgeByDesigner(const FVector2D& NudgeDirection, const TOptional<int32>& GridSnapSize) override;
virtual bool DragDropPreviewByDesigner(const FVector2D& LocalCursorPosition, const TOptional<int32>& XGridSnapSize, const TOptional<int32>& YGridSnapSize) override;
virtual void SynchronizeFromTemplate(const UPanelSlot* const TemplateSlot) override;
#endif //WITH_EDITOR
UFUNCTION(BlueprintCallable, Category = "Layout|Radial Menu Slot")
void SetOffset(FVector2D InOffset);
UFUNCTION(BlueprintCallable, Category = "Layout|Radial Menu Slot")
FVector2D GetOffset() const;
UFUNCTION(BlueprintCallable, Category = "Layout|Radial Menu Slot")
void SetScale(float InScale);
UFUNCTION(BlueprintCallable, Category = "Layout|Radial Menu Slot")
float GetScale() const;
UFUNCTION(BlueprintCallable, Category = "Layout|Radial Menu Slot")
void SetAlpha(float InAlpha);
UFUNCTION(BlueprintCallable, Category = "Layout|Radial Menu Slot")
float GetAlpha() const;
public:
STobiiRadialMenuWidget::FSlot* GetSlateSlot() { return Slot; }
void BuildSlot(TSharedRef<STobiiRadialMenuWidget> RadialMenu);
// UPanelSlot interface
virtual void SynchronizeProperties() override;
// End of UPanelSlot interface
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
#if WITH_EDITOR
// UObject interface
virtual void PreEditChange(class FEditPropertyChain& PropertyAboutToChange) override;
virtual void PostEditChangeChainProperty(struct FPropertyChangedChainEvent& PropertyChangedEvent) override;
// End of UObject interface
#endif
private:
STobiiRadialMenuWidget::FSlot* Slot;
};
/**
*/
UCLASS(BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent, DisplayName= "Tobii Radial Menu"))
class TOBIIGTOM_API UTobiiRadialMenuWidget : public UPanelWidget
{
GENERATED_UCLASS_BODY()
public:
//A hard border will not be blended with the panel, while a soft border will be blended
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu")
bool bUseHardBorder;
//The color of the borders around each panel
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu")
FColor BorderColor;
//The color of the panels themselves
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu")
FColor PanelColor;
//The thickness of the border area
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu")
float BorderThicknessPx;
//The amount of pixels separating each segment
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu")
float SegmentSeparationPx;
//This is how rotated the panel is. Useful if you want another orientation.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu")
float AngularDisplacementDeg;
//This is the preferred radius. This will inform the desired size of the widget
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu")
float RadiusPx;
//The amount of vertices that will be used for the outside of the circle segments in total.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu")
int32 VertexCount;
public:
UFUNCTION(BlueprintCallable, Category = "Radial Menu")
void SetUseHardBorder(bool bNewUseHardBorder);
UFUNCTION(BlueprintCallable, Category = "Radial Menu")
void SetBorderColor(FColor NewBorderColor);
UFUNCTION(BlueprintCallable, Category = "Radial Menu")
void SetPanelColor(FColor NewPanelColor);
UFUNCTION(BlueprintCallable, Category = "Radial Menu")
void SetBorderThicknessPx(float NewBorderThicknessPx);
UFUNCTION(BlueprintCallable, Category = "Radial Menu")
void SetSegmentSeparationPx(float NewSegmentSeparationPx);
UFUNCTION(BlueprintCallable, Category = "Radial Menu")
void SetAngularDisplacementDeg(float NewAngularDisplacementDeg);
UFUNCTION(BlueprintCallable, Category = "Radial Menu")
void SetRadiusPx(float NewRadiusPx);
UFUNCTION(BlueprintCallable, Category = "Radial Menu")
void SetVertexCount(int32 NewVertexCount);
public:
/** */
UFUNCTION(BlueprintCallable, Category = "Radial Menu")
UTobiiRadialMenuSlot* AddRadialMenuChild(UWidget* Content);
/** Computes the geometry for a particular slot based on the current geometry of the canvas. */
bool GetGeometryForSlot(int32 SlotIndex, FGeometry& ArrangedGeometry) const;
bool GetGeometryForSlot(UTobiiRadialMenuSlot* InSlot, FGeometry& ArrangedGeometry) const;
void ReleaseSlateResources(bool bReleaseChildren) override;
#if WITH_EDITOR
// UWidget interface
virtual const FText GetPaletteCategory() override;
// End UWidget interface
// UWidget interface
virtual bool LockToPanelOnDrag() const override
{
return true;
}
// End UWidget interface
#endif
protected:
// UPanelWidget
virtual UClass* GetSlotClass() const override;
virtual void OnSlotAdded(UPanelSlot* Slot) override;
virtual void OnSlotRemoved(UPanelSlot* Slot) override;
// End UPanelWidget
protected:
TSharedPtr<class STobiiRadialMenuWidget> MyRadialMenu;
protected:
// UWidget interface
virtual TSharedRef<SWidget> RebuildWidget() override;
// End of UWidget interface
};

View File

@@ -0,0 +1,156 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "GTOMAwareUI/TobiiGazeFocusableWidget.h"
#include "CoreMinimal.h"
#include "Components/WidgetComponent.h"
#include "Components/WidgetInteractionComponent.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "TobiiGTOMBlueprintLibrary.generated.h"
/**
* Simplified interface for blueprint use. Only exposes the features most likely to be consumed from BP.
*/
UCLASS()
class TOBIIGTOM_API UTobiiGTOMBlueprintLibrary : public UBlueprintFunctionLibrary
{
GENERATED_UCLASS_BODY()
public:
/**
* Set which player controller should be used for GTOM.
*/
UFUNCTION(BlueprintCallable, Category = "Tobii GTOM")
static void SetGTOMPlayerController(APlayerController* NewGTOMPlayerController);
/**
* Get the component the focus system believes the user is looking at.
*/
UFUNCTION(BlueprintPure, Category = "Tobii GTOM")
static bool GetGazeFocusData(FTobiiGazeFocusData& OutFocusData);
/**
* Get a sorted list of components that the focus system believes the user is looking at.
*/
UFUNCTION(BlueprintPure, Category = "Tobii GTOM")
static bool GetAllGazeFocusData(TArray<FTobiiGazeFocusData>& OutFocusData);
/**
* Get the component the focus system believes the user is looking at. This variant allows you to filter out certain objects in accordance with a filter list.
*
* @param FocusLayerFilterList These are the filters that will determine what focus data you will receive. The behavior of the filters are controlled by bIsWhiteList.
* @param bIsWhiteList This controls how the filters in FocusFilterList are used. If this is true, only focusables with a layer that is in the FocusFilterList will be considered. If this is false, the FocusFilterList will act like a black list, excluding any focusables whos layer can be found in the array.
* @param bWantPrimitives If this is true, focus data with valid UPrimitiveComponents will be included in the output results.
* @param bWantWidgets If this is true, focus data with valid UWidgets will be included in the output results.
* @param OutFocusData Output focus data.
*/
UFUNCTION(BlueprintPure, Category = "Tobii GTOM")
static bool GetFilteredGazeFocusData(const TArray<FName>& FocusLayerFilterList, const bool bIsWhiteList, const bool bWantPrimitives, const bool bWantWidgets, FTobiiGazeFocusData& OutFocusData);
/**
* Get a sorted list of components that the focus system believes the user is looking at. This variant allows you to filter out certain objects in accordance with a filter list.
*
* @param FocusLayerFilterList These are the filters that will determine what focus data you will receive. The behavior of the filters are controlled by bIsWhiteList.
* @param bIsWhiteList This controls how the filters in FocusFilterList are used. If this is true, only focusables with a layer that is in the FocusFilterList will be considered. If this is false, the FocusFilterList will act like a black list, excluding any focusables whos layer can be found in the array.
* @param bWantPrimitives If this is true, focus data with valid UPrimitiveComponents will be included in the output results.
* @param bWantWidgets If this is true, focus data with valid UWidgets will be included in the output results.
* @param OutFocusData Output focus data.
*/
UFUNCTION(BlueprintPure, Category = "Tobii GTOM")
static bool GetAllFilteredGazeFocusData(const TArray<FName>& FocusLayerFilterList, const bool bIsWhiteList, const bool bWantPrimitives, const bool bWantWidgets, TArray<FTobiiGazeFocusData>& OutFocusData);
/************************************************************************/
/* Utils */
/************************************************************************/
/**
* Gets a simple ray hit along combined gaze
*/
UFUNCTION(BlueprintPure, Category = "Tobii GTOM Utils")
static const FHitResult& GetNaiveGazeHit();
/**
* Tests to see whether a primitive component is gaze focusable
*/
UFUNCTION(BlueprintPure, Category = "Tobii GTOM Utils")
static bool IsPrimitiveComponentGazeFocusable(UPrimitiveComponent* PrimitiveComponent);
/**
* Tests to see whether a widget is gaze focusable
*/
UFUNCTION(BlueprintPure, Category = "Tobii GTOM Utils")
static bool IsWidgetGazeFocusable(UWidget* Widget);
/**
* Gets the *untransformed* focus offset for a primitive component.
*/
UFUNCTION(BlueprintPure, Category = "Tobii GTOM Utils")
static bool GetPrimitiveComponentFocusOffset(USceneComponent* Component, FVector& OutFocusOffset);
/**
* Gets the focus point on the primitive.
*/
UFUNCTION(BlueprintPure, Category = "Tobii GTOM Utils")
static bool GetPrimitiveComponentFocusLocation(USceneComponent* Component, FVector& OutFocusLocation);
/*
* This function will traverse a widget tree given a root and add any UTobiiGazeFocusableWidget to the screen space widget collection.
*/
UFUNCTION(BlueprintCallable, Category = "Tobii GTOM Utils")
static void RegisterScreenSpaceGazeFocusableWidgets(UWidget* Root);
/**
* Helper function to simulate gaze focus using a widget interaction component
*/
UFUNCTION(BlueprintCallable, Category = "Tobii GTOM Utils")
static bool MakeGazeFocusDataForWidgetInteractionComponent(UWidgetInteractionComponent* WidgetInteraction, ECollisionChannel CollisionChannel, FTobiiGazeFocusData& OutGazeFocusData);
/**
* Helper function to simulate gaze focus using a widget interaction component
*/
UFUNCTION(BlueprintCallable, Category = "Tobii GTOM Utils")
static void EmulateGazeFocusUsingWidgetInteractionComponent(UWidgetInteractionComponent* WidgetInteraction, ECollisionChannel CollisionChannel);
/**
* This function will find the largest possible rectangle that can be inscribed into a given circle.
*/
UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils Utils")
static bool FindLargestInscribedAlignedRect(float CircleSegmentAngleRad, float CircleRadius, FVector2D& LargestInscribedRectSize, float& DistanceToCenter);
/**
* This function will project a local point in a widget hosted by a widget component into world space
*/
UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils")
static bool TransformWidgetLocalPointToWorld(UWidgetComponent* Component, const FVector2D& LocalWidgetLocation, FVector& OutWorldLocation);
/**
* This function will project a world point to the widget's local space
*/
UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils")
static bool TransformWorldPointToWidgetLocal(UWidgetComponent* Component, const FVector& WorldLocation, const FVector& WorldDirection, FVector2D& OutLocalWidgetLocation);
/**
* This function will test whether a right angle cone and a sphere intersects. Very useful for world space eye tracking.
*/
UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils")
static bool TestConeSphereIntersection(const FVector& ConeApex, const FVector& ConeDirection, const float ConeAngleDeg, const FVector& SphereCenter, const float SphereRadius);
/**
* This function will test whether a rectangle and a rotated ellipse intersects. Very useful for screen space eye tracking.
*/
UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils")
static bool TestRectEllipseIntersection(const FVector2D& RectangleCenter, const FVector2D& RectangleRightAxis, const FVector2D& RectangleUpAxis, const FVector2D& RectangleExtents, const FVector2D& EllipseCenter, const FVector2D& EllipseRadii, const float& EllipseRotationDeg);
/**
* Get a random point on a circle centered at the origin that is uniformly distributed.
*/
UFUNCTION(BlueprintPure, Category = "Tobii Math Utils")
static FVector2D GetUniformlyDistributedRandomCirclePoint(float CircleRadius);
};

View File

@@ -0,0 +1,93 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "CoreMinimal.h"
#include "Engine/EngineTypes.h"
#include "TobiiGTOMTypes.generated.h"
class UTobiiGazeFocusableWidget;
typedef uint32 FTobiiFocusableUID; //Only use this ID type for our focus container types like UTobiiGazeFocusableComponents and UTobiiGazeFocusableWidgets
typedef uint32 FEngineFocusableUID; //Only use this for base engine things that can be focused, like UPrimitiveComponents and UWidgets
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FPrimitiveReceivedGazeFocusSignature, UPrimitiveComponent*, FocusedComponent);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FPrimitiveLostGazeFocusSignature, UPrimitiveComponent*, FocusedComponent);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FWidgetReceivedGazeFocusSignature, UTobiiGazeFocusableWidget*, FocusedWidget);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FWidgetLostGazeFocusSignature, UTobiiGazeFocusableWidget*, FocusedWidget);
UENUM(BlueprintType)
enum class ETobiiCleanUIMode : uint8
{
/** CleanUI is disabled for this widget. */
Disabled,
/** CleanUI will only consider the gaze point for determining alpha, and will ignore nearby widgets. */
Normal,
/** This widget is aware of it's neighbors and only one widget can be faded in at a time. */
FocusExclusive,
/** This widget will still calcualte its CleanUI alpha value, but it will not apply it when calculating its own color. */
Silent
};
/**
* Information about an object that is being considered when determining user focus.
* When utilizing gaze, this is almost always what you should be using as your input data.
*/
USTRUCT(BlueprintType)
struct FTobiiGazeFocusData
{
GENERATED_USTRUCT_BODY()
public:
FTobiiGazeFocusData()
: FocusedActor()
, FocusedPrimitiveComponent()
, FocusedWidget()
, LastVisibleWorldLocation(0.0f, 0.0f, 0.0f)
, FocusConfidence(0.0f)
{}
//This is the actor that the Focused Primitive Component belongs to.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Focus Data")
TWeakObjectPtr<AActor> FocusedActor;
//This is the primitive component that is most likely to hold the user's focus.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Focus Data")
TWeakObjectPtr<UPrimitiveComponent> FocusedPrimitiveComponent;
//This is the primitive component that is most likely to hold the user's focus.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Focus Data")
TWeakObjectPtr<UTobiiGazeFocusableWidget> FocusedWidget;
//This is the point on the Focused Primitive Component that was last confirmed visible to the user. This is very useful when aligning objects towards a focused object.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Focus Data")
FVector LastVisibleWorldLocation;
//This is how confident the focus system is that this object is in focus. The object with the highest confidence is not necessarily the object with focus however.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Focus Data")
float FocusConfidence;
};
//This contains the tags that can optionally be used to inform the GTOM system about the primitive component they are attached to. These only exist since we cannot add UPROPERTY's to primitive components without engine modifications.
//If you want to override behavior or widgets, please change the relevant properties on the TobiiGazeFocusableWidget since we have full access to that type.
class TOBIIGTOM_API FTobiiPrimitiveComponentGazeFocusTags
{
public:
static FName HasGazeFocusTag; //A primitive component with this tag is the primitive component that most likely has focus irregardless of layer filters.
static FName GazeFocusableTag; //A primitive component with this tag will participate in GTOM even if the actor owning the primitive doesn't have a GazeFocusableComponent, or if the default for the gaze focusable is off.
static FName NotGazeFocusableTag; //A primitive component with this tag will not participate in GTOM even if the actor owning the primitive component has a GazeFocusableComponent.
static FString GazeFocusablePriorityTag; //A primitive component with this tag will modify it's priority. Priority is used when only a certain number of focusables can participate in an operation. An example of this is ID buffer based GTOM. This is an argument tag, this means you must provide the value after the tag. Usage example: GazeFocusablePriorityTag 100
static FString GazeFocusableMaximumDistanceTag; //A primitive component with this tag and has the GazeFocusableTag will only be considered if the distance between the GTOM source and the component is shorter than the argument part of the tag. Please note that this tag cannot be used to force the GTOM line traces to be longer than default, only exclude the primitive if the distance is longer than this argument. This is an argument tag, this means you must provide the value after the tag. Usage example: GazeFocusableMaximumDistanceTag 10000
static FString GazeFocusableLayerTag; //A focus manager can opt to only query a subset of focusables by using a focus layer. A primitive will this tag will only be subject to the layer supplied as the argument. This is an argument tag, this means you must provide the value after the tag. Usage example: GazeFocusableLayerTag Enemies
static FString PrimitiveFocusOffsetXTag; //If this tag is present, it tells the various systems interacting with gaze focus that the actual focus point is relative to the primitive origin by this amount.
static FString PrimitiveFocusOffsetYTag; //If this tag is present, it tells the various systems interacting with gaze focus that the actual focus point is relative to the primitive origin by this amount.
static FString PrimitiveFocusOffsetZTag; //If this tag is present, it tells the various systems interacting with gaze focus that the actual focus point is relative to the primitive origin by this amount.
};

View File

@@ -0,0 +1,88 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "TobiiGTOMTypes.h"
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Delegates/DelegateCombinations.h"
#include "TobiiGazeFocusManagerComponent.generated.h"
/*
* Tobii focus managers are an easier way to set up focus layer filters and access the focusables associated with them.
* It also offers a way to get notified of changes in gaze focus via events.
*/
UCLASS(BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent))
class TOBIIGTOM_API UTobiiGazeFocusManagerComponent : public UActorComponent
{
GENERATED_BODY()
public:
UTobiiGazeFocusManagerComponent();
public:
//If this is true, focus data with valid UPrimitiveComponents will be included in the output results.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus Management")
bool bWantPrimitives;
//If this is true, focus data with valid UWidgets will be included in the output results
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus Management")
bool bWantWidgets;
//This controls how the filters in FocusFilterList are used. If this is true, only focusables with a layer that is in the FocusFilterList will be considered. If this is false, the FocusFilterList will act like a black list, excluding any focusables whos layer can be found in the array.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus Management")
bool bIsWhiteList;
//These are the filters that will determine what focus data you will receive. The behavior of the filters are controlled by bIsWhiteList.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus Management")
TArray<FName> FocusLayerFilters;
public:
//These functions will notify when gaze focus is lost for this focus manager's filtered set.
UPROPERTY(BlueprintAssignable, Category = "Gaze Focus Management")
FPrimitiveReceivedGazeFocusSignature OnPrimitiveReceivedGazeFocus;
UPROPERTY(BlueprintAssignable, Category = "Gaze Focus Management")
FPrimitiveLostGazeFocusSignature OnPrimitiveLostGazeFocus;
UPROPERTY(BlueprintAssignable, Category = "Gaze Focus Management")
FWidgetReceivedGazeFocusSignature OnWidgetReceivedGazeFocus;
UPROPERTY(BlueprintAssignable, Category = "Gaze Focus Management")
FWidgetLostGazeFocusSignature OnWidgetLostGazeFocus;
public:
//Get the component the focus system believes the user is looking at, given the filters the focus manager is configured with.
UFUNCTION(BlueprintPure, Category = "Gaze Focus Management")
void GetBestFilteredPrimitiveComponentFocusData(FTobiiGazeFocusData& OutFocusData) const;
UFUNCTION(BlueprintPure, Category = "Gaze Focus Management")
void GetBestFilteredWidgetComponentFocusData(FTobiiGazeFocusData& OutFocusData) const;
//Get a sorted list of components that the focus system believes the user is looking at, given the filters the focus manager is configured with.
UFUNCTION(BlueprintPure, Category = "Gaze Focus Management")
void GetAllFilteredFocusData(TArray<FTobiiGazeFocusData>& OutFocusData) const;
/************************************************************************/
/* UActorComponent */
/************************************************************************/
public:
virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
private:
TWeakObjectPtr<UPrimitiveComponent> PreviouslyFocusedPrimitiveComponent;
TWeakObjectPtr<UTobiiGazeFocusableWidget> PreviouslyFocusedWidget;
FTobiiGazeFocusData BestPrimitiveComponentFocusData;
FTobiiGazeFocusData BestWidgetFocusData;
TArray<FTobiiGazeFocusData> AllFocusData;
void ResetFocusData();
void NotifyPrimitiveComponentGazeFocusReceived(UPrimitiveComponent& PrimitiveComponentToReceiveFocus);
void NotifyPrimitiveComponentGazeFocusLost(UPrimitiveComponent& PrimitiveComponentToLoseFocus);
};

View File

@@ -0,0 +1,111 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "TobiiGazeFocusableWidget.h"
#include "TobiiGTOMTypes.h"
#include "CoreMinimal.h"
#include "SceneTypes.h"
#include "Components/ActorComponent.h"
#include "TobiiGazeFocusableComponent.generated.h"
class UPrimitiveComponent;
class UTobiiGazeFocusManagerComponent;
/**
* This component is a helper that can make it easier to setup GTOM, as well as provide an focus event interface for this object.
* If you don't use specific tags on your primitive components, the values on this component will be used instead.
* This component is REQUIRED for GPU based GTOM.
* This component is also REQUIRED for world space widget focus.
* This should be placed on the root actor for every component tree. This means that if you have an actor that has a child actor that has primitive components you would like to control, you need to add one of these components to that child actor as well.
* Having more than one of these components on one actor is not recommended, as it is unclear which focusable component's values will be used.
* If you want world space UI widgets to participate in GTOM, you must attach one of these components to the actor carrying the WidgetComponent(s) to affect
*/
UCLASS(BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent))
class TOBIIGTOM_API UTobiiGazeFocusableComponent : public UActorComponent
{
GENERATED_BODY()
public:
//The default value for focusability to be used by all primitives on the same actor as this component. It can be useful to set this to false if you only want a few primitives on the actor to be focusable.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Default Focus Settings")
bool bDefaultIsFocusable;
//The focus priority is optional input that you can provide to GTOM. Objects with higher focus priority are more "important" and are therefore more likely to be construed as being in focus.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Default Focus Settings")
float DefaultFocusPriority;
//If this object is at least this far away from the player, it will not participate in GTOM. This will only be in effect if it is greater than zero.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Default Focus Settings")
float DefaultMaxFocusDistance;
//A developer can opt to only query a subset of focusables by using a focus layer. This is the default layer that will be used if none is set with tags on primitives or on a gazefocusablewidget.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Default Focus Settings")
FName DefaultFocusLayer;
public:
//These functions will notify when gaze focus is lost or gained irregardless of focus layers.
UPROPERTY(BlueprintAssignable, Category = "Gaze Focus")
FPrimitiveReceivedGazeFocusSignature OnPrimitiveReceivedGazeFocus;
UPROPERTY(BlueprintAssignable, Category = "Gaze Focus")
FPrimitiveLostGazeFocusSignature OnPrimitiveLostGazeFocus;
UPROPERTY(BlueprintAssignable, Category = "Gaze Focus")
FWidgetReceivedGazeFocusSignature OnWidgetReceivedGazeFocus;
UPROPERTY(BlueprintAssignable, Category = "Gaze Focus")
FWidgetLostGazeFocusSignature OnWidgetLostGazeFocus;
public:
UFUNCTION(BlueprintCallable, Category = "CleanUI")
void RefreshOwnedWidgets();
public:
UTobiiGazeFocusableComponent();
virtual void PrimitiveReceivedGazeFocus(UPrimitiveComponent* FocusedComponent);
virtual void PrimitiveLostGazeFocus(UPrimitiveComponent* FocusedComponent);
virtual void WidgetReceivedGazeFocus(UTobiiGazeFocusableWidget* FocusedWidget);
virtual void WidgetLostGazeFocus(UTobiiGazeFocusableWidget* FocusedWidget);
const TMap<FTobiiFocusableUID, TWeakObjectPtr<UTobiiGazeFocusableWidget>>& GetAllFocusableWidgets() { return AllFocusableWidgets; }
const TArray<TWeakObjectPtr<UTobiiGazeFocusableWidget>>& GetFocusableWidgetsForPrimitiveComponent(UPrimitiveComponent* PrimitiveComponent);
/************************************************************************/
/* UActorComponent */
/************************************************************************/
public:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
public:
static void ClearFocusableComponents();
static const TMap<FTobiiFocusableUID, TWeakObjectPtr<UTobiiGazeFocusableComponent>>& GetFocusableComponents();
static void UpdateGazeFocusPrio(FVector POVActorLocation, int32 MaxNrFocusables);
static const TSet<FPrimitiveComponentId>* GetGazeFocusPrioSet();
static bool IsPrimitiveFocusable(UPrimitiveComponent* Primitive);
static bool GetMaxFocusDistanceForPrimitive(UPrimitiveComponent* Primitive, float& OutMaxDistance);
static float GetFocusPriorityForPrimitive(UPrimitiveComponent* Primitive);
static FName GetFocusLayerForPrimitive(UPrimitiveComponent* Primitive);
static bool IsWidgetFocusable(UTobiiGazeFocusableWidget* GazeFocusableWidget);
static bool GetMaxFocusDistanceForWidget(UTobiiGazeFocusableWidget* GazeFocusableWidget, float& OutMaxDistance);
static float GetFocusPriorityForWidget(UTobiiGazeFocusableWidget* GazeFocusableWidget);
static FName GetFocusLayerForWidget(UTobiiGazeFocusableWidget* GazeFocusableWidget);
private:
TMap<FTobiiFocusableUID, TWeakObjectPtr<UTobiiGazeFocusableWidget>> AllFocusableWidgets;
TMap<FEngineFocusableUID, TArray<TWeakObjectPtr<UTobiiGazeFocusableWidget>>> PrimitiveMap;
bool bWidgetsRefreshedOnce;
void GatherGazeFocusableWidgets(UWidget* Parent, TArray<TWeakObjectPtr<UTobiiGazeFocusableWidget>>& WidgetArray, UWidgetComponent* OptionalHostWidgetComponent);
};

View File

@@ -0,0 +1,100 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
using System;
using System.IO;
using System.Diagnostics;
using Tools.DotNETCommon;
namespace UnrealBuildTool.Rules
{
public class TobiiGTOM : ModuleRules
{
public TobiiGTOM(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PrivateDependencyModuleNames.AddRange(new string[]
{
"Core"
, "CoreUObject"
, "Engine"
, "Slate"
, "SlateCore"
, "UMG"
, "HeadMountedDisplay"
});
PublicDependencyModuleNames.AddRange(new string[]
{
"EyeTracker"
});
PrivateIncludePaths.AddRange(new string[]
{
"TobiiGTOM/Public"
, "TobiiGTOM/Public/GTOMAwareUI"
});
string AssemblyLocation = Path.GetDirectoryName(new Uri(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).LocalPath);
DirectoryReference RootDirectory = new DirectoryReference(Path.Combine(AssemblyLocation, "..", "..", ".."));
bool IsEnginePlugin = RootDirectory.GetDirectoryName() == "Engine";
PublicDefinitions.Add("TOBII_COMPILE_AS_ENGINE_PLUGIN=" + (IsEnginePlugin ? 1 : 0));
//Platform specific
if (Target.Platform == UnrealTargetPlatform.Win64 || Target.Platform == UnrealTargetPlatform.Win32)
{
string PlatformString = (Target.Platform == UnrealTargetPlatform.Win64) ? "Win64" : "Win32";
string PluginsPath = Path.Combine(ModuleDirectory, "../../../");
string TobiiRelativeAPIPath = "TobiiEyetracking/ThirdParty/GameIntegration";
string TobiiRelativeIncludePath = Path.Combine(TobiiRelativeAPIPath, "include");
string TobiiRelativeLibraryBasePath = Path.Combine(TobiiRelativeAPIPath, "lib");
//Includes
PrivateIncludePaths.Add(Path.Combine(PluginsPath, TobiiRelativeIncludePath));
//Add libraries
AddLibrary(Path.Combine(PluginsPath, TobiiRelativeLibraryBasePath, PlatformString, "tobii_g2om.lib"));
//Add DLL
string RelativeG2OMDllPath = "";
const string G2OMDllName = "tobii_g2om.dll";
if (IsEnginePlugin)
{
RelativeG2OMDllPath = Path.Combine("Binaries/ThirdParty/TobiiEyetracking", PlatformString, G2OMDllName);
RuntimeDependencies.Add("$(EngineDir)/" + RelativeG2OMDllPath);
}
else
{
RelativeG2OMDllPath = Path.Combine(TobiiRelativeLibraryBasePath, PlatformString, G2OMDllName);
RuntimeDependencies.Add(Path.Combine(PluginsPath, RelativeG2OMDllPath));
}
PublicDefinitions.Add("TOBII_G2OM_RELATIVE_DLL_PATH=R\"(" + RelativeG2OMDllPath + ")\"");
PublicDelayLoadDLLs.Add(G2OMDllName);
PublicDefinitions.Add("TOBII_EYETRACKING_ACTIVE=1");
}
else
{
PublicDefinitions.Add("TOBII_EYETRACKING_ACTIVE=0");
}
}
private void AddLibrary(string libraryPath)
{
if (File.Exists(libraryPath))
{
PublicAdditionalLibraries.Add(libraryPath);
}
else
{
Debug.WriteLine("Cannot find Tobii API Lib file. Path does not exist! " + libraryPath);
}
}
}
}

View File

@@ -0,0 +1,261 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#include "TobiiProjectileComponent.h"
#include "TobiiGTOMBlueprintLibrary.h"
#include "TobiiInteractionsBlueprintLibrary.h"
#include "Components/PrimitiveComponent.h"
#include "Engine/World.h"
#include "GameFramework/Actor.h"
UTobiiProjectileComponent::UTobiiProjectileComponent()
: InitialVelocity(1.0f, 0.0f, 0.0f)
, bAffectedByGravity(true)
, WorldSpaceGravity(0.0f, 0.0f, 0.0f)
, InitialFuelSecs(0.0f)
, MaxLifeTimeSecs(0.0f)
, HomingBehavior(ETobiiProjectileHomingBehavior::Acceleration)
, SteeringMaxTurnSpeedDegPerSec(10.0f)
, GuidanceSystem(ETobiiProjectileGuidanceSystem::ComplexPrediction)
, GuidanceSystemUpdateFreq(0.0f)
, GuidanceSystemMaximumTargetAngleDeg(0.0f)
, GuidanceSystemTarget()
, AccelerationVectorTowardsTarget()
, bTargetOffCourse(false)
{
bCanThrust = true;
bUsesFuel = InitialFuelSecs > 0.0f;
CurrentFuelSecs = InitialFuelSecs;
CurrentLifeTime = 0.0f;
GuidanceSystemUpdateTimerSecsLeft = 0.0f;
LastTargetVelocity = FVector::ZeroVector;
TargetAcceleration = FVector::ZeroVector;
HomingAccelerationMagnitude = 10000.0f;
PrimaryComponentTick.bCanEverTick = true;
}
void UTobiiProjectileComponent::BeginPlay()
{
Super::BeginPlay();
bCanThrust = true;
bUsesFuel = InitialFuelSecs > 0.0f;
CurrentFuelSecs = InitialFuelSecs;
CurrentLifeTime = 0.0f;
GuidanceSystemUpdateTimerSecsLeft = 0.0f;
LastTargetVelocity = FVector::ZeroVector;
TargetAcceleration = FVector::ZeroVector;
if (WorldSpaceGravity.IsZero())
{
WorldSpaceGravity.Set(0.0f, 0.0f, GetGravityZ());
}
//This is a copy from the base class InitializeComponent function. This should really be done at beginplay to let any instigator configure initial speed etc.
if (InitialVelocity.SizeSquared() > 0.f)
{
FVector NewVelocity;
if (InitialSpeed > 0.f)
{
NewVelocity = InitialVelocity.GetSafeNormal() * InitialSpeed;
}
else
{
NewVelocity = InitialVelocity;
}
if (bInitialVelocityInLocalSpace)
{
SetVelocityInLocalSpace(NewVelocity);
}
if (bRotationFollowsVelocity)
{
if (UpdatedComponent)
{
UpdatedComponent->SetWorldRotation(NewVelocity.Rotation());
}
}
UpdateComponentVelocity();
if (UpdatedPrimitive && UpdatedPrimitive->IsSimulatingPhysics())
{
UpdatedPrimitive->SetPhysicsLinearVelocity(NewVelocity);
}
}
}
void UTobiiProjectileComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
CurrentLifeTime += DeltaTime;
if (bUsesFuel && bCanThrust)
{
CurrentFuelSecs -= DeltaTime;
if (CurrentFuelSecs <= 0.0f)
{
CurrentFuelSecs = 0.0f;
bCanThrust = false;
}
}
bTargetOffCourse = false;
if(GuidanceSystemMaximumTargetAngleDeg > FLT_EPSILON && HomingTargetComponent.IsValid())
{
FVector TargetFocusPosition;
UTobiiGTOMBlueprintLibrary::GetPrimitiveComponentFocusLocation(HomingTargetComponent.Get(), TargetFocusPosition);
//If our target is too far off course, disable thrust
FVector DeltaVector = TargetFocusPosition - GetOwner()->GetActorLocation();
FVector MyDirection = Velocity;
bool bHasValidVectors = DeltaVector.Normalize();
bHasValidVectors = bHasValidVectors && MyDirection.Normalize();
if (bHasValidVectors)
{
float SeparationAngle = FMath::RadiansToDegrees(FMath::Acos(FVector::DotProduct(DeltaVector, MyDirection)));
if (SeparationAngle > GuidanceSystemMaximumTargetAngleDeg)
{
bTargetOffCourse = true;
}
}
}
if (HomingTargetComponent.IsValid() && !bTargetOffCourse)
{
TargetAcceleration = HomingTargetComponent->GetComponentVelocity() - LastTargetVelocity;
LastTargetVelocity = HomingTargetComponent->GetComponentVelocity();
if (GuidanceSystemUpdateFreq == 0.0f)
{
SimulateGuidanceSystem();
}
else
{
GuidanceSystemUpdateTimerSecsLeft -= DeltaTime;
if (GuidanceSystemUpdateTimerSecsLeft <= 0.0f)
{
GuidanceSystemUpdateTimerSecsLeft = 1.0f / GuidanceSystemUpdateFreq;
SimulateGuidanceSystem();
}
}
//Steering behavior
if (bIsHomingProjectile && bCanThrust && HomingBehavior == ETobiiProjectileHomingBehavior::Steering)
{
FVector ForwardDir = Velocity;
FVector TowardsTarget = GuidanceSystemTarget - GetOwner()->GetActorLocation();
bool bHasValidVectors = ForwardDir.Normalize();
bHasValidVectors = bHasValidVectors && TowardsTarget.Normalize();
if (bHasValidVectors)
{
float AngularDifferenceRad = FMath::Acos(FVector::DotProduct(ForwardDir, TowardsTarget));
float AlphaStep = FMath::Clamp(FMath::DegreesToRadians(SteeringMaxTurnSpeedDegPerSec) * DeltaTime / AngularDifferenceRad, 0.0f, 1.0f);
FVector NewVelocityDirection = FQuat::Slerp(ForwardDir.ToOrientationQuat(), TowardsTarget.ToOrientationQuat(), AlphaStep).GetForwardVector();
Velocity = NewVelocityDirection * Velocity.Size();
UpdateComponentVelocity();
}
}
}
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (MaxLifeTimeSecs > 0.0f && CurrentLifeTime > MaxLifeTimeSecs)
{
GetOwner()->Destroy();
}
}
FVector UTobiiProjectileComponent::ComputeAcceleration(const FVector& InVelocity, float DeltaTime) const
{
FVector Acceleration(FVector::ZeroVector);
if (bAffectedByGravity)
{
Acceleration += WorldSpaceGravity;
}
if (bCanThrust && !bTargetOffCourse && bIsHomingProjectile && HomingTargetComponent.IsValid() && HomingBehavior == ETobiiProjectileHomingBehavior::Acceleration)
{
Acceleration += AccelerationVectorTowardsTarget;
}
return Acceleration;
}
void UTobiiProjectileComponent::SimulateGuidanceSystem()
{
FVector TargetFocusPosition;
UTobiiGTOMBlueprintLibrary::GetPrimitiveComponentFocusLocation(HomingTargetComponent.Get(), TargetFocusPosition);
switch (GuidanceSystem)
{
case ETobiiProjectileGuidanceSystem::Prediction:
case ETobiiProjectileGuidanceSystem::ComplexPrediction:
{
FTobiiAccelerationBasedHomingData InterceptData;
InterceptData.ProjectilePosition = GetOwner()->GetActorLocation();
InterceptData.ProjectileVelocity = Velocity;
InterceptData.ProjectileAccelerationMagnitude = HomingAccelerationMagnitude;
InterceptData.TargetPosition = TargetFocusPosition;
InterceptData.TargetVelocity = HomingTargetComponent->GetComponentVelocity();
InterceptData.TargetAcceleration = TargetAcceleration;
InterceptData.bAttemptClosestApproachSolution = GuidanceSystem == ETobiiProjectileGuidanceSystem::ComplexPrediction;
FTobiiAccelerationBasedHomingResult HomingResult;
if (UTobiiInteractionsBlueprintLibrary::FindNeededAccelerationForAccelerationBasedHomingProjectile(InterceptData, HomingResult))
{
GuidanceSystemTarget = HomingResult.ExpectedInterceptLocation;
AccelerationVectorTowardsTarget = HomingResult.SuggestedAcceleration;
}
else
{
GuidanceSystemTarget = TargetFocusPosition;
AccelerationVectorTowardsTarget = (GuidanceSystemTarget - UpdatedComponent->GetComponentLocation()).GetSafeNormal() * HomingAccelerationMagnitude;
}
break;
}
case ETobiiProjectileGuidanceSystem::Simple:
default:
{
GuidanceSystemTarget = TargetFocusPosition;
AccelerationVectorTowardsTarget = (GuidanceSystemTarget - UpdatedComponent->GetComponentLocation()).GetSafeNormal() * HomingAccelerationMagnitude;
}
}
}
void UTobiiProjectileComponent::StartHoming(USceneComponent* NewHomingTargetComponent)
{
HomingTargetComponent = NewHomingTargetComponent;
if (HomingTargetComponent != nullptr)
{
bIsHomingProjectile = true;
}
bSimulationEnabled = true;
if (UpdatedComponent == nullptr)
{
UpdatedComponent = GetOwner()->GetRootComponent();
}
if (Cast<UPrimitiveComponent>(UpdatedComponent) != nullptr)
{
Cast<UPrimitiveComponent>(UpdatedComponent)->SetSimulatePhysics(false);
}
PrimaryComponentTick.SetTickFunctionEnable(true);
}
void UTobiiProjectileComponent::StopHoming()
{
bIsHomingProjectile = false;
HomingTargetComponent = nullptr;
}

View File

@@ -0,0 +1,466 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#include "TobiiThrowAtGazeComponent.h"
#include "TobiiInteractionsBlueprintLibrary.h"
#include "../TobiiInteractionsInternalTypes.h"
#include "IEyeTracker.h"
#include "DrawDebugHelpers.h"
#include "Camera/CameraComponent.h"
#include "Engine/Engine.h"
#include "Engine/World.h"
#include "HAL/IConsoleManager.h"
static TAutoConsoleVariable<int32> CVarThrowAtGazeEnabled(TEXT("tobii.interaction.ThrowAtGazeEnabled"), 1, TEXT("Throw where your are looking."));
static TAutoConsoleVariable<int32> CVarThrowAtGazeDebug(TEXT("tobii.debug.ThrowAtGaze"), 1, TEXT("Draw throw at gaze debug data."));
UTobiiThrowAtGazeComponent::UTobiiThrowAtGazeComponent()
: ThrowTarget(nullptr)
, MaxThrowSpeedCmPerSecs(1000.0f)
, MaxIterations(5)
, bAlwaysPreferLowApex(false)
, bPreferLowApexForTargetsBelowThrower(true)
, bShouldTraceResult(true)
, ThrowApexOffsetMinimumCm(10.0f)
, ThrowApexOffsetMaximumCm(500.0f)
, MinimumApexDeltaCm(10.0f)
, TraceRadiusCm(1.0f)
, MaxNrTraceSteps(50)
, TraceStepSizeSecs(0.1f)
, MaxTraceDistance(10000.0f)
, NoTargetAcceptanceThreshold(10.0f)
, TraceChannel(ECC_Visibility)
, TraceIgnoreActors()
, bShouldTraceIgnoreOwner(true)
, bUseCustomGravity(false)
, CustomProjectileGravity(FVector::ZeroVector)
, LastTargetVelocity(FVector::ZeroVector)
, CalculatedTargetAcceleration(FVector::ZeroVector)
{
}
FVector UTobiiThrowAtGazeComponent::CalculateProjectileGravityVector()
{
return bUseCustomGravity ? CustomProjectileGravity : FVector(0.0f, 0.0f, GetWorld()->GetGravityZ());
}
bool UTobiiThrowAtGazeComponent::ThrowAtGazeAvailable()
{
return UTobiiInteractionsBlueprintLibrary::IsThrowAtGazeEnabled();
}
bool UTobiiThrowAtGazeComponent::TraceBallisticProjectilePath(FVector ProjectileOrigin, FVector ProjectileInitialVelocity, TArray<FVector>& OutTracedPath)
{
TArray<AActor*> IgnoredActors = TraceIgnoreActors;
if (bShouldTraceIgnoreOwner)
{
IgnoredActors.Add(GetOwner());
}
FTobiiProjectileTraceData TraceData;
TraceData.TraceChannel = TraceChannel;
TraceData.IgnoredActors = IgnoredActors;
TraceData.MaxNrSteps = MaxNrTraceSteps;
TraceData.StepSizeSecs = TraceStepSizeSecs;
TraceData.TraceRadiusCm = TraceRadiusCm;
TraceData.ProjectileInitialPosition = ProjectileOrigin;
TraceData.ProjectileVelocity = ProjectileInitialVelocity;
TraceData.ProjectileAcceleration = CalculateProjectileGravityVector();
FHitResult HitResult;
return UTobiiInteractionsBlueprintLibrary::TraceBallisticProjectilePath(GetWorld(), TraceData, OutTracedPath, HitResult);
}
void UTobiiThrowAtGazeComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
if (ThrowTarget.IsValid())
{
const float AccelerationLerpSpeed = 0.7f;
FVector CurrentTargetVelocity = ThrowTarget->GetOwner()->GetVelocity();
CalculatedTargetAcceleration = FMath::Lerp(CalculatedTargetAcceleration, CurrentTargetVelocity - LastTargetVelocity, AccelerationLerpSpeed);
LastTargetVelocity = CurrentTargetVelocity;
}
}
FVector UTobiiThrowAtGazeComponent::CorrectThrow(const FVector& ThrowOrigin, const FVector& OriginalThrowVector, float ThrowAngleThresholdDeg, float ThrowSpeedThreshold)
{
FTobiiBallisticResult BallisticResult;
TArray<FVector> TracedPath;
ETobiiThrowAtGazeResult Result = CalculateThrowAtGazeVector(ThrowOrigin, BallisticResult, TracedPath);
switch (Result)
{
case ETobiiThrowAtGazeResult::OutOfRange:
case ETobiiThrowAtGazeResult::DirectHit:
{
if (ThrowAngleThresholdDeg > FLT_EPSILON)
{
FVector SuggestedDirection = BallisticResult.SuggestedInitialVelocity;
FVector OriginalDirection = OriginalThrowVector;
bool bHasValidVectors = SuggestedDirection.Normalize();
bHasValidVectors = bHasValidVectors && OriginalDirection.Normalize();
if (bHasValidVectors)
{
float SeparationDot = FVector::DotProduct(SuggestedDirection, OriginalDirection);
float SeparationAngleDeg = FMath::RadiansToDegrees(FMath::Acos(SeparationDot));
if (SeparationAngleDeg > ThrowAngleThresholdDeg)
{
static const auto DrawDebugCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.debug"));
if (DrawDebugCVar->GetInt() && CVarThrowAtGazeDebug.GetValueOnGameThread())
{
UE_LOG(LogTemp, Warning, TEXT("Throw diverged too much. Separation angle was: %f"), SeparationAngleDeg);
}
return OriginalThrowVector;
}
}
}
if (ThrowSpeedThreshold > FLT_EPSILON)
{
float SuggestedSpeedSq = BallisticResult.SuggestedInitialVelocity.SizeSquared();
float OriginalSpeedSq = OriginalThrowVector.SizeSquared();
float ThresholdSq = ThrowSpeedThreshold * ThrowSpeedThreshold;
//Only invalidate too weak throws
if (OriginalSpeedSq + ThresholdSq < SuggestedSpeedSq)
{
static const auto DrawDebugCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.debug"));
if (DrawDebugCVar->GetInt() && CVarThrowAtGazeDebug.GetValueOnGameThread())
{
UE_LOG(LogTemp, Warning, TEXT("Throw was too weak. Throw speed was: %f. Suggested speed was: %f"), OriginalThrowVector.Size(), BallisticResult.SuggestedInitialVelocity.Size());
}
return OriginalThrowVector;
}
}
return BallisticResult.SuggestedInitialVelocity;
}
default:
{
return OriginalThrowVector;
}
}
}
ETobiiThrowAtGazeResult UTobiiThrowAtGazeComponent::CalculateThrowAtGazeVector(const FVector& ThrowOrigin, FTobiiBallisticResult& OutBallisticResult, TArray<FVector>& OutTracedPath)
{
UWorld* World = GetWorld();
if (World == nullptr || GEngine == nullptr || !GEngine->EyeTrackingDevice.IsValid())
{
return ETobiiThrowAtGazeResult::UnknownError;
}
if (ThrowTarget.IsValid())
{
return CalculateThrowArc(ThrowOrigin, ThrowTarget->GetCenterOfMass(), ThrowTarget->GetOwner()->GetVelocity(), CalculatedTargetAcceleration, OutBallisticResult, OutTracedPath);
}
else if (World->GetFirstPlayerController() != nullptr && World->GetFirstPlayerController()->PlayerCameraManager != nullptr)
{
FVector GazeTargetPosition;
FVector GazeRayOrigin = FVector::ZeroVector;
FVector GazeRayDirection = FVector::ZeroVector;
FEyeTrackerGazeData CombinedGazeData;
GEngine->EyeTrackingDevice->GetEyeTrackerGazeData(CombinedGazeData);
if (CombinedGazeData.ConfidenceValue > 0.5f)
{
GazeRayOrigin = CombinedGazeData.GazeOrigin;
GazeRayDirection = CombinedGazeData.GazeDirection;
}
else
{
// If eyetracking data is not available this frame, just use the center of the screen
GazeRayOrigin = World->GetFirstPlayerController()->PlayerCameraManager->GetCameraLocation();
GazeRayDirection = World->GetFirstPlayerController()->PlayerCameraManager->GetCameraRotation().Vector();
}
FVector EndPoint = GazeRayOrigin + GazeRayDirection * MaxTraceDistance;
FHitResult HitResult;
if (World->LineTraceSingleByChannel(HitResult, GazeRayOrigin, EndPoint, TraceChannel))
{
GazeTargetPosition = HitResult.Location;
}
else
{
GazeTargetPosition = EndPoint;
}
return CalculateThrowArc(ThrowOrigin, GazeTargetPosition, FVector::ZeroVector, FVector::ZeroVector, OutBallisticResult, OutTracedPath);
}
return ETobiiThrowAtGazeResult::NoEyetrackingInput;
}
ETobiiThrowAtGazeResult UTobiiThrowAtGazeComponent::CalculateThrowArc(const FVector& ThrowOrigin, const FVector& TargetLocation, const FVector& TargetVelocity, const FVector& TargetAcceleration, FTobiiBallisticResult& OutBallisticResult, TArray<FVector>& OutTracedPath)
{
OutBallisticResult = FTobiiBallisticResult();
OutTracedPath = TArray<FVector>();
if (GetWorld() == nullptr)
{
return ETobiiThrowAtGazeResult::UnknownError;
}
static const auto DrawDebugCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.debug"));
const FVector ProjectileAcceleration = CalculateProjectileGravityVector();
const float BaseAcceptanceThreshold = TraceRadiusCm + NoTargetAcceptanceThreshold;
const float TraceAcceptanceThresholdSq = BaseAcceptanceThreshold * BaseAcceptanceThreshold;
bool bShouldDrawDebug = DrawDebugCVar->GetInt() && CVarThrowAtGazeDebug.GetValueOnGameThread();
if (bShouldDrawDebug)
{
DrawDebugSphere(GetWorld(), TargetLocation, 20.0f, 16, FColor::Yellow, false, 5.0f);
}
FTobiiBallisticData BallisticData;
BallisticData.ProjectileInitialPosition = ThrowOrigin;
BallisticData.ProjectileAcceleration = ProjectileAcceleration;
BallisticData.TargetPosition = TargetLocation;
BallisticData.TargetVelocity = TargetVelocity;
BallisticData.TargetAcceleration = TargetAcceleration;
TArray<AActor*> IgnoredActors = TraceIgnoreActors;
if (bShouldTraceIgnoreOwner)
{
IgnoredActors.Add(GetOwner());
}
FTobiiProjectileTraceData TraceData;
TraceData.TraceChannel = TraceChannel;
TraceData.IgnoredActors = IgnoredActors;
TraceData.MaxNrSteps = MaxNrTraceSteps;
TraceData.StepSizeSecs = TraceStepSizeSecs;
TraceData.TraceRadiusCm = TraceRadiusCm;
TraceData.ProjectileInitialPosition = ThrowOrigin;
TraceData.ProjectileAcceleration = ProjectileAcceleration;
TArray<float> ApexesToTest;
if (MaxIterations > 0)
{
//We want to first decide which apexes we should test, and then we can use logic to suss out which one ends up being the best.
int32 Iterations = FMath::Max(0, MaxIterations);
float ApexStep = (ThrowApexOffsetMaximumCm - ThrowApexOffsetMinimumCm) / Iterations;
for (int32 ApexIdx = 0; ApexIdx <= Iterations; ApexIdx++)
{
float CurrentApex = ThrowApexOffsetMinimumCm + ApexIdx * ApexStep;
ApexesToTest.Add(CurrentApex);
}
}
else
{
ApexesToTest.Add((ThrowApexOffsetMaximumCm + ThrowApexOffsetMinimumCm) / 2);
}
// Loop variables
float BestResultDistanceToTargetSq = FLT_MAX;
ETobiiThrowAtGazeResult BestResultStatus = ETobiiThrowAtGazeResult::NoPath;
while (ApexesToTest.Num() > 0)
{
// Pick the next apex
int32 SelectedApexIndex;
if (bAlwaysPreferLowApex || (bPreferLowApexForTargetsBelowThrower && TargetLocation.Z < ThrowOrigin.Z))
{
SelectedApexIndex = 0;
}
else
{
SelectedApexIndex = (int32)(ApexesToTest.Num() / 2.0f);
}
SelectedApexIndex = FMath::Clamp(SelectedApexIndex, 0, ApexesToTest.Num() - 1);
BallisticData.ProjectileApexOffsetCm = ApexesToTest[SelectedApexIndex];
ApexesToTest.RemoveAt(SelectedApexIndex);
// Find initial velocity for given apex
TArray<FTobiiBallisticResult> Results;
if (UTobiiInteractionsBlueprintLibrary::FindNeededInitialVelocityForBallisticProjectile(BallisticData, Results))
{
for (FTobiiBallisticResult& Result : Results)
{
float SuggestedInitialSpeed = Result.SuggestedInitialVelocity.Size();
if (SuggestedInitialSpeed < FLT_EPSILON)
{
continue;
}
bool bIsInRange = SuggestedInitialSpeed < MaxThrowSpeedCmPerSecs;
if (!bIsInRange)
{
Result.SuggestedInitialVelocity.Normalize();
Result.SuggestedInitialVelocity *= MaxThrowSpeedCmPerSecs;
}
TArray<FVector> TracedPath;
bool bWayIsClear = true;
float DistanceToTargetSq = FLT_MAX;
if (bShouldTraceResult)
{
TraceData.ProjectileVelocity = Result.SuggestedInitialVelocity;
FHitResult HitResult;
if (UTobiiInteractionsBlueprintLibrary::TraceBallisticProjectilePath(GetWorld(), TraceData, TracedPath, HitResult))
{
FVector HitToTarget = TargetLocation - HitResult.Location;
float ToTargetDistSq = HitToTarget.SizeSquared();
if (ThrowTarget.IsValid())
{
if (HitResult.Actor != ThrowTarget->GetOwner())
{
bWayIsClear = false; // If we have a target, and hit something, that's okay as long as it's our target.
}
}
else
{
bWayIsClear = ToTargetDistSq < TraceAcceptanceThresholdSq;
}
DistanceToTargetSq = ToTargetDistSq;
}
if (bShouldDrawDebug)
{
if (TracedPath.Num() > 1)
{
FColor DrawColor = bWayIsClear ? FColor::Green : FColor::Red;
FVector PrevPoint = TracedPath[0];
int32 MiddlePointIdx = (int32)(TracedPath.Num() / 2.0f);
if (MiddlePointIdx >= 0 && MiddlePointIdx < TracedPath.Num())
{
DrawDebugString(GetWorld(), TracedPath[MiddlePointIdx] + FVector(0.0f, 0.0f, 10.0f), FString::Printf(TEXT("%f"), BallisticData.ProjectileApexOffsetCm), nullptr, DrawColor, 5.0f);
}
for (auto& TracePoint : TracedPath)
{
DrawDebugLine(GetWorld(), PrevPoint, TracePoint, DrawColor, false, 5.0f, 0, 1.0f);
PrevPoint = TracePoint;
}
}
}
}
if (bWayIsClear)
{
if (bIsInRange)
{
//We're done! Best outcome.
OutBallisticResult = Result;
OutTracedPath = TracedPath;
BestResultDistanceToTargetSq = DistanceToTargetSq;
BestResultStatus = ETobiiThrowAtGazeResult::DirectHit;
break;
}
else if (BestResultStatus <= ETobiiThrowAtGazeResult::OutOfRange)
{
bool bIsFaster = Result.ExpectedInterceptTimeSecs < OutBallisticResult.ExpectedInterceptTimeSecs;
if (bIsFaster)
{
OutBallisticResult = Result;
OutTracedPath = TracedPath;
BestResultDistanceToTargetSq = DistanceToTargetSq;
BestResultStatus = ETobiiThrowAtGazeResult::OutOfRange;
}
}
}
else if (TracedPath.Num() > 0)
{
if (bIsInRange)
{
if (BestResultStatus <= ETobiiThrowAtGazeResult::BlockedByWorldInRange)
{
bool bIsCloser = DistanceToTargetSq < BestResultDistanceToTargetSq;
if (bIsCloser)
{
OutBallisticResult = Result;
OutTracedPath = TracedPath;
BestResultDistanceToTargetSq = DistanceToTargetSq;
BestResultStatus = ETobiiThrowAtGazeResult::BlockedByWorldInRange;
}
}
}
else
{
if (BestResultStatus <= ETobiiThrowAtGazeResult::BlockedByWorldAndOutOfRange)
{
bool bIsCloser = DistanceToTargetSq < BestResultDistanceToTargetSq;
if (bIsCloser)
{
OutBallisticResult = Result;
OutTracedPath = TracedPath;
BestResultDistanceToTargetSq = DistanceToTargetSq;
BestResultStatus = ETobiiThrowAtGazeResult::BlockedByWorldAndOutOfRange;
}
}
}
}
}
}
if (BestResultStatus == ETobiiThrowAtGazeResult::DirectHit)
{
break;
}
//Use what we have learned to prune apexes for future loops
if (Results.Num() == 0)
{
//If we didn't have any solutions or we are out of range, it means the apex is too low. Clear all apexes that are smaller than our current.
for (int32 CurrentApexIdx = 0; CurrentApexIdx < ApexesToTest.Num(); CurrentApexIdx++)
{
if (ApexesToTest[CurrentApexIdx] <= BallisticData.ProjectileApexOffsetCm)
{
ApexesToTest.RemoveAt(CurrentApexIdx);
CurrentApexIdx--;
}
}
}
else if (BestResultStatus == ETobiiThrowAtGazeResult::OutOfRange)
{
//If we are out of range, we need to select an apex closer to a 45 degree throw angle.
//@TODO: Should we maybe do a solve for throw force given 45 degree throw angle here to optimize?
FVector VelNorm = OutBallisticResult.SuggestedInitialVelocity;
FVector ProjectedToXY = FVector(VelNorm.X, VelNorm.Y, 0.0f);
bool bHasValidVectors = VelNorm.Normalize();
bHasValidVectors = bHasValidVectors && ProjectedToXY.Normalize();
if (bHasValidVectors)
{
float ThrowAngleCos = FVector::DotProduct(VelNorm, ProjectedToXY);
//1 means it is along the ground, 0 is straight up. So if the value is between 0.5 and 1 we need to aim higher and if it's between 0 and 0.5 we need to go lower
if (ThrowAngleCos < 0.5f)
{
for (int32 CurrentApexIdx = 0; CurrentApexIdx < ApexesToTest.Num(); CurrentApexIdx++)
{
if (ApexesToTest[CurrentApexIdx] >= BallisticData.ProjectileApexOffsetCm)
{
ApexesToTest.RemoveAt(CurrentApexIdx);
CurrentApexIdx--;
}
}
}
else
{
for (int32 CurrentApexIdx = 0; CurrentApexIdx < ApexesToTest.Num(); CurrentApexIdx++)
{
if (ApexesToTest[CurrentApexIdx] <= BallisticData.ProjectileApexOffsetCm)
{
ApexesToTest.RemoveAt(CurrentApexIdx);
CurrentApexIdx--;
}
}
}
}
}
}
// Output results
return BestResultStatus;
}

View File

@@ -0,0 +1,141 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#include "TobiiAimAtGazeComponent.h"
#include "TobiiInteractionsBlueprintLibrary.h"
#include "TobiiInteractionsInternalTypes.h"
#include "TobiiGTOMBlueprintLibrary.h"
#include "DrawDebugHelpers.h"
#include "Engine/World.h"
#include "GameFramework/Pawn.h"
#include "GameFramework/PlayerController.h"
#include "HAL/IConsoleManager.h"
static TAutoConsoleVariable<int32> CVarAimAtGazeEnabled(TEXT("tobii.interaction.AimAtGazeEnabled"), 1, TEXT("Aim@gaze will rotate the view to align with the gaze point when the user triggers it, usually by entering ADS (Aim Down Sights). 0 - Aim@gaze is off. 1 - Aim@gaze is on."));
static TAutoConsoleVariable<float> CVarAimAtGazeDoneThreshold(TEXT("tobii.interaction.AimAtGazeDoneThreshold"), 0.0001, TEXT("If the dot product between the current and target direction quats is smaller than this, we are done rotating."));
static TAutoConsoleVariable<int32> CVarAimAtGazeDebug(TEXT("tobii.debug.AimAtGaze"), 1, TEXT("0 - Will not visualize aim@gaze debug data. 1 - Will visualize aim@gaze debug data."));
UTobiiAimAtGazeComponent::UTobiiAimAtGazeComponent()
: bAllowRetarget(false)
, AimSpeed(0.2f)
, CurrentFocusComponent(nullptr)
, CurrentAimTarget(0.0f, 0.0f, 0.0f)
, bIsGazeAiming(false)
{
PrimaryComponentTick.bCanEverTick = true;
}
bool UTobiiAimAtGazeComponent::AimAtGazeAvailable()
{
return UTobiiInteractionsBlueprintLibrary::IsAimAtGazeEnabled();
}
void UTobiiAimAtGazeComponent::AimAtGaze()
{
if (!AimAtGazeAvailable())
{
return;
}
CurrentFocusComponent.Reset();
FTobiiGazeFocusData FocusData;
UTobiiGTOMBlueprintLibrary::GetFilteredGazeFocusData(FocusLayerFilters, bIsWhiteList, true, false, FocusData);
if (FocusData.FocusedActor.IsValid())
{
CurrentFocusComponent = FocusData.FocusedPrimitiveComponent;
CurrentAimTarget = FocusData.LastVisibleWorldLocation;
bIsGazeAiming = true;
}
else
{
const FHitResult& GazeHitData = UTobiiGTOMBlueprintLibrary::GetNaiveGazeHit();
CurrentAimTarget = GazeHitData.Location;
bIsGazeAiming = true;
}
}
void UTobiiAimAtGazeComponent::ContinuousAimAtGaze()
{
if (!AimAtGazeAvailable())
{
CurrentFocusComponent = nullptr;
return;
}
if (bAllowRetarget)
{
FTobiiGazeFocusData FocusData;
UTobiiGTOMBlueprintLibrary::GetFilteredGazeFocusData(FocusLayerFilters, bIsWhiteList, true, false, FocusData);
if (FocusData.FocusedPrimitiveComponent.IsValid())
{
CurrentFocusComponent = FocusData.FocusedPrimitiveComponent;
}
}
if (CurrentFocusComponent.IsValid())
{
FVector TargetFocusPosition;
UTobiiGTOMBlueprintLibrary::GetPrimitiveComponentFocusLocation(CurrentFocusComponent.Get(), TargetFocusPosition);
CurrentAimTarget = TargetFocusPosition;
bIsGazeAiming = true;
}
else
{
CurrentFocusComponent.Reset();
}
}
void UTobiiAimAtGazeComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
static const auto DrawDebugCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.debug"));
if (CurrentFocusComponent != nullptr && DrawDebugCVar->GetInt() && CVarAimAtGazeDebug.GetValueOnGameThread())
{
DrawDebugBox(GetWorld(), CurrentAimTarget, FVector(50.0f), FColor::Cyan, false, 0.0f);
}
APlayerController* PlayerController = nullptr;
APawn* OwnerPawn = Cast<APawn>(GetOwner());
// We make it possible to attach AimAtGaze component to either Pawn or PlayerController
if (OwnerPawn != nullptr)
{
PlayerController = Cast<APlayerController>(OwnerPawn->GetController());
}
else
{
PlayerController = Cast<APlayerController>(GetOwner());
}
if (PlayerController != nullptr)
{
if (AimAtGazeAvailable() && bIsGazeAiming && PlayerController->PlayerCameraManager != nullptr)
{
FVector CameraLocation = PlayerController->PlayerCameraManager->GetCameraLocation();
FVector AimTargetDirection = CurrentAimTarget - CameraLocation;
FQuat TargetQuat = AimTargetDirection.ToOrientationQuat();
FQuat CurrentControlQuat = FQuat(PlayerController->GetControlRotation());
CurrentControlQuat = FQuat::Slerp(CurrentControlQuat, TargetQuat, AimSpeed);
float DotDistance = (CurrentControlQuat | TargetQuat);
if ((1.0f - DotDistance) < CVarAimAtGazeDoneThreshold.GetValueOnGameThread())
{
bIsGazeAiming = false;
CurrentControlQuat = TargetQuat;
}
PlayerController->SetControlRotation(CurrentControlQuat.Rotator());
}
}
}

View File

@@ -0,0 +1,114 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#include "TobiiFireAtGazeComponent.h"
#include "TobiiInteractionsBlueprintLibrary.h"
#include "TobiiGTOMBlueprintLibrary.h"
#include "../TobiiInteractionsInternalTypes.h"
#include "Engine/Engine.h"
#include "IEyeTracker.h"
#include "Camera/CameraComponent.h"
#include "Engine/World.h"
#include "HAL/IConsoleManager.h"
static TAutoConsoleVariable<int32> CVarFireAtGazeEnabled(TEXT("tobii.interaction.FireAtGazeEnabled"), 1, TEXT("Fire at gaze lets you fire where you look when not in ADS (Aim Down Sights) mode. 0 - Fire at gaze is disabled. 1 - Fire at gaze is enabled."));
UTobiiFireAtGazeComponent::UTobiiFireAtGazeComponent()
: CameraComponent(nullptr)
, FireAtGazeTargetActor(nullptr)
, FireAtGazeTargetComponent(nullptr)
, FireAtGazeTargetLocation(0.0f, 0.0f, 0.0f)
, MaxDistance(10000.0f)
, NoTargetBehavior(ETobiiFireAtGazeNoTargetBehavior::PointGunToGaze)
, TraceChannel(ECC_Visibility)
{
PrimaryComponentTick.bCanEverTick = true;
}
bool UTobiiFireAtGazeComponent::FireAtGazeAvailable()
{
return CameraComponent != nullptr
&& UTobiiInteractionsBlueprintLibrary::IsFireAtGazeEnabled();
}
bool UTobiiFireAtGazeComponent::WantsCrosshair()
{
if (!FireAtGazeAvailable())
{
return false;
}
switch (NoTargetBehavior)
{
case ETobiiFireAtGazeNoTargetBehavior::PointGunForward:
return !FireAtGazeTargetActor.IsValid();
case ETobiiFireAtGazeNoTargetBehavior::PointGunToGaze:
return true;
default:
return false;
}
}
void UTobiiFireAtGazeComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (!FireAtGazeAvailable() || GEngine == nullptr || !GEngine->EyeTrackingDevice.IsValid())
{
return;
}
FTobiiGazeFocusData FocusData;
UTobiiGTOMBlueprintLibrary::GetFilteredGazeFocusData(FocusLayerFilters, bIsWhiteList, true, false, FocusData);
if (FocusData.FocusedPrimitiveComponent.IsValid())
{
FireAtGazeTargetActor = FocusData.FocusedActor;
FireAtGazeTargetComponent = FocusData.FocusedPrimitiveComponent;
FireAtGazeTargetLocation = FocusData.LastVisibleWorldLocation;
}
else
{
FVector GazeRayOrigin, GazeRayDirection;
switch (NoTargetBehavior)
{
case ETobiiFireAtGazeNoTargetBehavior::PointGunForward:
{
GazeRayOrigin = CameraComponent->GetComponentLocation();
GazeRayDirection = CameraComponent->GetForwardVector();
break;
}
case ETobiiFireAtGazeNoTargetBehavior::PointGunToGaze:
default:
{
FEyeTrackerGazeData CombinedGazeData;
GEngine->EyeTrackingDevice->GetEyeTrackerGazeData(CombinedGazeData);
GazeRayOrigin = CombinedGazeData.GazeOrigin;
GazeRayDirection = CombinedGazeData.GazeDirection;
break;
}
}
FHitResult HitResult;
UWorld* World = GetWorld();
if (World != nullptr && World->LineTraceSingleByChannel(HitResult, GazeRayOrigin, GazeRayOrigin + GazeRayDirection * MaxDistance, TraceChannel)
&& UTobiiGTOMBlueprintLibrary::IsPrimitiveComponentGazeFocusable(HitResult.GetComponent()))
{
FireAtGazeTargetActor = HitResult.GetActor();
FireAtGazeTargetComponent = HitResult.GetComponent();
FireAtGazeTargetLocation = HitResult.Location;
}
else
{
FireAtGazeTargetActor = nullptr;
FireAtGazeTargetComponent = nullptr;
FireAtGazeTargetLocation = GazeRayOrigin + GazeRayDirection * MaxDistance;
}
}
}

View File

@@ -0,0 +1,490 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
* Thanks to Jochen Schwarze (schwarze@isa.de) for functions to solve square, cubic and quartic functions as found here:
* https://github.com/erich666/GraphicsGems/blob/240a34f2ad3fa577ef57be74920db6c4b00605e4/gems/Roots3And4.c
******************************************************************************/
#include "TobiiInteractionsBlueprintLibrary.h"
#include "TobiiRootFinders.h"
#include "Components/WidgetComponent.h"
#include "IEyeTracker.h"
#include "Engine/Engine.h"
#include "Engine/World.h"
#include "Engine/Texture2D.h"
#include "Engine/TextureRenderTarget2D.h"
#include "Framework/Application/SlateApplication.h"
#include "HAL/IConsoleManager.h"
#include "Slate/WidgetRenderer.h"
UTobiiInteractionsBlueprintLibrary::UTobiiInteractionsBlueprintLibrary(const class FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
/**
* Renders a UMG Widget to a texture with the specified size.
*
* @param Widget The widget to be rendered.
* @param DrawSize The size to render the Widget to. Also will be the texture size.
* @return The texture containing the rendered widget.
*/
UTexture2D* UTobiiInteractionsBlueprintLibrary::TextureFromWidget(UUserWidget* const Widget, const FVector2D& DrawSize)
{
if (FSlateApplication::IsInitialized()
&& Widget != nullptr && Widget->IsValidLowLevel()
&& DrawSize.X >= 1 && DrawSize.Y >= 1)
{
TSharedPtr<SWidget> SlateWidget(Widget->TakeWidget());
if (!SlateWidget.IsValid())
{
return nullptr;
}
FWidgetRenderer WidgetRenderer = FWidgetRenderer(true);
UTextureRenderTarget2D* TextureRenderTarget = WidgetRenderer.DrawWidget(SlateWidget.ToSharedRef(), DrawSize);
// Creates Texture2D to store RenderTexture content
UTexture2D *Texture = UTexture2D::CreateTransient(DrawSize.X, DrawSize.Y, PF_B8G8R8A8);
#if WITH_EDITORONLY_DATA
Texture->MipGenSettings = TMGS_NoMipmaps;
#endif
// Lock and copies the data between the textures
TArray<FColor> SurfData;
FRenderTarget* RenderTarget = TextureRenderTarget->GameThread_GetRenderTargetResource();
RenderTarget->ReadPixels(SurfData);
void* TextureData = Texture->PlatformData->Mips[0].BulkData.Lock(LOCK_READ_WRITE);
const int32 TextureDataSize = SurfData.Num() * 4;
FMemory::Memcpy(TextureData, SurfData.GetData(), TextureDataSize);
Texture->PlatformData->Mips[0].BulkData.Unlock();
Texture->UpdateResource();
// Free resources
SurfData.Empty();
TextureRenderTarget->ConditionalBeginDestroy();
SlateWidget.Reset();
return Texture;
}
return nullptr;
}
bool UTobiiInteractionsBlueprintLibrary::IsInfiniteScreenEnabled()
{
static const auto EyetrackingEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.EnableEyetracking"));
static const auto InfiniteScreenEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.desktop.InfiniteScreenEnabled"));
if (EyetrackingEnabledCVar != nullptr && InfiniteScreenEnabledCVar != nullptr && GEngine != nullptr && GEngine->EyeTrackingDevice.IsValid())
{
return GEngine->EyeTrackingDevice->GetEyeTrackerStatus() >= EEyeTrackerStatus::Tracking
&& EyetrackingEnabledCVar->GetInt()
&& InfiniteScreenEnabledCVar->GetInt();
}
return false;
}
bool UTobiiInteractionsBlueprintLibrary::IsCleanUIEnabled()
{
static const auto EyetrackingEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.EnableEyetracking"));
static const auto CleanUIEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.interaction.EnableCleanUI"));
if (EyetrackingEnabledCVar != nullptr && CleanUIEnabledCVar != nullptr && GEngine != nullptr && GEngine->EyeTrackingDevice.IsValid())
{
return GEngine->EyeTrackingDevice->GetEyeTrackerStatus() >= EEyeTrackerStatus::Tracking
&& EyetrackingEnabledCVar->GetInt()
&& CleanUIEnabledCVar->GetInt();
}
return false;
}
bool UTobiiInteractionsBlueprintLibrary::IsAimAtGazeEnabled()
{
static const auto EyetrackingEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.EnableEyetracking"));
static const auto AimAtGazeEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.interaction.AimAtGazeEnabled"));
if (EyetrackingEnabledCVar != nullptr && AimAtGazeEnabledCVar != nullptr && GEngine != nullptr && GEngine->EyeTrackingDevice.IsValid())
{
return GEngine->EyeTrackingDevice->GetEyeTrackerStatus() >= EEyeTrackerStatus::Tracking
&& EyetrackingEnabledCVar->GetInt()
&& AimAtGazeEnabledCVar->GetInt();
}
return false;
}
bool UTobiiInteractionsBlueprintLibrary::IsFireAtGazeEnabled()
{
static const auto EyetrackingEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.EnableEyetracking"));
static const auto FireAtGazeEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.interaction.FireAtGazeEnabled"));
if (EyetrackingEnabledCVar != nullptr && FireAtGazeEnabledCVar != nullptr && GEngine != nullptr && GEngine->EyeTrackingDevice.IsValid())
{
return GEngine->EyeTrackingDevice->GetEyeTrackerStatus() >= EEyeTrackerStatus::Tracking
&& EyetrackingEnabledCVar->GetInt()
&& FireAtGazeEnabledCVar->GetInt();
}
return false;
}
bool UTobiiInteractionsBlueprintLibrary::IsThrowAtGazeEnabled()
{
static const auto EyetrackingEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.EnableEyetracking"));
static const auto ThrowAtGazeEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.interaction.ThrowAtGazeEnabled"));
if (EyetrackingEnabledCVar != nullptr && ThrowAtGazeEnabledCVar != nullptr && GEngine != nullptr && GEngine->EyeTrackingDevice.IsValid())
{
return GEngine->EyeTrackingDevice->GetEyeTrackerStatus() >= EEyeTrackerStatus::Tracking
&& EyetrackingEnabledCVar->GetInt()
&& ThrowAtGazeEnabledCVar->GetInt();
}
return false;
}
float UTobiiInteractionsBlueprintLibrary::CalculateSmoothPitchStep(float ViewPitch)
{
//Limit infinite screen yaw depending on input device pitch
float NormalizedPitch = FRotator::NormalizeAxis(ViewPitch);
float PitchScale = 1.0f - (FMath::Abs(NormalizedPitch) / 90.0f);
return FMath::SmoothStep(0.0f, 1.0f, PitchScale);
}
FRotator UTobiiInteractionsBlueprintLibrary::MakeInfiniteScreenCameraRotator(FRotator OriginalCameraRotation, FRotator InfiniteScreenAngles)
{
FQuat WorldSpaceYawRotation = FQuat(FVector::UpVector, InfiniteScreenAngles.Yaw);
FQuat LocalSpacePitchRotation = FQuat(FVector::RightVector, -InfiniteScreenAngles.Pitch);
FQuat InfiniteScreenViewRotation(OriginalCameraRotation);
InfiniteScreenViewRotation = InfiniteScreenViewRotation * LocalSpacePitchRotation; //Local space by multiplying on the right
InfiniteScreenViewRotation = WorldSpaceYawRotation * InfiniteScreenViewRotation; //World space by multiplying on the left
return InfiniteScreenViewRotation.Rotator();
}
////////////////////////////////////////////////////////////////////////////
void UTobiiInteractionsBlueprintLibrary::FindRealSquareRoots(float A, float B, float C, TArray<float>& OutRealRoots)
{
double Coefficients[3]{ C, B, A };
double Solutions[2]{ 0.0, 0.0 };
int32 NrSolutions = SolveQuadric(Coefficients, Solutions);
OutRealRoots.Empty(NrSolutions);
for (int32 SolutionIdx = 0; SolutionIdx < NrSolutions; SolutionIdx++)
{
OutRealRoots.Add(Solutions[SolutionIdx]);
}
}
void UTobiiInteractionsBlueprintLibrary::FindRealCubicRoots(float A, float B, float C, float D, TArray<float>& OutRealRoots)
{
double Coefficients[4]{ D, C, B, A };
double Solutions[3]{ 0.0, 0.0, 0.0 };
int32 NrSolutions = SolveCubic(Coefficients, Solutions);
OutRealRoots.Empty(NrSolutions);
for (int32 SolutionIdx = 0; SolutionIdx < NrSolutions; SolutionIdx++)
{
OutRealRoots.Add(Solutions[SolutionIdx]);
}
}
void UTobiiInteractionsBlueprintLibrary::FindRealQuarticRoots(float A, float B, float C, float D, float E, TArray<float>& OutRealRoots)
{
double Coefficients[5]{ E, D, C, B, A };
double Solutions[4]{ 0.0, 0.0, 0.0, 0.0 };
int32 NrSolutions = SolveQuartic(Coefficients, Solutions);
OutRealRoots.Empty(NrSolutions);
for (int32 SolutionIdx = 0; SolutionIdx < NrSolutions; SolutionIdx++)
{
OutRealRoots.Add(Solutions[SolutionIdx]);
}
}
/**
* Try to find the appropriate acceleration to hit a moving target.
* We do this by setting up an equation system with 4 equations in 4 unknowns.
* First we solve for time, and then we plug that into the other equations to find the wanted acceleration.
*
* VARIABLES:
* Time: t <--- Need to first solve for this
* Projectile Pos: PPX, PPY, PPZ
* Projectile Vel: PVX, PVY, PVZ
* Projectile Acc: pax, pay, paz <--- Solving for this is our goal
* Projectile AccMagnitude: PAM
* Target Pos: TPX, TPY, TPZ
* Target Vel: TVX, TVY, TVZ
* Target Acc: TAX, TAY, TAZ
*
* EQUATIONS:
* PPX + PVX * t + (1/2) * pax * t^2 = TPX + TVX * t + (1/2) * TAX * t^2
* PPY + PVY * t + (1/2) * pay * t^2 = TPY + TVY * t + (1/2) * TAY * t^2
* PPZ + PVZ * t + (1/2) * paz * t^2 = TPZ + TVZ * t + (1/2) * TAZ * t^2
* PAM^2 = pax^2 + pay^2 + paz^2
*
* SOLVE FOR ACCELERATION:
* pax = 2.0 * (TPX + TVX * t + (1/2) * TAX * t^2 - PPX - PVX * t) / t^2
* pay = 2.0 * (TPY + TVY * t + (1/2) * TAY * t^2 - PPY - PVY * t) / t^2
* paz = 2.0 * (TPZ + TVZ * t + (1/2) * TAZ * t^2 - PPZ - PVZ * t) / t^2
*
* SIMPLIFY:
* DPX = TPX - PPX
* DPY = TPY - PPY
* DPZ = TPZ - PPZ
* DVX = TVX - PVX
* DVY = TVY - PVY
* DVZ = TVZ - PVZ
* pax = 2.0 * (DPX + DVX * t + (1/2) * TAX * t^2) / t^2
* pay = 2.0 * (DPY + DVY * t + (1/2) * TAY * t^2) / t^2
* paz = 2.0 * (DPZ + DVZ * t + (1/2) * TAZ * t^2) / t^2
*
* SQUARE SO WE CAN SUBSTITUTE LATER:
* pax^2 = 4.0 * (DPX + DVX * t + (1/2) * TAX * t^2)^2 / t^4
* pay^2 = 4.0 * (DPY + DVY * t + (1/2) * TAY * t^2)^2 / t^4
* paz^2 = 4.0 * (DPZ + DVZ * t + (1/2) * TAZ * t^2)^2 / t^4
*
* EXPAND THE SQUARES SO WE CAN EXTRACT COEFFICIENTS LATER:
* pax^2 = 4.0 * (DPX^2 + 2.0*DPX*DVX*t + DPX*TAX*t^2 + DVX^2*t^2 + DVX*TAX*t^3 + (1/4)*TAX^2*t^4) / t^4
* pay^2 = 4.0 * (DPY^2 + 2.0*DPY*DVY*t + DPY*TAY*t^2 + DVY^2*t^2 + DVY*TAY*t^3 + (1/4)*TAY^2*t^4) / t^4
* paz^2 = 4.0 * (DPZ^2 + 2.0*DPZ*DVZ*t + DPZ*TAZ*t^2 + DVZ^2*t^2 + DVZ*TAZ*t^3 + (1/4)*TAZ^2*t^4) / t^4
*
* SUBSTITUTE INTO OUR PAM EQUATION:
* PAM^2 = 4.0 * (DPX^2 + 2.0*DPX*DVX*t + DPX*TAX*t^2 + DVX^2*t^2 + DVX*TAX*t^3 + (1/4)*TAX^2*t^4) / t^4
* + 4.0 * (DPY^2 + 2.0*DPY*DVY*t + DPY*TAY*t^2 + DVY^2*t^2 + DVY*TAY*t^3 + (1/4)*TAY^2*t^4) / t^4
* + 4.0 * (DPZ^2 + 2.0*DPZ*DVZ*t + DPZ*TAZ*t^2 + DVZ^2*t^2 + DVZ*TAZ*t^3 + (1/4)*TAZ^2*t^4) / t^4
*
* MULTIPLY BY t^4:
* PAM^2 * t^4 = 4.0 * (DPX^2 + 2.0*DPX*DVX*t + DPX*TAX*t^2 + DVX^2*t^2 + DVX*TAX*t^3 + (1/4)*TAX^2*t^4)
* + 4.0 * (DPY^2 + 2.0*DPY*DVY*t + DPY*TAY*t^2 + DVY^2*t^2 + DVY*TAY*t^3 + (1/4)*TAY^2*t^4)
* + 4.0 * (DPZ^2 + 2.0*DPZ*DVZ*t + DPZ*TAZ*t^2 + DVZ^2*t^2 + DVZ*TAZ*t^3 + (1/4)*TAZ^2*t^4)
*
* MAKE 0 EQUATION AND FORM TERMS:
* 0 = 4.0*DPX^2 + 8.0*DPX*DVX*t + 4.0*DPX*TAX*t^2 + 4.0*DVX^2*t^2 + 4.0*DVX*TAX*t^3 + TAX^2*t^4
* + 4.0*DPY^2 + 8.0*DPY*DVY*t + 4.0*DPY*TAY*t^2 + 4.0*DVY^2*t^2 + 4.0*DVY*TAY*t^3 + TAY^2*t^4
* + 4.0*DPZ^2 + 8.0*DPZ*DVZ*t + 4.0*DPZ*TAZ*t^2 + 4.0*DVZ^2*t^2 + 4.0*DVZ*TAZ*t^3 + TAZ^2*t^4
* - PAM^2 * t^4
*
* ARRANGE IN COEFFICIENT FORM:
* 0 = (TAX^2 + TAY^2 + TAZ^2 - PAM^2) * t^4
* 4.0 * (DVX*TAX + DVY*TAY + DVZ*TAZ) * t^3
* 4.0 * (DVX^2 + DVY^2 + DVZ^2 + DPX*TAX + DPY*TAY + DPZ*TAZ) * t^2
* 8.0 * (DPX*DVX + DPY*DVY + DPZ*DVZ) * t^1
* 4.0 * (DPX^2 + DPY^2 + DPZ^2) * t^0
*
* Solve the quartic!
* Then finally insert the smallest root (time) into the (pax, pay, paz) formulas to get the wanted acceleration.
*/
bool UTobiiInteractionsBlueprintLibrary::FindNeededAccelerationForAccelerationBasedHomingProjectile(const FTobiiAccelerationBasedHomingData& InputData, FTobiiAccelerationBasedHomingResult& BestResult)
{
const FVector DeltaPosition = InputData.TargetPosition - InputData.ProjectilePosition;
if (DeltaPosition.SizeSquared() < FLT_EPSILON)
{
return false;
}
const FVector DeltaVelocity = InputData.TargetVelocity - InputData.ProjectileVelocity;
const double T4Coefficient = FVector::DotProduct(InputData.TargetAcceleration, InputData.TargetAcceleration) - InputData.ProjectileAccelerationMagnitude * InputData.ProjectileAccelerationMagnitude;
const double T3Coefficient = 4.0 * FVector::DotProduct(DeltaVelocity, InputData.TargetAcceleration);
const double T2Coefficient = 4.0 * FVector::DotProduct(DeltaVelocity, DeltaVelocity) + FVector::DotProduct(DeltaPosition, InputData.TargetAcceleration);
const double T1Coefficient = 8.0 * FVector::DotProduct(DeltaPosition, DeltaVelocity);
const double T0Coefficient = 4.0 * FVector::DotProduct(DeltaPosition, DeltaPosition);
TArray<double> Solutions;
double DirectHitCoefficients[5]{ T0Coefficient, T1Coefficient, T2Coefficient, T3Coefficient, T4Coefficient };
double DirectHitSolutions[4]{ 0.0, 0.0, 0.0, 0.0 };
int32 NrDirectHitSolutions = SolveQuartic(DirectHitCoefficients, DirectHitSolutions);
for (int32 SolutionIdx = 0; SolutionIdx < NrDirectHitSolutions; SolutionIdx++)
{
const double Solution = DirectHitSolutions[SolutionIdx];
if (FMath::IsFinite(Solution) && !FMath::IsNaN(Solution) && Solution > DBL_EPSILON)
{
Solutions.Add(Solution);
}
}
if (Solutions.Num() == 0 && InputData.bAttemptClosestApproachSolution)
{
//Since we couldn't find a direct hit, attempt to find a closest approach instead as backup.
double ClosestApproachCoefficients[4]{ T1Coefficient, 2.0 * T2Coefficient, 3.0 * T3Coefficient, 4.0 * T4Coefficient };
double ClosestApproachSolutions[3]{ 0.0, 0.0, 0.0 };
TMap<double, double> ClosestApproachDistancesToInterceptTimes;
int32 NrClosestApproachSolutions = SolveCubic(ClosestApproachCoefficients, ClosestApproachSolutions);
for (int32 SolutionIdx = 0; SolutionIdx < NrClosestApproachSolutions; SolutionIdx++)
{
const double Solution = ClosestApproachSolutions[SolutionIdx];
if (FMath::IsFinite(Solution) && !FMath::IsNaN(Solution) && Solution > DBL_EPSILON
&& Solution >= 0.0 && !ClosestApproachDistancesToInterceptTimes.Contains(Solution))
{
const double Real = Solution;
const double RealSq = Real * Real;
const double RealCub = RealSq * Real;
const double RealQuart = RealCub * Real;
const double Distance = FMath::Abs(T4Coefficient * RealQuart + T3Coefficient * RealCub + T2Coefficient * RealSq + T1Coefficient * Real + T0Coefficient);
ClosestApproachDistancesToInterceptTimes.Add(Distance, Real);
}
}
ClosestApproachDistancesToInterceptTimes.KeySort(TLess<float>());
for (auto& Pair : ClosestApproachDistancesToInterceptTimes)
{
Solutions.Add(Pair.Value);
break;
}
}
if (Solutions.Num() == 0)
{
return false;
}
Solutions.Sort(TLess<double>());
const double InterceptTime = Solutions[0];
const double InterceptTimeSquare = InterceptTime * InterceptTime;
BestResult.Type = ETobiiInterceptType::DirectHit;
BestResult.ExpectedInterceptTimeSecs = InterceptTime;
BestResult.ExpectedInterceptLocation = InputData.TargetPosition + InputData.TargetVelocity * InterceptTime + (1.0 / 2.0) * InputData.TargetAcceleration * InterceptTimeSquare;
BestResult.SuggestedAcceleration = 2.0 * (DeltaPosition + DeltaVelocity * InterceptTime + (1.0 / 2.0) * InputData.TargetAcceleration * InterceptTimeSquare) / InterceptTimeSquare;
return true;
}
/**
* Try to find the appropriate velocity to hit a moving target given a ballistic projectile.
* We do this by setting up an equation system with 4 equations in 4 unknowns.
* First we solve for time, and then we plug that into the other equations to find the wanted acceleration.
*
* VARIABLES:
* Time: t <--- Need to first solve for this
* Projectile Pos: PPX, PPY, PPZ
* Projectile Vel: pvx, pvy, pvz
* Projectile Gravity: PAX, PAY, PAZ <--- Most likely gravity
* Projectile Apex: PAPEX <--- This is at when (1/2)*t
* Target Pos: TPX, TPY, TPZ
* Target Vel: TVX, TVY, TVZ
* Target Acc: TAX, TAY, TAZ
*
* END EQUATIONS:
* PPX + pvx * t + (1/2) * PAX * t^2 = TPX + TVX * t + (1/2) * TAX * t^2
* PPY + pvy * t + (1/2) * PAY * t^2 = TPY + TVY * t + (1/2) * TAY * t^2
* PPZ + pvz * t + (1/2) * PAZ * t^2 = TPZ + TVZ * t + (1/2) * TAZ * t^2
*
* KNOWN Z EQUATIONS:
* PAPEX = PPZ + (1/2) * pvz * t + (1/8) * PAZ * t^2
*
* SOLVE FOR VELOCITY:
* pvx = (TPX + TVX * t + (1/2) * TAX * t^2 - PPX - (1/2) * PAX * t^2) / t
* pvy = (TPY + TVY * t + (1/2) * TAY * t^2 - PPY - (1/2) * PAY * t^2) / t
* pvz = (TPZ + TVZ * t + (1/2) * TAZ * t^2 - PPZ - (1/2) * PAZ * t^2) / t
*
* SIMPLIFY:
* DPX = TPX - PPX
* DPY = TPY - PPY
* DPZ = TPZ - PPZ
* DAX = (1/2) * (TAX - PAX)
* DAY = (1/2) * (TAY - PAY)
* DAZ = (1/2) * (TAZ - PAZ)
* pvx = (DPX + TVX * t + DAX * t^2) / t
* pvy = (DPY + TVY * t + DAY * t^2) / t
* pvz = (DPZ + TVZ * t + DAZ * t^2) / t
*
* SUBSTITUTE INTO OUR MIDPOINT EQUATIONS:
* PAPEX = PPZ + (1/2) * [(DPZ + TVZ * t + DAZ * t^2) / t] * t + (1/8) * PAZ * t^2
*
* MAKE 0 EQUATION AND FORM TERMS:
* 0 = PPZ + (1/2) * DPZ - PAPEX + (1/2) * TVZ * t + (1/2) * DAZ * t^2 + (1/8) * PAZ * t^2
*
* ARRANGE IN COEFFICIENT FORM:
* 0 = ((1/2) * DAZ + (1/8) * PAZ) * t^2
* (1/2) * TVZ * t^1
* PPZ + (1/2) * DPZ - PAPEX * t^0
*
* Solve the square for time!
* Then finally insert the roots (time) into the (pvx, pvy, pvz) formulas to get the possible velocities.
*/
bool UTobiiInteractionsBlueprintLibrary::FindNeededInitialVelocityForBallisticProjectile(const FTobiiBallisticData& InputData, TArray<FTobiiBallisticResult>& Results)
{
const FVector DeltaPosition = InputData.TargetPosition - InputData.ProjectileInitialPosition;
if (DeltaPosition.SizeSquared() < FLT_EPSILON)
{
return false;
}
const float ApexZ = FMath::Max(InputData.ProjectileInitialPosition.Z, InputData.TargetPosition.Z) + InputData.ProjectileApexOffsetCm;
const FVector DeltaAcceleration = 0.5f * (InputData.TargetAcceleration - InputData.ProjectileAcceleration);
const double T2Coefficient = 0.5 * DeltaAcceleration.Z + 0.125 * InputData.ProjectileAcceleration.Z;
const double T1Coefficient = 0.5 * InputData.TargetVelocity.Z;
const double T0Coefficient = InputData.ProjectileInitialPosition.Z + 0.5 * DeltaPosition.Z - ApexZ;
double TimeCoefficients[3] { T0Coefficient, T1Coefficient, T2Coefficient };
double TimeSolutions[2] { 0.0, 0.0 };
int32 NrTimeSolutions = SolveQuadric(TimeCoefficients, TimeSolutions);
for (int32 SolutionIdx = 0; SolutionIdx < NrTimeSolutions; SolutionIdx++)
{
const double Time = TimeSolutions[SolutionIdx];
if (FMath::IsFinite(Time) && !FMath::IsNaN(Time) && Time > DBL_EPSILON)
{
const double HTime = Time / 2.0;
const double HTimeSq = HTime * HTime;
const double TimeSq = Time * Time;
FTobiiBallisticResult NewResult;
NewResult.ExpectedInterceptTimeSecs = Time;
NewResult.SuggestedInitialVelocity = (DeltaPosition + InputData.TargetVelocity * Time + DeltaAcceleration * TimeSq) / Time;
NewResult.ExpectedInterceptLocation = InputData.ProjectileInitialPosition + NewResult.SuggestedInitialVelocity * Time + 0.5f * InputData.ProjectileAcceleration * TimeSq;
Results.Add(NewResult);
}
}
return Results.Num() > 0;
}
bool UTobiiInteractionsBlueprintLibrary::TraceBallisticProjectilePath(UObject* WorldContextObject, const FTobiiProjectileTraceData& InputData, TArray<FVector>& OutTracedPath, FHitResult& OutHitResult)
{
if (WorldContextObject == nullptr)
{
return false;
}
float CurrentTime = InputData.StepSizeSecs;
FVector StartPoint = InputData.ProjectileInitialPosition;
FVector EndPoint = StartPoint + InputData.ProjectileVelocity * CurrentTime + 0.5f * InputData.ProjectileAcceleration * CurrentTime * CurrentTime;
FCollisionQueryParams CollisionParams;
CollisionParams.AddIgnoredActors(InputData.IgnoredActors);
OutTracedPath.Empty();
OutTracedPath.Add(StartPoint);
for (int32 StepCount = 0; StepCount < InputData.MaxNrSteps; StepCount++)
{
if (WorldContextObject->GetWorld()->SweepSingleByChannel(OutHitResult, StartPoint, EndPoint, FQuat::Identity, InputData.TraceChannel, FCollisionShape::MakeSphere(InputData.TraceRadiusCm), CollisionParams))
{
OutTracedPath.Add(OutHitResult.Location);
return true;
}
else
{
OutTracedPath.Add(EndPoint);
}
StartPoint = EndPoint;
CurrentTime += InputData.StepSizeSecs;
EndPoint = InputData.ProjectileInitialPosition + InputData.ProjectileVelocity * CurrentTime + 0.5f * InputData.ProjectileAcceleration * CurrentTime * CurrentTime;
}
return false;
}

View File

@@ -0,0 +1,11 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "CoreMinimal.h"
DEFINE_LOG_CATEGORY_STATIC(LogTobiiInteraction, All, All);

View File

@@ -0,0 +1,32 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#include "TobiiInteractionsModule.h"
#include "TobiiInteractionsStyle.h"
#include "GameFramework/HUD.h"
IMPLEMENT_MODULE(FTobiiInteractionsModule, TobiiInteractions);
void FTobiiInteractionsModule::StartupModule()
{
#if WITH_EDITOR
if (GIsEditor)
{
FTobiiInteractionsStyle::Initialize();
}
#endif
}
void FTobiiInteractionsModule::ShutdownModule()
{
#if WITH_EDITOR
if (GIsEditor)
{
FTobiiInteractionsStyle::Shutdown();
}
#endif
}

View File

@@ -0,0 +1,19 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleInterface.h"
#include "Modules/ModuleManager.h"
class FTobiiInteractionsModule : public IModuleInterface
{
private:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};

View File

@@ -0,0 +1,77 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#include "TobiiInteractionsStyle.h"
#include "Framework/Application/SlateApplication.h"
#include "Slate/SlateGameResources.h"
#include "Styling/SlateStyleRegistry.h"
TSharedPtr<FSlateStyleSet> FTobiiInteractionsStyle::TobiiEyetrackingInteractionsStyleInstance = nullptr;
void FTobiiInteractionsStyle::Initialize()
{
if (!TobiiEyetrackingInteractionsStyleInstance.IsValid())
{
TobiiEyetrackingInteractionsStyleInstance = Create();
FSlateStyleRegistry::RegisterSlateStyle(*TobiiEyetrackingInteractionsStyleInstance);
}
}
void FTobiiInteractionsStyle::Shutdown()
{
FSlateStyleRegistry::UnRegisterSlateStyle(*TobiiEyetrackingInteractionsStyleInstance);
ensure(TobiiEyetrackingInteractionsStyleInstance.IsUnique());
TobiiEyetrackingInteractionsStyleInstance.Reset();
}
FName FTobiiInteractionsStyle::GetStyleSetName()
{
static FName StyleSetName(TEXT("TobiiStyle"));
return StyleSetName;
}
#define IMAGE_BRUSH( RelativePath, ... ) FSlateImageBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ )
#define BOX_BRUSH( RelativePath, ... ) FSlateBoxBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ )
#define BORDER_BRUSH( RelativePath, ... ) FSlateBorderBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ )
#define TTF_FONT( RelativePath, ... ) FSlateFontInfo( Style->RootToContentDir( RelativePath, TEXT(".ttf") ), __VA_ARGS__ )
#define OTF_FONT( RelativePath, ... ) FSlateFontInfo( Style->RootToContentDir( RelativePath, TEXT(".otf") ), __VA_ARGS__ )
const FVector2D Icon16x16(16.0f, 16.0f);
const FVector2D Icon20x20(20.0f, 20.0f);
const FVector2D Icon40x40(40.0f, 40.0f);
TSharedRef<FSlateStyleSet> FTobiiInteractionsStyle::Create()
{
TSharedRef<FSlateStyleSet> Style = MakeShareable(new FSlateStyleSet("TobiiStyle"));
#if TOBII_COMPILE_AS_ENGINE_PLUGIN
Style->SetContentRoot(FPaths::EnginePluginsDir() / TEXT("Runtime/TobiiEyetracking/Resources"));
#else
Style->SetContentRoot(FPaths::ProjectPluginsDir() / TEXT("TobiiEyetracking/Resources"));
#endif
//The names here after ClassIcon. must match the type names of the types we want to offer the icon for.
Style->Set("ClassIcon.TobiiCleanUIContainer", new IMAGE_BRUSH(TEXT("CleanUI16x"), Icon16x16));
return Style;
}
#undef IMAGE_BRUSH
#undef BOX_BRUSH
#undef BORDER_BRUSH
#undef TTF_FONT
#undef OTF_FONT
void FTobiiInteractionsStyle::ReloadTextures()
{
FSlateApplication::Get().GetRenderer()->ReloadTextureResources();
}
const ISlateStyle& FTobiiInteractionsStyle::Get()
{
return *TobiiEyetrackingInteractionsStyleInstance;
}

View File

@@ -0,0 +1,24 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "CoreMinimal.h"
#include "Styling/ISlateStyle.h"
class TOBIIINTERACTIONS_API FTobiiInteractionsStyle
{
public:
static void Initialize();
static void Shutdown();
static void ReloadTextures();
static const ISlateStyle& Get();
static FName GetStyleSetName();
private:
static TSharedRef<class FSlateStyleSet> Create();
static TSharedPtr<class FSlateStyleSet> TobiiEyetrackingInteractionsStyleInstance;
};

View File

@@ -0,0 +1,279 @@
/*
* Utility functions to find cubic and quartic roots,
* coefficients are passed like this:
*
* c[0] + c[1]*x + c[2]*x^2 + c[3]*x^3 + c[4]*x^4 = 0
*
* The functions return the number of non-complex roots and
* put the values into the s array.
*
* Author: Jochen Schwarze (schwarze@isa.de)
*
* Jan 26, 1990 Version for Graphics Gems
* Oct 11, 1990 Fixed sign problem for negative q's in SolveQuartic
* (reported by Mark Podlipec),
* Old-style function definitions,
* IsZero() as a macro
* Nov 23, 1990 Some systems do not declare acos() and cbrt() in
* <math.h>, though the functions exist in the library.
* If large coefficients are used, EQN_EPS should be
* reduced considerably (e.g. to 1E-30), results will be
* correct but multiple roots might be reported more
* than once.
*
* Graphics gems resources:
* http://www.realtimerendering.com/resources/GraphicsGems/
*
* License:
* All graphics gems code can be used without restrictions. [See above link]
*/
#pragma once
#include <math.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
/* epsilon surrounding for near zero values */
#define EQN_EPS 1E-30
#define IsZero(x) ((x) > -EQN_EPS && (x) < EQN_EPS)
#define IsOne(x) ((x) > (1.0-EQN_EPS) && (x) < (1.0+EQN_EPS))
#ifdef NOCBRT
#define cbrt(x) ((x) > 0.0 ? pow((double)(x), 1.0/3.0) : \
((x) < 0.0 ? -pow((double)-(x), 1.0/3.0) : 0.0))
#endif
int SolveQuadric(double c[3], double s[2])
{
double p, q, D;
if (IsZero(c[2]))
{
if (IsZero(c[1]))
{
return 0;
}
s[0] = -c[0] / c[1];
return 1;
}
/* normal form: x^2 + px + q = 0 */
p = c[ 1 ] / (2 * c[ 2 ]);
q = c[ 0 ] / c[ 2 ];
D = p * p - q;
if (IsZero(D))
{
s[0] = -p;
return 1;
}
else if (D < 0)
{
return 0;
}
else /* if (D > 0) */
{
double sqrt_D = sqrt(D);
s[0] = sqrt_D - p;
s[1] = -sqrt_D - p;
return 2;
}
}
int SolveCubic(double c[4], double s[3])
{
int i, num;
double sub;
double A, B, C;
double sq_A, p, q;
double cb_p, D;
if (IsZero(c[3]))
{
SolveQuadric(c, s);
}
/* normal form: x^3 + Ax^2 + Bx + C = 0 */
A = c[2];
B = c[1];
C = c[0];
if (!IsOne(c[3]))
{
A /= c[3];
B /= c[3];
C /= c[3];
}
/* substitute x = y - A/3 to eliminate quadric term:
x^3 +px + q = 0 */
sq_A = A * A;
p = 1.0/3 * (- 1.0/3 * sq_A + B);
q = 1.0/2 * (2.0/27 * A * sq_A - 1.0/3 * A * B + C);
/* use Cardano's formula */
cb_p = p * p * p;
D = q * q + cb_p;
if (IsZero(D))
{
if (IsZero(q)) /* one triple solution */
{
s[ 0 ] = 0;
num = 1;
}
else /* one single and one double solution */
{
double u = cbrt(-q);
s[ 0 ] = 2 * u;
s[ 1 ] = - u;
num = 2;
}
}
else if (D < 0) /* Casus irreducibilis: three real solutions */
{
double phi = 1.0 / 3 * acos(-q / sqrt(-cb_p));
double t = 2 * sqrt(-p);
s[0] = t * cos(phi);
s[1] = -t * cos(phi + M_PI / 3);
s[2] = -t * cos(phi - M_PI / 3);
num = 3;
}
else /* one real solution */
{
double sqrt_D = sqrt(D);
double u = cbrt(sqrt_D - q);
double v = - cbrt(sqrt_D + q);
s[ 0 ] = u + v;
num = 1;
}
/* resubstitute */
sub = 1.0/3 * A;
for (i = 0; i < num; ++i)
s[ i ] -= sub;
return num;
}
int SolveQuartic(double c[5], double s[4])
{
double coeffs[ 4 ];
double z, u, v, sub;
double A, B, C, D;
double sq_A, p, q, r;
int i, num;
if (IsZero(c[4]))
{
SolveCubic(c, s);
}
/* normal form: x^4 + Ax^3 + Bx^2 + Cx + D = 0 */
A = c[3];
B = c[2];
C = c[1];
D = c[0];
if (!IsOne(c[4]))
{
A /= c[4];
B /= c[4];
C /= c[4];
D /= c[4];
}
/* substitute x = y - A/4 to eliminate cubic term:
x^4 + px^2 + qx + r = 0 */
sq_A = A * A;
p = - 3.0/8 * sq_A + B;
q = 1.0/8 * sq_A * A - 1.0/2 * A * B + C;
r = - 3.0/256*sq_A*sq_A + 1.0/16*sq_A*B - 1.0/4*A*C + D;
if (IsZero(r))
{
/* no absolute term: y(y^3 + py + q) = 0 */
coeffs[0] = q;
coeffs[1] = p;
coeffs[2] = 0;
coeffs[3] = 1;
num = SolveCubic(coeffs, s);
s[num++] = 0;
}
else
{
/* solve the resolvent cubic ... */
coeffs[ 0 ] = 1.0/2 * r * p - 1.0/8 * q * q;
coeffs[ 1 ] = - r;
coeffs[ 2 ] = - 1.0/2 * p;
coeffs[ 3 ] = 1;
(void) SolveCubic(coeffs, s);
/* ... and take the one real solution ... */
z = s[ 0 ];
/* ... to build two quadric equations */
u = z * z - r;
v = 2 * z - p;
if (IsZero(u))
u = 0;
else if (u > 0)
u = sqrt(u);
else
return 0;
if (IsZero(v))
v = 0;
else if (v > 0)
v = sqrt(v);
else
return 0;
coeffs[ 0 ] = z - u;
coeffs[ 1 ] = q < 0 ? -v : v;
coeffs[ 2 ] = 1;
num = SolveQuadric(coeffs, s);
coeffs[ 0 ]= z + u;
coeffs[ 1 ] = q < 0 ? v : -v;
coeffs[ 2 ] = 1;
num += SolveQuadric(coeffs, s + num);
}
/* resubstitute */
sub = 1.0/4 * A;
for (i = 0; i < num; ++i)
s[ i ] -= sub;
return num;
}

View File

@@ -0,0 +1,120 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "TobiiProjectileComponent.generated.h"
UENUM()
enum class ETobiiProjectileHomingBehavior
{
//This is the default behavior. The projectile will accelerate towards the target. This also means the projectile will speed up. If this is not what you want, consider using another behavior.
Acceleration,
//In this mode, imagine the projectile has side thrusters to enable it to turn gradually towards the target. This behavior preserves projectile speed.
Steering
};
UENUM()
enum class ETobiiProjectileGuidanceSystem
{
//This is the default behavior. The projectile will home towards the target's last known location.
Simple,
//This mode will predict possible impact times and locations to avoid orbiting. Considers position, velocity and acceleration. Is quite expensive.
Prediction,
//In addition to normal prediction, this guidance system mode will also calculate the closest approach if no direct hit is possible. More expensive than standard prediction
ComplexPrediction
};
UCLASS(BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent), ClassGroup=(Tobii))
class UTobiiProjectileComponent : public UProjectileMovementComponent
{
GENERATED_BODY()
public:
//Use this if you need to set starting values from the outside, don't use Velocity.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile")
FVector InitialVelocity;
//If this is true, gravity acceleration will be added to the projectile.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile")
bool bAffectedByGravity;
//If bAffectedByGravity is true, this is the gravity acceleration vector that will be applied to the projectile. If this is zero, the default GetGravityZ will be used.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile")
FVector WorldSpaceGravity;
//If fuel runs out, homing behavior stops for the projectile. 0 means infinite fuel.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile")
float InitialFuelSecs;
//The projectile is destroyed if it manages to stay alive for this amount of time. 0 means infinite lifetime.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile")
float MaxLifeTimeSecs;
public:
//Allows you to customize the homing behavior if you have homing projectiles on. The homing target will always be set to the user's gaze target.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Homing")
ETobiiProjectileHomingBehavior HomingBehavior;
//This is the fastest the projectile is able to turn when using the steering homing behavior.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Homing")
float SteeringMaxTurnSpeedDegPerSec;
public:
//Allows you to customize the targeting part of the homing behavior. The different options generally provide different trade offs between performance and accuracy. Good accuracy also helps to avoid orbiting projectiles.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Guidance System")
ETobiiProjectileGuidanceSystem GuidanceSystem;
//This is how often the guidance system will update it's target location. A higher frequency will be more expensive but more accurate and vice versa. The guidance system will never update more than once per frame. 0 means every frame.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Guidance System")
float GuidanceSystemUpdateFreq;
//Sometimes you might not want the homing behavior to track targets that are too far off course. This is the maximium angle allowed between the projectile forward vector and the vector towards the target. If this threshold is breached, the homing will stop. 0 means there is no threshold.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Guidance System")
float GuidanceSystemMaximumTargetAngleDeg;
public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Guidance System")
FVector GuidanceSystemTarget;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Guidance System")
FVector AccelerationVectorTowardsTarget;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Guidance System")
bool bTargetOffCourse;
public:
//This function will attempt to set up the actor to start homing. This entails disabling physics simulation etc. Please note that if the actor is intersecting the ground or other geometry, the simulation might stop immediately since it will "hit" it. To avoid this, set the appropriate collision masks or turn off collision during launch.
UFUNCTION(BlueprintCallable, Category = "Homing")
void StartHoming(USceneComponent* NewHomingTargetComponent);
UFUNCTION(BlueprintCallable, Category = "Homing")
void StopHoming();
public:
UTobiiProjectileComponent();
public:
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
virtual FVector ComputeAcceleration(const FVector& InVelocity, float DeltaTime) const override;
virtual void SimulateGuidanceSystem();
private:
bool bCanThrust;
bool bUsesFuel;
float CurrentFuelSecs;
float CurrentLifeTime;
float GuidanceSystemUpdateTimerSecsLeft;
FVector LastTargetVelocity;
FVector TargetAcceleration;
};

View File

@@ -0,0 +1,169 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "TobiiInteractionsTypes.h"
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Components/PrimitiveComponent.h"
#include "TobiiThrowAtGazeComponent.generated.h"
/**
* This component will generate gaze contingent throw vectors on demand.
* You can use this vector to either simulate a trajectory yourself, or simply apply it to the rigid body of some object.
*/
UCLASS(BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent), ClassGroup = (Tobii))
class TOBIIINTERACTIONS_API UTobiiThrowAtGazeComponent : public UActorComponent
{
GENERATED_BODY()
public:
UTobiiThrowAtGazeComponent();
public:
// This is the component that you're trying to hit with the throw. You can set this on the same frame you are trying to throw, but setting it earlier will allow the component to "aim" a bit better. If this is not set, you can still use the component, but it will then try to simply use the gaze vector instead.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze")
TWeakObjectPtr<UPrimitiveComponent> ThrowTarget;
// Throw force applied to the throw vector can never be greater than this value.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze")
float MaxThrowSpeedCmPerSecs;
// This is how high above the thrower the arc apex will be.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze")
float ThrowApexOffsetCm;
// This is how many times we will run the angle finding code. More iterations will mean more tested apexes between the limits below. Setting this to 0 will only run one test using the average of the apexes.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration")
int32 MaxIterations;
// If this is true, the algorithm will always test low apexes first.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration")
bool bAlwaysPreferLowApex;
// If this is true, the algorithm will prefer low apexes rather than mid or high ones for a more natural looking throw.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration")
bool bPreferLowApexForTargetsBelowThrower;
// If this is true, the algorithm will attempt to trace the solution in the world to make sure nothing is in the way.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration")
bool bShouldTraceResult;
// If bShouldTraceResult is true, an apex smaller than this value will never be tested
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration")
float ThrowApexOffsetMinimumCm;
// If bShouldTraceResult is true, an apex larger than this value will never be tested
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration")
float ThrowApexOffsetMaximumCm;
// When testing different apexes, if the delta between the last test and the next one is smaller than this value, tests will be aborted
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration")
float MinimumApexDeltaCm;
// If bShouldTraceResult is true, this is the size that will be used for tracing. Should be larger or equal to the projectile size.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration")
float TraceRadiusCm;
// If bShouldTraceResult is true, this is the length of each trace step done.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration")
int32 MaxNrTraceSteps;
// If bShouldTraceResult is true, this is the length of each trace step done in seconds.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration")
float TraceStepSizeSecs;
// We don't allow traces longer than this
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration")
float MaxTraceDistance;
// If we don't have a ThrowTarget, we will consider hits closer than this distance to the target to be "hits"
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration")
float NoTargetAcceptanceThreshold;
// This is the channel that will be used when tracing.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration")
TEnumAsByte<ECollisionChannel> TraceChannel;
// If you have any additional actors that should be ignored by the tracing
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration")
TArray<AActor*> TraceIgnoreActors;
// Let's you ignore this component's owner collision when tracing.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration")
bool bShouldTraceIgnoreOwner;
// If this is false, the world gravity is used for calculations. If this is true, CustomProjectileGravity is used instead.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Gravity")
bool bUseCustomGravity;
// If bUseCustomGravity is true, this is the gravity that will be used in calculations
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Gravity")
FVector CustomProjectileGravity;
public:
UFUNCTION(BlueprintPure, Category = "Fire@gaze")
FVector CalculateProjectileGravityVector();
UFUNCTION(BlueprintPure, Category = "Fire@gaze")
bool ThrowAtGazeAvailable();
UFUNCTION(BlueprintPure, Category = "Fire@gaze")
bool TraceBallisticProjectilePath(FVector ProjectileOrigin, FVector ProjectileInitialVelocity, TArray<FVector>& OutTracedPath);
/**
* This function is useful when throwing things in XR and you have a throw vector from the player's attempt to throw something.
* Don't forget to set collision on the thing you're throwing etc before using this though, otherwise tracing might not work.
* It will only correct trajectories where we can expect a direct hit, or where we are simply out of range.
*
* @param ThrowOrigin This is where the projectile will start.
* @param OriginalThrowVector This is the throw vector supplied by the developer to correct.
* @param ThrowAngleThresholdDeg If the angle difference between the OriginalThrowVector and the suggested vector is greater than this parameter, the vector will not be corrected. This is not applied if it is 0.
* @param ThrowSpeedThreshold If the OriginalThrowVector speed is lower than the suggested vector's speed by at least this amount, the vector will not be corrected. This is measured in centimeters per second. This is not applied if it is 0.
* @param bUseSimple If this is true, CalculateThrowAtGazeVectorSimple is used in favor of the Complex version internally
* @return The potentially corrected throw vector
*/
UFUNCTION(BlueprintCallable, Category = "Throw@gaze")
FVector CorrectThrow(const FVector& ThrowOrigin, const FVector& OriginalThrowVector, float ThrowAngleThresholdDeg, float ThrowSpeedThresholdCmPerSec);
/**
* Call this function to request an impulse vector to use for gaze throwing.
* This function will start by testing the standard apex and force, and if that works it will just return it.
* If no solution is found, the algorithm will test other apex heights until either MaxNrIterations is reached or a result is found.
*
* @param ThrowOrigin This is where the projectile will start.
* @param OutBallisticResult This are our results.
* @param OutTracedPath If path tracing is on, this will contain the traced path.
* @return The result of the calculation. Please note that in the case of OutOfRange, the best solution will still be returned in OutGazeThrowVector, adjusted to your max throw speed
*/
UFUNCTION(BlueprintCallable, Category = "Throw@gaze")
ETobiiThrowAtGazeResult CalculateThrowAtGazeVector(const FVector& ThrowOrigin, FTobiiBallisticResult& OutBallisticResult, TArray<FVector>& OutTracedPath);
/**
* This is a companion function to CalculateThrowAtGazeVectorComplex that lets you calculate a throw vector towards a world location.
* You can specify target velocity and acceleration if you would like to.
*
* @param ThrowOrigin This is where the projectile will start.
* @param TargetLocation This is where the target is.
* @param TargetVelocity This is the target's velocity. Can be zero.
* @param TargetAcceleration This is the target's acceleration. Can be zero.
* @param OutBallisticResult This are our results.
* @param OutTracedPath If path tracing is on, this will contain the traced path.
* @return The result of the calculation. Please note that in the case of OutOfRange, the best solution will still be returned in OutGazeThrowVector, adjusted to your max throw speed
*/
UFUNCTION(BlueprintCallable, Category = "Throw@gaze")
ETobiiThrowAtGazeResult CalculateThrowArc(const FVector& ThrowOrigin, const FVector& TargetLocation, const FVector& TargetVelocity, const FVector& TargetAcceleration, FTobiiBallisticResult& OutBallisticResult, TArray<FVector>& OutTracedPath);
public:
void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
private:
FVector LastTargetVelocity;
FVector CalculatedTargetAcceleration;
};

View File

@@ -0,0 +1,60 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "TobiiAimAtGazeComponent.generated.h"
UCLASS(BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent))
class TOBIIINTERACTIONS_API UTobiiAimAtGazeComponent : public UActorComponent
{
GENERATED_BODY()
public:
UTobiiAimAtGazeComponent();
public:
//This controls how the filters in FocusFilterList are used. If this is true, only focusables with a layer that is in the FocusFilterList will be considered. If this is false, the FocusFilterList will act like a black list, excluding any focusables whos layer can be found in the array.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus Management")
bool bIsWhiteList;
//These are the filters that will determine what focus data you will receive. The behavior of the filters are controlled by bIsWhiteList.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus Management")
TArray<FName> FocusLayerFilters;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aim@gaze")
bool bAllowRetarget;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aim@gaze")
float AimSpeed;
public:
//Use this to test if the user has turned aim@gaze on and it is available.
UFUNCTION(BlueprintPure, Category = "Aim@gaze")
bool AimAtGazeAvailable();
//Trigger an aim@gaze
UFUNCTION(BlueprintCallable, Category = "Aim@gaze")
void AimAtGaze();
//Call this every frame you want to continually track the aim@gaze target
UFUNCTION(BlueprintCallable, Category = "Aim@gaze")
void ContinuousAimAtGaze();
/************************************************************************/
/* UActorComponent */
/************************************************************************/
public:
virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
private:
TWeakObjectPtr<class UPrimitiveComponent> CurrentFocusComponent;
FVector CurrentAimTarget;
bool bIsGazeAiming;
};

View File

@@ -0,0 +1,68 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "TobiiFireAtGazeComponent.generated.h"
UENUM(BlueprintType)
enum class ETobiiFireAtGazeNoTargetBehavior : uint8
{
PointGunForward,
PointGunToGaze
};
UCLASS(BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent))
class TOBIIINTERACTIONS_API UTobiiFireAtGazeComponent : public UActorComponent
{
GENERATED_BODY()
public:
UTobiiFireAtGazeComponent();
public:
//This controls how the filters in FocusFilterList are used. If this is true, only focusables with a layer that is in the FocusFilterList will be considered. If this is false, the FocusFilterList will act like a black list, excluding any focusables whos layer can be found in the array.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus Management")
bool bIsWhiteList;
//These are the filters that will determine what focus data you will receive. The behavior of the filters are controlled by bIsWhiteList.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus Management")
TArray<FName> FocusLayerFilters;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Fire@gaze")
class UCameraComponent* CameraComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Fire@gaze")
TWeakObjectPtr<AActor> FireAtGazeTargetActor;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Fire@gaze")
TWeakObjectPtr<UPrimitiveComponent> FireAtGazeTargetComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Fire@gaze")
FVector FireAtGazeTargetLocation;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Fire@gaze Settings")
float MaxDistance;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Fire@gaze Settings")
ETobiiFireAtGazeNoTargetBehavior NoTargetBehavior;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Fire@gaze Settings")
TEnumAsByte<ECollisionChannel> TraceChannel;
public:
//Use this to test if the user has turned fire@gaze on and it is available.
UFUNCTION(BlueprintPure, Category = "Fire@gaze")
bool FireAtGazeAvailable();
UFUNCTION(BlueprintPure, Category = "Fire@gaze")
bool WantsCrosshair();
/************************************************************************/
/* UActorComponent */
/************************************************************************/
public:
virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
};

View File

@@ -0,0 +1,103 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "TobiiInteractionsTypes.h"
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "TobiiInteractionsBlueprintLibrary.generated.h"
/**
* Simplified interface for blueprint use. Only exposes the features most likely to be consumed from BP.
*/
UCLASS()
class TOBIIINTERACTIONS_API UTobiiInteractionsBlueprintLibrary : public UBlueprintFunctionLibrary
{
GENERATED_UCLASS_BODY()
public:
UFUNCTION(BlueprintPure, Category = "Tobii Desktop Interaction")
static bool IsInfiniteScreenEnabled();
//Util functions to see if the feature can be considered enabled by the user.
UFUNCTION(BlueprintPure, Category = "Tobii Desktop Interaction")
static bool IsCleanUIEnabled();
UFUNCTION(BlueprintPure, Category = "Tobii Desktop Interaction")
static bool IsAimAtGazeEnabled();
UFUNCTION(BlueprintPure, Category = "Tobii Desktop Interaction")
static bool IsFireAtGazeEnabled();
UFUNCTION(BlueprintPure, Category = "Tobii Desktop Interaction")
static bool IsThrowAtGazeEnabled();
public:
/**
* This function will determine an appropriate amount to dampen infinite screen depending on view pitch.
*/
UFUNCTION(BlueprintCallable, Category = "Tobii Infinite Screen")
static float CalculateSmoothPitchStep(float ViewPitch);
/**
* This function will determine an appropriate amount to dampen infinite screen depending on view pitch.
*/
UFUNCTION(BlueprintCallable, Category = "Tobii Infinite Screen")
static FRotator MakeInfiniteScreenCameraRotator(FRotator OriginalCameraRotation, FRotator InfiniteScreenAngles);
public:
/*
* Render UI to a texture so we can use it on arbitrary meshes.
* Thanks to https://answers.unrealengine.com/questions/353532/rendering-a-umg-widget-on-a-static-mesh.html for this.
*/
UTexture2D* TextureFromWidget(UUserWidget* const Widget, const FVector2D& DrawSize);
/**
* This function will find and return all real roots to the square equation Ax^2 + Bx + C = 0
*/
UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils")
static void FindRealSquareRoots(float A, float B, float C, TArray<float>& OutRealRoots);
/**
* This function will find and return all real roots to the cubic equation Ax^3 + Bx^2 + Cx + D = 0
*/
UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils")
static void FindRealCubicRoots(float A, float B, float C, float D, TArray<float>& OutRealRoots);
/**
* This function will find and return all real roots to the quartic equation Ax^4 + Bx^3 + Cx^2 + Dx + E = 0
* WARNING: The roots of a full degree quartic are generally not cheap to compute. Use with care.
*/
UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils")
static void FindRealQuarticRoots(float A, float B, float C, float D, float E, TArray<float>& OutRealRoots);
/**
* This function will try to find which acceleration you will need for the current frame to hit a certain target given a homing projectile that uses acceleration for it's movement behavior.
*/
UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils")
static bool FindNeededAccelerationForAccelerationBasedHomingProjectile(const FTobiiAccelerationBasedHomingData& InputData, FTobiiAccelerationBasedHomingResult& BestResult);
/**
* This function will try to find the initial velocity needed to hit a moving target with a ballistic projectile that passes through a selected apex point.
* To find an arc that works with your environment and thrower you might have to call this function multiple times and trace the results in your scene.
*
* @return Whether a valid velocity could be found. If this is false, it usually means that SourceZ + ProjectileApexOffset was smaller than TargetZ. Try increasing the apex offset and try again.
*/
UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils")
static bool FindNeededInitialVelocityForBallisticProjectile(const FTobiiBallisticData& InputData, TArray<FTobiiBallisticResult>& Results);
/**
* This function will trace along a ballistic path until it hits something. It will then return the traced path as well as what it hit.
*
* @return Whether anything was hit tracing the path
*/
UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils", meta = (WorldContext = "WorldContextObject"))
static bool TraceBallisticProjectilePath(UObject* WorldContextObject, const FTobiiProjectileTraceData& InputData, TArray<FVector>& OutTracedPath, FHitResult& OutHitResult);
};

View File

@@ -0,0 +1,146 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
#pragma once
#include "CoreMinimal.h"
#include "CollisionQueryParams.h"
#include "TobiiInteractionsTypes.generated.h"
UENUM(BlueprintType)
enum class ETobiiInterceptType : uint8
{
ClosestApproach,
DirectHit
};
UENUM(BlueprintType)
enum class ETobiiThrowAtGazeResult : uint8
{
// Bad input in some way
UnknownError,
// Neither a throw target, nor gaze input is available
NoEyetrackingInput,
// If no valid path could be found
NoPath,
// A path is mathematically possible, but it is blocked by something and out of range.
BlockedByWorldAndOutOfRange,
// A path is mathematically possible and is within range, but it is blocked by something.
BlockedByWorldInRange,
// If a valid path could be found, but it requires more throw force than allowed
OutOfRange,
// If a valid path exists and the force requirement is within limits
DirectHit
};
USTRUCT(BlueprintType)
struct FTobiiAccelerationBasedHomingData
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data")
bool bAttemptClosestApproachSolution;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Source Data")
FVector ProjectilePosition;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Source Data")
FVector ProjectileVelocity;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Source Data")
float ProjectileAccelerationMagnitude;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Target Data")
FVector TargetPosition;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Target Data")
FVector TargetVelocity;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Target Data")
FVector TargetAcceleration;
};
USTRUCT(BlueprintType)
struct FTobiiBallisticData
{
GENERATED_BODY()
public:
//This is how high the projectile should be above the initial position at it's midpoint
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data")
float ProjectileApexOffsetCm;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Source Data")
FVector ProjectileInitialPosition;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Source Data")
FVector ProjectileAcceleration;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Target Data")
FVector TargetPosition;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Target Data")
FVector TargetVelocity;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Target Data")
FVector TargetAcceleration;
};
USTRUCT(BlueprintType)
struct FTobiiAccelerationBasedHomingResult
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Intercept Data")
ETobiiInterceptType Type;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Intercept Data")
FVector SuggestedAcceleration;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Intercept Data")
FVector ExpectedInterceptLocation;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Intercept Data")
float ExpectedInterceptTimeSecs;
};
USTRUCT(BlueprintType)
struct FTobiiBallisticResult
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Intercept Data")
FVector SuggestedInitialVelocity;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Intercept Data")
FVector ExpectedInterceptLocation;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Intercept Data")
float ExpectedInterceptTimeSecs;
};
USTRUCT(BlueprintType)
struct FTobiiProjectileTraceData
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data")
TEnumAsByte<ECollisionChannel> TraceChannel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data")
TArray<AActor*> IgnoredActors;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data")
int32 MaxNrSteps;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data")
float StepSizeSecs;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data")
float TraceRadiusCm;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Source Data")
FVector ProjectileInitialPosition;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Source Data")
FVector ProjectileVelocity;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Source Data")
FVector ProjectileAcceleration;
};

View File

@@ -0,0 +1,62 @@
/******************************************************************************
* Copyright 2017- Tobii Technology AB. All rights reserved.
*
* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran
******************************************************************************/
using System;
using System.IO;
using Tools.DotNETCommon;
namespace UnrealBuildTool.Rules
{
public class TobiiInteractions : ModuleRules
{
public TobiiInteractions(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PrivateDependencyModuleNames.AddRange(new string[]
{
"Core"
, "CoreUObject"
, "Engine"
, "Slate"
, "SlateCore"
, "UMG"
, "HeadMountedDisplay"
});
PublicDependencyModuleNames.AddRange(new string[]
{
"EyeTracker"
, "TobiiGTOM"
});
PrivateIncludePaths.AddRange(new string[]
{
"TobiiInteractions/Public"
, "TobiiInteractions/Public/Common"
, "TobiiInteractions/Public/Desktop"
});
string AssemblyLocation = Path.GetDirectoryName(new Uri(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).LocalPath);
DirectoryReference RootDirectory = new DirectoryReference(Path.Combine(AssemblyLocation, "..", "..", ".."));
bool IsEnginePlugin = RootDirectory.GetDirectoryName() == "Engine";
PublicDefinitions.Add("TOBII_COMPILE_AS_ENGINE_PLUGIN=" + (IsEnginePlugin ? 1 : 0));
//Platform specific
if (Target.Platform == UnrealTargetPlatform.Win64 || Target.Platform == UnrealTargetPlatform.Win32)
{
string PlatformString = (Target.Platform == UnrealTargetPlatform.Win64) ? "Win64" : "Win32";
string PluginsPath = Path.Combine(ModuleDirectory, "../../../");
string TobiiRelativeAPIPath = "TobiiEyetracking/ThirdParty/GameIntegration";
string TobiiRelativeIncludePath = Path.Combine(TobiiRelativeAPIPath, "include");
//Includes
PrivateIncludePaths.Add(Path.Combine(PluginsPath, TobiiRelativeIncludePath));
}
}
}
}