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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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