diff --git a/CHANGES.md b/CHANGES.md index c9ed9b681..a0eae06fe 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,10 @@ ### Next version (not released yet) +##### Additions :tada: + +- Added `SampleHeightMostDetailed` function to `Cesium3DTileset`, which asynchronously queries the height of a tileset at a list of positions. It is callable from both C++ and Blueprints. + ##### Fixes :wrench: - Drastically reduced tile mesh memory usage in UE 5.3 and 5.4 by working around a bug that causes those engine versions to add more texture coordinate sets than necessary. diff --git a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp index a4aea0780..623a3fcee 100644 --- a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp +++ b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp @@ -125,6 +125,72 @@ void ACesium3DTileset::SetMobility(EComponentMobility::Type NewMobility) { } } +void ACesium3DTileset::SampleHeightMostDetailed( + const TArray& LongitudeLatitudeHeightArray, + FCesiumSampleHeightMostDetailedCallback OnHeightsSampled) { + if (this->_pTileset == nullptr) { + this->LoadTileset(); + } + + std::vector positions; + positions.reserve(LongitudeLatitudeHeightArray.Num()); + + for (const FVector& position : LongitudeLatitudeHeightArray) { + positions.emplace_back(CesiumGeospatial::Cartographic::fromDegrees( + position.X, + position.Y, + position.Z)); + } + + CesiumAsync::Future future = + this->_pTileset + ? this->_pTileset->sampleHeightMostDetailed(positions) + : getAsyncSystem().createResolvedFuture( + Cesium3DTilesSelection::SampleHeightResult{ + std::move(positions), + std::vector(positions.size(), false), + {"Could not sample heights from tileset because it has not " + "been created."}}); + + std::move(future).thenImmediately( + [this, OnHeightsSampled = std::move(OnHeightsSampled)]( + Cesium3DTilesSelection::SampleHeightResult&& result) { + if (!IsValid(this)) + return; + + check(result.positions.size() == result.sampleSuccess.size()); + + // This should do nothing, but will prevent undefined behavior if + // the array sizes are unexpectedly different. + result.sampleSuccess.resize(result.positions.size(), false); + + TArray sampleHeightResults; + sampleHeightResults.Reserve(result.positions.size()); + + for (size_t i = 0; i < result.positions.size(); ++i) { + const CesiumGeospatial::Cartographic& position = result.positions[i]; + + FCesiumSampleHeightResult unrealResult; + unrealResult.LongitudeLatitudeHeight = FVector( + CesiumUtility::Math::radiansToDegrees(position.longitude), + CesiumUtility::Math::radiansToDegrees(position.latitude), + position.height); + unrealResult.SampleSuccess = result.sampleSuccess[i]; + + sampleHeightResults.Emplace(std::move(unrealResult)); + } + + TArray warnings; + warnings.Reserve(result.warnings.size()); + + for (const std::string& warning : result.warnings) { + warnings.Emplace(UTF8_TO_TCHAR(warning.c_str())); + } + + OnHeightsSampled.ExecuteIfBound(this, sampleHeightResults, warnings); + }); +} + void ACesium3DTileset::SetGeoreference( TSoftObjectPtr NewGeoreference) { this->Georeference = NewGeoreference; diff --git a/Source/CesiumRuntime/Private/CesiumEncodedMetadataConversions.h b/Source/CesiumRuntime/Private/CesiumEncodedMetadataConversions.h index eb38ca9db..09ad5fb60 100644 --- a/Source/CesiumRuntime/Private/CesiumEncodedMetadataConversions.h +++ b/Source/CesiumRuntime/Private/CesiumEncodedMetadataConversions.h @@ -2,6 +2,7 @@ #pragma once +#include "HAL/Platform.h" #include enum class ECesiumMetadataType : uint8; diff --git a/Source/CesiumRuntime/Private/CesiumMetadataPrimitive.cpp b/Source/CesiumRuntime/Private/CesiumMetadataPrimitive.cpp index 521c1b66f..38aad2253 100644 --- a/Source/CesiumRuntime/Private/CesiumMetadataPrimitive.cpp +++ b/Source/CesiumRuntime/Private/CesiumMetadataPrimitive.cpp @@ -1,10 +1,10 @@ // Copyright 2020-2024 CesiumGS, Inc. and Contributors -PRAGMA_DISABLE_DEPRECATION_WARNINGS - #include "CesiumMetadataPrimitive.h" #include "CesiumGltf/Model.h" +PRAGMA_DISABLE_DEPRECATION_WARNINGS + FCesiumMetadataPrimitive::FCesiumMetadataPrimitive( const FCesiumPrimitiveFeatures& PrimitiveFeatures, const FCesiumPrimitiveMetadata& PrimitiveMetadata, diff --git a/Source/CesiumRuntime/Private/CesiumSampleHeightMostDetailedAsyncAction.cpp b/Source/CesiumRuntime/Private/CesiumSampleHeightMostDetailedAsyncAction.cpp new file mode 100644 index 000000000..24d06196f --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumSampleHeightMostDetailedAsyncAction.cpp @@ -0,0 +1,34 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors +#include "CesiumSampleHeightMostDetailedAsyncAction.h" +#include "Cesium3DTileset.h" +#include "CesiumRuntime.h" + +/*static*/ UCesiumSampleHeightMostDetailedAsyncAction* +UCesiumSampleHeightMostDetailedAsyncAction::SampleHeightMostDetailed( + ACesium3DTileset* Tileset, + const TArray& LongitudeLatitudeHeightArray) { + UCesiumSampleHeightMostDetailedAsyncAction* pAsyncAction = + NewObject(); + pAsyncAction->_pTileset = Tileset; + pAsyncAction->_longitudeLatitudeHeightArray = LongitudeLatitudeHeightArray; + + return pAsyncAction; +} + +void UCesiumSampleHeightMostDetailedAsyncAction::Activate() { + this->RegisterWithGameInstance(this->_pTileset); + + this->_pTileset->SampleHeightMostDetailed( + this->_longitudeLatitudeHeightArray, + FCesiumSampleHeightMostDetailedCallback::CreateUObject( + this, + &UCesiumSampleHeightMostDetailedAsyncAction::RaiseOnHeightsSampled)); +} + +void UCesiumSampleHeightMostDetailedAsyncAction::RaiseOnHeightsSampled( + ACesium3DTileset* Tileset, + const TArray& Result, + const TArray& Warnings) { + this->OnHeightsSampled.Broadcast(Result, Warnings); + this->SetReadyToDestroy(); +} diff --git a/Source/CesiumRuntime/Private/Tests/CesiumLoadTestCore.cpp b/Source/CesiumRuntime/Private/Tests/CesiumLoadTestCore.cpp index efdc27401..d2853711e 100644 --- a/Source/CesiumRuntime/Private/Tests/CesiumLoadTestCore.cpp +++ b/Source/CesiumRuntime/Private/Tests/CesiumLoadTestCore.cpp @@ -36,11 +36,13 @@ struct LoadTestContext { LoadTestContext gLoadTestContext; -DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER( +DEFINE_LATENT_AUTOMATION_COMMAND_FOUR_PARAMETER( TimeLoadingCommand, FString, loggingName, SceneGenerationContext&, + creationContext, + SceneGenerationContext&, playContext, TestPass&, pass); @@ -75,31 +77,43 @@ bool TimeLoadingCommand::Update() { bool tilesetsloaded = playContext.areTilesetsDoneLoading(); bool timedOut = pass.elapsedTime >= testTimeout; - if (tilesetsloaded || timedOut) { - pass.endMark = timeMark; - UE_LOG(LogCesium, Display, TEXT("-- Load end mark -- %s"), *loggingName); + if (timedOut) { + UE_LOG( + LogCesium, + Error, + TEXT("TIMED OUT: Loading stopped after %.2f seconds"), + pass.elapsedTime); + // Command is done + pass.testInProgress = false; + return true; + } - if (timedOut) { - UE_LOG( - LogCesium, - Error, - TEXT("TIMED OUT: Loading stopped after %.2f seconds"), - pass.elapsedTime); - } else { + if (tilesetsloaded) { + // Run verify step as part of timing + // This is useful for running additional logic after a load, or if the step + // exists in the pass solely, timing very specific functionality (like + // terrain queries) + bool verifyComplete = true; + if (pass.verifyStep) + verifyComplete = + pass.verifyStep(creationContext, playContext, pass.optionalParameter); + + if (verifyComplete) { + pass.endMark = FPlatformTime::Seconds(); + UE_LOG(LogCesium, Display, TEXT("-- Load end mark -- %s"), *loggingName); + + pass.elapsedTime = pass.endMark - pass.startMark; UE_LOG( LogCesium, Display, - TEXT("Tileset load completed in %.2f seconds"), + TEXT("Pass completed in %.2f seconds"), pass.elapsedTime); - } - if (pass.verifyStep) - pass.verifyStep(playContext, pass.optionalParameter); + pass.testInProgress = false; - pass.testInProgress = false; - - // Command is done - return true; + // Command is done + return true; + } } // Let world tick, we'll come back to this command @@ -163,8 +177,6 @@ bool TestCleanupCommand::Update() { else defaultReportStep(context.testPasses); - // Turn on the editor tileset updates so we can see what we loaded - // gLoadTestContext.creationContext.setSuspendUpdate(false); return true; } @@ -246,8 +258,11 @@ bool RunLoadTest( // Do our timing capture FString loggingName = testName + "-" + pass.name; - ADD_LATENT_AUTOMATION_COMMAND( - TimeLoadingCommand(loggingName, context.playContext, pass)); + ADD_LATENT_AUTOMATION_COMMAND(TimeLoadingCommand( + loggingName, + context.creationContext, + context.playContext, + pass)); ADD_LATENT_AUTOMATION_COMMAND(FWaitLatentCommand(1.0f)); diff --git a/Source/CesiumRuntime/Private/Tests/CesiumLoadTestCore.h b/Source/CesiumRuntime/Private/Tests/CesiumLoadTestCore.h index 637d76e85..ef09a10d5 100644 --- a/Source/CesiumRuntime/Private/Tests/CesiumLoadTestCore.h +++ b/Source/CesiumRuntime/Private/Tests/CesiumLoadTestCore.h @@ -14,11 +14,14 @@ namespace Cesium { struct TestPass { typedef swl::variant TestingParameter; typedef std::function - PassCallback; + SetupCallback; + typedef std::function< + bool(SceneGenerationContext&, SceneGenerationContext&, TestingParameter)> + VerifyCallback; FString name; - PassCallback setupStep; - PassCallback verifyStep; + SetupCallback setupStep; + VerifyCallback verifyStep; TestingParameter optionalParameter; bool testInProgress = false; diff --git a/Source/CesiumRuntime/Private/Tests/CesiumTestHelpers.cpp b/Source/CesiumRuntime/Private/Tests/CesiumTestHelpers.cpp index a1b53d17f..01fda781b 100644 --- a/Source/CesiumRuntime/Private/Tests/CesiumTestHelpers.cpp +++ b/Source/CesiumRuntime/Private/Tests/CesiumTestHelpers.cpp @@ -4,6 +4,10 @@ #include "CesiumGeoreference.h" #include "Engine/Engine.h" +#if WITH_EDITOR +#include "Editor/EditorPerformanceSettings.h" +#endif + namespace CesiumTestHelpers { UWorld* getGlobalWorldContext() { @@ -63,6 +67,37 @@ FName getUniqueTag(UActorComponent* pComponent) { return FName(FString::Printf(TEXT("%lld"), pComponent)); } +#if WITH_EDITOR +namespace { +size_t timesAllowingEditorTick = 0; +bool originalEditorTickState = true; +} // namespace +#endif + +void pushAllowTickInEditor() { +#if WITH_EDITOR + if (timesAllowingEditorTick == 0) { + UEditorPerformanceSettings* pSettings = + GetMutableDefault(); + originalEditorTickState = pSettings->bThrottleCPUWhenNotForeground; + pSettings->bThrottleCPUWhenNotForeground = false; + } + + ++timesAllowingEditorTick; +#endif +} + +void popAllowTickInEditor() { +#if WITH_EDITOR + --timesAllowingEditorTick; + if (timesAllowingEditorTick == 0) { + UEditorPerformanceSettings* pSettings = + GetMutableDefault(); + pSettings->bThrottleCPUWhenNotForeground = originalEditorTickState; + } +#endif +} + void trackForPlay(AActor* pEditorActor) { pEditorActor->Tags.Add(getUniqueTag(pEditorActor)); } diff --git a/Source/CesiumRuntime/Private/Tests/CesiumTestHelpers.h b/Source/CesiumRuntime/Private/Tests/CesiumTestHelpers.h index 2e0c67ac5..f6595478d 100644 --- a/Source/CesiumRuntime/Private/Tests/CesiumTestHelpers.h +++ b/Source/CesiumRuntime/Private/Tests/CesiumTestHelpers.h @@ -52,7 +52,10 @@ void waitForImpl( done.Execute(); } else { pWorld->GetTimerManager().SetTimerForNextTick( - [done, pWorld, condition, timerHandle]() mutable { + [done, + pWorld, + condition = std::forward(condition), + timerHandle]() mutable { waitForImpl(done, pWorld, std::move(condition), timerHandle); }); } @@ -135,6 +138,20 @@ FName getUniqueTag(AActor* pActor); /// The unique tag. FName getUniqueTag(UActorComponent* pComponent); +/// +/// By default, UE 5.3+ don't tick in a headless Editor, which is often used to +/// run tests. Call this at the start of a test that requires ticking to +/// override this default. Call popAllowTickInEditor after the test to restore +/// the default. +/// +void pushAllowTickInEditor(); + +/// +/// Call this after a test that needs working ticking to restore the default +/// state. +/// +void popAllowTickInEditor(); + #if WITH_EDITOR /// diff --git a/Source/CesiumRuntime/Private/Tests/Google3dTilesLoadTest.cpp b/Source/CesiumRuntime/Private/Tests/GooglePhotorealistic3dTilesLoad.perf.cpp similarity index 84% rename from Source/CesiumRuntime/Private/Tests/Google3dTilesLoadTest.cpp rename to Source/CesiumRuntime/Private/Tests/GooglePhotorealistic3dTilesLoad.perf.cpp index 4cedace0f..f1039fba2 100644 --- a/Source/CesiumRuntime/Private/Tests/Google3dTilesLoadTest.cpp +++ b/Source/CesiumRuntime/Private/Tests/GooglePhotorealistic3dTilesLoad.perf.cpp @@ -15,48 +15,48 @@ using namespace Cesium; IMPLEMENT_SIMPLE_AUTOMATION_TEST( - FGoogleTilesPompidou, - "Cesium.Performance.GoogleTiles.LocalePompidou", + FLoadTilesetGooglePompidou, + "Cesium.Performance.Tileset Loading.Google P3DT Pompidou", EAutomationTestFlags::EditorContext | EAutomationTestFlags::PerfFilter) IMPLEMENT_SIMPLE_AUTOMATION_TEST( - FGoogleTilesChrysler, - "Cesium.Performance.GoogleTiles.LocaleChrysler", + FLoadTilesetGoogleChrysler, + "Cesium.Performance.Tileset Loading.Google P3DT Chrysler", EAutomationTestFlags::EditorContext | EAutomationTestFlags::PerfFilter) IMPLEMENT_SIMPLE_AUTOMATION_TEST( - FGoogleTilesChryslerWarm, - "Cesium.Performance.GoogleTiles.LocaleChrysler (Warm)", + FLoadTilesetGoogleChryslerWarm, + "Cesium.Performance.Tileset Loading.Google P3DT Chrysler, warm cache", EAutomationTestFlags::EditorContext | EAutomationTestFlags::PerfFilter) IMPLEMENT_SIMPLE_AUTOMATION_TEST( - FGoogleTilesGuggenheim, - "Cesium.Performance.GoogleTiles.LocaleGuggenheim", + FLoadTilesetGoogleGuggenheim, + "Cesium.Performance.Tileset Loading.Google P3DT Guggenheim", EAutomationTestFlags::EditorContext | EAutomationTestFlags::PerfFilter) IMPLEMENT_SIMPLE_AUTOMATION_TEST( - FGoogleTilesDeathValley, - "Cesium.Performance.GoogleTiles.LocaleDeathValley", + FLoadTilesetGoogleDeathValley, + "Cesium.Performance.Tileset Loading.Google P3DT DeathValley", EAutomationTestFlags::EditorContext | EAutomationTestFlags::PerfFilter) IMPLEMENT_SIMPLE_AUTOMATION_TEST( - FGoogleTilesDeathValleyWarm, - "Cesium.Performance.GoogleTiles.LocaleDeathValley (Warm)", + FLoadTilesetGoogleDeathValleyWarm, + "Cesium.Performance.Tileset Loading.Google P3DT DeathValley, warm cache", EAutomationTestFlags::EditorContext | EAutomationTestFlags::PerfFilter) IMPLEMENT_SIMPLE_AUTOMATION_TEST( - FGoogleTilesTokyo, - "Cesium.Performance.GoogleTiles.LocaleTokyo", + FLoadTilesetGoogleTokyo, + "Cesium.Performance.Tileset Loading.Google P3DT Tokyo", EAutomationTestFlags::EditorContext | EAutomationTestFlags::PerfFilter) IMPLEMENT_SIMPLE_AUTOMATION_TEST( - FGoogleTilesGoogleplex, - "Cesium.Performance.GoogleTiles.LocaleGoogleplex", + FLoadTilesetGoogleGoogleplex, + "Cesium.Performance.Tileset Loading.Google P3DT Googleplex", EAutomationTestFlags::EditorContext | EAutomationTestFlags::PerfFilter) IMPLEMENT_SIMPLE_AUTOMATION_TEST( - FGoogleTilesMaxTileLoads, - "Cesium.Performance.GoogleTiles.VaryMaxTileLoads", + FLoadTilesetGoogleChryslerVaryMaxTileLoads, + "Cesium.Performance.Tileset Loading.Google P3DT Chrysler, vary max tile loads", EAutomationTestFlags::EditorContext | EAutomationTestFlags::PerfFilter) #define TEST_SCREEN_WIDTH 1280 @@ -183,7 +183,7 @@ void setupForGoogleplex(SceneGenerationContext& context) { context.tilesets.push_back(tileset); } -bool FGoogleTilesPompidou::RunTest(const FString& Parameters) { +bool FLoadTilesetGooglePompidou::RunTest(const FString& Parameters) { std::vector testPasses; testPasses.push_back(TestPass{"Cold Cache", googleSetupClearCache, nullptr}); @@ -195,7 +195,7 @@ bool FGoogleTilesPompidou::RunTest(const FString& Parameters) { TEST_SCREEN_HEIGHT); } -bool FGoogleTilesChrysler::RunTest(const FString& Parameters) { +bool FLoadTilesetGoogleChrysler::RunTest(const FString& Parameters) { std::vector testPasses; testPasses.push_back(TestPass{"Cold Cache", googleSetupClearCache, nullptr}); @@ -207,7 +207,7 @@ bool FGoogleTilesChrysler::RunTest(const FString& Parameters) { TEST_SCREEN_HEIGHT); } -bool FGoogleTilesChryslerWarm::RunTest(const FString& Parameters) { +bool FLoadTilesetGoogleChryslerWarm::RunTest(const FString& Parameters) { std::vector testPasses; testPasses.push_back( TestPass{"Warm Cache", googleSetupRefreshTilesets, nullptr}); @@ -220,7 +220,7 @@ bool FGoogleTilesChryslerWarm::RunTest(const FString& Parameters) { TEST_SCREEN_HEIGHT); } -bool FGoogleTilesGuggenheim::RunTest(const FString& Parameters) { +bool FLoadTilesetGoogleGuggenheim::RunTest(const FString& Parameters) { std::vector testPasses; testPasses.push_back(TestPass{"Cold Cache", googleSetupClearCache, nullptr}); @@ -232,7 +232,7 @@ bool FGoogleTilesGuggenheim::RunTest(const FString& Parameters) { TEST_SCREEN_HEIGHT); } -bool FGoogleTilesDeathValley::RunTest(const FString& Parameters) { +bool FLoadTilesetGoogleDeathValley::RunTest(const FString& Parameters) { std::vector testPasses; testPasses.push_back(TestPass{"Cold Cache", googleSetupClearCache, nullptr}); @@ -244,7 +244,7 @@ bool FGoogleTilesDeathValley::RunTest(const FString& Parameters) { TEST_SCREEN_HEIGHT); } -bool FGoogleTilesDeathValleyWarm::RunTest(const FString& Parameters) { +bool FLoadTilesetGoogleDeathValleyWarm::RunTest(const FString& Parameters) { std::vector testPasses; testPasses.push_back( TestPass{"Warm Cache", googleSetupRefreshTilesets, nullptr}); @@ -257,7 +257,7 @@ bool FGoogleTilesDeathValleyWarm::RunTest(const FString& Parameters) { TEST_SCREEN_HEIGHT); } -bool FGoogleTilesTokyo::RunTest(const FString& Parameters) { +bool FLoadTilesetGoogleTokyo::RunTest(const FString& Parameters) { std::vector testPasses; testPasses.push_back(TestPass{"Cold Cache", googleSetupClearCache, nullptr}); @@ -269,7 +269,7 @@ bool FGoogleTilesTokyo::RunTest(const FString& Parameters) { TEST_SCREEN_HEIGHT); } -bool FGoogleTilesGoogleplex::RunTest(const FString& Parameters) { +bool FLoadTilesetGoogleGoogleplex::RunTest(const FString& Parameters) { std::vector testPasses; testPasses.push_back(TestPass{"Cold Cache", googleSetupClearCache, nullptr}); @@ -281,7 +281,8 @@ bool FGoogleTilesGoogleplex::RunTest(const FString& Parameters) { TEST_SCREEN_HEIGHT); } -bool FGoogleTilesMaxTileLoads::RunTest(const FString& Parameters) { +bool FLoadTilesetGoogleChryslerVaryMaxTileLoads::RunTest( + const FString& Parameters) { auto setupPass = [this]( SceneGenerationContext& context, TestPass::TestingParameter parameter) { diff --git a/Source/CesiumRuntime/Private/Tests/SampleHeightCallbackReceiver.h b/Source/CesiumRuntime/Private/Tests/SampleHeightCallbackReceiver.h new file mode 100644 index 000000000..7c354d601 --- /dev/null +++ b/Source/CesiumRuntime/Private/Tests/SampleHeightCallbackReceiver.h @@ -0,0 +1,39 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#pragma once + +#include "CesiumSampleHeightMostDetailedAsyncAction.h" +#include "UObject/Object.h" +#include "UObject/ObjectMacros.h" +#include +#include "SampleHeightCallbackReceiver.generated.h" + +UCLASS() +class USampleHeightCallbackReceiver : public UObject { + GENERATED_BODY() + +public: + using TFunction = std::function< + void(const TArray&, const TArray&)>; + + static void + Bind(FCesiumSampleHeightMostDetailedComplete& delegate, TFunction callback) { + USampleHeightCallbackReceiver* p = + NewObject(); + p->_callback = callback; + p->AddToRoot(); + + delegate.AddUniqueDynamic(p, &USampleHeightCallbackReceiver::Receiver); + } + +private: + UFUNCTION() + void Receiver( + const TArray& Result, + const TArray& Warnings) { + this->_callback(Result, Warnings); + this->RemoveFromRoot(); + } + + TFunction _callback; +}; diff --git a/Source/CesiumRuntime/Private/Tests/SampleHeightMostDetailed.perf.cpp b/Source/CesiumRuntime/Private/Tests/SampleHeightMostDetailed.perf.cpp new file mode 100644 index 000000000..f54441636 --- /dev/null +++ b/Source/CesiumRuntime/Private/Tests/SampleHeightMostDetailed.perf.cpp @@ -0,0 +1,484 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#if WITH_EDITOR + +#include "CesiumLoadTestCore.h" + +#include "Misc/AutomationTest.h" + +#include "CesiumAsync/ICacheDatabase.h" +#include "CesiumGltfComponent.h" +#include "CesiumIonRasterOverlay.h" +#include "CesiumRuntime.h" +#include "CesiumSunSky.h" +#include "GlobeAwareDefaultPawn.h" + +#include "Engine/StaticMeshActor.h" + +using namespace Cesium; +using namespace std::chrono_literals; + +namespace { +void setupDenverHillsCesiumWorldTerrain(SceneGenerationContext& context); +void setupDenverHillsGoogle(SceneGenerationContext& context); +bool RunSingleQueryTest( + const FString& testName, + std::function setup); +bool RunMultipleQueryTest( + const FString& testName, + std::function setup); +} // namespace + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FSampleHeightMostDetailedCesiumWorldTerrainSingle, + "Cesium.Performance.SampleHeightMostDetailed.Single query against Cesium World Terrain", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::PerfFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FSampleHeightMostDetailedCesiumWorldTerrainMultiple, + "Cesium.Performance.SampleHeightMostDetailed.Multiple queries against Cesium World Terrain", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::PerfFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FSampleHeightMostDetailedGoogleSingle, + "Cesium.Performance.SampleHeightMostDetailed.Single query against Google Photorealistic 3D Tiles", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::PerfFilter) + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FSampleHeightMostDetailedGoogleMultiple, + "Cesium.Performance.SampleHeightMostDetailed.Multiple queries against Google Photorealistic 3D Tiles", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::PerfFilter) + +bool FSampleHeightMostDetailedCesiumWorldTerrainSingle::RunTest( + const FString& Parameters) { + return RunSingleQueryTest( + this->GetBeautifiedTestName(), + setupDenverHillsCesiumWorldTerrain); +} + +bool FSampleHeightMostDetailedCesiumWorldTerrainMultiple::RunTest( + const FString& Parameters) { + return RunMultipleQueryTest( + this->GetBeautifiedTestName(), + setupDenverHillsCesiumWorldTerrain); +} + +bool FSampleHeightMostDetailedGoogleSingle::RunTest(const FString& Parameters) { + return RunSingleQueryTest( + this->GetBeautifiedTestName(), + setupDenverHillsGoogle); +} + +bool FSampleHeightMostDetailedGoogleMultiple::RunTest( + const FString& Parameters) { + return RunMultipleQueryTest( + this->GetBeautifiedTestName(), + setupDenverHillsGoogle); +} + +namespace { +// Our test model path +// +// Uses a simple cube, but to see trees instead, download 'temperate Vegetation: +// Spruce Forest' from the Unreal Engine Marketplace then use the following +// path... +// "'/Game/PN_interactiveSpruceForest/Meshes/full/low/spruce_full_01_low.spruce_full_01_low'" +FString terrainQueryTestModelPath( + TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'")); + +void setupDenverHillsCesiumWorldTerrain(SceneGenerationContext& context) { + context.setCommonProperties( + FVector(-105.238887, 39.756177, 1887.175525), + FVector(0, 0, 0), + FRotator(-7, -226, -5), + 90.0f); + + // Add Cesium World Terrain + ACesium3DTileset* worldTerrainTileset = + context.world->SpawnActor(); + worldTerrainTileset->SetTilesetSource(ETilesetSource::FromCesiumIon); + worldTerrainTileset->SetIonAssetID(1); + worldTerrainTileset->SetIonAccessToken(SceneGenerationContext::testIonToken); + worldTerrainTileset->SetActorLabel(TEXT("Cesium World Terrain")); + worldTerrainTileset->MaximumCachedBytes = 0; + + context.tilesets.push_back(worldTerrainTileset); +} + +void setupDenverHillsGoogle(SceneGenerationContext& context) { + context.setCommonProperties( + FVector(-105.238887, 39.756177, 1887.175525), + FVector(0, 0, 0), + FRotator(-7, -226, -5), + 90.0f); + + // Add Cesium World Terrain + ACesium3DTileset* googleTileset = + context.world->SpawnActor(); + googleTileset->SetTilesetSource(ETilesetSource::FromCesiumIon); + googleTileset->SetIonAssetID(2275207); + googleTileset->SetIonAccessToken(SceneGenerationContext::testIonToken); + googleTileset->SetActorLabel(TEXT("Google Photorealistic 3D Tiles")); + googleTileset->MaximumCachedBytes = 0; + + context.tilesets.push_back(googleTileset); +} + +bool RunSingleQueryTest( + const FString& testName, + std::function setup) { + auto clearCache = [](SceneGenerationContext& context, + TestPass::TestingParameter parameter) { + std::shared_ptr pCacheDatabase = + getCacheDatabase(); + pCacheDatabase->clearAll(); + }; + + struct TestResults { + std::atomic queryFinished = false; + TArray heightResults; + TArray warnings; + }; + + static TestResults testResults; + + auto issueQueries = [&testResults = testResults]( + SceneGenerationContext& context, + TestPass::TestingParameter parameter) { + // Test right at camera position + double testLongitude = -105.257595; + double testLatitude = 39.743103; + + // Make a grid of test points + const size_t gridRowCount = 20; + const size_t gridColumnCount = 20; + double cartographicSpacing = 0.001; + + TArray queryInput; + + for (size_t rowIndex = 0; rowIndex < gridRowCount; ++rowIndex) { + double rowLatitude = testLatitude + (cartographicSpacing * rowIndex); + + for (size_t columnIndex = 0; columnIndex < gridColumnCount; + ++columnIndex) { + FVector queryInstance = { + testLongitude + (cartographicSpacing * columnIndex), + rowLatitude, + 0.0}; + + queryInput.Add(queryInstance); + } + } + + ACesium3DTileset* tileset = context.tilesets[0]; + + tileset->SampleHeightMostDetailed( + queryInput, + FCesiumSampleHeightMostDetailedCallback::CreateLambda( + [&testResults]( + ACesium3DTileset* Tileset, + const TArray& Results, + const TArray& Warnings) { + testResults.heightResults = Results; + testResults.warnings = Warnings; + testResults.queryFinished = true; + })); + }; + + auto waitForQueries = [&testResults = testResults]( + SceneGenerationContext& creationContext, + SceneGenerationContext& playContext, + TestPass::TestingParameter parameter) { + return (bool)testResults.queryFinished; + }; + + auto showResults = [&testResults = testResults]( + SceneGenerationContext& creationContext, + SceneGenerationContext& playContext, + TestPass::TestingParameter parameter) { + // Turn on the editor tileset updates so we can see what we loaded + creationContext.setSuspendUpdate(false); + + // Place an object on the ground to verify position + UWorld* World = creationContext.world; + + UStaticMesh* testMesh = + LoadObject(nullptr, *terrainQueryTestModelPath); + + ACesium3DTileset* tileset = playContext.tilesets[0]; + Cesium3DTilesSelection::Tileset* nativeTileset = tileset->GetTileset(); + + // Log any warnings + for (const FString& warning : testResults.warnings) { + UE_LOG(LogCesium, Warning, TEXT("Height query warning: %s"), *warning); + } + + int32 resultCount = testResults.heightResults.Num(); + for (int32 resultIndex = 0; resultIndex < resultCount; ++resultIndex) { + const FVector& queryLongitudeLatitudeHeight = + testResults.heightResults[resultIndex].LongitudeLatitudeHeight; + + if (!testResults.heightResults[resultIndex].SampleSuccess) { + UE_LOG( + LogCesium, + Error, + TEXT("The height at (%f,%f) was not sampled successfully."), + queryLongitudeLatitudeHeight.X, + queryLongitudeLatitudeHeight.Y); + continue; + } + + FVector unrealPosition = + tileset->ResolveGeoreference() + ->TransformLongitudeLatitudeHeightPositionToUnreal( + queryLongitudeLatitudeHeight); + + // Now bring the hit point to unreal world coordinates + FVector unrealWorldPosition = + tileset->GetActorTransform().TransformFVector4(unrealPosition); + + AStaticMeshActor* staticMeshActor = World->SpawnActor(); + staticMeshActor->GetStaticMeshComponent()->SetStaticMesh(testMesh); + staticMeshActor->SetActorLocation(unrealWorldPosition); + staticMeshActor->SetActorScale3D(FVector(7, 7, 7)); + staticMeshActor->SetActorLabel( + FString::Printf(TEXT("Hit %d"), resultIndex)); + staticMeshActor->SetFolderPath("/QueryResults"); + } + + return true; + }; + + std::vector testPasses; + testPasses.push_back( + TestPass{"Load terrain from cold cache", clearCache, nullptr}); + testPasses.push_back( + TestPass{"Issue height queries and wait", issueQueries, waitForQueries}); + testPasses.push_back( + TestPass{"Populate scene with results", nullptr, showResults}); + + return RunLoadTest(testName, setup, testPasses, 1280, 768); +} + +bool RunMultipleQueryTest( + const FString& testName, + std::function setup) { + struct QueryObject { + FVector coordinateDegrees; + + AStaticMeshActor* creationMeshActor = nullptr; + AStaticMeshActor* playMeshActor = nullptr; + + bool queryFinished = false; + }; + + struct TestProcess { + std::vector queryObjects; + }; + + auto pProcess = std::make_shared(); + + // + // Setup all object positions that will receive queries + // + // Test right at camera position + double testLongitude = -105.257595; + double testLatitude = 39.743103; + + // Make a grid of test points + const size_t gridRowCount = 20; + const size_t gridColumnCount = 20; + double cartographicSpacing = 0.001; + + for (size_t rowIndex = 0; rowIndex < gridRowCount; ++rowIndex) { + double rowLatitude = testLatitude + (cartographicSpacing * rowIndex); + + for (size_t columnIndex = 0; columnIndex < gridColumnCount; ++columnIndex) { + FVector position( + testLongitude + (cartographicSpacing * columnIndex), + rowLatitude, + 2190.0); + + QueryObject newQueryObject = {position}; + + pProcess->queryObjects.push_back(std::move(newQueryObject)); + } + } + + auto clearCache = [](SceneGenerationContext&, TestPass::TestingParameter) { + std::shared_ptr pCacheDatabase = + getCacheDatabase(); + pCacheDatabase->clearAll(); + }; + + auto addTestObjects = [pProcess]( + SceneGenerationContext& creationContext, + SceneGenerationContext& playContext, + TestPass::TestingParameter) { + // Place an object on the ground to verify position + UWorld* creationWorld = creationContext.world; + UWorld* playWorld = playContext.world; + + UStaticMesh* testMesh = + LoadObject(nullptr, *terrainQueryTestModelPath); + + ACesium3DTileset* tileset = playContext.tilesets[0]; + Cesium3DTilesSelection::Tileset* nativeTileset = tileset->GetTileset(); + + for (size_t queryIndex = 0; queryIndex < pProcess->queryObjects.size(); + ++queryIndex) { + QueryObject& queryObject = pProcess->queryObjects[queryIndex]; + + FVector unrealPosition = + tileset->ResolveGeoreference() + ->TransformLongitudeLatitudeHeightPositionToUnreal( + queryObject.coordinateDegrees); + + // Now bring the hit point to unreal world coordinates + FVector unrealWorldPosition = + tileset->GetActorTransform().TransformFVector4(unrealPosition); + + { + AStaticMeshActor* staticMeshActor = + creationWorld->SpawnActor(); + staticMeshActor->SetMobility(EComponentMobility::Movable); + staticMeshActor->GetStaticMeshComponent()->SetStaticMesh(testMesh); + staticMeshActor->SetActorLocation(unrealWorldPosition); + staticMeshActor->SetActorScale3D(FVector(7, 7, 7)); + staticMeshActor->SetActorLabel( + FString::Printf(TEXT("Hit %d"), queryIndex)); + staticMeshActor->SetFolderPath("/QueryResults"); + queryObject.creationMeshActor = staticMeshActor; + } + + { + AStaticMeshActor* staticMeshActor = + playWorld->SpawnActor(); + staticMeshActor->SetMobility(EComponentMobility::Movable); + staticMeshActor->GetStaticMeshComponent()->SetStaticMesh(testMesh); + staticMeshActor->SetActorLocation(unrealWorldPosition); + staticMeshActor->SetActorScale3D(FVector(7, 7, 7)); + staticMeshActor->SetActorLabel( + FString::Printf(TEXT("Hit %d"), queryIndex)); + staticMeshActor->SetFolderPath("/QueryResults"); + queryObject.playMeshActor = staticMeshActor; + } + } + return true; + }; + + auto issueQueries = [pProcess]( + SceneGenerationContext& context, + TestPass::TestingParameter) { + ACesium3DTileset* tileset = context.tilesets[0]; + + for (QueryObject& queryObject : pProcess->queryObjects) { + tileset->SampleHeightMostDetailed( + {queryObject.coordinateDegrees}, + FCesiumSampleHeightMostDetailedCallback::CreateLambda( + [tileset, &queryObject]( + ACesium3DTileset* pTileset, + const TArray& results, + const TArray& warnings) { + queryObject.queryFinished = true; + + // Log any warnings + for (const FString& warning : warnings) { + UE_LOG( + LogCesium, + Warning, + TEXT("Height query traversal warning: %s"), + *warning); + } + + if (results.Num() != 1) { + UE_LOG( + LogCesium, + Warning, + TEXT("Unexpected number of results received")); + return; + } + + const FVector& newCoordinate = + results[0].LongitudeLatitudeHeight; + if (!results[0].SampleSuccess) { + UE_LOG( + LogCesium, + Error, + TEXT( + "The height at (%f,%f) was not sampled successfully."), + newCoordinate.X, + newCoordinate.Y); + return; + } + + const FVector& originalCoordinate = + queryObject.coordinateDegrees; + + if (!FMath::IsNearlyEqual( + originalCoordinate.X, + newCoordinate.X, + 1e-12) || + !FMath::IsNearlyEqual( + originalCoordinate.Y, + newCoordinate.Y, + 1e-12)) { + UE_LOG( + LogCesium, + Warning, + TEXT("Hit result doesn't match original input")); + return; + } + + FVector unrealPosition = + tileset->ResolveGeoreference() + ->TransformLongitudeLatitudeHeightPositionToUnreal( + newCoordinate); + + // Now bring the hit point to unreal world coordinates + FVector unrealWorldPosition = + tileset->GetActorTransform().TransformFVector4( + unrealPosition); + + queryObject.creationMeshActor->SetActorLocation( + unrealWorldPosition); + + queryObject.playMeshActor->SetActorLocation( + unrealWorldPosition); + })); + } + }; + + auto waitForQueries = [pProcess]( + SceneGenerationContext&, + SceneGenerationContext&, + TestPass::TestingParameter) { + for (QueryObject& queryObject : pProcess->queryObjects) { + if (!queryObject.queryFinished) + return false; + } + return true; + }; + + auto showResults = [](SceneGenerationContext& creationContext, + SceneGenerationContext&, + TestPass::TestingParameter) { + // Turn on the editor tileset updates so we can see what we loaded + creationContext.setSuspendUpdate(false); + return true; + }; + + std::vector testPasses; + testPasses.push_back( + TestPass{"Load terrain from cold cache", clearCache, nullptr}); + testPasses.push_back(TestPass{"Add test objects", nullptr, addTestObjects}); + testPasses.push_back( + TestPass{"Issue height queries and wait", issueQueries, waitForQueries}); + testPasses.push_back(TestPass{"Show results", nullptr, showResults}); + + return RunLoadTest(testName, setup, testPasses, 1280, 720); +} + +} // namespace + +#endif diff --git a/Source/CesiumRuntime/Private/Tests/SampleHeightMostDetailed.spec.cpp b/Source/CesiumRuntime/Private/Tests/SampleHeightMostDetailed.spec.cpp new file mode 100644 index 000000000..4dd80b517 --- /dev/null +++ b/Source/CesiumRuntime/Private/Tests/SampleHeightMostDetailed.spec.cpp @@ -0,0 +1,301 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#include "Cesium3DTileset.h" +#include "CesiumSampleHeightMostDetailedAsyncAction.h" +#include "CesiumSceneGeneration.h" +#include "CesiumTestHelpers.h" +#include "Misc/AutomationTest.h" +#include "SampleHeightCallbackReceiver.h" + +BEGIN_DEFINE_SPEC( + FSampleHeightMostDetailedSpec, + "Cesium.Unit.SampleHeightMostDetailed", + EAutomationTestFlags::ApplicationContextMask | + EAutomationTestFlags::ProductFilter) + +TObjectPtr pTileset; + +END_DEFINE_SPEC(FSampleHeightMostDetailedSpec) + +// The intention of these tests is not to verify that height querying produces +// correct heights, because the cesium-native tests already do that. It's just +// to verify that the Unreal wrapper API around cesium-native is working +// correctly. + +void FSampleHeightMostDetailedSpec::Define() { + Describe("Cesium World Terrain", [this]() { + BeforeEach([this]() { + CesiumTestHelpers::pushAllowTickInEditor(); + + UWorld* pWorld = CesiumTestHelpers::getGlobalWorldContext(); + pTileset = pWorld->SpawnActor(); + pTileset->SetIonAssetID(1); +#if WITH_EDITOR + pTileset->SetIonAccessToken(Cesium::SceneGenerationContext::testIonToken); + pTileset->SetActorLabel(TEXT("Cesium World Terrain")); +#endif + }); + + AfterEach(EAsyncExecution::TaskGraphMainThread, [this]() { + pTileset->Destroy(); + + CesiumTestHelpers::popAllowTickInEditor(); + }); + + LatentIt( + "works with an empty array of positions", + EAsyncExecution::TaskGraphMainThread, + [this](const FDoneDelegate& done) { + pTileset->SampleHeightMostDetailed( + {}, + FCesiumSampleHeightMostDetailedCallback::CreateLambda( + [this, done]( + ACesium3DTileset* pTileset, + const TArray& result, + const TArray& warnings) { + TestEqual("Number of results", result.Num(), 0); + TestEqual("Number of warnings", warnings.Num(), 0); + done.ExecuteIfBound(); + })); + }); + + LatentIt( + "works with a single position", + EAsyncExecution::TaskGraphMainThread, + [this](const FDoneDelegate& done) { + pTileset->SampleHeightMostDetailed( + {FVector(-105.1, 40.1, 1.0)}, + FCesiumSampleHeightMostDetailedCallback::CreateLambda( + [this, done]( + ACesium3DTileset* pTileset, + const TArray& result, + const TArray& warnings) { + TestEqual("Number of results", result.Num(), 1); + TestEqual("Number of warnings", warnings.Num(), 0); + TestTrue("SampleSuccess", result[0].SampleSuccess); + TestEqual( + "Longitude", + result[0].LongitudeLatitudeHeight.X, + -105.1, + 1e-12); + TestEqual( + "Latitude", + result[0].LongitudeLatitudeHeight.Y, + 40.1, + 1e-12); + TestTrue( + "Height", + !FMath::IsNearlyEqual( + result[0].LongitudeLatitudeHeight.Z, + 1.0, + 1.0)); + done.ExecuteIfBound(); + })); + }); + + LatentIt( + "works with multiple positions", + EAsyncExecution::TaskGraphMainThread, + [this](const FDoneDelegate& done) { + pTileset->SampleHeightMostDetailed( + {FVector(-105.1, 40.1, 1.0), FVector(105.1, -40.1, 1.0)}, + FCesiumSampleHeightMostDetailedCallback::CreateLambda( + [this, done]( + ACesium3DTileset* pTileset, + const TArray& result, + const TArray& warnings) { + TestEqual("Number of results", result.Num(), 2); + TestEqual("Number of warnings", warnings.Num(), 0); + TestTrue("SampleSuccess", result[0].SampleSuccess); + TestEqual( + "Longitude", + result[0].LongitudeLatitudeHeight.X, + -105.1, + 1e-12); + TestEqual( + "Latitude", + result[0].LongitudeLatitudeHeight.Y, + 40.1, + 1e-12); + TestTrue( + "Height", + !FMath::IsNearlyEqual( + result[0].LongitudeLatitudeHeight.Z, + 1.0, + 1.0)); + TestTrue("SampleSuccess", result[1].SampleSuccess); + TestEqual( + "Longitude", + result[1].LongitudeLatitudeHeight.X, + 105.1, + 1e-12); + TestEqual( + "Latitude", + result[1].LongitudeLatitudeHeight.Y, + -40.1, + 1e-12); + TestTrue( + "Height", + !FMath::IsNearlyEqual( + result[1].LongitudeLatitudeHeight.Z, + 1.0, + 1.0)); + done.ExecuteIfBound(); + })); + }); + }); + + Describe("Melbourne Photogrammetry", [this]() { + BeforeEach([this]() { + CesiumTestHelpers::pushAllowTickInEditor(); + + UWorld* pWorld = CesiumTestHelpers::getGlobalWorldContext(); + pTileset = pWorld->SpawnActor(); + pTileset->SetIonAssetID(69380); +#if WITH_EDITOR + pTileset->SetIonAccessToken(Cesium::SceneGenerationContext::testIonToken); + pTileset->SetActorLabel(TEXT("Melbourne Photogrammetry")); +#endif + }); + + AfterEach(EAsyncExecution::TaskGraphMainThread, [this]() { + pTileset->Destroy(); + + CesiumTestHelpers::popAllowTickInEditor(); + }); + + LatentIt( + "indicates !HeightSampled for position outside tileset", + EAsyncExecution::TaskGraphMainThread, + [this](const FDoneDelegate& done) { + pTileset->SampleHeightMostDetailed( + // Somewhere in Sydney, not Melbourne + {FVector(151.20972, -33.87100, 1.0)}, + FCesiumSampleHeightMostDetailedCallback::CreateLambda( + [this, done]( + ACesium3DTileset* pTileset, + const TArray& result, + const TArray& warnings) { + TestEqual("Number of results", result.Num(), 1); + TestEqual("Number of warnings", warnings.Num(), 0); + TestTrue("SampleSuccess", !result[0].SampleSuccess); + TestEqual( + "Longitude", + result[0].LongitudeLatitudeHeight.X, + 151.20972, + 1e-12); + TestEqual( + "Latitude", + result[0].LongitudeLatitudeHeight.Y, + -33.87100, + 1e-12); + TestEqual( + "Height", + result[0].LongitudeLatitudeHeight.Z, + 1.0, + 1e-12); + done.ExecuteIfBound(); + })); + }); + + LatentIt( + "can be queried via Blueprint interface", + EAsyncExecution::TaskGraphMainThread, + [this](const FDoneDelegate& done) { + UCesiumSampleHeightMostDetailedAsyncAction* pAsync = + UCesiumSampleHeightMostDetailedAsyncAction:: + SampleHeightMostDetailed( + pTileset, + {FVector(144.93406, -37.82457, 1.0)}); + + USampleHeightCallbackReceiver::Bind( + pAsync->OnHeightsSampled, + [this, done]( + const TArray& result, + const TArray& warnings) { + TestEqual("Number of results", result.Num(), 1); + TestEqual("Number of warnings", warnings.Num(), 0); + TestTrue("SampleSuccess", result[0].SampleSuccess); + TestEqual( + "Longitude", + result[0].LongitudeLatitudeHeight.X, + 144.93406, + 1e-12); + TestEqual( + "Latitude", + result[0].LongitudeLatitudeHeight.Y, + -37.82457, + 1e-12); + TestTrue( + "Height", + !FMath::IsNearlyEqual( + result[0].LongitudeLatitudeHeight.Z, + 1.0, + 1.0)); + done.ExecuteIfBound(); + }); + + pAsync->Activate(); + }); + }); + + Describe("Two tilesets in rapid succession", [this]() { + BeforeEach([this]() { CesiumTestHelpers::pushAllowTickInEditor(); }); + + AfterEach(EAsyncExecution::TaskGraphMainThread, [this]() { + CesiumTestHelpers::popAllowTickInEditor(); + }); + + LatentIt( + "", + EAsyncExecution::TaskGraphMainThread, + [this](const FDoneDelegate& done) { + UWorld* pWorld = CesiumTestHelpers::getGlobalWorldContext(); + + ACesium3DTileset* pTileset1 = pWorld->SpawnActor(); + pTileset1->SetIonAssetID(1); +#if WITH_EDITOR + pTileset1->SetIonAccessToken( + Cesium::SceneGenerationContext::testIonToken); +#endif + + pTileset1->SampleHeightMostDetailed( + {FVector(-105.1, 40.1, 1.0)}, + FCesiumSampleHeightMostDetailedCallback::CreateLambda( + [this, pWorld, done]( + ACesium3DTileset* pTileset, + const TArray& result, + const TArray& warnings) { + TestEqual("Number of results", result.Num(), 1); + TestEqual("Number of warnings", warnings.Num(), 0); + TestTrue("SampleSuccess", result[0].SampleSuccess); + + ACesium3DTileset* pTileset2 = + pWorld->SpawnActor(); + pTileset2->SetIonAssetID(1); +#if WITH_EDITOR + pTileset2->SetIonAccessToken( + Cesium::SceneGenerationContext::testIonToken); +#endif + pTileset2->SampleHeightMostDetailed( + {FVector(105.1, 40.1, 1.0)}, + FCesiumSampleHeightMostDetailedCallback::CreateLambda( + [this, pWorld, done]( + ACesium3DTileset* pTileset, + const TArray& result, + const TArray& warnings) { + TestEqual("Number of results", result.Num(), 1); + TestEqual( + "Number of warnings", + warnings.Num(), + 0); + TestTrue( + "SampleSuccess", + result[0].SampleSuccess); + + done.ExecuteIfBound(); + })); + })); + }); + }); +} diff --git a/Source/CesiumRuntime/Private/Tests/CesiumLoadTestSamples.cpp b/Source/CesiumRuntime/Private/Tests/SampleTilesetsLoad.perf.cpp similarity index 90% rename from Source/CesiumRuntime/Private/Tests/CesiumLoadTestSamples.cpp rename to Source/CesiumRuntime/Private/Tests/SampleTilesetsLoad.perf.cpp index 84c89b0d8..18de2f33c 100644 --- a/Source/CesiumRuntime/Private/Tests/CesiumLoadTestSamples.cpp +++ b/Source/CesiumRuntime/Private/Tests/SampleTilesetsLoad.perf.cpp @@ -16,23 +16,23 @@ using namespace Cesium; IMPLEMENT_SIMPLE_AUTOMATION_TEST( - FCesiumSampleDenver, - "Cesium.Performance.SampleLocaleDenver", + FLoadTilesetDenver, + "Cesium.Performance.Tileset Loading.Aerometrex Denver", EAutomationTestFlags::EditorContext | EAutomationTestFlags::PerfFilter) IMPLEMENT_SIMPLE_AUTOMATION_TEST( - FCesiumSampleMelbourne, - "Cesium.Performance.SampleLocaleMelbourne", + FLoadTilesetMelbourne, + "Cesium.Performance.Tileset Loading.Melbourne photogrammetry (open data)", EAutomationTestFlags::EditorContext | EAutomationTestFlags::PerfFilter) IMPLEMENT_SIMPLE_AUTOMATION_TEST( - FCesiumSampleMontrealPointCloud, - "Cesium.Performance.SampleTestPointCloud", + FLoadTilesetMontrealPointCloud, + "Cesium.Performance.Tileset Loading.Montreal point cloud (open data)", EAutomationTestFlags::EditorContext | EAutomationTestFlags::PerfFilter) IMPLEMENT_SIMPLE_AUTOMATION_TEST( - FSampleMaxTileLoads, - "Cesium.Performance.SampleVaryMaxTileLoads", + FLoadTilesetMelbourneVaryMaxTileLoads, + "Cesium.Performance.Tileset Loading.Melbourne photogrammetry (open data), vary max tile loads", EAutomationTestFlags::EditorContext | EAutomationTestFlags::PerfFilter) void samplesClearCache(SceneGenerationContext&, TestPass::TestingParameter) { @@ -146,7 +146,7 @@ void setupForMontrealPointCloud(SceneGenerationContext& context) { context.tilesets.push_back(montrealTileset); } -bool FCesiumSampleDenver::RunTest(const FString& Parameters) { +bool FLoadTilesetDenver::RunTest(const FString& Parameters) { std::vector testPasses; testPasses.push_back(TestPass{"Cold Cache", samplesClearCache, nullptr}); testPasses.push_back(TestPass{"Warm Cache", samplesRefreshTilesets, nullptr}); @@ -159,7 +159,7 @@ bool FCesiumSampleDenver::RunTest(const FString& Parameters) { 768); } -bool FCesiumSampleMelbourne::RunTest(const FString& Parameters) { +bool FLoadTilesetMelbourne::RunTest(const FString& Parameters) { std::vector testPasses; testPasses.push_back(TestPass{"Cold Cache", samplesClearCache, nullptr}); testPasses.push_back(TestPass{"Warm Cache", samplesRefreshTilesets, nullptr}); @@ -172,7 +172,7 @@ bool FCesiumSampleMelbourne::RunTest(const FString& Parameters) { 768); } -bool FCesiumSampleMontrealPointCloud::RunTest(const FString& Parameters) { +bool FLoadTilesetMontrealPointCloud::RunTest(const FString& Parameters) { auto adjustCamera = [this]( SceneGenerationContext& context, TestPass::TestingParameter parameter) { @@ -185,10 +185,11 @@ bool FCesiumSampleMontrealPointCloud::RunTest(const FString& Parameters) { }; auto verifyVisibleTiles = [this]( - SceneGenerationContext& context, + SceneGenerationContext& creationContext, + SceneGenerationContext& playContext, TestPass::TestingParameter parameter) { Cesium3DTilesSelection::Tileset* pTileset = - context.tilesets[0]->GetTileset(); + playContext.tilesets[0]->GetTileset(); if (TestNotNull("Tileset", pTileset)) { int visibleTiles = 0; pTileset->forEachLoadedTile([&](Cesium3DTilesSelection::Tile& tile) { @@ -210,6 +211,8 @@ bool FCesiumSampleMontrealPointCloud::RunTest(const FString& Parameters) { TestEqual("visibleTiles", visibleTiles, 1); } + + return true; }; std::vector testPasses; @@ -224,7 +227,7 @@ bool FCesiumSampleMontrealPointCloud::RunTest(const FString& Parameters) { 512); } -bool FSampleMaxTileLoads::RunTest(const FString& Parameters) { +bool FLoadTilesetMelbourneVaryMaxTileLoads::RunTest(const FString& Parameters) { auto setupPass = [this]( SceneGenerationContext& context, diff --git a/Source/CesiumRuntime/Public/Cesium3DTileset.h b/Source/CesiumRuntime/Public/Cesium3DTileset.h index 385694264..955ea533f 100644 --- a/Source/CesiumRuntime/Public/Cesium3DTileset.h +++ b/Source/CesiumRuntime/Public/Cesium3DTileset.h @@ -12,6 +12,7 @@ #include "CesiumGeoreference.h" #include "CesiumIonServer.h" #include "CesiumPointCloudShading.h" +#include "CesiumSampleHeightResult.h" #include "CoreMinimal.h" #include "CustomDepthParameters.h" #include "Engine/EngineTypes.h" @@ -47,6 +48,12 @@ DECLARE_MULTICAST_DELEGATE_OneParam( FCesium3DTilesetLoadFailure, const FCesium3DTilesetLoadFailureDetails&); +DECLARE_DELEGATE_ThreeParams( + FCesiumSampleHeightMostDetailedCallback, + ACesium3DTileset*, + const TArray&, + const TArray&); + /** * The delegate for the Acesium3DTileset::OnTilesetLoaded, * which is triggered from UpdateLoadStatus @@ -101,6 +108,27 @@ class CESIUMRUNTIME_API ACesium3DTileset : public AActor { UFUNCTION(BlueprintCallable, meta = (DeprecatedFunction)) void SetMobility(EComponentMobility::Type NewMobility); + /** + * @brief Initiates an asynchronous query for the height of this tileset at a + * list of cartographic positions, where the Longitude (X) and Latitude (Y) + * are given in degrees. The most detailed available tiles are used to + * determine each height. + * + * The height of the input positions is ignored, unless height sampling fails + * at that location. The output height is expressed in meters above the + * ellipsoid (usually WGS84), which should not be confused with a height above + * mean sea level. + * + * @param LongitudeLatitudeHeightArray The cartographic positions for which to + * sample heights. The Longitude (X) and Latitude (Y) are expressed in + * degrees, while Height (Z) is given in meters. + * @param OnHeightsSampled A callback that is invoked in the game thread when + * heights have been sampled for all positions. + */ + void SampleHeightMostDetailed( + const TArray& LongitudeLatitudeHeightArray, + FCesiumSampleHeightMostDetailedCallback OnHeightsSampled); + private: /** * The designated georeference actor controlling how the actor's diff --git a/Source/CesiumRuntime/Public/CesiumSampleHeightMostDetailedAsyncAction.h b/Source/CesiumRuntime/Public/CesiumSampleHeightMostDetailedAsyncAction.h new file mode 100644 index 000000000..769182f69 --- /dev/null +++ b/Source/CesiumRuntime/Public/CesiumSampleHeightMostDetailedAsyncAction.h @@ -0,0 +1,71 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors +#pragma once + +#include "CesiumSampleHeightResult.h" +#include "Kismet/BlueprintAsyncActionBase.h" +#include "CesiumSampleHeightMostDetailedAsyncAction.generated.h" + +class ACesium3DTileset; + +/** + * The delegate used to asynchronously return sampled heights. + * @param Result The result of the height sampling. This array contains the + * outputs for each input cartographic position. Each result has a HeightSampled + * property indicating whether the height was successfully sampled at that + * position, and a LongitudeLatitudeHeight property with the complete position, + * including the sampled height. If the sample was unsuccessful, the original + * position is returned. + * @param Warnings Provides information about problems, if any, that were + * encountered while sampling heights. + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams( + FCesiumSampleHeightMostDetailedComplete, + const TArray&, + Result, + const TArray&, + Warnings); + +UCLASS() +class CESIUMRUNTIME_API UCesiumSampleHeightMostDetailedAsyncAction + : public UBlueprintAsyncActionBase { + GENERATED_BODY() + +public: + /** + * Asynchronously samples the height of the tileset at a list of cartographic + * positions, each expressed as a Longitude (X) and Latitude (Y) in degrees. + * The Height (Z) provided on input is ignored unless the sampling fails at + * that position, in which case it is passed through to the output. + * @param Tileset The tileset from which to query heights. + * @param LongitudeLatitudeHeightArray The array of cartographic positions at + * which to query heights, with Longitude in the X component and Latitude in + * the Y component. + */ + UFUNCTION( + BlueprintCallable, + Category = "Cesium", + meta = (BlueprintInternalUseOnly = true)) + static UCesiumSampleHeightMostDetailedAsyncAction* SampleHeightMostDetailed( + ACesium3DTileset* Tileset, + const TArray& LongitudeLatitudeHeightArray); + + /** + * Called when height has been sampled at all of the given positions. The + * Result property contains an element for each input position and in the same + * order. The Warnings property provides information about problems that were + * encountered while sampling heights. + */ + UPROPERTY(BlueprintAssignable) + FCesiumSampleHeightMostDetailedComplete OnHeightsSampled; + + virtual void Activate() override; + +private: + void RaiseOnHeightsSampled( + ACesium3DTileset* Tileset, + const TArray& Result, + const TArray& Warnings); + + ACesium3DTileset* _pTileset; + TArray _longitudeLatitudeHeightArray; +}; diff --git a/Source/CesiumRuntime/Public/CesiumSampleHeightResult.h b/Source/CesiumRuntime/Public/CesiumSampleHeightResult.h new file mode 100644 index 000000000..364678cd5 --- /dev/null +++ b/Source/CesiumRuntime/Public/CesiumSampleHeightResult.h @@ -0,0 +1,32 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#pragma once + +#include "CesiumSampleHeightResult.generated.h" + +/** + * The result of sampling the height on a tileset at the given cartographic + * position. + */ +USTRUCT(BlueprintType) +struct CESIUMRUNTIME_API FCesiumSampleHeightResult { + GENERATED_BODY() + + /** + * The Longitude (X) and Latitude (Y) are the same values provided on input. + * The Height (Z) is the height sampled from the tileset if the SampleSuccess + * property is true, or the original height provided on input if SampleSuccess + * is false. + */ + UPROPERTY(BlueprintReadWrite, Category = "Cesium") + FVector LongitudeLatitudeHeight; + + /** + * True if the height as sampled from the tileset successfully. False if the + * tileset doesn't have any height at that position, or if something went + * wrong. If something went wrong, the Warnings pin of the sampling function + * will have more information about the problem. + */ + UPROPERTY(BlueprintReadWrite, Category = "Cesium") + bool SampleSuccess; +}; diff --git a/Source/CesiumRuntime/Public/GeoTransforms.h b/Source/CesiumRuntime/Public/GeoTransforms.h index 9ab610640..6ab5ad8cb 100644 --- a/Source/CesiumRuntime/Public/GeoTransforms.h +++ b/Source/CesiumRuntime/Public/GeoTransforms.h @@ -5,6 +5,7 @@ #include "CesiumGeospatial/Ellipsoid.h" #include "CesiumGeospatial/LocalHorizontalCoordinateSystem.h" #include "HAL/Platform.h" +#include "Math/Matrix.h" #include #include diff --git a/Source/CesiumRuntime/Public/UnrealAssetAccessor.h b/Source/CesiumRuntime/Public/UnrealAssetAccessor.h index cb06d0892..0a76c3fe6 100644 --- a/Source/CesiumRuntime/Public/UnrealAssetAccessor.h +++ b/Source/CesiumRuntime/Public/UnrealAssetAccessor.h @@ -4,6 +4,7 @@ #include "CesiumAsync/AsyncSystem.h" #include "CesiumAsync/IAssetAccessor.h" +#include "Containers/Map.h" #include "Containers/UnrealString.h" #include "HAL/Platform.h" #include diff --git a/TestsProject/Config/DefaultEngine.ini b/TestsProject/Config/DefaultEngine.ini index faad2eecf..379bfe0c4 100644 --- a/TestsProject/Config/DefaultEngine.ini +++ b/TestsProject/Config/DefaultEngine.ini @@ -14,7 +14,7 @@ AppliedDefaultGraphicsPerformance=Maximum DefaultGraphicsRHI=DefaultGraphicsRHI_DX12 [/Script/Engine.RendererSettings] -r.GenerateMeshDistanceFields=True +r.GenerateMeshDistanceFields=False r.DynamicGlobalIlluminationMethod=1 r.ReflectionMethod=1 r.Shadow.Virtual.Enable=1 diff --git a/TestsProject/TestsProject.uproject b/TestsProject/TestsProject.uproject index 19b8c7063..7e940bfc4 100644 --- a/TestsProject/TestsProject.uproject +++ b/TestsProject/TestsProject.uproject @@ -1,6 +1,6 @@ { "FileVersion": 3, - "EngineAssociation": "5.0", + "EngineAssociation": "5.2", "Category": "", "Description": "", "Plugins": [ diff --git a/extern/cesium-native b/extern/cesium-native index f64fa6af9..ad18abfc0 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit f64fa6af9b067c631b6c71b7c7a8a39eaeeec1f2 +Subproject commit ad18abfc03c4ad041e02a643d06663a1903ef9c0