diff --git a/Source/CesiumRuntime/Private/CesiumFlyToComponent.cpp b/Source/CesiumRuntime/Private/CesiumFlyToComponent.cpp index 04a3bcc4d..17c3d03a7 100644 --- a/Source/CesiumRuntime/Private/CesiumFlyToComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumFlyToComponent.cpp @@ -54,78 +54,45 @@ void UCesiumFlyToComponent::FlyToLocationEarthCenteredEarthFixed( } PitchAtDestination = glm::clamp(PitchAtDestination, -89.99, 89.99); + // Compute source location in ECEF FVector ecefSource = GlobeAnchor->GetEarthCenteredEarthFixedPosition(); + // Create curve + std::optional curve = + CesiumGeospatial::SimplePlanarEllipsoidCurve:: + fromEarthCenteredEarthFixedCoordinates( + CesiumGeospatial::Ellipsoid::WGS84, + 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->_destinationEcef = EarthCenteredEarthFixedDestination; - - // Compute axis/Angle transform - glm::dvec3 glmEcefSource = VecMath::createVector3D( - UCesiumWgs84Ellipsoid::ScaleToGeodeticSurface(ecefSource)); - glm::dvec3 glmEcefDestination = VecMath::createVector3D( - UCesiumWgs84Ellipsoid::ScaleToGeodeticSurface(this->_destinationEcef)); - - glm::dquat flyQuat = glm::rotation( - glm::normalize(glmEcefSource), - glm::normalize(glmEcefDestination)); - - glm::dvec3 flyToRotationAxis = glm::axis(flyQuat); - this->_rotationAxis.Set( - flyToRotationAxis.x, - flyToRotationAxis.y, - flyToRotationAxis.z); - - this->_totalAngle = glm::angle(flyQuat); - this->_totalAngle = CesiumUtility::Math::radiansToDegrees(this->_totalAngle); this->_currentFlyTime = 0.0f; - // We will not create a curve projected along the ellipsoid as we want to take - // altitude while flying. The radius of the current point will evolve as - // follows: - // - Project the point on the ellipsoid - Will give a default radius - // depending on ellipsoid location. - // - Interpolate the altitudes : get source/destination altitude, and make a - // linear interpolation between them. This will allow for flying from/to any - // point smoothly. - // - Add as flightProfile offset /-\ defined by a curve. - - // Compute actual altitude at source and destination points by getting their - // cartographic height - this->_sourceHeight = 0.0; - this->_destinationHeight = 0.0; - - FVector cartographicSource = - UCesiumWgs84Ellipsoid::EarthCenteredEarthFixedToLongitudeLatitudeHeight( - ecefSource); - this->_sourceHeight = cartographicSource.Z; - - cartographicSource.Z = 0.0; - FVector zeroHeightSource = - UCesiumWgs84Ellipsoid::LongitudeLatitudeHeightToEarthCenteredEarthFixed( - cartographicSource); - - this->_sourceDirection = zeroHeightSource.GetSafeNormal(); - - FVector cartographicDestination = - UCesiumWgs84Ellipsoid::EarthCenteredEarthFixedToLongitudeLatitudeHeight( - EarthCenteredEarthFixedDestination); - this->_destinationHeight = cartographicDestination.Z; - // Compute a wanted height from curve this->_maxHeight = 0.0; if (this->HeightPercentageCurve != NULL) { this->_maxHeight = 30000.0; if (this->MaximumHeightByDistanceCurve != NULL) { - double flyToDistance = - (EarthCenteredEarthFixedDestination - ecefSource).Length(); this->_maxHeight = - this->MaximumHeightByDistanceCurve->GetFloatValue(flyToDistance); + this->MaximumHeightByDistanceCurve->GetFloatValue(this->_length); } } @@ -133,6 +100,7 @@ void UCesiumFlyToComponent::FlyToLocationEarthCenteredEarthFixed( this->_canInterruptByMoving = CanInterruptByMoving; this->_previousPositionEcef = ecefSource; this->_flightInProgress = true; + this->_destinationEcef = EarthCenteredEarthFixedDestination; } void UCesiumFlyToComponent::FlyToLocationLongitudeLatitudeHeight( @@ -210,6 +178,8 @@ void UCesiumFlyToComponent::TickComponent( return; } + check(this->_currentCurve); + UCesiumGlobeAnchorComponent* GlobeAnchor = this->GetGlobeAnchor(); if (!IsValid(GlobeAnchor)) { return; @@ -242,7 +212,7 @@ void UCesiumFlyToComponent::TickComponent( // If we reached the end, set actual destination location and // orientation if (flyPercentage >= 1.0f || - (this->_totalAngle == 0.0 && + (this->_length == 0.0 && this->_sourceRotation == this->_destinationRotation)) { GlobeAnchor->MoveToEarthCenteredEarthFixedPosition(this->_destinationEcef); this->SetCurrentRotationEastSouthUp(this->_destinationRotation); @@ -256,37 +226,25 @@ void UCesiumFlyToComponent::TickComponent( return; } - // We're currently in flight. Interpolate the position and orientation: - - // Get the current position by interpolating with flyPercentage - // Rotate our normalized source direction, interpolating with time - FVector rotatedDirection = this->_sourceDirection.RotateAngleAxis( - flyPercentage * this->_totalAngle, - this->_rotationAxis); - - // Map the result to a position on our reference ellipsoid - FVector geodeticPosition = - UCesiumWgs84Ellipsoid::ScaleToGeodeticSurface(rotatedDirection); - - // Calculate the geodetic up at this position - FVector geodeticUp = - UCesiumWgs84Ellipsoid::GeodeticSurfaceNormal(geodeticPosition); - - // Add the altitude offset. Start with linear path between source and - // destination If we have a profile curve, use this as well - double altitudeOffset = - glm::mix(this->_sourceHeight, this->_destinationHeight, flyPercentage); + // 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; + altitudeOffset = curveOffset; } - FVector currentPosition = geodeticPosition + geodeticUp * altitudeOffset; + glm::dvec3 currentPositionEcef = + _currentCurve->getPosition(flyPercentage, altitudeOffset); + + FVector currentPositionVector( + currentPositionEcef.x, + currentPositionEcef.y, + currentPositionEcef.z); // Set Location - GlobeAnchor->MoveToEarthCenteredEarthFixedPosition(currentPosition); + GlobeAnchor->MoveToEarthCenteredEarthFixedPosition(currentPositionVector); // Interpolate rotation in the ESU frame. The local ESU ControlRotation will // be transformed to the appropriate world rotation as we fly. diff --git a/Source/CesiumRuntime/Private/Tests/GlobeAwareDefaultPawn.spec.cpp b/Source/CesiumRuntime/Private/Tests/GlobeAwareDefaultPawn.spec.cpp index 29382980b..f8e66c1c6 100644 --- a/Source/CesiumRuntime/Private/Tests/GlobeAwareDefaultPawn.spec.cpp +++ b/Source/CesiumRuntime/Private/Tests/GlobeAwareDefaultPawn.spec.cpp @@ -3,8 +3,10 @@ #if WITH_EDITOR #include "CesiumFlyToComponent.h" +#include "CesiumGeoreference.h" #include "CesiumGlobeAnchorComponent.h" #include "CesiumTestHelpers.h" +#include "CesiumWgs84Ellipsoid.h" #include "Editor.h" #include "Engine/World.h" #include "EngineUtils.h" @@ -23,6 +25,16 @@ FDelegateHandle subscriptionPostPIEStarted; END_DEFINE_SPEC(FGlobeAwareDefaultPawnSpec) void FGlobeAwareDefaultPawnSpec::Define() { + const FVector philadelphiaEcef = + FVector(1253264.69280105, -4732469.91065521, 4075112.40412297); + + // The antipodal position from the philadelphia coordinates above + const FVector philadelphiaAntipodeEcef = + FVector(-1253369.920224856, 4732412.7444064, -4075146.2160252854); + + const FVector tokyoEcef = + FVector(-3960158.65587452, 3352568.87555906, 3697235.23506459); + Describe( "should not spike altitude when very close to final destination", [this]() { @@ -90,6 +102,248 @@ void FGlobeAwareDefaultPawnSpec::Define() { pPawn->Destroy(); }); }); + + Describe( + "should interpolate between positions and rotations correctly", + [this, tokyoEcef, philadelphiaEcef, philadelphiaAntipodeEcef]() { + LatentBeforeEach( + EAsyncExecution::TaskGraphMainThread, + [this](const FDoneDelegate& done) { + UWorld* pWorld = FAutomationEditorCommonUtils::CreateNewMap(); + + AGlobeAwareDefaultPawn* pPawn = + pWorld->SpawnActor(); + UCesiumFlyToComponent* pFlyTo = + Cast(pPawn->AddComponentByClass( + UCesiumFlyToComponent::StaticClass(), + false, + FTransform::Identity, + false)); + pFlyTo->RotationToUse = + ECesiumFlyToRotation::ControlRotationInEastSouthUp; + + subscriptionPostPIEStarted = + FEditorDelegates::PostPIEStarted.AddLambda( + [done](bool isSimulating) { done.Execute(); }); + FRequestPlaySessionParams params{}; + GEditor->RequestPlaySession(params); + }); + BeforeEach(EAsyncExecution::TaskGraphMainThread, [this]() { + FEditorDelegates::PostPIEStarted.Remove(subscriptionPostPIEStarted); + }); + AfterEach(EAsyncExecution::TaskGraphMainThread, [this]() { + GEditor->RequestEndPlayMap(); + }); + It("should match the beginning and ending points of the fly-to", + [this]() { + UWorld* pWorld = GEditor->PlayWorld; + + TActorIterator it(pWorld); + AGlobeAwareDefaultPawn* pPawn = *it; + UCesiumFlyToComponent* pFlyTo = + pPawn->FindComponentByClass(); + TestNotNull("pFlyTo", pFlyTo); + pFlyTo->Duration = 5.0f; + + UCesiumGlobeAnchorComponent* pGlobeAnchor = + pPawn->FindComponentByClass(); + TestNotNull("pGlobeAnchor", pGlobeAnchor); + + pGlobeAnchor->MoveToLongitudeLatitudeHeight( + FVector(25.0, 10.0, 100.0)); + + // Start flying somewhere else + pFlyTo->FlyToLocationLongitudeLatitudeHeight( + FVector(25.0, 25.0, 100.0), + 0.0, + 0.0, + false); + + TestEqual( + "Location is the same as the start point", + pGlobeAnchor->GetLongitudeLatitudeHeight(), + FVector(25.0, 10.0, 100.0)); + + // Tick to the end + Cast(pFlyTo)->TickComponent( + 5.0f, + ELevelTick::LEVELTICK_All, + nullptr); + + TestEqual( + "Location is the same as the end point", + pGlobeAnchor->GetLongitudeLatitudeHeight(), + FVector(25.0, 25.0, 100.0)); + + pPawn->Destroy(); + }); + It("should correctly compute the midpoint of the flight", + [this, tokyoEcef, philadelphiaEcef]() { + UWorld* pWorld = GEditor->PlayWorld; + + TActorIterator it(pWorld); + AGlobeAwareDefaultPawn* pPawn = *it; + UCesiumFlyToComponent* pFlyTo = + pPawn->FindComponentByClass(); + TestNotNull("pFlyTo", pFlyTo); + pFlyTo->Duration = 5.0f; + pFlyTo->HeightPercentageCurve = nullptr; + pFlyTo->MaximumHeightByDistanceCurve = nullptr; + pFlyTo->ProgressCurve = nullptr; + + UCesiumGlobeAnchorComponent* pGlobeAnchor = + pPawn->FindComponentByClass(); + TestNotNull("pGlobeAnchor", pGlobeAnchor); + + pGlobeAnchor->MoveToEarthCenteredEarthFixedPosition( + philadelphiaEcef); + pFlyTo->FlyToLocationEarthCenteredEarthFixed(tokyoEcef, 0, 0, 0); + + // Tick half way through + Cast(pFlyTo)->TickComponent( + 2.5f, + ELevelTick::LEVELTICK_All, + nullptr); + + FVector expectedResult = FVector( + -2062499.3622640674, + -1052346.4221710551, + 5923430.4378960524); + + // calculate relative epsilon + const float epsilon = FMath::Max3( + expectedResult.X, + expectedResult.Y, + expectedResult.Z) * + 1e-6; + + TestEqual( + "Midpoint location is correct", + pGlobeAnchor->GetEarthCenteredEarthFixedPosition(), + expectedResult, + epsilon); + + pPawn->Destroy(); + }); + It("should match the start and end rotations", + [this, tokyoEcef, philadelphiaEcef]() { + UWorld* pWorld = GEditor->PlayWorld; + + TActorIterator it(pWorld); + AGlobeAwareDefaultPawn* pPawn = *it; + UCesiumFlyToComponent* pFlyTo = + pPawn->FindComponentByClass(); + TestNotNull("pFlyTo", pFlyTo); + pFlyTo->Duration = 5.0f; + pFlyTo->HeightPercentageCurve = nullptr; + pFlyTo->MaximumHeightByDistanceCurve = nullptr; + pFlyTo->ProgressCurve = nullptr; + + UCesiumGlobeAnchorComponent* pGlobeAnchor = + pPawn->FindComponentByClass(); + TestNotNull("pGlobeAnchor", pGlobeAnchor); + + FQuat sourceRotation = FRotator(0, 0, 0).Quaternion(); + FQuat targetRotation = FRotator(45, 180, 0).Quaternion(); + FQuat midpointRotation = + FQuat::Slerp(sourceRotation, targetRotation, 0.5); + + pGlobeAnchor->MoveToEarthCenteredEarthFixedPosition( + philadelphiaEcef); + pGlobeAnchor->SetEastSouthUpRotation(sourceRotation); + pFlyTo->FlyToLocationEarthCenteredEarthFixed( + tokyoEcef, + 180, + 45, + false); + + TestTrue( + "Start rotation is correct", + pPawn->Controller->GetControlRotation().Quaternion().Equals( + sourceRotation, + CesiumUtility::Math::Epsilon4)); + + // Tick half way through + Cast(pFlyTo)->TickComponent( + 2.5f, + ELevelTick::LEVELTICK_All, + nullptr); + + TestTrue( + "Midpoint rotation is correct", + pPawn->Controller->GetControlRotation().Quaternion().Equals( + midpointRotation, + CesiumUtility::Math::Epsilon4)); + + FVector currentEastSouthRotationEuler = + pGlobeAnchor->GetEastSouthUpRotation().Euler(); + FVector midpointEuler = midpointRotation.Euler(); + + // Tick to the end + Cast(pFlyTo)->TickComponent( + 2.5f, + ELevelTick::LEVELTICK_All, + nullptr); + + TestTrue( + "End location is correct", + pPawn->Controller->GetControlRotation().Quaternion().Equals( + targetRotation, + CesiumUtility::Math::Epsilon4)); + + FVector endEastSouthRotationEuler = + pGlobeAnchor->GetEastSouthUpRotation().Euler(); + FVector endpointEuler = targetRotation.Euler(); + + pPawn->Destroy(); + }); + It("shouldn't fly through the earth", + [this, philadelphiaEcef, philadelphiaAntipodeEcef]() { + UWorld* pWorld = GEditor->PlayWorld; + + TActorIterator it(pWorld); + AGlobeAwareDefaultPawn* pPawn = *it; + UCesiumFlyToComponent* pFlyTo = + pPawn->FindComponentByClass(); + TestNotNull("pFlyTo", pFlyTo); + + pFlyTo->Duration = 5.0f; + pFlyTo->HeightPercentageCurve = nullptr; + pFlyTo->MaximumHeightByDistanceCurve = nullptr; + pFlyTo->ProgressCurve = nullptr; + + UCesiumGlobeAnchorComponent* pGlobeAnchor = + pPawn->FindComponentByClass(); + TestNotNull("pGlobeAnchor", pGlobeAnchor); + + pGlobeAnchor->MoveToEarthCenteredEarthFixedPosition( + philadelphiaEcef); + pFlyTo->FlyToLocationEarthCenteredEarthFixed( + philadelphiaAntipodeEcef, + 0, + 0, + false); + + const int steps = 100; + const double timeStep = 5.0 / (double)steps; + + double time = 0; + for (int i = 0; i <= steps; i++) { + Cast(pFlyTo)->TickComponent( + (float)timeStep, + ELevelTick::LEVELTICK_All, + nullptr); + + const FVector cartographic = UCesiumWgs84Ellipsoid:: + EarthCenteredEarthFixedToLongitudeLatitudeHeight( + pGlobeAnchor->GetEarthCenteredEarthFixedPosition()); + + TestTrue("height above zero", cartographic.Z > 0); + + time += timeStep; + } + }); + }); } #endif diff --git a/Source/CesiumRuntime/Public/CesiumFlyToComponent.h b/Source/CesiumRuntime/Public/CesiumFlyToComponent.h index 51da04923..eb550f36f 100644 --- a/Source/CesiumRuntime/Public/CesiumFlyToComponent.h +++ b/Source/CesiumRuntime/Public/CesiumFlyToComponent.h @@ -2,6 +2,7 @@ #pragma once +#include "CesiumGeospatial/SimplePlanarEllipsoidCurve.h" #include "CesiumGlobeAnchoredActorComponent.h" #include "CesiumFlyToComponent.generated.h" @@ -209,15 +210,12 @@ class CESIUMRUNTIME_API UCesiumFlyToComponent bool _flightInProgress = false; bool _canInterruptByMoving; + float _currentFlyTime; + double _maxHeight; FVector _destinationEcef; FQuat _sourceRotation; FQuat _destinationRotation; - FVector _rotationAxis; - double _totalAngle; - float _currentFlyTime; - double _sourceHeight; - double _destinationHeight; - FVector _sourceDirection; - double _maxHeight; FVector _previousPositionEcef; + TUniquePtr _currentCurve; + double _length; }; diff --git a/extern/cesium-native b/extern/cesium-native index fe54002df..36b9a0716 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit fe54002dfb25f7c177e1166e9abf344653615f94 +Subproject commit 36b9a0716c87aff03b5d4ee4353923b71ccff009