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,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);
}
}
}
}