diff --git a/Source/CesiumRuntime/Private/CesiumGlobeAnchorComponent.cpp b/Source/CesiumRuntime/Private/CesiumGlobeAnchorComponent.cpp index 3454c35d9..4f950c2cb 100644 --- a/Source/CesiumRuntime/Private/CesiumGlobeAnchorComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumGlobeAnchorComponent.cpp @@ -87,6 +87,11 @@ void UCesiumGlobeAnchorComponent::SetGeoreference( } } +ACesiumGeoreference* +UCesiumGlobeAnchorComponent::GetResolvedGeoreference() const { + return this->ResolvedGeoreference; +} + FVector UCesiumGlobeAnchorComponent::GetEarthCenteredEarthFixedPosition() const { if (!this->_actorToECEFIsValid) { @@ -184,9 +189,16 @@ void UCesiumGlobeAnchorComponent::SnapLocalUpToEllipsoidNormal() { FMatrix alignmentRotation = FQuat::FindBetween(up, ellipsoidNormal).ToMatrix(); - // Compute the new actor rotation and apply it + // Apply the new rotation to the Actor->ECEF transform. + // Note that FMatrix multiplication order is opposite glm::dmat4 + // multiplication order! FMatrix newActorToECEF = - alignmentRotation * this->ActorToEarthCenteredEarthFixedMatrix; + this->ActorToEarthCenteredEarthFixedMatrix * alignmentRotation; + + // We don't want to rotate the origin, though, so re-set it. + newActorToECEF.SetOrigin( + this->ActorToEarthCenteredEarthFixedMatrix.GetOrigin()); + this->_updateFromNativeGlobeAnchor(createNativeGlobeAnchor(newActorToECEF)); #if WITH_EDITOR @@ -250,16 +262,6 @@ void UCesiumGlobeAnchorComponent::InvalidateResolvedGeoreference() { } FVector UCesiumGlobeAnchorComponent::GetLongitudeLatitudeHeight() const { - if (!this->_actorToECEFIsValid || !this->ResolvedGeoreference) { - UE_LOG( - LogCesium, - Warning, - TEXT( - "CesiumGlobeAnchorComponent %s globe position is invalid because the component is not yet registered."), - *this->GetName()); - return FVector(0.0); - } - return UCesiumWgs84Ellipsoid:: EarthCenteredEarthFixedToLongitudeLatitudeHeight( this->GetEarthCenteredEarthFixedPosition()); diff --git a/Source/CesiumRuntime/Private/Tests/CesiumGlobeAnchor.spec.cpp b/Source/CesiumRuntime/Private/Tests/CesiumGlobeAnchor.spec.cpp new file mode 100644 index 000000000..81d434d79 --- /dev/null +++ b/Source/CesiumRuntime/Private/Tests/CesiumGlobeAnchor.spec.cpp @@ -0,0 +1,223 @@ +// Copyright 2020-2023 CesiumGS, Inc. and Contributors + +#include "CesiumGeoreference.h" +#include "CesiumGlobeAnchorComponent.h" +#include "CesiumTestHelpers.h" +#include "CesiumWgs84Ellipsoid.h" +#include "Misc/AutomationTest.h" + +BEGIN_DEFINE_SPEC( + FCesiumGlobeAnchorSpec, + "Cesium.GlobeAnchor", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::ProductFilter) + +TObjectPtr pActor; +TObjectPtr pGlobeAnchor; + +END_DEFINE_SPEC(FCesiumGlobeAnchorSpec) + +void FCesiumGlobeAnchorSpec::Define() { + BeforeEach([this]() { + UWorld* pWorld = CesiumTestHelpers::getGlobalWorldContext(); + ACesiumGeoreference* pGeoreference = + ACesiumGeoreference::GetDefaultGeoreference(pWorld); + pGeoreference->SetOriginLongitudeLatitudeHeight(FVector(1.0, 2.0, 3.0)); + + this->pActor = pWorld->SpawnActor(); + this->pActor->AddComponentByClass( + USceneComponent::StaticClass(), + false, + FTransform::Identity, + false); + this->pActor->SetActorRelativeTransform(FTransform()); + + this->pGlobeAnchor = + Cast(pActor->AddComponentByClass( + UCesiumGlobeAnchorComponent::StaticClass(), + false, + FTransform::Identity, + false)); + }); + + It("immediately syncs globe position from transform when added", [this]() { + TestEqual("Longitude", this->pGlobeAnchor->GetLongitude(), 1.0); + TestEqual("Latitude", this->pGlobeAnchor->GetLatitude(), 2.0); + TestEqual("Height", this->pGlobeAnchor->GetHeight(), 3.0); + }); + + It("maintains globe position when switching to a new georeference", [this]() { + FTransform beforeTransform = this->pActor->GetActorTransform(); + FVector beforeLLH = this->pGlobeAnchor->GetLongitudeLatitudeHeight(); + + UWorld* pWorld = this->pActor->GetWorld(); + ACesiumGeoreference* pNewGeoref = pWorld->SpawnActor(); + pNewGeoref->SetOriginLongitudeLatitudeHeight(FVector(10.0, 20.0, 30.0)); + this->pGlobeAnchor->SetGeoreference(pNewGeoref); + + TestEqual( + "ResolvedGeoreference", + this->pGlobeAnchor->GetResolvedGeoreference(), + pNewGeoref); + TestFalse( + "Transforms are equal", + this->pActor->GetActorTransform().Equals(beforeTransform)); + TestEqual( + "Globe Position", + this->pGlobeAnchor->GetLongitudeLatitudeHeight(), + beforeLLH); + }); + + It("updates actor transform when globe anchor position is changed", [this]() { + FTransform beforeTransform = this->pActor->GetActorTransform(); + this->pGlobeAnchor->MoveToLongitudeLatitudeHeight(FVector(4.0, 5.0, 6.0)); + TestEqual( + "LongitudeLatitudeHeight", + this->pGlobeAnchor->GetLongitudeLatitudeHeight(), + FVector(4.0, 5.0, 6.0)); + TestFalse( + "Transforms are equal", + this->pActor->GetActorTransform().Equals(beforeTransform)); + }); + + It("updates globe anchor position when actor transform is changed", [this]() { + FVector beforeLLH = this->pGlobeAnchor->GetLongitudeLatitudeHeight(); + this->pActor->SetActorLocation(FVector(1000.0, 2000.0, 3000.0)); + TestNotEqual( + "globe position", + this->pGlobeAnchor->GetLongitudeLatitudeHeight(), + beforeLLH); + }); + + It("allows the actor transform to be set when not registered", [this]() { + FVector beforeLLH = this->pGlobeAnchor->GetLongitudeLatitudeHeight(); + + this->pGlobeAnchor->UnregisterComponent(); + this->pActor->SetActorLocation(FVector(1000.0, 2000.0, 3000.0)); + + // Globe position doesn't initially update while unregistered + TestEqual( + "globe position", + this->pGlobeAnchor->GetLongitudeLatitudeHeight(), + beforeLLH); + + // After we re-register, actor transform should be maintained and globe + // transform should be updated. + this->pGlobeAnchor->RegisterComponent(); + TestEqual( + "actor position", + this->pActor->GetActorLocation(), + FVector(1000.0, 2000.0, 3000.0)); + TestNotEqual( + "globe position", + this->pGlobeAnchor->GetLongitudeLatitudeHeight(), + beforeLLH); + }); + + It("adjusts orientation for globe when actor position is set immediately after adding anchor", + [this]() { + FRotator beforeRotation = this->pActor->GetActorRotation(); + + ACesiumGeoreference* pGeoreference = + this->pGlobeAnchor->GetResolvedGeoreference(); + this->pActor->SetActorLocation( + pGeoreference->TransformLongitudeLatitudeHeightPositionToUnreal( + FVector(90.0, 2.0, 3.0))); + TestNotEqual( + "rotation", + this->pActor->GetActorRotation(), + beforeRotation); + }); + + It("adjusts orientation for globe when globe position is set immediately after adding anchor", + [this]() { + FRotator beforeRotation = this->pActor->GetActorRotation(); + + this->pGlobeAnchor->MoveToLongitudeLatitudeHeight( + FVector(90.0, 2.0, 3.0)); + TestNotEqual( + "rotation", + this->pActor->GetActorRotation(), + beforeRotation); + }); + + It("does not adjust orientation for globe when that feature is disabled", + [this]() { + this->pGlobeAnchor->SetAdjustOrientationForGlobeWhenMoving(false); + FRotator beforeRotation = this->pActor->GetActorRotation(); + + ACesiumGeoreference* pGeoreference = + this->pGlobeAnchor->GetResolvedGeoreference(); + this->pActor->SetActorLocation( + pGeoreference->TransformLongitudeLatitudeHeightPositionToUnreal( + FVector(90.0, 2.0, 3.0))); + TestEqual("rotation", this->pActor->GetActorRotation(), beforeRotation); + + this->pGlobeAnchor->MoveToLongitudeLatitudeHeight( + FVector(45.0, 25.0, 300.0)); + TestEqual("rotation", this->pActor->GetActorRotation(), beforeRotation); + }); + + It("gains correct orientation on call to SnapToEastSouthUp", [this]() { + ACesiumGeoreference* pGeoreference = + this->pGlobeAnchor->GetResolvedGeoreference(); + + this->pGlobeAnchor->MoveToLongitudeLatitudeHeight( + FVector(-20.0, -10.0, 1000.0)); + this->pGlobeAnchor->SnapToEastSouthUp(); + + const FTransform& transform = this->pActor->GetActorTransform(); + FVector actualEcefEast = + pGeoreference + ->TransformUnrealDirectionToEarthCenteredEarthFixed( + transform.TransformVector(FVector::XAxisVector)) + .GetSafeNormal(); + FVector actualEcefSouth = + pGeoreference + ->TransformUnrealDirectionToEarthCenteredEarthFixed( + transform.TransformVector(FVector::YAxisVector)) + .GetSafeNormal(); + FVector actualEcefUp = + pGeoreference + ->TransformUnrealDirectionToEarthCenteredEarthFixed( + transform.TransformVector(FVector::ZAxisVector)) + .GetSafeNormal(); + + FMatrix enuToEcef = + UCesiumWgs84Ellipsoid::EastNorthUpToEarthCenteredEarthFixed( + this->pGlobeAnchor->GetEarthCenteredEarthFixedPosition()); + FVector expectedEcefEast = + enuToEcef.TransformVector(FVector::XAxisVector).GetSafeNormal(); + FVector expectedEcefSouth = + -enuToEcef.TransformVector(FVector::YAxisVector).GetSafeNormal(); + FVector expectedEcefUp = + enuToEcef.TransformVector(FVector::ZAxisVector).GetSafeNormal(); + + TestEqual("east", actualEcefEast, expectedEcefEast); + TestEqual("south", actualEcefSouth, expectedEcefSouth); + TestEqual("up", actualEcefUp, expectedEcefUp); + }); + + It("gains correct orientation on call to SnapLocalUpToEllipsoidNormal", + [this]() { + ACesiumGeoreference* pGeoreference = + this->pGlobeAnchor->GetResolvedGeoreference(); + + this->pGlobeAnchor->MoveToLongitudeLatitudeHeight( + FVector(-20.0, -10.0, 1000.0)); + this->pActor->SetActorRotation(FQuat::Identity); + this->pGlobeAnchor->SnapLocalUpToEllipsoidNormal(); + + const FTransform& transform = this->pActor->GetActorTransform(); + FVector actualEcefUp = + pGeoreference + ->TransformUnrealDirectionToEarthCenteredEarthFixed( + transform.TransformVector(FVector::ZAxisVector)) + .GetSafeNormal(); + + FVector surfaceNormal = UCesiumWgs84Ellipsoid::GeodeticSurfaceNormal( + this->pGlobeAnchor->GetEarthCenteredEarthFixedPosition()); + + TestEqual("up", actualEcefUp, surfaceNormal); + }); +} diff --git a/Source/CesiumRuntime/Public/CesiumGlobeAnchorComponent.h b/Source/CesiumRuntime/Public/CesiumGlobeAnchorComponent.h index 1fe1e220d..fbc975dc2 100644 --- a/Source/CesiumRuntime/Public/CesiumGlobeAnchorComponent.h +++ b/Source/CesiumRuntime/Public/CesiumGlobeAnchorComponent.h @@ -31,6 +31,10 @@ class CESIUMRUNTIME_API UCesiumGlobeAnchorComponent : public UActorComponent { * If this is null, the Component will find and use the first Georeference * Actor in the level, or create one if necessary. To get the active/effective * Georeference from Blueprints or C++, use ResolvedGeoreference instead. + * + * If setting this property changes the CesiumGeoreference, the globe position + * will be maintained and the Actor's transform will be updated according to + * the new CesiumGeoreference. */ UPROPERTY( EditAnywhere, @@ -97,6 +101,7 @@ class CESIUMRUNTIME_API UCesiumGlobeAnchorComponent : public UActorComponent { UPROPERTY( Transient, BlueprintReadOnly, + BlueprintGetter = GetResolvedGeoreference, Category = "Cesium", Meta = (AllowPrivateAccess)) ACesiumGeoreference* ResolvedGeoreference = nullptr; @@ -221,6 +226,18 @@ class CESIUMRUNTIME_API UCesiumGlobeAnchorComponent : public UActorComponent { UFUNCTION(BlueprintSetter) void SetGeoreference(TSoftObjectPtr NewGeoreference); + /** + * Gets the resolved georeference used by this component. This is not + * serialized because it may point to a Georeference in the PersistentLevel + * while this component is in a sub-level. If the Georeference property is + * specified, however then this property will have the same value. + * + * This property will be null before ResolveGeoreference is called, which + * happens automatically when the component is registered. + */ + UFUNCTION(BlueprintGetter) + ACesiumGeoreference* GetResolvedGeoreference() const; + /** * Resolves the Cesium Georeference to use with this Component. Returns * the value of the Georeference property if it is set. Otherwise, finds a