// Copyright 2020-2024 CesiumGS, Inc. and Contributors #include "CesiumFlyToComponent.h" #include "CesiumGeoreference.h" #include "CesiumGlobeAnchorComponent.h" #include "CesiumWgs84Ellipsoid.h" #include "Curves/CurveFloat.h" #include "GameFramework/Controller.h" #include "GameFramework/Pawn.h" #include "UObject/ConstructorHelpers.h" #include "VecMath.h" #include UCesiumFlyToComponent::UCesiumFlyToComponent() { // Structure to hold one-time initialization struct FConstructorStatics { ConstructorHelpers::FObjectFinder ProgressCurve; ConstructorHelpers::FObjectFinder HeightPercentageCurve; ConstructorHelpers::FObjectFinder MaximumHeightByDistanceCurve; FConstructorStatics() : ProgressCurve(TEXT( "/CesiumForUnreal/Curves/FlyTo/Curve_CesiumFlyToDefaultProgress_Float.Curve_CesiumFlyToDefaultProgress_Float")), HeightPercentageCurve(TEXT( "/CesiumForUnreal/Curves/FlyTo/Curve_CesiumFlyToDefaultHeightPercentage_Float.Curve_CesiumFlyToDefaultHeightPercentage_Float")), MaximumHeightByDistanceCurve(TEXT( "/CesiumForUnreal/Curves/FlyTo/Curve_CesiumFlyToDefaultMaximumHeightByDistance_Float.Curve_CesiumFlyToDefaultMaximumHeightByDistance_Float")) { } }; static FConstructorStatics ConstructorStatics; this->ProgressCurve = ConstructorStatics.ProgressCurve.Object; this->HeightPercentageCurve = ConstructorStatics.HeightPercentageCurve.Object; this->MaximumHeightByDistanceCurve = ConstructorStatics.MaximumHeightByDistanceCurve.Object; this->PrimaryComponentTick.bCanEverTick = true; } void UCesiumFlyToComponent::FlyToLocationEarthCenteredEarthFixed( const FVector& EarthCenteredEarthFixedDestination, double YawAtDestination, double PitchAtDestination, bool CanInterruptByMoving) { if (this->_flightInProgress) { UE_LOG( LogCesium, Error, TEXT("Cannot start a flight because one is already in progress.")); return; } UCesiumGlobeAnchorComponent* GlobeAnchor = this->GetGlobeAnchor(); if (!IsValid(GlobeAnchor)) { return; } PitchAtDestination = glm::clamp(PitchAtDestination, -89.99, 89.99); // Compute source location in ECEF FVector ecefSource = GlobeAnchor->GetEarthCenteredEarthFixedPosition(); // Obtain Ellipsoid UCesiumEllipsoid* ellipsoid = GlobeAnchor->ResolveGeoreference()->GetEllipsoid(); check(IsValid(ellipsoid)); // Create curve std::optional curve = CesiumGeospatial::SimplePlanarEllipsoidCurve:: fromEarthCenteredEarthFixedCoordinates( ellipsoid->GetNativeEllipsoid(), glm::dvec3(ecefSource.X, ecefSource.Y, ecefSource.Z), glm::dvec3( EarthCenteredEarthFixedDestination.X, EarthCenteredEarthFixedDestination.Y, EarthCenteredEarthFixedDestination.Z)); if (!curve.has_value()) { return; } this->_currentCurve = MakeUnique(curve.value()); this->_length = (EarthCenteredEarthFixedDestination - ecefSource).Length(); // The source and destination rotations are expressed in East-South-Up // coordinates. this->_sourceRotation = this->GetCurrentRotationEastSouthUp(); this->_destinationRotation = FRotator(PitchAtDestination, YawAtDestination, 0.0).Quaternion(); this->_currentFlyTime = 0.0f; // Compute a wanted height from curve this->_maxHeight = 0.0; if (this->HeightPercentageCurve != NULL) { this->_maxHeight = 30000.0; if (this->MaximumHeightByDistanceCurve != NULL) { this->_maxHeight = this->MaximumHeightByDistanceCurve->GetFloatValue(this->_length); } } // Tell the tick we will be flying from now this->_canInterruptByMoving = CanInterruptByMoving; this->_previousPositionEcef = ecefSource; this->_flightInProgress = true; this->_destinationEcef = EarthCenteredEarthFixedDestination; } void UCesiumFlyToComponent::FlyToLocationLongitudeLatitudeHeight( const FVector& LongitudeLatitudeHeightDestination, double YawAtDestination, double PitchAtDestination, bool CanInterruptByMoving) { UCesiumEllipsoid* ellipsoid = this->GetGlobeAnchor()->ResolveGeoreference()->GetEllipsoid(); FVector Ecef = ellipsoid->LongitudeLatitudeHeightToEllipsoidCenteredEllipsoidFixed( LongitudeLatitudeHeightDestination); this->FlyToLocationEarthCenteredEarthFixed( Ecef, YawAtDestination, PitchAtDestination, CanInterruptByMoving); } void UCesiumFlyToComponent::FlyToLocationUnreal( const FVector& UnrealDestination, double YawAtDestination, double PitchAtDestination, bool CanInterruptByMoving) { UCesiumGlobeAnchorComponent* GlobeAnchor = this->GetGlobeAnchor(); if (!IsValid(GlobeAnchor)) { UE_LOG( LogCesium, Warning, TEXT( "CesiumFlyToComponent cannot FlyToLocationUnreal because the Actor has no CesiumGlobeAnchorComponent.")); return; } ACesiumGeoreference* Georeference = GlobeAnchor->ResolveGeoreference(); if (!IsValid(Georeference)) { UE_LOG( LogCesium, Warning, TEXT( "CesiumFlyToComponent cannot FlyToLocationUnreal because the globe anchor has no associated CesiumGeoreference.")); return; } this->FlyToLocationEarthCenteredEarthFixed( Georeference->TransformUnrealPositionToEarthCenteredEarthFixed( UnrealDestination), YawAtDestination, PitchAtDestination, CanInterruptByMoving); } void UCesiumFlyToComponent::InterruptFlight() { this->_flightInProgress = false; UCesiumGlobeAnchorComponent* GlobeAnchor = this->GetGlobeAnchor(); if (IsValid(GlobeAnchor)) { // fix Actor roll to 0.0 FRotator currentRotator = this->GetCurrentRotationEastSouthUp().Rotator(); currentRotator.Roll = 0.0; FQuat eastSouthUpRotation = currentRotator.Quaternion(); this->SetCurrentRotationEastSouthUp(eastSouthUpRotation); } // Trigger callback accessible from BP UE_LOG(LogCesium, Verbose, TEXT("Broadcasting OnFlightInterrupt")); OnFlightInterrupted.Broadcast(); } void UCesiumFlyToComponent::TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); if (!this->_flightInProgress) { return; } check(this->_currentCurve); UCesiumGlobeAnchorComponent* GlobeAnchor = this->GetGlobeAnchor(); if (!IsValid(GlobeAnchor)) { return; } if (this->_canInterruptByMoving && this->_previousPositionEcef != GlobeAnchor->GetEarthCenteredEarthFixedPosition()) { this->InterruptFlight(); return; } this->_currentFlyTime += DeltaTime; // In order to accelerate at start and slow down at end, we use a progress // profile curve float flyPercentage; if (this->_currentFlyTime >= this->Duration) { flyPercentage = 1.0f; } else if (this->ProgressCurve) { flyPercentage = glm::clamp( this->ProgressCurve->GetFloatValue( this->_currentFlyTime / this->Duration), 0.0f, 1.0f); } else { flyPercentage = this->_currentFlyTime / this->Duration; } // If we reached the end, set actual destination location and // orientation if (flyPercentage >= 1.0f || (this->_length == 0.0 && this->_sourceRotation == this->_destinationRotation)) { GlobeAnchor->MoveToEarthCenteredEarthFixedPosition(this->_destinationEcef); this->SetCurrentRotationEastSouthUp(this->_destinationRotation); this->_flightInProgress = false; this->_currentFlyTime = 0.0f; // Trigger callback accessible from BP UE_LOG(LogCesium, Verbose, TEXT("Broadcasting OnFlightComplete")); OnFlightComplete.Broadcast(); return; } // Get altitude offset from profile curve if one is specified double altitudeOffset = 0.0; if (this->_maxHeight != 0.0 && this->HeightPercentageCurve) { double curveOffset = this->_maxHeight * this->HeightPercentageCurve->GetFloatValue(flyPercentage); altitudeOffset = curveOffset; } glm::dvec3 currentPositionEcef = _currentCurve->getPosition(flyPercentage, altitudeOffset); FVector currentPositionVector( currentPositionEcef.x, currentPositionEcef.y, currentPositionEcef.z); // Set Location GlobeAnchor->MoveToEarthCenteredEarthFixedPosition(currentPositionVector); // Interpolate rotation in the ESU frame. The local ESU ControlRotation will // be transformed to the appropriate world rotation as we fly. FQuat currentQuat = FQuat::Slerp( this->_sourceRotation, this->_destinationRotation, flyPercentage); this->SetCurrentRotationEastSouthUp(currentQuat); this->_previousPositionEcef = GlobeAnchor->GetEarthCenteredEarthFixedPosition(); } FQuat UCesiumFlyToComponent::GetCurrentRotationEastSouthUp() { if (this->RotationToUse != ECesiumFlyToRotation::Actor) { APawn* Pawn = Cast(this->GetOwner()); const TObjectPtr Controller = IsValid(Pawn) ? Pawn->Controller : nullptr; if (Controller) { FRotator rotator = Controller->GetControlRotation(); if (this->RotationToUse == ECesiumFlyToRotation::ControlRotationInUnreal) { USceneComponent* PawnRoot = Pawn->GetRootComponent(); if (IsValid(PawnRoot)) { rotator = GetGlobeAnchor() ->ResolveGeoreference() ->TransformUnrealRotatorToEastSouthUp( Controller->GetControlRotation(), PawnRoot->GetRelativeLocation()); } } return rotator.Quaternion(); } } return this->GetGlobeAnchor()->GetEastSouthUpRotation(); } void UCesiumFlyToComponent::SetCurrentRotationEastSouthUp( const FQuat& EastSouthUpRotation) { bool controlRotationSet = false; if (this->RotationToUse != ECesiumFlyToRotation::Actor) { APawn* Pawn = Cast(this->GetOwner()); const TObjectPtr Controller = IsValid(Pawn) ? Pawn->Controller : nullptr; if (Controller) { FRotator rotator = EastSouthUpRotation.Rotator(); if (this->RotationToUse == ECesiumFlyToRotation::ControlRotationInUnreal) { USceneComponent* PawnRoot = Pawn->GetRootComponent(); if (IsValid(PawnRoot)) { rotator = GetGlobeAnchor() ->ResolveGeoreference() ->TransformEastSouthUpRotatorToUnreal( EastSouthUpRotation.Rotator(), PawnRoot->GetRelativeLocation()); } } Controller->SetControlRotation(EastSouthUpRotation.Rotator()); controlRotationSet = true; } } if (!controlRotationSet) { this->GetGlobeAnchor()->SetEastSouthUpRotation(EastSouthUpRotation); } }