// Copyright (c) Meta Platforms, Inc. and affiliates. #include "Editor.h" #include "Editor/UnrealEdEngine.h" #include "MeshActor.h" #include "Misc/AutomationTest.h" #include "Misc/EngineVersionComparison.h" #include "MRUtilityKitAnchor.h" #include "MRUtilityKitAnchorActorSpawner.h" #include "MRUtilityKitSubsystem.h" #include "Tests/AutomationEditorCommon.h" #include "TestHelper.h" #include "UnrealEdGlobals.h" #if UE_VERSION_OLDER_THAN(5, 5, 0) BEGIN_DEFINE_SPEC(FMRUKSpec, TEXT("MR Utility Kit"), EAutomationTestFlags::ProductFilter | EAutomationTestFlags::ApplicationContextMask) #else BEGIN_DEFINE_SPEC(FMRUKSpec, TEXT("MR Utility Kit"), EAutomationTestFlags::ProductFilter | EAutomationTestFlags_ApplicationContextMask) #endif UMRUKSubsystem* ToolkitSubsystem; void SetupMRUKSubsystem(); void LoadSceneFromJson(); void TeardownMRUKSubsystem(); using FAutomationTestBase::TestEqual; // Allows base class function overloads to be accessed bool TestEqual(const TCHAR* What, const FVector2D Actual, const FVector2D Expected, float Tolerance = UE_KINDA_SMALL_NUMBER); END_DEFINE_SPEC(FMRUKSpec) void FMRUKSpec::SetupMRUKSubsystem() { BeforeEach([this]() { // Load map and start play in editor const auto ContentDir = FPaths::ProjectContentDir(); FAutomationEditorCommonUtils::LoadMap(ContentDir + "/Common/Maps/TestLevel.umap"); StartPIE(true); }); BeforeEach(EAsyncExecution::ThreadPool, []() { while (!GEditor->IsPlayingSessionInEditor()) { // Wait until play session starts FGenericPlatformProcess::Yield(); } }); BeforeEach([this]() { // Get a reference to the subsystem const auto World = GEditor->GetPIEWorldContext()->World(); const auto GameInstance = World->GetGameInstance(); ToolkitSubsystem = GameInstance->GetSubsystem(); }); } void FMRUKSpec::LoadSceneFromJson() { BeforeEach([this]() { // Load scene from Json ToolkitSubsystem->LoadSceneFromJsonString(ExampleRoomJson); }); } void FMRUKSpec::TeardownMRUKSubsystem() { // Caution: Order of these statements is important AfterEach(EAsyncExecution::ThreadPool, []() { while (GEditor->IsPlayingSessionInEditor()) { // Wait until play session ends FGenericPlatformProcess::Yield(); } }); AfterEach([]() { // Request end of play session GUnrealEd->RequestEndPlayMap(); }); } // There is no TestEqual for FVector2D in FAutomationTestBase, so implement our own here bool FMRUKSpec::TestEqual(const TCHAR* What, const FVector2D Actual, const FVector2D Expected, float Tolerance) { if (!Expected.Equals(Actual, Tolerance)) { AddError(FString::Printf(TEXT("Expected '%s' to be %s, but it was %s within tolerance %f."), What, *Expected.ToString(), *Actual.ToString(), Tolerance), 1); return false; } return true; } void FMRUKSpec::Define() { Describe(TEXT("Interior spawner"), [this] { AMRUKAnchorActorSpawner* InteriorSpawner; SetupMRUKSubsystem(); BeforeEach([this, &InteriorSpawner]() { const auto World = GEditor->GetPIEWorldContext()->World(); FActorSpawnParameters Params{}; InteriorSpawner = World->SpawnActor(Params); InteriorSpawner->SpawnGroups[FMRUKLabels::Couch].Actors.Push({ AMeshActor::StaticClass() }); InteriorSpawner->SpawnGroups[FMRUKLabels::WindowFrame].Actors.Push({ AMeshActor::StaticClass() }); }); LoadSceneFromJson(); It(TEXT("Spawns interior correct"), [this]() { const auto World = GEditor->GetPIEWorldContext()->World(); const auto Subsystem = World->GetGameInstance()->GetSubsystem(); const auto Room = Subsystem->GetCurrentRoom(); if (!TestNotNull(TEXT("Current room is set"), Room)) { return; } const auto WindowAnchor = Subsystem->GetCurrentRoom()->GetFirstAnchorByLabel(FMRUKLabels::WindowFrame); if (!TestNotNull(TEXT("Window anchor is set"), WindowAnchor)) { return; } TArray WindowChildActors; WindowAnchor->GetAttachedActors(WindowChildActors); if (!TestFalse(TEXT("Window has child actors"), WindowChildActors.IsEmpty())) { return; } const auto WindowMeshActor = WindowChildActors[0]; if (!TestNotNull(TEXT("Window mesh is set"), WindowMeshActor)) { return; } constexpr double Tolerance = 0.01; TestEqual(TEXT("Window mesh location"), WindowMeshActor->GetActorLocation(), FVector(86.993, 160.305, 27.147), Tolerance); TestEqual(TEXT("Window mesh rotation"), WindowMeshActor->GetActorRotation(), FRotator(-0.000010, -168.053020, 0.012821), Tolerance); TestEqual(TEXT("Window mesh scale"), WindowMeshActor->GetActorScale(), FVector(1.442, 1.154, 1.729), Tolerance); const auto CouchAnchor = Subsystem->GetCurrentRoom()->GetFirstAnchorByLabel("COUCH"); if (!TestNotNull(TEXT("Couch anchor is set"), CouchAnchor)) { return; } TArray CouchChildActors; CouchAnchor->GetAttachedActors(CouchChildActors); if (!TestFalse(TEXT("Couch has child actors"), CouchChildActors.IsEmpty())) { return; } const auto CouchMeshActor = CouchChildActors[0]; if (!TestNotNull(TEXT("Couch mesh is set"), CouchMeshActor)) { return; } TestEqual(TEXT("Couch mesh location"), CouchMeshActor->GetActorLocation(), FVector(-145.872, 107.079, -101.393), Tolerance); TestEqual(TEXT("Couch mesh rotation"), CouchMeshActor->GetActorRotation(), FRotator(0.0, -79.625847, 0.0), Tolerance); TestEqual(TEXT("Couch mesh scale"), CouchMeshActor->GetActorScale(), FVector(0.902, 2.029, 0.566), Tolerance); }); TeardownMRUKSubsystem(); }); Describe(TEXT("Serialization"), [this] { SetupMRUKSubsystem(); It(TEXT("Deserializes correctly"), [this]() { ToolkitSubsystem->LoadSceneFromJsonString(ExampleRoomJson); auto Rooms = ToolkitSubsystem->Rooms; if (!TestEqual(TEXT("Number of rooms"), Rooms.Num(), 1)) { return; } auto Room = Rooms[0]; if (!TestNotNull(TEXT("Room"), Room.Get())) { return; } TestEqual(TEXT("Number of anchors"), Room->AllAnchors.Num(), 23); TestEqual(TEXT("Number of walls"), Room->WallAnchors.Num(), 8); if (TestNotNull(TEXT("Floor anchor"), Room->FloorAnchor.Get())) { if (TestEqual(TEXT("Number of floor semantic classifications"), Room->FloorAnchor->SemanticClassifications.Num(), 1)) { TestEqual(TEXT("Wall semantic classification"), Room->FloorAnchor->SemanticClassifications[0], FMRUKLabels::Floor); } } if (TestNotNull(TEXT("Ceiling anchor"), Room->CeilingAnchor.Get())) { if (TestEqual(TEXT("Number of ceiling semantic classifications"), Room->CeilingAnchor->SemanticClassifications.Num(), 1)) { TestEqual(TEXT("Wall semantic classification"), Room->CeilingAnchor->SemanticClassifications[0], FMRUKLabels::Ceiling); } } for (auto Wall : Room->WallAnchors) { if (!TestNotNull(TEXT("Wall anchor"), Wall.Get())) { break; } if (!TestEqual(TEXT("Number of wall semantic classifications"), Wall->SemanticClassifications.Num(), 1)) { break; } TestEqual(TEXT("Wall semantic classification"), Wall->SemanticClassifications[0], FMRUKLabels::WallFace); } }); It(TEXT("Serializes correctly"), [this]() { ToolkitSubsystem->LoadSceneFromJsonString(ExampleRoomJson); auto Serialized = ToolkitSubsystem->SaveSceneToJsonString(); TArray SerializedLines; TArray SourceLines; Serialized.ParseIntoArrayLines(SerializedLines, true); FString(ExampleRoomJson).ParseIntoArrayLines(SourceLines, true); if (TestEqual(TEXT("Number of lines"), SerializedLines.Num(), SourceLines.Num())) { // Go line by line in the JSON string and verify they match for (int i = 0; i < SerializedLines.Num(); ++i) { // When serializing/deserializing Rotations it does some conversion between Euler // rotation and quaternion. This results in some precision loss and in some cases // the same rotation can be represented in different ways so the numbers are quite // different even know they represent the same rotation so it is difficult to compare // them textually. For now we will just ignore these lines. if (SourceLines[i].Contains(TEXT("\"Rotation\""))) { continue; } TestEqual(TEXT("JSON line"), SerializedLines[i], SourceLines[i]); } } }); TeardownMRUKSubsystem(); }); Describe(TEXT("Room Loading"), [this]() { SetupMRUKSubsystem(); It(TEXT("Load invalid string"), [this]() { // Load scene from invalid JSON string ToolkitSubsystem->LoadSceneFromJsonString(TEXT("[[[")); TestEqual("Scene load status", ToolkitSubsystem->SceneLoadStatus, EMRUKInitStatus::Failed); }); It(TEXT("Load empty room"), [this]() { // Load scene from empty JSON string ToolkitSubsystem->LoadSceneFromJsonString(TEXT(R"({"Rooms": []})")); TestEqual("Scene load status", ToolkitSubsystem->SceneLoadStatus, EMRUKInitStatus::Failed); }); TeardownMRUKSubsystem(); }); Describe(TEXT("Update room"), [this]() { SetupMRUKSubsystem(); LoadSceneFromJson(); It(TEXT("Change nothing in room"), [this]() { auto O = NewObject(); ToolkitSubsystem->OnRoomCreated.AddDynamic(O, &URoomAndAnchorObserver::OnRoomCreated); ToolkitSubsystem->OnRoomUpdated.AddDynamic(O, &URoomAndAnchorObserver::OnRoomUpdated); ToolkitSubsystem->OnRoomRemoved.AddDynamic(O, &URoomAndAnchorObserver::OnRoomRemoved); for (auto Room : ToolkitSubsystem->Rooms) { Room->OnAnchorCreated.AddDynamic(O, &URoomAndAnchorObserver::OnAnchorCreated); Room->OnAnchorUpdated.AddDynamic(O, &URoomAndAnchorObserver::OnAnchorUpdated); Room->OnAnchorRemoved.AddDynamic(O, &URoomAndAnchorObserver::OnAnchorRemoved); } ToolkitSubsystem->LoadSceneFromJsonString(ExampleRoomJson); TestEqual(TEXT("No rooms created"), O->RoomsCreated.Num(), 0); TestEqual(TEXT("One room updated"), O->RoomsUpdated.Num(), 1); TestEqual(TEXT("No rooms removed"), O->RoomsRemoved.Num(), 0); TestEqual(TEXT("No anchors created"), O->AnchorsCreated.Num(), 0); TestEqual(TEXT("No anchors updated"), O->AnchorsUpdated.Num(), 0); TestEqual(TEXT("No anchors removed"), O->AnchorsRemoved.Num(), 0); O->MarkAsGarbage(); }); It(TEXT("Add, update and remove furniture in room"), [this]() { auto O = NewObject(); ToolkitSubsystem->OnRoomCreated.AddDynamic(O, &URoomAndAnchorObserver::OnRoomCreated); ToolkitSubsystem->OnRoomUpdated.AddDynamic(O, &URoomAndAnchorObserver::OnRoomUpdated); ToolkitSubsystem->OnRoomRemoved.AddDynamic(O, &URoomAndAnchorObserver::OnRoomRemoved); for (auto Room : ToolkitSubsystem->Rooms) { Room->OnAnchorCreated.AddDynamic(O, &URoomAndAnchorObserver::OnAnchorCreated); Room->OnAnchorUpdated.AddDynamic(O, &URoomAndAnchorObserver::OnAnchorUpdated); Room->OnAnchorRemoved.AddDynamic(O, &URoomAndAnchorObserver::OnAnchorRemoved); } ToolkitSubsystem->LoadSceneFromJsonString(ExampleRoomFurnitureAddedJson); TestEqual(TEXT("No rooms created"), O->RoomsCreated.Num(), 0); TestEqual(TEXT("Rooms updated"), O->RoomsUpdated.Num(), 1); TestEqual(TEXT("No rooms removed"), O->RoomsRemoved.Num(), 0); TestEqual(TEXT("Anchors created"), O->AnchorsCreated.Num(), 2); TestEqual(TEXT("No anchors updated"), O->AnchorsUpdated.Num(), 0); TestEqual(TEXT("No anchors removed"), O->AnchorsRemoved.Num(), 0); O->Clear(); ToolkitSubsystem->LoadSceneFromJsonString(ExampleRoomMoreFurnitureAddedJson); TestEqual(TEXT("No rooms created"), O->RoomsCreated.Num(), 0); TestEqual(TEXT("Rooms updated"), O->RoomsUpdated.Num(), 1); TestEqual(TEXT("No rooms removed"), O->RoomsRemoved.Num(), 0); TestEqual(TEXT("Anchors created"), O->AnchorsCreated.Num(), 1); TestEqual(TEXT("No anchors updated"), O->AnchorsUpdated.Num(), 0); TestEqual(TEXT("No anchors removed"), O->AnchorsRemoved.Num(), 0); O->Clear(); ToolkitSubsystem->LoadSceneFromJsonString(ExampleRoomFurnitureModifiedJson); TestEqual(TEXT("No rooms created"), O->RoomsCreated.Num(), 0); TestEqual(TEXT("Rooms updated"), O->RoomsUpdated.Num(), 1); TestEqual(TEXT("No rooms removed"), O->RoomsRemoved.Num(), 0); TestEqual(TEXT("No anchors created"), O->AnchorsCreated.Num(), 0); TestEqual(TEXT("Anchors updated"), O->AnchorsUpdated.Num(), 2); TestEqual(TEXT("Anchors removed"), O->AnchorsRemoved.Num(), 1); O->Clear(); ToolkitSubsystem->LoadSceneFromJsonString(ExampleRoomJson); TestEqual(TEXT("No rooms created"), O->RoomsCreated.Num(), 0); TestEqual(TEXT("Rooms updated"), O->RoomsUpdated.Num(), 1); TestEqual(TEXT("No rooms removed"), O->RoomsRemoved.Num(), 0); TestEqual(TEXT("No anchors created"), O->AnchorsCreated.Num(), 0); TestEqual(TEXT("No anchors updated"), O->AnchorsUpdated.Num(), 0); TestEqual(TEXT("Anchors removed"), O->AnchorsRemoved.Num(), 2); O->MarkAsGarbage(); }); It(TEXT("Add and remove room"), [this]() { auto O = NewObject(); ToolkitSubsystem->OnRoomCreated.AddDynamic(O, &URoomAndAnchorObserver::OnRoomCreated); ToolkitSubsystem->OnRoomUpdated.AddDynamic(O, &URoomAndAnchorObserver::OnRoomUpdated); ToolkitSubsystem->OnRoomRemoved.AddDynamic(O, &URoomAndAnchorObserver::OnRoomRemoved); ToolkitSubsystem->LoadSceneFromJsonString(ExampleOtherRoomJson); TestEqual(TEXT("Room created"), O->RoomsCreated.Num(), 1); TestEqual(TEXT("No rooms updated"), O->RoomsUpdated.Num(), 0); TestEqual(TEXT("Rooms removed"), O->RoomsRemoved.Num(), 1); O->MarkAsGarbage(); }); TeardownMRUKSubsystem(); }); Describe("Room", [this]() { SetupMRUKSubsystem(); LoadSceneFromJson(); It(TEXT("Is initialized"), [this]() { TestEqual("Scene load status", ToolkitSubsystem->SceneLoadStatus, EMRUKInitStatus::Complete); auto Room = ToolkitSubsystem->GetCurrentRoom(); if (!TestNotNull(TEXT("Current room"), Room)) { return; } TestTrue("Room bounds valid", (bool)Room->RoomBounds.IsValid); }); It(TEXT("Compute Centroid"), [this]() { TestEqual("Scene load status", ToolkitSubsystem->SceneLoadStatus, EMRUKInitStatus::Complete); const auto Room = ToolkitSubsystem->GetCurrentRoom(); if (!TestNotNull(TEXT("Current room"), Room)) { return; } const TArray> Inputs = { { 0.0, FVector(-63.212, 41.209, -129.629) }, { 0.5, FVector(-65.876, 40.507, 1.873) }, { 1.0, FVector(-68.540, 39.804, 133.376) }, }; for (int32 I = 0; I < Inputs.Num(); ++I) { const double Z = Inputs[I].Get<0>(); const FVector& ExpectedCentroid = Inputs[I].Get<1>(); const FVector Centroid = Room->ComputeCentroid(Z); constexpr double Tolerance = 0.001; TestEqual(TEXT("Centroid matches"), Centroid, ExpectedCentroid, Tolerance); } }); It(TEXT("Does room have"), [this]() { auto Room = ToolkitSubsystem->GetCurrentRoom(); if (!TestNotNull(TEXT("Current room"), Room)) { return; } TArray ContainedLabels = { FMRUKLabels::WallFace, FMRUKLabels::Ceiling, FMRUKLabels::Floor }; TestTrue(TEXT("Room contains all labels"), Room->DoesRoomHave(ContainedLabels)); TArray PartlyContainedLabels = { FMRUKLabels::WallFace, "FOO", FMRUKLabels::Floor }; TestFalse(TEXT("Room contains not all labels"), Room->DoesRoomHave(PartlyContainedLabels)); TArray EmptyLabels; TestTrue(TEXT("Room contains empty labels"), Room->DoesRoomHave(EmptyLabels)); }); It(TEXT("Get forward facing direction"), [this]() { auto Room = ToolkitSubsystem->GetCurrentRoom(); if (!TestNotNull(TEXT("Current room"), Room)) { return; } const auto Couch = Room->GetFirstAnchorByLabel("COUCH"); if (!TestNotNull(TEXT("Couch"), Room)) { return; } const auto ActualFacingDirection = Couch->GetFacingDirection(); constexpr double Tolerance = 0.001; TestEqual(TEXT("Facing direction"), ActualFacingDirection, FVector{ 0.180, -0.984, 0.000 }, Tolerance); }); It(TEXT("Get anchors by label"), [this] { auto Room = ToolkitSubsystem->GetCurrentRoom(); if (!TestNotNull(TEXT("Current room"), Room)) { return; } const TArray& Anchors = Room->GetAnchorsByLabel(TEXT("WALL_FACE")); TestEqual(TEXT("All walls found"), Anchors.Num(), 8); }); It(TEXT("Try get closest seat pose"), [this]() { auto Room = ToolkitSubsystem->GetCurrentRoom(); if (!TestNotNull(TEXT("Current room"), Room)) { return; } const auto Couch = Room->GetFirstAnchorByLabel("COUCH"); if (!TestNotNull(TEXT("Couch"), Room)) { return; } struct TestData { FVector RayOrigin; FVector RayDirection; FVector ExpectedLocation; FQuat ExpectedRotation; FOculusXRUUID ExpectedAnchorUUID; }; TArray AllTestData = { { { -119.510, 51.224, -68.187 }, { -0.474, 0.815, -0.332 }, { -145.872, 107.079, -85.245 }, { 0.0, 0.0, -0.640282977, 0.768139121 }, { { 0xBE, 0xEE, 0x3C, 0xD7, 0x5B, 0xC0, 0x2A, 0xBF, 0x21, 0x9D, 0x99, 0x89, 0x48, 0x1A, 0xE7, 0x98 } } } }; for (const auto& TestData : AllTestData) { FTransform ActualSeatTransform{}; const auto Actor = Room->TryGetClosestSeatPose(TestData.RayOrigin, TestData.RayDirection, ActualSeatTransform); if (!TestNotNull(TEXT("Actor is set"), Actor)) { return; } constexpr double Tolerance = 0.001; TestEqual(TEXT("Actor UUID is the same"), Actor->AnchorUUID, TestData.ExpectedAnchorUUID); TestEqual(TEXT("Location is the same"), ActualSeatTransform.GetLocation(), TestData.ExpectedLocation, Tolerance); TestTrue(TEXT("Rotation is the same"), ActualSeatTransform.GetRotation().Equals(TestData.ExpectedRotation, Tolerance)); } }); It(TEXT("Try get closest point in room"), [this]() { auto Room = ToolkitSubsystem->GetCurrentRoom(); if (!TestNotNull(TEXT("Current room"), Room)) { return; } struct TestData { FVector WorldLocation; TOptional ExpectedSurfaceLocation; TOptional ExpectedAnchorUUID; }; TArray AllTestData = { { { -157.383, -117.528, -50.459 }, { { -155.038, -132.843, -50.459 } }, { { { 0xB3, 0x69, 0xDA, 0xC9, 0x91, 0x3F, 0x46, 0x53, 0xCF, 0x00, 0x06, 0xD2, 0x92, 0xB4, 0x7E, 0x67 } } }, }, { { 70.723, 34.300, -18.577 }, { { 80.085, 36.095, -18.577 } }, { { { 0x4F, 0x3D, 0x50, 0x69, 0x41, 0xFC, 0xDF, 0x2C, 0xDE, 0x52, 0x9B, 0x77, 0x8F, 0x7E, 0x2C, 0xA0 } } }, }, { { -6.787, 178.944, -15.669 }, { { -6.787, 178.944, -25.322 } }, { { { 0x39, 0x02, 0x15, 0x4E, 0x70, 0x0E, 0x7D, 0xFC, 0xEF, 0xCD, 0xD3, 0x1C, 0x7C, 0xEE, 0xA2, 0x1D } } }, }, }; FMRUKLabelFilter LabelFilter; LabelFilter.ExcludedLabels = { FMRUKLabels::Ceiling, FMRUKLabels::Floor }; for (const auto& TestData : AllTestData) { FVector ActualSurfaceLocation{}; const auto ActualAnchor = ToolkitSubsystem->TryGetClosestSurfacePosition(TestData.WorldLocation, ActualSurfaceLocation, LabelFilter); if (TestData.ExpectedAnchorUUID.IsSet()) { if (!TestNotNull(TEXT("Anchor is set"), ActualAnchor)) { continue; } TestEqual(TEXT("Anchor Uuid"), ActualAnchor->AnchorUUID, TestData.ExpectedAnchorUUID.GetValue()); } else { TestNull(TEXT("Anchor not set"), ActualAnchor); } if (TestData.ExpectedSurfaceLocation.IsSet()) { constexpr double Tolerance = 0.01; TestEqual(TEXT("Surface location"), ActualSurfaceLocation, TestData.ExpectedSurfaceLocation.GetValue(), Tolerance); } } }); It(TEXT("Get best pose from raycast"), [this]() { auto Room = ToolkitSubsystem->GetCurrentRoom(); if (!TestNotNull(TEXT("Current room"), Room)) { return; } struct TestData { FVector RayOrigin; FVector RayDirection; double MaxDist; FVector ExpectedLocation; FQuat ExpectedRotation; TOptional ExpectedAnchorUUID; EMRUKPositioningMethod PositioningMethod; }; TArray AllTestData = { // Desk { { -15.428, 52.817, 36.223 }, { 0.519, -0.143, -0.842 }, 0.0, { 19.154, 34.502, -51.185 }, { 0.0, 0.0, -0.996050404, 0.088789598 }, { { { 0x1E, 0x57, 0x8C, 0x3E, 0x9E, 0x7B, 0x49, 0x5E, 0x9F, 0x8B, 0xB0, 0x0A, 0x37, 0x22, 0xB6, 0x19 } } }, EMRUKPositioningMethod::Edge }, // Desk { { 0.0, 0.0, 0.0 }, { 1.0, 0.0, 0.0 }, 0.0, { 121.404, 0.0, 0.0 }, { 0.000000072, 0.000000008, -0.994469644, 0.105024412 }, { { { 0x61, 0x34, 0x5A, 0x4C, 0xA1, 0xA0, 0x0C, 0x5C, 0x18, 0xC7, 0xAC, 0x34, 0x23, 0x37, 0x69, 0x01 } } }, EMRUKPositioningMethod::Default }, // Desk { { 0.0, 0.0, 0.0 }, { 0.202, -0.725, -0.659 }, 0.0, { 21.056, -75.154, -52.927 }, { 0.0, 0.0, 0.767014414, 0.641629869 }, { { { 0xB8, 0x82, 0x3B, 0xA2, 0x41, 0xD6, 0x9E, 0x1B, 0x5A, 0x3E, 0x71, 0x6F, 0xDC, 0x16, 0xC0, 0x35 } } }, EMRUKPositioningMethod::Center }, // Wall { { 7.062, 83.282, -12.268 }, { 0.989, 0.146, 0.021 }, 0.0, { 100.662, 97.104, -10.286 }, { 0.000000072, 0.000000008, -0.994469644, 0.105024412 }, { { { 0x61, 0x34, 0x5A, 0x4C, 0xA1, 0xA0, 0x0C, 0x5C, 0x18, 0xC7, 0xAC, 0x34, 0x23, 0x37, 0x69, 0x01 } } }, EMRUKPositioningMethod::Default }, // Floor { { -55.000, 14.315, -64.814 }, { 0.054, 0.039, -0.998 }, 0.0, { -51.499, 16.821, -129.629 }, { -0.000000000, 0.000000000, -0.952129375, 0.305695361 }, { { { 0x44, 0x08, 0xBC, 0xA3, 0x0B, 0x3E, 0xA7, 0x79, 0x99, 0x2A, 0x41, 0x33, 0x9E, 0x9B, 0xD1, 0x5E } } }, EMRUKPositioningMethod::Default }, }; for (const auto& TestData : AllTestData) { FTransform ActualOutPose{}; const auto Actor = Room->GetBestPoseFromRaycast(TestData.RayOrigin, TestData.RayDirection, TestData.MaxDist, {}, ActualOutPose, TestData.PositioningMethod); if (!TestData.ExpectedAnchorUUID.IsSet()) { TestNull(TEXT("Actor is null"), Actor); } else { if (!TestNotNull(TEXT("Actor is set"), Actor)) { return; } constexpr double Tolerance = 0.15; TestEqual(TEXT("Actor UUID is the same"), Actor->AnchorUUID, TestData.ExpectedAnchorUUID.GetValue()); TestEqual(TEXT("Location is the same"), ActualOutPose.GetLocation(), TestData.ExpectedLocation, Tolerance); TestTrue(TEXT("Rotation is the same"), ActualOutPose.GetRotation().Equals(TestData.ExpectedRotation, Tolerance)); } } }); It(TEXT("Get key wall"), [this]() { auto Room = ToolkitSubsystem->GetCurrentRoom(); if (!TestNotNull(TEXT("Current room"), Room)) { return; } const auto KeyWallAnchor = Room->GetKeyWall(); if (!TestNotNull(TEXT("Key wall anchor is not null"), KeyWallAnchor)) { return; } TestEqual(TEXT("Key wall anchor UUID is correct"), KeyWallAnchor->AnchorUUID, FOculusXRUUID({ 0x93, 0x4C, 0xE7, 0x5D, 0x63, 0xF0, 0x85, 0x6A, 0x94, 0x38, 0xDA, 0xB3, 0xAD, 0xA9, 0x54, 0x09 })); }); It(TEXT("Get largest surface"), [this]() { auto Room = ToolkitSubsystem->GetCurrentRoom(); if (!TestNotNull(TEXT("Current room"), Room)) { return; } auto Anchor = Room->GetLargestSurface(FMRUKLabels::WallArt); if (!TestNotNull(TEXT("Anchor is not null"), Anchor)) { return; } TestEqual(TEXT("Anchor UUID is correct"), Anchor->AnchorUUID, FOculusXRUUID({ 0xDE, 0x8D, 0xDD, 0xD9, 0x90, 0xAD, 0x5F, 0xCB, 0x8E, 0x7E, 0x23, 0x7E, 0x93, 0x8C, 0xB0, 0xD3 })); Anchor = Room->GetLargestSurface(FMRUKLabels::Screen); if (!TestNotNull(TEXT("Anchor is not null"), Anchor)) { return; } TestEqual(TEXT("Anchor UUID is correct"), Anchor->AnchorUUID, FOculusXRUUID({ 0x4F, 0x3D, 0x50, 0x69, 0x41, 0xFC, 0xDF, 0x2C, 0xDE, 0x52, 0x9B, 0x77, 0x8F, 0x7E, 0x2C, 0xA0 })); }); It(TEXT("Point inside room"), [this]() { auto Room = ToolkitSubsystem->GetCurrentRoom(); if (!TestNotNull(TEXT("Current room"), Room)) { return; } TArray PointsInRoom = { { 95.046, -12.727, 74.243 }, { -135.569, 88.847, -64.303 }, { -51.55, 69.412, 81.61 }, { 38.378, -21.614, -34.768 }, { -241.943, 167.824, 55.476 }, { 26.605, -72.533, -39.321 }, { -234.718, 147.491, 49.483 }, { -114.777, -32.589, -86.116 }, { -244.759, 113.77, -88.077 }, { 38.533, 24.966, -112.239 }, { 94.798, 22.833, 42.658 }, { -208.202, 86.037, -4.801 }, { -111.049, 140.856, -15.237 }, { 15.575, 70.207, 100.421 }, { -8.435, 186.92, -33.971 }, { 83.304, -1.605, 44.165 }, { -142.314, 88.862, -48.251 }, { 82.577, -21.908, -80.743 }, { -238.493, 105.266, -40.407 }, { -14.607, 2.971, -12.6 }, { -179.35, 191.319, 27.259 }, { -86.745, 75.297, 22.939 }, { -200.003, 150.154, 63.187 }, { 69.257, 137.913, -79.824 }, { -217.763, -15.493, 40.155 }, { -175.591, 187.332, -77.132 }, }; TArray PointsOutsideRoom = { { -245.656, 252.788, 115.908 }, { 125.832, 197.925, -136.036 }, { -257.614, 152.096, 152.655 }, { -249.694, -184.363, -59.927 }, { 153.152, -12.065, 122.478 }, { -61.405, 91.172, 170.071 }, { -85.013, 214.58, 165.639 }, { 97.861, 122.995, -74.949 }, { -67.454, -164.751, 25.332 }, { -307.367, 117.743, 58.999 }, { -16.464, -96.471, -146.66 }, { -163.323, 258.776, 28.556 }, { -195.161, 206.385, 85.72 }, { -216.943, 235.398, 126.688 }, { -288.013, 219.714, -48.284 }, { -222.667, -128.603, -173.879 }, { 7.732, 271.929, -105.47 }, { 68.097, 168.956, -145.375 }, { 55.597, -87.261, 141.444 }, { 99.099, -97.089, 0.583 }, { -228.267, -126.999, -88.321 }, { 181.973, -23.026, 129.291 }, { -107.073, -43.962, -158.281 }, { 172.614, 114.712, -128.513 }, { 161.491, 217.493, 140.58 }, { 154.978, 216.728, -52.095 }, { -280.619, 16.374, 38.726 }, { -195.068, -163.147, -70.396 }, { 156.432, 181.094, -58.986 }, { 67.725, -186.069, -149.307 }, { -256.253, 95.056, -20.023 }, { 149.64, 88.759, -59.373 }, { 25.693, 122.745, 148.911 }, { -62.503, -143.933, -103.133 }, { 121.933, 51.698, 168.209 }, { 72.676, 20.743, -163.189 }, { -284.95, 41.311, 111.688 }, { 4.034, 105.002, -160.053 }, { 6.185, -177.904, -154.869 }, { 21.763, 277.166, 179.376 }, { -290.38, -65.427, 134.753 }, { 161.042, 119.184, -29.429 }, { -292.113, -180.332, 161.352 }, { -79.784, 192.026, 119.786 }, { -295.718, 6.046, 135.894 }, { -273.905, 223.437, -26.792 }, { 177.765, 139.767, -13.221 }, { 133.273, 5.384, -18.993 }, { -199.152, -169.606, -87.036 }, { 30.782, -84.009, -134.418 }, { -73.394, -175.653, 54.978 }, { -44.264, 279.844, -118.853 }, { 155.457, 66.602, -94.68 }, { 164.383, 267.044, -158.237 }, { -25.081, 253.362, -175.43 }, { -312.178, -141.447, 154.339 }, { -82.336, -10.668, -142.827 }, { -312.781, 62.6, -70.972 }, { -68.104, 245.8, -18.926 }, { -116.402, 241.062, -10.163 }, { 123.96, 47.02, -20.643 }, { 181.741, -122.821, -64.27 }, { -68.568, 288.024, 19.715 }, { -285.368, 110.666, -93.628 }, { 43.468, -93.616, 164.687 }, { 171.453, 103.427, 125.081 }, { -204.397, -179.14, 174.834 }, { 49.192, 14.358, -143.414 }, { -136.188, 15.623, 133.944 }, { 77.812, 216.154, -108.196 }, { 166.967, 0.175, 66.023 }, { -282.119, 168.515, 86.119 }, { 80.612, -53.231, 175.787 }, { 97.66, -146.566, 35.457 } }; for (auto Point : PointsInRoom) { TestTrue("Point in room", Room->IsPositionInRoom(Point, true)); } for (auto Point : PointsOutsideRoom) { TestFalse("Point outside room", Room->IsPositionInRoom(Point, true)); } }); It(TEXT("Generate random position in room"), [this]() { auto Room = ToolkitSubsystem->GetCurrentRoom(); if (!TestNotNull(TEXT("Current room"), Room)) { return; } // Generate 100 random positions in the room and verify that they are all indeed inside the room and not in scene volumes // using different minimum distances to surfaces FRandomStream RandomStream; TArray MinDistanceToSurfaces = { 0.0f, 30.0f, 100.0f }; for (float MinDistanceToSurface : MinDistanceToSurfaces) { AddInfo(FString::Printf(TEXT("Minimum distance to surface: %f"), MinDistanceToSurface)); for (int i = 0; i < 100; ++i) { FVector Position; if (TestTrue(TEXT("Generated position successfully"), Room->GenerateRandomPositionInRoomFromStream(Position, RandomStream, MinDistanceToSurface, true))) { TestTrue(TEXT("Position is in room"), Room->IsPositionInRoom(Position)); TestNull(TEXT("Position is not in a scene volume"), Room->IsPositionInSceneVolume(Position, true, MinDistanceToSurface)); if (MinDistanceToSurface > 0.0f) { FVector SurfacePosition; double SurfaceDistance; TestNull(TEXT("Position is not within min distance of another surface"), Room->TryGetClosestSurfacePosition(Position, SurfacePosition, SurfaceDistance, {}, MinDistanceToSurface)); } } } } // If the MinDistanceToSurface is too large such that there are no valid points, then it should fail to generate a random position in the room constexpr float LargeMinDistance = 300.0f; FVector Position; TestFalse(TEXT("No valid positions"), Room->GenerateRandomPositionInRoomFromStream(Position, RandomStream, LargeMinDistance)); }); It(TEXT("Ray cast"), [this]() { auto Room = ToolkitSubsystem->GetCurrentRoom(); if (!TestNotNull(TEXT("Current room"), Room)) { return; } struct FRecordedRaycast { // Input FVector Position; FVector Direction; float MaxDist; EMRUKComponentType ComponentTypes; // Output FString Label; FVector HitPosition; FVector HitNormal; float HitDistance; }; TArray RecordedRaycasts = { // Hits { { 37.393, -14.003, -24.613 }, { 0.990, -0.048, -0.129 }, 0.0f, EMRUKComponentType::All, FMRUKLabels::Screen, { 84.823, -16.322, -30.780 }, { -0.995, 0.100, 0.000 }, 47.885 }, { { 37.393, -14.003, -24.613 }, { 0.990, -0.048, -0.129 }, 50.0f, EMRUKComponentType::All, FMRUKLabels::Screen, { 84.823, -16.322, -30.780 }, { -0.995, 0.100, 0.000 }, 47.885 }, { { -7.405, 27.794, -12.428 }, { 0.844, 0.003, -0.536 }, 0.0f, EMRUKComponentType::All, FMRUKLabels::Table, { 53.665, 28.008, -51.185 }, { 0.000, 0.000, 1.000 }, 72.33 }, { { -22.253, 36.487, 1.660 }, { 0.211, 0.054, -0.976 }, 0.0f, EMRUKComponentType::All, FMRUKLabels::Floor, { 6.171, 43.785, -129.629 }, { 0.000, -0.000, 1.000 }, 134.529 }, { { -22.253, 36.487, 1.660 }, { 0.432, 0.270, 0.861 }, 0.0f, EMRUKComponentType::All, FMRUKLabels::Ceiling, { 43.807, 77.766, 133.376 }, { 0.000, -0.000, -1.000 }, 153.026 }, { { -77.859, 204.433, -26.619 }, { 0.110, -0.534, -0.839 }, 0.0f, EMRUKComponentType::All, FMRUKLabels::Couch, { -70.131, 166.787, -85.797 }, { -0.180, 0.984, -0.000 }, 70.561 }, { { -127.986, 126.202, -94.119 }, { 0.606, -0.593, -0.530 }, 0.0f, EMRUKComponentType::All, FMRUKLabels::Floor, { -87.385, 86.442, -129.629 }, { 0.000, -0.000, 1.000 }, 67.009 }, { { -138.228, 108.241, -74.798 }, { -0.950, -0.240, -0.201 }, 0.0f, EMRUKComponentType::All, FMRUKLabels::Couch, { -187.570, 95.756, -85.245 }, { -0.000, 0.000, 1.000 }, 51.958 }, { { -298.759, 101.686, -76.521 }, { 0.968, -0.146, -0.204 }, 0.0f, EMRUKComponentType::All, FMRUKLabels::Couch, { -246.590, 93.797, -87.543 }, { -0.984, -0.180, -0.000 }, 53.901 }, { { -196.813, 0.663, -71.069 }, { 0.250, 0.908, -0.335 }, 0.0f, EMRUKComponentType::All, FMRUKLabels::Couch, { -181.948, 54.632, -91.002 }, { 0.180, -0.984, 0.000 }, 59.422 }, { { -115.905, 47.604, -6.411 }, { -0.199, 0.488, -0.850 }, 0.0f, EMRUKComponentType::All, FMRUKLabels::Couch, { -131.525, 85.882, -73.090 }, { -0.000, 0.000, 1.000 }, 78.456 }, { { -127.926, 109.603, 59.458 }, { -0.274, 0.955, -0.115 }, 0.0f, EMRUKComponentType::All, FMRUKLabels::WallArt, { -144.809, 168.482, 52.371 }, { 0.136, -0.991, -0.000 }, 61.66 }, { { 17.710, 137.262, 55.542 }, { 0.902, 0.409, -0.139 }, 0.0f, EMRUKComponentType::All, FMRUKLabels::WindowFrame, { 85.375, 167.955, 45.100 }, { -0.978, -0.207, -0.000 }, 75.031 }, { { -115.905, 47.604, -6.411 }, { -0.199, 0.488, -0.850 }, 0.0f, EMRUKComponentType::Plane, FMRUKLabels::Couch, { -134.361, 92.864, -85.245 }, { -0.000, 0.000, 1.000 }, 92.758 }, { { -115.905, 47.604, -6.411 }, { -0.199, 0.488, -0.850 }, 0.0f, EMRUKComponentType::Volume, FMRUKLabels::Couch, { -131.525, 85.882, -73.090 }, { -0.000, 0.000, 1.000 }, 78.456 }, // Misses { { -77.859, 204.433, -26.619 }, { 0.000, 0.000, -1.000 }, 0.0f, EMRUKComponentType::All, TEXT(""), { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f }, 0.0f }, { { 37.393, -14.003, -24.613 }, { 0.990, -0.048, -0.129 }, 40.0f, EMRUKComponentType::All, TEXT(""), { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f }, 0.0f }, { { -7.405, 27.794, -12.428 }, { 0.844, 0.003, -0.536 }, 70.0f, EMRUKComponentType::All, TEXT(""), { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f }, 0.0f }, }; for (const auto& Recorded : RecordedRaycasts) { FMRUKHit Result; auto Anchor = Room->Raycast(Recorded.Position, Recorded.Direction, Recorded.MaxDist, { .ComponentTypes = static_cast(Recorded.ComponentTypes) }, Result); // This is a fairly generous tolerance, but necessary because ray pos and direction were only recorded // up to 3 decimal places and small changes in direction can result in fairly large differences in hit position // over long distances. constexpr float Tolerance = 0.1; if (Anchor == nullptr) { TestTrue(TEXT("No hit"), Recorded.Label.IsEmpty()); } else { TestEqual(TEXT("Hit position"), Result.HitPosition, Recorded.HitPosition, Tolerance); TestEqual(TEXT("Hit normal"), Result.HitNormal, Recorded.HitNormal, Tolerance); TestEqual(TEXT("Hit distance"), Result.HitDistance, Recorded.HitDistance, Tolerance); if (TestTrue(TEXT("Has semantic classification"), Anchor->SemanticClassifications.Num() > 0)) { TestEqual(TEXT("Hit label"), Anchor->SemanticClassifications[0], Recorded.Label); } } } }); It(TEXT("Ray cast all"), [this]() { auto Room = ToolkitSubsystem->GetCurrentRoom(); if (!TestNotNull(TEXT("Current room"), Room)) { return; } struct FRecordedRaycastHit { FString Label; FVector HitPosition; FVector HitNormal; float HitDistance; }; struct FRecordedRaycastAll { // Input FVector Position; FVector Direction; float MaxDist; EMRUKComponentType ComponentTypes; // Output TArray Hits; }; TArray RecordedRaycasts = { { { 83.193, 58.82, 11.548 }, { 0.044, -0.904, -0.426 }, 0.0f, EMRUKComponentType::All, { { FMRUKLabels::WallFace, { 90.736, -95.398, -61.109 }, { -0.151, 0.989, 0 }, 170.643 }, { FMRUKLabels::Table, { 89.705, -74.333, -51.185 }, { 0, 0, 1 }, 147.335 }, { FMRUKLabels::Table, { 89.705, -74.333, -51.185 }, { 0, 0, 1 }, 147.335 }, { FMRUKLabels::Screen, { 84.702, 27.972, -2.986 }, { 0, -0, 1 }, 34.133 }, { FMRUKLabels::Screen, { 86.2, -2.658, -17.416 }, { -0.995, 0.1, 0 }, 68.026 }, } }, { { 83.193, 58.82, 11.548 }, { 0.044, -0.904, -0.426 }, 0.0f, EMRUKComponentType::Plane, { { FMRUKLabels::WallFace, { 90.736, -95.398, -61.109 }, { -0.151, 0.989, 0 }, 170.643 }, { FMRUKLabels::Table, { 89.705, -74.333, -51.185 }, { 0, 0, 1 }, 147.335 }, } }, { { 83.193, 58.82, 11.548 }, { 0.044, -0.904, -0.426 }, 0.0f, EMRUKComponentType::Volume, { { FMRUKLabels::Table, { 89.705, -74.333, -51.185 }, { 0, 0, 1 }, 147.335 }, { FMRUKLabels::Screen, { 84.702, 27.972, -2.986 }, { 0, -0, 1 }, 34.133 }, { FMRUKLabels::Screen, { 86.2, -2.658, -17.416 }, { -0.995, 0.1, 0 }, 68.026 }, } } }; for (const auto& Recorded : RecordedRaycasts) { TArray Hits; TArray Anchors; Room->RaycastAll(Recorded.Position, Recorded.Direction, Recorded.MaxDist, { .ComponentTypes = static_cast(Recorded.ComponentTypes) }, Hits, Anchors); // This is a fairly generous tolerance, but necessary because ray pos and direction were only recorded // up to 3 decimal places and small changes in direction can result in fairly large differences in hit position // over long distances. constexpr float Tolerance = 0.2; if (!TestEqual("Number of hits", Hits.Num(), Recorded.Hits.Num())) { continue; } if (!TestEqual("Number of anchors", Anchors.Num(), Recorded.Hits.Num())) { continue; } for (int i = 0; i < Recorded.Hits.Num(); ++i) { TestEqual(TEXT("Hit position"), Hits[i].HitPosition, Recorded.Hits[i].HitPosition, Tolerance); TestEqual(TEXT("Hit normal"), Hits[i].HitNormal, Recorded.Hits[i].HitNormal, Tolerance); TestEqual(TEXT("Hit distance"), Hits[i].HitDistance, Recorded.Hits[i].HitDistance, Tolerance); if (TestTrue(TEXT("Has semantic classification"), Anchors[i]->SemanticClassifications.Num() > 0)) { TestEqual(TEXT("Hit label"), Anchors[i]->SemanticClassifications[0], Recorded.Hits[i].Label); } } } }); It(TEXT("Parent/child relationship"), [this]() { auto Room = ToolkitSubsystem->GetCurrentRoom(); if (!TestNotNull(TEXT("Current room"), Room)) { return; } // Verify that child/parent relationship is consistent for (const auto& Anchor : Room->AllAnchors) { for (const auto& ChildAnchor : Anchor->ChildAnchors) { TestEqual(TEXT("Parent Anchor"), ChildAnchor->ParentAnchor, Anchor); } if (Anchor->ParentAnchor) { TestTrue(TEXT("Parent contains anchor in child list"), Anchor->ParentAnchor->ChildAnchors.Contains(Anchor)); } } TestEqual(TEXT("Floor has the right number of children"), Room->FloorAnchor->ChildAnchors.Num(), 6); for (const auto& Anchor : Room->AllAnchors) { if (Anchor->SemanticClassifications.IsEmpty()) { continue; } if (Anchor->SemanticClassifications[0] == FMRUKLabels::Screen) { if (TestNotNull(TEXT("Screen has parent"), Anchor->ParentAnchor.Get())) { if (TestTrue(TEXT("Screen parent has label"), !Anchor->ParentAnchor->SemanticClassifications.IsEmpty())) { TestEqual(TEXT("Screen is on table"), Anchor->ParentAnchor->SemanticClassifications[0], FMRUKLabels::Table); } } } if (Anchor->SemanticClassifications[0] == FMRUKLabels::Table) { TestEqual(TEXT("Table is on the floor"), Anchor->ParentAnchor, Room->FloorAnchor); } if (Anchor->SemanticClassifications[0] == FMRUKLabels::Storage) { TestEqual(TEXT("Storage is on the floor"), Anchor->ParentAnchor, Room->FloorAnchor); } if (Anchor->SemanticClassifications[0] == FMRUKLabels::Couch) { TestEqual(TEXT("Couch is on the floor"), Anchor->ParentAnchor, Room->FloorAnchor); } if (Anchor->SemanticClassifications[0] == FMRUKLabels::DoorFrame) { TestTrue(TEXT("Door frame is on wall"), Room->WallAnchors.Contains(Anchor->ParentAnchor)); } if (Anchor->SemanticClassifications[0] == FMRUKLabels::WindowFrame) { TestTrue(TEXT("Window frame is on wall"), Room->WallAnchors.Contains(Anchor->ParentAnchor)); } if (Anchor->SemanticClassifications[0] == FMRUKLabels::WallArt) { TestTrue(TEXT("Wall art is on wall"), Room->WallAnchors.Contains(Anchor->ParentAnchor)); } } }); TeardownMRUKSubsystem(); }); Describe(TEXT("Toolkit"), [this] { SetupMRUKSubsystem(); LoadSceneFromJson(); It(TEXT("IsPositionInSceneVolume"), [this]() { struct TestData { FVector WorldPosition; bool TestVerticalBounds; double Tolerance; TOptional ExpectedUUID; }; TArray AllTestData = { { { 79.121, -20.291, -70.864 }, true, 0.0, { { { 0x1E, 0x57, 0x8C, 0x3E, 0x9E, 0x7B, 0x49, 0x5E, 0x9F, 0x8B, 0xB0, 0x0A, 0x37, 0x22, 0xB6, 0x19 } } } }, { { 39.355, 0.683, -51.092 }, true, 1.0, { { { 0x1E, 0x57, 0x8C, 0x3E, 0x9E, 0x7B, 0x49, 0x5E, 0x9F, 0x8B, 0xB0, 0x0A, 0x37, 0x22, 0xB6, 0x19 } } } }, { { 66.716, 4.072, -15.717 }, false, 0.0, { { { 0x1E, 0x57, 0x8C, 0x3E, 0x9E, 0x7B, 0x49, 0x5E, 0x9F, 0x8B, 0xB0, 0x0A, 0x37, 0x22, 0xB6, 0x19 } } } }, { { 16.802, 23.632, -5.421 }, false, 0.0, {} }, }; for (const auto& TestData : AllTestData) { const auto ActualActor = ToolkitSubsystem->IsPositionInSceneVolume(TestData.WorldPosition, TestData.TestVerticalBounds, TestData.Tolerance); if (TestData.ExpectedUUID.IsSet()) { TestNotNull(TEXT("Actual actor set"), ActualActor); TestEqual(TEXT("Actual actor UUID"), ActualActor->AnchorUUID, TestData.ExpectedUUID.GetValue()); } else { TestNull(TEXT("Actual actor not set"), ActualActor); } } }); TeardownMRUKSubsystem(); }); Describe(TEXT("Anchor"), [this] { SetupMRUKSubsystem(); LoadSceneFromJson(); It(TEXT("HasLabel"), [this]() { const auto Room = ToolkitSubsystem->GetCurrentRoom(); if (!TestNotNull(TEXT("Current room"), Room)) { return; } if (!TestTrue(TEXT("Wall anchors available"), Room->WallAnchors.Num() > 0)) { return; } for (const auto& WallAnchor : Room->WallAnchors) { TestTrue(TEXT("Has WALL_FACE label"), WallAnchor->HasLabel(FMRUKLabels::WallFace)); TestFalse(TEXT("Has not FLOOR label"), WallAnchor->HasLabel(FMRUKLabels::Floor)); TestFalse(TEXT("Has not CEILING label"), WallAnchor->HasLabel(FMRUKLabels::Ceiling)); } if (!TestNotNull(TEXT("Floor anchor"), Room->FloorAnchor.Get())) { return; } TestTrue(TEXT("Has FLOOR label"), Room->FloorAnchor->HasLabel(FMRUKLabels::Floor)); TestFalse(TEXT("Has not WALL_FACE label"), Room->FloorAnchor->HasLabel(FMRUKLabels::WallFace)); TestFalse(TEXT("Has not CEILING label"), Room->FloorAnchor->HasLabel(FMRUKLabels::Ceiling)); if (!TestNotNull(TEXT("Ceiling anchor"), Room->CeilingAnchor.Get())) { return; } TestTrue(TEXT("Has CEILING label"), Room->CeilingAnchor->HasLabel(FMRUKLabels::Ceiling)); TestFalse(TEXT("Has not FLOOR label"), Room->CeilingAnchor->HasLabel(FMRUKLabels::Floor)); TestFalse(TEXT("Has not WALL_FACE label"), Room->CeilingAnchor->HasLabel(FMRUKLabels::WallFace)); }); It(TEXT("ProceduralMesh"), [this]() { const auto Room = ToolkitSubsystem->GetCurrentRoom(); if (!TestNotNull(TEXT("Current room"), Room)) { return; } if (!TestNotNull(TEXT("Floor anchor"), Room->FloorAnchor.Get())) { return; } Room->FloorAnchor->AttachProceduralMesh(); auto ProceduralMeshComponent = Room->FloorAnchor->ProceduralMeshComponent; if (!TestNotNull(TEXT("Has Procedural Mesh Component"), ProceduralMeshComponent.Get())) { return; } auto Section = ProceduralMeshComponent->GetProcMeshSection(0); if (!TestNotNull(TEXT("Mesh Section"), Section)) { return; } TestEqual(TEXT("Floor vertices equal to number of walls"), Section->ProcVertexBuffer.Num(), Room->WallAnchors.Num()); TestEqual(TEXT("Floor triangles equal to number of walls - 2"), Section->ProcIndexBuffer.Num(), 3 * (Room->WallAnchors.Num() - 2)); // NOTE: There are a number of valid ways to triangulate the mesh, and it doesn't need to be exactly like this but // this will at least highlight if we unexpectedly caused the mesh generation to change its output. TArray ExpectedIndexBuffer = { 6, 7, 0, 0, 1, 2, 4, 5, 6, 0, 2, 3, 4, 6, 0, 0, 3, 4 }; if (TestEqual(TEXT("Triangle index buffer size"), Section->ProcIndexBuffer.Num(), ExpectedIndexBuffer.Num())) { for (int i = 0; i < ExpectedIndexBuffer.Num(); i++) { TestEqual(TEXT("Floor triangle index buffer"), Section->ProcIndexBuffer[i], ExpectedIndexBuffer[i]); } } TArray WallTextureCoordinateModes{ { EMRUKCoordModeU::Stretch, EMRUKCoordModeV::MaintainAspectRatio }, { EMRUKCoordModeU::MetricSeamless, EMRUKCoordModeV::MaintainAspectRatio }, { EMRUKCoordModeU::MaintainAspectRatio, EMRUKCoordModeV::Stretch }, { EMRUKCoordModeU::MaintainAspectRatioSeamless, EMRUKCoordModeV::Metric }, }; Room->AttachProceduralMeshToWalls(WallTextureCoordinateModes, {}); if (TestTrue(TEXT("Has walls"), !Room->WallAnchors.IsEmpty())) { ProceduralMeshComponent = Room->WallAnchors[0]->ProceduralMeshComponent; if (!TestNotNull(TEXT("Has Procedural Mesh Component"), ProceduralMeshComponent.Get())) { return; } Section = ProceduralMeshComponent->GetProcMeshSection(0); if (!TestNotNull(TEXT("Mesh Section"), Section)) { return; } if (TestEqual(TEXT("Vertex buffer size"), Section->ProcVertexBuffer.Num(), 4)) { TestEqual(TEXT("Vertex 0 UV 0"), Section->ProcVertexBuffer[0].UV0, FVector2D(0.770758, 0.184412)); TestEqual(TEXT("Vertex 0 UV 1"), Section->ProcVertexBuffer[0].UV1, FVector2D(10.790611, 2.581766)); TestEqual(TEXT("Vertex 0 UV 2"), Section->ProcVertexBuffer[0].UV2, FVector2D(4.179547, 1.000000)); TestEqual(TEXT("Vertex 0 UV 3"), Section->ProcVertexBuffer[0].UV3, FVector2D(10.790611, 2.630046)); TestEqual(TEXT("Vertex 1 UV 0"), Section->ProcVertexBuffer[1].UV0, FVector2D(1.000000, 0.184412)); TestEqual(TEXT("Vertex 1 UV 1"), Section->ProcVertexBuffer[1].UV1, FVector2D(14.000000, 2.581766)); TestEqual(TEXT("Vertex 1 UV 2"), Section->ProcVertexBuffer[1].UV2, FVector2D(5.422645, 1.000000)); TestEqual(TEXT("Vertex 1 UV 3"), Section->ProcVertexBuffer[1].UV3, FVector2D(14.000000, 2.630046)); TestEqual(TEXT("Vertex 2 UV 0"), Section->ProcVertexBuffer[2].UV0, FVector2D(1.000000, -0.000000)); TestEqual(TEXT("Vertex 2 UV 1"), Section->ProcVertexBuffer[2].UV1, FVector2D(14.000000, -0.000000)); TestEqual(TEXT("Vertex 2 UV 2"), Section->ProcVertexBuffer[2].UV2, FVector2D(5.422645, -0.000000)); TestEqual(TEXT("Vertex 2 UV 3"), Section->ProcVertexBuffer[2].UV3, FVector2D(14.000000, -0.000000)); TestEqual(TEXT("Vertex 3 UV 0"), Section->ProcVertexBuffer[3].UV0, FVector2D(0.770758, -0.000000)); TestEqual(TEXT("Vertex 3 UV 1"), Section->ProcVertexBuffer[3].UV1, FVector2D(10.790611, -0.000000)); TestEqual(TEXT("Vertex 3 UV 2"), Section->ProcVertexBuffer[3].UV2, FVector2D(4.179547, -0.000000)); TestEqual(TEXT("Vertex 3 UV 3"), Section->ProcVertexBuffer[3].UV3, FVector2D(10.790611, -0.000000)); } } }); It(TEXT("ProceduralMesh with holes"), [this]() { const auto Room = ToolkitSubsystem->GetCurrentRoom(); if (!TestNotNull(TEXT("Current room"), Room)) { return; } Room->AttachProceduralMeshToWalls({}, { FMRUKLabels::WindowFrame, FMRUKLabels::DoorFrame }); if (TestTrue(TEXT("Has walls"), !Room->WallAnchors.IsEmpty())) { const auto ProceduralMeshComponent = Room->WallAnchors[6]->ProceduralMeshComponent; if (!TestNotNull(TEXT("Has Procedural Mesh Component"), ProceduralMeshComponent.Get())) { return; } const auto Section = ProceduralMeshComponent->GetProcMeshSection(0); if (!TestNotNull(TEXT("Mesh Section"), Section)) { return; } if (TestEqual(TEXT("Vertex buffer size"), Section->ProcVertexBuffer.Num(), 8)) { constexpr double Tolerance = 0.001; TestEqual(TEXT("Vertex 0"), Section->ProcVertexBuffer[0].Position, FVector(0.0, -168.041, -131.505), Tolerance); TestEqual(TEXT("Vertex 1"), Section->ProcVertexBuffer[1].Position, FVector(0.0, 168.041, -131.505), Tolerance); TestEqual(TEXT("Vertex 2"), Section->ProcVertexBuffer[2].Position, FVector(0.0, 168.041, 131.505), Tolerance); TestEqual(TEXT("Vertex 3"), Section->ProcVertexBuffer[3].Position, FVector(0.0, -168.041, 131.505), Tolerance); TestEqual(TEXT("Vertex 4"), Section->ProcVertexBuffer[4].Position, FVector(0.0, 28.075, -61.585), Tolerance); TestEqual(TEXT("Vertex 5"), Section->ProcVertexBuffer[5].Position, FVector(0.0, 143.493, -61.585), Tolerance); TestEqual(TEXT("Vertex 6"), Section->ProcVertexBuffer[6].Position, FVector(0.0, 143.493, 111.358), Tolerance); TestEqual(TEXT("Vertex 7"), Section->ProcVertexBuffer[7].Position, FVector(0.0, 28.075, 111.358), Tolerance); } if (TestEqual(TEXT("Index buffer size"), Section->ProcIndexBuffer.Num(), 24)) { TestEqual(TEXT("Index 0"), Section->ProcIndexBuffer[0], 3); TestEqual(TEXT("Index 1"), Section->ProcIndexBuffer[1], 0); TestEqual(TEXT("Index 2"), Section->ProcIndexBuffer[2], 4); TestEqual(TEXT("Index 3"), Section->ProcIndexBuffer[3], 5); TestEqual(TEXT("Index 4"), Section->ProcIndexBuffer[4], 4); TestEqual(TEXT("Index 5"), Section->ProcIndexBuffer[5], 0); TestEqual(TEXT("Index 6"), Section->ProcIndexBuffer[6], 3); TestEqual(TEXT("Index 7"), Section->ProcIndexBuffer[7], 4); TestEqual(TEXT("Index 8"), Section->ProcIndexBuffer[8], 7); TestEqual(TEXT("Index 9"), Section->ProcIndexBuffer[9], 5); TestEqual(TEXT("Index 10"), Section->ProcIndexBuffer[10], 0); TestEqual(TEXT("Index 11"), Section->ProcIndexBuffer[11], 1); TestEqual(TEXT("Index 12"), Section->ProcIndexBuffer[12], 2); TestEqual(TEXT("Index 13"), Section->ProcIndexBuffer[13], 3); TestEqual(TEXT("Index 14"), Section->ProcIndexBuffer[14], 7); TestEqual(TEXT("Index 15"), Section->ProcIndexBuffer[15], 6); TestEqual(TEXT("Index 16"), Section->ProcIndexBuffer[16], 5); TestEqual(TEXT("Index 17"), Section->ProcIndexBuffer[17], 1); TestEqual(TEXT("Index 18"), Section->ProcIndexBuffer[18], 2); TestEqual(TEXT("Index 19"), Section->ProcIndexBuffer[19], 7); TestEqual(TEXT("Index 20"), Section->ProcIndexBuffer[20], 6); TestEqual(TEXT("Index 21"), Section->ProcIndexBuffer[21], 6); TestEqual(TEXT("Index 22"), Section->ProcIndexBuffer[22], 1); TestEqual(TEXT("Index 23"), Section->ProcIndexBuffer[23], 2); } } }); TeardownMRUKSubsystem(); }); Describe(TEXT("Utilities"), [this] { It(TEXT("Label Filter"), [this]() { FMRUKLabelFilter Filter; Filter.IncludedLabels.Push("FOO"); Filter.IncludedLabels.Push("BAR"); Filter.ExcludedLabels.Push("BAZ"); Filter.ExcludedLabels.Push("QUX"); TestTrue(TEXT("BAM, BAR Passes Filter"), Filter.PassesFilter({ { TEXT("BAM"), TEXT("BAR") } })); TestFalse(TEXT("BAZ Fails Filter"), Filter.PassesFilter({ { TEXT("BAZ") } })); TestFalse(TEXT("BAR, QUX Fails Filter"), Filter.PassesFilter({ { TEXT("BAR"), TEXT("QUX") } })); TestFalse(TEXT("BAM Fails Filter"), Filter.PassesFilter({ { TEXT("BAM") } })); Filter.IncludedLabels.Empty(); TestTrue(TEXT("BAM Passes Filter"), Filter.PassesFilter({ { TEXT("BAM") } })); }); }); }