// Copyright (c) Meta Platforms, Inc. and affiliates. #include "MRUtilityKitRoom.h" #include "MRUtilityKitAnchor.h" #include "MRUtilityKitSerializationHelpers.h" #include "MRUtilityKitSeatsComponent.h" #include "MRUtilityKitSubsystem.h" #include "MRUtilityKitBPLibrary.h" #include "Engine/GameInstance.h" #include "GameFramework/Pawn.h" #include "GameFramework/WorldSettings.h" #include "Kismet/KismetMathLibrary.h" #include "Misc/EngineVersionComparison.h" #define LOCTEXT_NAMESPACE "MRUtilityKitRoom" namespace { double GetSeamlessFactor(double Perimeter, double StepSize) { double RoundedPerimeter = FMath::RoundHalfFromZero(Perimeter / StepSize); if (RoundedPerimeter <= 0.0) { RoundedPerimeter = 1.0; } return Perimeter / RoundedPerimeter; } FBox2D GetBoundsFromBoxForSide(const EMRUKBoxSide Side, const FBox& Box) { switch (Side) { case EMRUKBoxSide::XPos: case EMRUKBoxSide::XNeg: return FBox2D(FVector2D(Box.Min.Y, Box.Min.Z), FVector2D(Box.Max.Y, Box.Max.Z)); case EMRUKBoxSide::YPos: case EMRUKBoxSide::YNeg: return FBox2D(FVector2D(Box.Min.X, Box.Min.Z), FVector2D(Box.Max.X, Box.Max.Z)); case EMRUKBoxSide::ZPos: case EMRUKBoxSide::ZNeg: return FBox2D(FVector2D(Box.Min.X, Box.Min.Y), FVector2D(Box.Max.X, Box.Max.Y)); } return {}; } FVector GetNormalBoxSide(const EMRUKBoxSide Side) { switch (Side) { case EMRUKBoxSide::XPos: return FVector(1, 0, 0); case EMRUKBoxSide::XNeg: return FVector(-1, 0, 0); case EMRUKBoxSide::YPos: return FVector(0, 1, 0); case EMRUKBoxSide::YNeg: return FVector(0, -1, 0); case EMRUKBoxSide::ZPos: return FVector(0, 0, 1); case EMRUKBoxSide::ZNeg: return FVector(0, 0, -1); } return {}; } FVector GetWorldPos(const FVector2D Pos2D, const AMRUKAnchor* ParentAnchor, const EMRUKBoxSide Side) { FVector LocalPos = FVector::Zero(); switch (Side) { case EMRUKBoxSide::XPos: LocalPos = FVector(ParentAnchor->VolumeBounds.Max.X, Pos2D.X, Pos2D.Y); break; case EMRUKBoxSide::XNeg: LocalPos = FVector(ParentAnchor->VolumeBounds.Min.X, Pos2D.X, Pos2D.Y); break; case EMRUKBoxSide::YPos: LocalPos = FVector(Pos2D.X, ParentAnchor->VolumeBounds.Max.Y, Pos2D.Y); break; case EMRUKBoxSide::YNeg: LocalPos = FVector(Pos2D.X, ParentAnchor->VolumeBounds.Min.Y, Pos2D.Y); break; case EMRUKBoxSide::ZPos: LocalPos = FVector(Pos2D.X, Pos2D.Y, ParentAnchor->VolumeBounds.Max.Z); break; case EMRUKBoxSide::ZNeg: LocalPos = FVector(Pos2D.X, Pos2D.Y, ParentAnchor->VolumeBounds.Min.Z); break; } return ParentAnchor->ActorToWorld().TransformPosition(LocalPos); } const float InvSqrt2 = 1.0f / FMath::Sqrt(2.0f); bool IsActorOrientationHorizontal(const AActor* Actor) { bool bRet = false; if (Actor == nullptr) bRet = false; else if (Actor->GetActorUpVector().Z >= InvSqrt2) // walls, door or similar bRet = false; else if (FMath::Abs(Actor->GetActorUpVector().X) >= InvSqrt2) bRet = true; return bRet; } } // namespace AMRUKRoom::AMRUKRoom(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { // Create a scene component as root so we can attach spawned actors to it RootComponent = CreateDefaultSubobject(TEXT("SceneComponent")); } void AMRUKRoom::EndPlay(EEndPlayReason::Type Reason) { for (const auto& Anchor : AllAnchors) { OnAnchorRemoved.Broadcast(Anchor); Anchor->Destroy(); } GetGameInstance()->GetSubsystem()->UnregisterRoom(this); Super::EndPlay(Reason); } void AMRUKRoom::LoadFromData(UMRUKRoomData* RoomData) { check(RoomData); AnchorUUID = RoomData->SpaceQuery.UUID; SpaceHandle = RoomData->SpaceQuery.Space; RoomLayout = RoomData->RoomLayout; auto AnchorsToRemove = AllAnchors; AllAnchors.Empty(); CeilingAnchor = nullptr; FloorAnchor = nullptr; WallAnchors.Empty(); SeatAnchors.Empty(); TArray> AnchorsCreated; TArray> AnchorsUpdated; for (const auto& AnchorData : RoomData->AnchorsData) { const auto AnchorFound = AnchorsToRemove.FindByPredicate([AnchorData](TObjectPtr Anchor) { return Anchor && Anchor->AnchorUUID == AnchorData->SpaceQuery.UUID; }); AMRUKAnchor* Anchor = nullptr; if (AnchorFound) { Anchor = *AnchorFound; UE_LOG(LogMRUK, Log, TEXT("Update existing anchor in room")); if (Anchor->LoadFromData(AnchorData)) { AnchorsUpdated.Push(Anchor); } AnchorsToRemove.Remove(Anchor); } else { UE_LOG(LogMRUK, Log, TEXT("Spawn new anchor in room")); Anchor = SpawnAnchor(); Anchor->LoadFromData(AnchorData); AnchorsCreated.Push(Anchor); } AddAnchorToRoom(Anchor); } UE_LOG(LogMRUK, Log, TEXT("Destroy %d old anchors"), AnchorsToRemove.Num()); for (auto& OldAnchor : AnchorsToRemove) { OnAnchorRemoved.Broadcast(OldAnchor); OldAnchor->Destroy(); } InitializeRoom(); for (auto& Anchor : AnchorsUpdated) { OnAnchorUpdated.Broadcast(Anchor); } for (auto& Anchor : AnchorsCreated) { OnAnchorCreated.Broadcast(Anchor); } } TSharedRef AMRUKRoom::JsonSerialize() { TSharedRef JsonObject = MakeShareable(new FJsonObject); TArray> AnchorsArray; for (const auto& Anchor : AllAnchors) { if (Anchor) { AnchorsArray.Add(MakeShareable(new FJsonValueObject(Anchor->JsonSerialize()))); } } JsonObject->SetField(TEXT("UUID"), MRUKSerialize(AnchorUUID)); JsonObject->SetField(TEXT("RoomLayout"), MRUKSerialize(RoomLayout)); JsonObject->SetArrayField(TEXT("Anchors"), AnchorsArray); return JsonObject; } bool AMRUKRoom::Corresponds(UMRUKRoomData* RoomData) const { if (!RoomData) { UE_LOG(LogMRUK, Warning, TEXT("Room query is null")); return false; } if (AnchorUUID == RoomData->SpaceQuery.UUID) { UE_LOG(LogMRUK, Log, TEXT("Rooms UUID equals")); return true; } for (const auto& Anchor : AllAnchors) { auto UUID = Anchor->AnchorUUID; const auto Found = RoomData->AnchorsData.FindByPredicate([UUID](TObjectPtr AnchorData) { return UUID == AnchorData->SpaceQuery.UUID; }); if (Found) { return true; } } UE_LOG(LogMRUK, Log, TEXT("Room is not equal")); return false; } AMRUKAnchor* AMRUKRoom::SpawnAnchor() { FActorSpawnParameters SpawnParameters{}; SpawnParameters.Owner = this; const auto Anchor = GetWorld()->SpawnActor(SpawnParameters); Anchor->Room = this; GetRootComponent()->SetMobility(EComponentMobility::Movable); Anchor->AttachToComponent(GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform); return Anchor; } void AMRUKRoom::AddAnchorToRoom(AMRUKAnchor* Anchor) { const FString Semantics = FString::Join(Anchor->SemanticClassifications, TEXT("-")); #if WITH_EDITOR if (Anchor->SemanticClassifications.Num() > 0) { Anchor->SetActorLabel(Semantics); } #endif UE_LOG(LogMRUK, Log, TEXT("Add '%s' anchor '%s' to room '%s'"), *Semantics, *Anchor->AnchorUUID.ToString(), *AnchorUUID.ToString()); if (Anchor->AnchorUUID == RoomLayout.FloorUuid) { FloorAnchor = Anchor; } if (Anchor->AnchorUUID == RoomLayout.CeilingUuid) { CeilingAnchor = Anchor; } if (RoomLayout.WallsUuid.Contains(Anchor->AnchorUUID)) { WallAnchors.Push(Anchor); } if (Anchor->HasLabel(FMRUKLabels::GlobalMesh)) { GlobalMeshAnchor = Anchor; } if (Anchor->HasLabel(FMRUKLabels::Couch)) { SeatAnchors.Push(Anchor); } AllAnchors.Push(Anchor); } void AMRUKRoom::InitializeRoom() { ComputeRoomBounds(); ComputeAnchorHierarchy(); ComputeSeats(); ComputeRoomEdges(); KeyWallAnchor = nullptr; } void AMRUKRoom::ComputeRoomBounds() { RoomBounds.Init(); for (auto& Anchor : { FloorAnchor, CeilingAnchor }) { if (Anchor) { auto Transform = Anchor->GetTransform(); for (const auto& Vertex : Anchor->PlaneBoundary2D) { const auto Pos = Transform.TransformPosition(FVector(0.0f, Vertex.X, Vertex.Y)); RoomBounds += Pos; } } } } void AMRUKRoom::ComputeAnchorHierarchy() { // Reset anchor hierarchy for (auto& Anchor : AllAnchors) { Anchor->ParentAnchor = nullptr; Anchor->ChildAnchors.Empty(); } constexpr float OffsetTolerance = 4.0f; // 4 cm offset allowed // Find things where are attached to walls such as doors, windows frames or wall art for (const auto& WallAnchor : WallAnchors) { if (!WallAnchor) { continue; } const auto& WallTransform = WallAnchor->GetTransform(); const auto WallNormal = WallTransform.GetUnitAxis(EAxis::X); for (const auto& ChildAnchor : AllAnchors) { // Don't parent walls to themselves if (ChildAnchor == WallAnchor) { continue; } const auto& ChildTransform = ChildAnchor->GetTransform(); const auto ChildNormal = ChildTransform.GetUnitAxis(EAxis::X); // Check that the two transforms face the same direction if (!FVector::Coincident(WallNormal, ChildNormal)) { continue; } // Check that the position is close to the surface (they are a little bit offset // to prevent Z fighting so allow for that). auto LocalPos = WallTransform.InverseTransformPosition(ChildTransform.GetLocation()); if (FMath::Abs(LocalPos.X) > OffsetTolerance) { continue; } // Check that the anchor is within the wall boundary if (!WallAnchor->IsPositionInBoundary(FVector2D(LocalPos.Y, LocalPos.Z))) { continue; } // We have a match ensureMsgf(!ChildAnchor->ParentAnchor, TEXT("This anchor already has a parent")); ChildAnchor->ParentAnchor = WallAnchor; WallAnchor->ChildAnchors.Push(ChildAnchor); } } // Find volumes on the floor if (FloorAnchor) { const auto& FloorTransform = FloorAnchor->GetTransform(); const auto FloorNormal = FloorTransform.GetUnitAxis(EAxis::X); ensureMsgf(FVector::Coincident(FloorNormal, FVector::DownVector), TEXT("Floor normal should be pointing downwards")); auto FloorHeight = FloorTransform.GetLocation().Z; for (const auto& ChildAnchor : AllAnchors) { // Don't parent the floor to itself if (ChildAnchor == FloorAnchor) { continue; } const auto& ChildTransform = ChildAnchor->GetTransform(); const auto ChildXAxis = ChildTransform.GetUnitAxis(EAxis::X); const auto& ChildVolumeBounds = ChildAnchor->VolumeBounds; // Only interested in scene volumes, the assumption is that all scene volumes have X axis pointing downwards if (!ChildVolumeBounds.IsValid || !FVector::Coincident(ChildXAxis, FVector::DownVector)) { continue; } auto ChildBottom = ChildTransform.GetLocation().Z - ChildVolumeBounds.Max.X; // Check that the volume is on the floor if (FMath::Abs(FloorHeight - ChildBottom) > OffsetTolerance) { continue; } auto LocalPos = FloorTransform.InverseTransformPosition(ChildTransform.GetLocation()); // Check that child anchor is within the bounds of the floor if (!FloorAnchor->IsPositionInBoundary(FVector2D(LocalPos.Y, LocalPos.Z))) { continue; } // We have a match ensureMsgf(!ChildAnchor->ParentAnchor, TEXT("This anchor already has a parent")); ChildAnchor->ParentAnchor = FloorAnchor; FloorAnchor->ChildAnchors.Push(ChildAnchor); } } // Find relationship between scene volumes for (const auto& ParentAnchor : AllAnchors) { if (!ParentAnchor) { continue; } const auto& ParentTransform = ParentAnchor->GetTransform(); const auto ParentXAxis = ParentTransform.GetUnitAxis(EAxis::X); const auto& ParentVolumeBounds = ParentAnchor->VolumeBounds; // Only interested in scene volumes, the assumption is that all scene volumes have X axis pointing downwards if (!ParentVolumeBounds.IsValid || !FVector::Coincident(ParentXAxis, FVector::DownVector)) { continue; } auto ParentTop = ParentTransform.GetLocation().Z - ParentVolumeBounds.Min.X; for (const auto& ChildAnchor : AllAnchors) { // Don't parent anchors to themselves if (ChildAnchor == ParentAnchor) { continue; } const auto& ChildTransform = ChildAnchor->GetTransform(); const auto ChildXAxis = ChildTransform.GetUnitAxis(EAxis::X); const auto& ChildVolumeBounds = ChildAnchor->VolumeBounds; // Only interested in scene volumes, the assumption is that all scene volumes have X axis pointing downwards if (!ChildVolumeBounds.IsValid || !FVector::Coincident(ChildXAxis, FVector::DownVector)) { continue; } auto ChildBottom = ChildTransform.GetLocation().Z - ChildVolumeBounds.Max.X; // Check that the two volumes are stack on top of each other if (FMath::Abs(ParentTop - ChildBottom) > OffsetTolerance) { continue; } // Check that at least one of the corners of the child volume is inside the bounds of the parent's volume // when projected onto the horizontal plane. This is to match the Scene Capture tool which requires the // user to defined stacked volumes by starting with one corner of the volume which must be on the parent's // volume. bool AnyCornerInside = false; for (int i = 0; i < 4; ++i) { // Get a different corner on each iteration of the loop (height is not important here) FVector ChildLocalPos(0.0f, i < 2 ? ChildVolumeBounds.Min.Y : ChildVolumeBounds.Max.Y, i % 2 == 0 ? ChildVolumeBounds.Min.Z : ChildVolumeBounds.Max.Z); auto LocalPos = ParentTransform.InverseTransformPosition(ChildTransform.TransformPosition(ChildLocalPos)); // Check that child anchor is within the bounds of the parent on the horizontal plane if (LocalPos.Y >= ParentVolumeBounds.Min.Y && LocalPos.Y <= ParentVolumeBounds.Max.Y && LocalPos.Z >= ParentVolumeBounds.Min.Z && LocalPos.Z <= ParentVolumeBounds.Max.Z) { AnyCornerInside = true; break; } } if (!AnyCornerInside) { continue; } // We have a match ensureMsgf(!ChildAnchor->ParentAnchor, TEXT("This anchor already has a parent")); ChildAnchor->ParentAnchor = ParentAnchor; ParentAnchor->ChildAnchors.Push(ChildAnchor); } } } void AMRUKRoom::ComputeSeats() { for (const auto& SeatAnchor : SeatAnchors) { if (SeatAnchor) { auto SeatsComponent = SeatAnchor->FindComponentByClass(); if (!SeatsComponent) { SeatsComponent = NewObject(SeatAnchor, TEXT("Seats")); SeatsComponent->RegisterComponent(); } SeatsComponent->CalculateSeatPoses(); } } } void AMRUKRoom::ComputeRoomEdges() { if (!FloorAnchor) { UE_LOG(LogMRUK, Warning, TEXT("Floor anchor not set, can not compute room edges")); return; } const auto& FloorBoundary = FloorAnchor->PlaneBoundary2D; const auto& FloorTransform = FloorAnchor->GetActorTransform(); #if UE_VERSION_OLDER_THAN(5, 5, 0) RoomEdges.SetNum(FloorBoundary.Num(), true); #else RoomEdges.SetNum(FloorBoundary.Num(), EAllowShrinking::Yes); #endif for (int i = 0; i < RoomEdges.Num(); ++i) { const auto& BoundaryPoint = FloorBoundary[i]; FVector Edge = FVector(0.0, BoundaryPoint.X, BoundaryPoint.Y); Edge = FloorTransform.TransformPosition(Edge); Edge.Z = 0.0; RoomEdges[i] = Edge; } } bool AMRUKRoom::IsPositionInRoom(const FVector& Position, bool TestVerticalBounds) { if (!FloorAnchor) { return false; } if (!(TestVerticalBounds ? RoomBounds.IsInside(Position) : RoomBounds.IsInsideXY(Position))) { return false; } const auto Transform = FloorAnchor->GetTransform(); const FVector LocalPos = Transform.InverseTransformPositionNoScale(Position); return FloorAnchor->IsPositionInBoundary(FVector2D(LocalPos.Y, LocalPos.Z)); } bool AMRUKRoom::GenerateRandomPositionInRoom(FVector& OutPosition, float MinDistanceToSurface, bool AvoidVolumes) { return GenerateRandomPositionInRoomFromStream(OutPosition, FRandomStream(NAME_None), MinDistanceToSurface, AvoidVolumes); } bool AMRUKRoom::GenerateRandomPositionInRoomFromStream(FVector& OutPosition, const FRandomStream& RandomStream, float MinDistanceToSurface, bool AvoidVolumes) { if (!FloorAnchor) { return false; } if (MinDistanceToSurface > RoomBounds.GetExtent().GetMin()) { // We can exit early here as we know it's not possible to generate a position in the room that satisfies // the MinDistanceToSurface requirement return false; } FVector Position; constexpr int MaxIterations = 1000; // Bail after MaxIteration tries to avoid infinite loop in case MinDistanceToSurface is too large // and we can't find a position which does not intersect with the walls and volumes for (int i = 0; i < MaxIterations; ++i) { if (MinDistanceToSurface > 0.0f) { // If MinDistanceToSurface is large then it can be more efficient to randomly generate points within // the shrunken bounds of the room Position.X = RandomStream.FRandRange(RoomBounds.Min.X + MinDistanceToSurface, RoomBounds.Max.X - MinDistanceToSurface); Position.Y = RandomStream.FRandRange(RoomBounds.Min.Y + MinDistanceToSurface, RoomBounds.Max.Y - MinDistanceToSurface); Position.Z = RandomStream.FRandRange(RoomBounds.Min.Z + MinDistanceToSurface, RoomBounds.Max.Z - MinDistanceToSurface); if (!IsPositionInRoom(Position)) { // Reject points that are outside the room continue; } FVector SurfacePos; double SurfaceDistance; FMRUKLabelFilter Filter; Filter.IncludedLabels = { FMRUKLabels::WallFace }; if (TryGetClosestSurfacePosition(Position, SurfacePos, SurfaceDistance, Filter, MinDistanceToSurface)) { // Reject points that are too close to the walls continue; } } else { Position = FloorAnchor->GenerateRandomPositionOnPlaneFromStream(RandomStream); Position = FloorAnchor->GetTransform().TransformPosition(Position); Position.Z = RandomStream.FRandRange(RoomBounds.Min.Z + MinDistanceToSurface, RoomBounds.Max.Z - MinDistanceToSurface); } if (AvoidVolumes && IsPositionInSceneVolume(Position, true, MinDistanceToSurface)) { // Reject points inside volumes if avoid volumes has been enabled continue; } OutPosition = Position; return true; } return false; } bool AMRUKRoom::GenerateRandomPositionOnSurface(EMRUKSpawnLocation SpawnLocation, float MinDistanceToEdge, FMRUKLabelFilter LabelFilter, FVector& OutPosition, FVector& OutNormal) { TArray Surfaces; float TotalUsableSurfaceArea = 0.0f; const float MinWidth = 2.0f * MinDistanceToEdge; OutPosition = FVector::ZeroVector; OutNormal = FVector::ForwardVector; for (auto& Anchor : AllAnchors) { if (!LabelFilter.PassesFilter(Anchor->SemanticClassifications)) { continue; } if (Anchor->PlaneBounds.bIsValid) { bool bSkipPlane = false; const bool bIsHorizontal = IsActorOrientationHorizontal(Anchor); // We skip the plane if it's vertical and if we are not spawning for vertical surfaces if (SpawnLocation == EMRUKSpawnLocation::VerticalSurfaces) { bSkipPlane = bIsHorizontal; } // We skip the plane if it's not horizontal and if it's the ceiling else if (SpawnLocation == EMRUKSpawnLocation::OnTopOfSurface) { bSkipPlane = !bIsHorizontal; if (Anchor->SemanticClassifications.Contains(FMRUKLabels::Ceiling)) bSkipPlane = true; } else if (SpawnLocation == EMRUKSpawnLocation::AnySurface) { bSkipPlane = false; } else if (SpawnLocation == EMRUKSpawnLocation::HangingDown) { bSkipPlane = !Anchor->SemanticClassifications.Contains(FMRUKLabels::Ceiling); } if (!bSkipPlane) { const auto Size = Anchor->PlaneBounds.GetSize(); if (Size.X > MinWidth && Size.Y > MinWidth) { const float UsableArea = (Size.X - MinWidth) * (Size.Y - MinWidth); TotalUsableSurfaceArea += UsableArea; Surfaces.Add({ Anchor, UsableArea, true, Anchor->PlaneBounds, EMRUKBoxSide{} }); } } } if (Anchor->VolumeBounds.IsValid) { for (int FaceIndex = 0; FaceIndex < 6; ++FaceIndex) { const EMRUKBoxSide BoxSide = static_cast(FaceIndex); // Only top when spawning on top of surfaces. The negative X face corresponds to the top surface. if (SpawnLocation == EMRUKSpawnLocation::OnTopOfSurface && BoxSide != EMRUKBoxSide::XNeg) continue; // Switch top and bottom faces. The vertical surfaces are the Y and Z faces. if (SpawnLocation == EMRUKSpawnLocation::VerticalSurfaces && FaceIndex < 2) continue; // Only bottom when spawning on hanging down. The positive X face corresponds to the top surface. if (SpawnLocation == EMRUKSpawnLocation::HangingDown && BoxSide != EMRUKBoxSide::XPos) continue; FBox2D Bound = GetBoundsFromBoxForSide(BoxSide, Anchor->VolumeBounds); if (const auto Size = Bound.GetSize(); Size.X > MinWidth && Size.Y > MinWidth) { const float UsableArea = (Size.X - MinWidth) * (Size.Y - MinWidth); TotalUsableSurfaceArea += UsableArea; Surfaces.Add({ Anchor, UsableArea, false, Bound, BoxSide }); } } } } if (Surfaces.Num() == 0) { return false; } constexpr int MaxIterations = 1000; for (int i = 0; i < MaxIterations; ++i) { // Pick a random surface weighted by surface area (surfaces with a larger // area have more chance of being chosen) float Rand = FMath::RandRange(0.f, TotalUsableSurfaceArea); int Index = 0; for (; Index < Surfaces.Num() - 1; ++Index) { Rand -= Surfaces[Index].UsableArea; if (Rand <= 0.0f) { break; } } auto& [Anchor, UsableArea, IsPlane, Bounds, BoxSide] = Surfaces[Index]; FVector2D Pos = FVector2D( FMath::RandRange(Bounds.Min.X + MinDistanceToEdge, Bounds.Max.X - MinDistanceToEdge), FMath::RandRange(Bounds.Min.Y + MinDistanceToEdge, Bounds.Max.Y - MinDistanceToEdge)); if (IsPlane && !Anchor->IsPositionInBoundary(Pos)) continue; if (IsPlane) { const FVector Pos3DPlane = Anchor->ActorToWorld().TransformPosition(FVector(0.f, Pos.X, Pos.Y)); OutPosition = Pos3DPlane; OutNormal = Anchor->ActorToWorld().TransformVector(FVector::BackwardVector); return true; } OutPosition = GetWorldPos(Pos, Anchor, BoxSide); OutNormal = Anchor->ActorToWorld().TransformVector(GetNormalBoxSide(BoxSide)); return true; } return false; } AMRUKAnchor* AMRUKRoom::Raycast(const FVector& Origin, const FVector& Direction, float MaxDist, const FMRUKLabelFilter& LabelFilter, FMRUKHit& OutHit) { AMRUKAnchor* HitComponent = nullptr; for (const auto& Anchor : AllAnchors) { if (!Anchor || !Anchor->PassesLabelFilter(LabelFilter)) { continue; } FMRUKHit HitResult; if (Anchor->Raycast(Origin, Direction, MaxDist, HitResult, LabelFilter.ComponentTypes)) { // Prevent further hits which are further away from being found MaxDist = HitResult.HitDistance; OutHit = HitResult; HitComponent = Anchor; } } return HitComponent; } bool AMRUKRoom::RaycastAll(const FVector& Origin, const FVector& Direction, float MaxDist, const FMRUKLabelFilter& LabelFilter, TArray& OutHits, TArray& OutAnchors) { bool HitAnything = false; for (const auto& Anchor : AllAnchors) { if (!Anchor || !Anchor->PassesLabelFilter(LabelFilter)) { continue; } if (Anchor->RaycastAll(Origin, Direction, MaxDist, OutHits, LabelFilter.ComponentTypes)) { HitAnything = true; // For each element in OutHits we want an equivalent entry in OutAnchors with the same index // which represents which anchor was hit. while (OutHits.Num() > OutAnchors.Num()) { OutAnchors.Push(Anchor); } } } return HitAnything; } void AMRUKRoom::ClearRoom() { RoomLayout = {}; AnchorUUID = {}; SpaceHandle = {}; RoomBounds.Init(); for (auto& Anchor : AllAnchors) { if (Anchor) { Anchor->Destroy(); } } AllAnchors.Empty(); WallAnchors.Empty(); SeatAnchors.Empty(); FloorAnchor = nullptr; CeilingAnchor = nullptr; KeyWallAnchor = nullptr; } bool AMRUKRoom::DoesRoomHave(const TArray& Labels) { if (Labels.IsEmpty()) { return true; } TArray RemainingLabels = Labels; for (const auto& Anchor : AllAnchors) { for (const auto& AnchorLabel : Anchor->SemanticClassifications) { const auto AnchorLabelIndex = RemainingLabels.Find(AnchorLabel); if (AnchorLabelIndex != INDEX_NONE) { RemainingLabels.RemoveAt(AnchorLabelIndex); if (RemainingLabels.IsEmpty()) { return true; } } } } return false; } AMRUKAnchor* AMRUKRoom::TryGetClosestSurfacePosition(const FVector& WorldPosition, FVector& OutSurfacePosition, double& OutSurfaceDistance, const FMRUKLabelFilter& LabelFilter, double MaxDistance) { if (MaxDistance <= 0.0) { MaxDistance = DBL_MAX; } OutSurfacePosition = FVector::Zero(); AMRUKAnchor* ClosestAnchor = nullptr; for (const auto& Anchor : AllAnchors) { if (!Anchor || !Anchor->PassesLabelFilter(LabelFilter)) { continue; } FVector SurfacePos{}; const auto Distance = Anchor->GetClosestSurfacePosition(WorldPosition, SurfacePos); if (Distance < MaxDistance) { MaxDistance = Distance; OutSurfacePosition = SurfacePos; ClosestAnchor = Anchor; } } OutSurfaceDistance = MaxDistance; return ClosestAnchor; } AMRUKAnchor* AMRUKRoom::IsPositionInSceneVolume(const FVector& WorldPosition, bool TestVerticalBounds, double Tolerance) { for (const auto& Anchor : AllAnchors) { if (!Anchor) { continue; } if (Anchor->IsPositionInVolumeBounds(WorldPosition, TestVerticalBounds, Tolerance)) { return Anchor; } } return nullptr; } AMRUKAnchor* AMRUKRoom::TryGetClosestSeatPose(const FVector& RayOrigin, const FVector& RayDirection, FTransform& OutSeatTransform) { FTransform ClosestPose{}; AMRUKAnchor* ClosestAnchor = nullptr; double ClosestDot = DBL_MIN; for (const auto& SeatAnchor : SeatAnchors) { if (!SeatAnchor) { continue; } const auto SeatsComponent = SeatAnchor->FindComponentByClass(); if (!SeatsComponent) { continue; } for (const auto& SeatPose : SeatsComponent->SeatPoses) { const auto VecToSeat = (SeatPose.GetLocation() - RayOrigin).GetSafeNormal(); const auto ThisDot = RayDirection.Dot(VecToSeat); if (ThisDot <= ClosestDot) { continue; } ClosestDot = ThisDot; ClosestPose = SeatPose; ClosestAnchor = SeatAnchor; } } OutSeatTransform = ClosestPose; return ClosestAnchor; } TArray AMRUKRoom::GetAnchorsByLabel(const FString& Label) const { TArray> Anchors; for (const auto& Anchor : AllAnchors) { if (Anchor && Anchor->HasLabel(Label)) { Anchors.Push(Anchor); } } return Anchors; } AMRUKAnchor* AMRUKRoom::GetFirstAnchorByLabel(const FString& Label) const { const auto Anchors = GetAnchorsByLabel(Label); if (Anchors.IsEmpty()) { return nullptr; } return Anchors[0]; } AMRUKAnchor* AMRUKRoom::GetBestPoseFromRaycast(const FVector& RayOrigin, const FVector& RayDirection, double MaxDist, const FMRUKLabelFilter& LabelFilter, FTransform& OutPose, EMRUKPositioningMethod PositioningMethod) { FTransform BestPose{}; FMRUKHit Hit{}; const auto HitAnchor = Raycast(RayOrigin, RayDirection, MaxDist, LabelFilter, Hit); if (!HitAnchor) { return nullptr; } FVector PosePosition = Hit.HitPosition; FVector PoseUp = FVector::UpVector; // By default, use the surface normal for pose forward // Caution: Make sure all the cases of this being "up" are caught below FVector PoseForward = Hit.HitNormal; constexpr double ParallelTolerance = 0.999; if (!HitAnchor->VolumeBounds.IsValid && Hit.HitNormal.Dot(PoseUp) >= ParallelTolerance) { // HitNormal and PoseUp are parallel. E.g. Walls and floors. PoseForward = FVector{ RayOrigin.X - Hit.HitPosition.X, RayOrigin.Y - Hit.HitPosition.Y, 0.0 }.GetSafeNormal(); } else if (HitAnchor->VolumeBounds.IsValid) { // This is a volume object, and the ray has hit the top surface if (Hit.HitNormal.Dot(FVector::UpVector) >= ParallelTolerance) { const auto& Transform = HitAnchor->GetActorTransform(); switch (PositioningMethod) { case EMRUKPositioningMethod::Center: { const auto HitLocalPos = Transform.InverseTransformPosition(Hit.HitPosition); double ShortestDistance = DBL_MAX; FVector Forward = FVector::ZeroVector; auto Dist = FMath::Abs(HitLocalPos.Y - HitAnchor->VolumeBounds.Min.Y); if (Dist < ShortestDistance) { ShortestDistance = Dist; Forward = -HitAnchor->GetActorRightVector(); } Dist = FMath::Abs(HitLocalPos.Y - HitAnchor->VolumeBounds.Max.Y); if (Dist < ShortestDistance) { ShortestDistance = Dist; Forward = HitAnchor->GetActorRightVector(); } Dist = FMath::Abs(HitLocalPos.Z - HitAnchor->VolumeBounds.Min.Z); if (Dist < ShortestDistance) { ShortestDistance = Dist; Forward = -HitAnchor->GetActorUpVector(); } Dist = FMath::Abs(HitLocalPos.Z - HitAnchor->VolumeBounds.Max.Z); if (Dist < ShortestDistance) { ShortestDistance = Dist; Forward = HitAnchor->GetActorUpVector(); } PoseForward = Forward; PosePosition = Transform.TransformPosition(FVector::ZeroVector); } break; case EMRUKPositioningMethod::Edge: { const auto HitLocalPos = Transform.InverseTransformPosition(Hit.HitPosition); double ShortestDistance = DBL_MAX; FVector PoseLocal = FVector::ZeroVector; auto Dist = FMath::Abs(HitLocalPos.Y - HitAnchor->VolumeBounds.Min.Y); if (Dist < ShortestDistance) { ShortestDistance = Dist; PoseForward = -HitAnchor->GetActorRightVector(); PoseLocal = { 0.0, HitAnchor->VolumeBounds.Min.Y, HitLocalPos.Z }; } Dist = FMath::Abs(HitLocalPos.Y - HitAnchor->VolumeBounds.Max.Y); if (Dist < ShortestDistance) { ShortestDistance = Dist; PoseForward = HitAnchor->GetActorRightVector(); PoseLocal = { 0.0, HitAnchor->VolumeBounds.Max.Y, HitLocalPos.Z }; } Dist = FMath::Abs(HitLocalPos.Z - HitAnchor->VolumeBounds.Min.Z); if (Dist < ShortestDistance) { ShortestDistance = Dist; PoseForward = -HitAnchor->GetActorUpVector(); PoseLocal = { 0.0, HitLocalPos.Y, HitAnchor->VolumeBounds.Min.Z }; } Dist = FMath::Abs(HitLocalPos.Z - HitAnchor->VolumeBounds.Max.Z); if (Dist < ShortestDistance) { ShortestDistance = Dist; PoseForward = HitAnchor->GetActorUpVector(); PoseLocal = { 0.0, HitLocalPos.Y, HitAnchor->VolumeBounds.Max.Z }; } PosePosition = Transform.TransformPosition(PoseLocal); } break; default: { const auto HitLocalPos = Transform.InverseTransformPosition(Hit.HitPosition); PosePosition = Transform.TransformPosition({ 0.0, HitLocalPos.Y, HitLocalPos.Z }); PoseForward = FVector{ RayOrigin.X - Hit.HitPosition.X, RayOrigin.Y - Hit.HitPosition.Y, 0.0 }.GetSafeNormal(); } break; } } } BestPose.SetLocation(PosePosition); BestPose.SetRotation(UKismetMathLibrary::MakeRotFromXZ(PoseForward, PoseUp).Quaternion()); OutPose = BestPose; return HitAnchor; } AMRUKAnchor* AMRUKRoom::GetKeyWall(double Tolerance) { if (KeyWallAnchor) { return KeyWallAnchor; } TArray> SortedWalls = WallAnchors; SortedWalls.Sort([](const AMRUKAnchor& a, const AMRUKAnchor& b) { return a.PlaneBounds.GetExtent().X < b.PlaneBounds.GetExtent().X; }); // Find the first one with no other walls behind it. // SortedWalls is sorted from shortest side to longest for (int i = SortedWalls.Num() - 1; i >= 0; --i) { const auto WallAnchor = SortedWalls[i]; bool NoPointsBehind = true; // Loop through the other corners, making sure none is behind the wall in question for (const auto& RoomEdge : RoomEdges) { auto VecToCorner = RoomEdge - WallAnchor->GetActorLocation(); // Due to anchor precision, we use a tolerance value. // For example, an adjacent wall edge may be just behind the wall, leading to a false result VecToCorner -= WallAnchor->GetActorForwardVector() * Tolerance; NoPointsBehind &= (-WallAnchor->GetActorForwardVector()).Dot(VecToCorner) >= 0.0; if (!NoPointsBehind) { break; } } if (NoPointsBehind) { KeyWallAnchor = WallAnchor; return WallAnchor; } } return nullptr; } AMRUKAnchor* AMRUKRoom::GetLargestSurface(const FString& Label) { AMRUKAnchor* LargestSurfaceAnchor = nullptr; double LargestSurfaceArea = 0.0; const auto LabelUpper = Label.ToUpper(); for (const auto& Anchor : AllAnchors) { if (!Anchor || !Anchor->HasLabel(Label)) { continue; } double ThisSurfaceArea = 0.0; if (Anchor->PlaneBounds.bIsValid) { ThisSurfaceArea = Anchor->PlaneBounds.GetArea(); } else if (Anchor->VolumeBounds.IsValid) { const auto VolumeSize = Anchor->VolumeBounds.GetSize(); ThisSurfaceArea = VolumeSize.Y * VolumeSize.Z; } if (ThisSurfaceArea > LargestSurfaceArea) { LargestSurfaceArea = ThisSurfaceArea; LargestSurfaceAnchor = Anchor; } } return LargestSurfaceAnchor; } void AMRUKRoom::AttachProceduralMeshToWalls(const TArray& CutHoleLabels, UMaterialInterface* ProceduralMaterial) { AttachProceduralMeshToWalls({}, CutHoleLabels, ProceduralMaterial); } void AMRUKRoom::ComputeWallMeshUVAdjustments(const TArray& WallTextureCoordinateModes, TArray& OutAnchorsWithPlaneUVs) { TArray> ConnectedWalls = ComputeConnectedWalls(); double Perimeter = 0.0; for (const auto& WallAnchor : ConnectedWalls) { Perimeter += WallAnchor->PlaneBounds.GetSize().X; } const float WorldToMeters = GetWorldSettings()->WorldToMeters; const double WallHeight = RoomBounds.GetSize().Z; const double SeamlessWorldToMeters = GetSeamlessFactor(Perimeter, WorldToMeters); double UOffset = 0.0; const TArray& TexCoordModes = WallTextureCoordinateModes.IsEmpty() ? TArray{ FMRUKTexCoordModes{} } : WallTextureCoordinateModes; for (const auto& WallAnchor : ConnectedWalls) { const double WallWidth = WallAnchor->PlaneBounds.GetSize().X; TArray PlaneUVAdjustments; for (const auto TexCoordMode : TexCoordModes) { float DenominatorX; float DenominatorY; // Determine the scaling in the V direction first, if this is set to maintain aspect // ratio we need to come back to it after U scaling has been determined. switch (TexCoordMode.V) { // Default to stretch in case maintain aspect ratio is set for both axes default: case EMRUKCoordModeV::Stretch: DenominatorY = WallHeight; break; case EMRUKCoordModeV::Metric: DenominatorY = WorldToMeters; break; } switch (TexCoordMode.U) { default: case EMRUKCoordModeU::Stretch: DenominatorX = Perimeter; break; case EMRUKCoordModeU::Metric: DenominatorX = WorldToMeters; break; case EMRUKCoordModeU::MetricSeamless: DenominatorX = SeamlessWorldToMeters; break; case EMRUKCoordModeU::MaintainAspectRatio: DenominatorX = DenominatorY; break; case EMRUKCoordModeU::MaintainAspectRatioSeamless: DenominatorX = GetSeamlessFactor(Perimeter, DenominatorY); break; } // Do another pass on V in case it has maintain aspect ratio set if (TexCoordMode.V == EMRUKCoordModeV::MaintainAspectRatio) { DenominatorY = DenominatorX; } const FVector2D Offset(UOffset / DenominatorX, 0); const FVector2D Scale(WallWidth / DenominatorX, WallHeight / DenominatorY); PlaneUVAdjustments.Push({ Offset, Scale }); } if (!WallAnchor->HasLabel(FMRUKLabels::InvisibleWallFace)) { OutAnchorsWithPlaneUVs.Push({ WallAnchor, PlaneUVAdjustments }); } UOffset += WallWidth; } } UProceduralMeshComponent* AMRUKRoom::GetOrCreateGlobalMeshProceduralMeshComponent(bool& OutExistedAlready) const { // Try to find the global mesh procedural mesh component if it already exists TArray ProcMeshComponents; GetComponents(ProcMeshComponents); for (const auto& ProcMeshComponent : ProcMeshComponents) { if (ProcMeshComponent->ComponentHasTag("GlobalMesh")) { OutExistedAlready = true; return ProcMeshComponent; } } // Create the procedural mesh component if it doesn't exist already const auto ProceduralMesh = NewObject(GlobalMeshAnchor, TEXT("GlobalMesh")); ProceduralMesh->ComponentTags.Add("GlobalMesh"); ProceduralMesh->RegisterComponent(); GlobalMeshAnchor->AddInstanceComponent(ProceduralMesh); OutExistedAlready = false; return ProceduralMesh; } void AMRUKRoom::SetupGlobalMeshProceduralMeshComponent(UProceduralMeshComponent& ProcMeshComponent, bool ExistedAlready, UMaterialInterface* Material) const { ProcMeshComponent.SetMaterial(0, Material); if (!ExistedAlready) { ProcMeshComponent.SetCollisionProfileName(TEXT("BlockAll")); GlobalMeshAnchor->AddOwnedComponent(GlobalMeshAnchor->GetRootComponent()); ProcMeshComponent.AttachToComponent(GlobalMeshAnchor->GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform); ProcMeshComponent.SetRelativeScale3D(FVector(GetWorld()->GetWorldSettings()->WorldToMeters)); } } bool AMRUKRoom::LoadGlobalMeshFromDevice(UMaterialInterface* Material) { if (!GlobalMeshAnchor) { UE_LOG(LogMRUK, Warning, TEXT("This room doesn't have a global mesh anchor")); return false; } bool ProcMeshExisted = false; UProceduralMeshComponent* ProcMesh = GetOrCreateGlobalMeshProceduralMeshComponent(ProcMeshExisted); if (!UMRUKBPLibrary::LoadGlobalMeshFromDevice(GlobalMeshAnchor->SpaceHandle, ProcMesh, true, GetWorld())) { UE_LOG(LogMRUK, Warning, TEXT("Could not load Global Mesh from device")); ProcMesh->DestroyComponent(); return false; } SetupGlobalMeshProceduralMeshComponent(*ProcMesh, ProcMeshExisted, Material); return true; } bool AMRUKRoom::LoadGlobalMeshFromJsonString(const FString& JsonString, UMaterialInterface* Material) { if (!GlobalMeshAnchor) { UE_LOG(LogMRUK, Warning, TEXT("A global mesh can only be loaded from a JSON string if it has a global mesh anchor. Please make sure you provide one.")); return false; } bool ProcMeshExisted = false; UProceduralMeshComponent* ProcMesh = GetOrCreateGlobalMeshProceduralMeshComponent(ProcMeshExisted); if (!UMRUKBPLibrary::LoadGlobalMeshFromJsonString(JsonString, AnchorUUID, ProcMesh, true)) { UE_LOG(LogMRUK, Warning, TEXT("Failed parsing global mesh from JSON string")); ProcMesh->DestroyComponent(); return false; } SetupGlobalMeshProceduralMeshComponent(*ProcMesh, ProcMeshExisted, Material); return true; } FVector AMRUKRoom::ComputeCentroid(double Z) { if (!FloorAnchor || !CeilingAnchor) { return FVector::ZeroVector; } Z = FMath::Clamp(Z, 0.0, 1.0); const FVector2D CentroidLS = UMRUKBPLibrary::ComputeCentroid(FloorAnchor->PlaneBoundary2D); const FVector CentroidWS = FloorAnchor->GetActorTransform().TransformPosition(FVector(0.0, CentroidLS.X, CentroidLS.Y)); const FVector Dir = (CeilingAnchor->GetActorLocation() - FloorAnchor->GetActorLocation()); const double Dist = Dir.Length(); return CentroidWS + Dir.GetSafeNormal() * FMath::Lerp(0.0, Dist, Z); } void AMRUKRoom::AttachProceduralMeshToWalls(const TArray& WallTextureCoordinateModes, const TArray& CutHoleLabels, UMaterialInterface* ProceduralMaterial) { TArray AnchorsWithPlaneUVs; ComputeWallMeshUVAdjustments(WallTextureCoordinateModes, AnchorsWithPlaneUVs); for (const auto& AnchorWithPlaneUVs : AnchorsWithPlaneUVs) { AnchorWithPlaneUVs.Anchor->AttachProceduralMesh(AnchorWithPlaneUVs.PlaneUVs, CutHoleLabels, true, ProceduralMaterial); } } TArray> AMRUKRoom::ComputeConnectedWalls() const { if (WallAnchors.IsEmpty()) { return {}; } TArray> ConnectedWalls; TArray> RemainingWalls = WallAnchors; for (int i = RemainingWalls.Num() - 1; i >= 0; --i) { if (RemainingWalls[i] == nullptr) { RemainingWalls.RemoveAt(i); } } ConnectedWalls.Reserve(WallAnchors.Num()); ConnectedWalls.Push(RemainingWalls.Last()); RemainingWalls.Pop(); while (!RemainingWalls.IsEmpty()) { const auto PrevWall = ConnectedWalls.Last(); FVector LocalMaxEdge(0, PrevWall->PlaneBounds.Max.X, 0); auto MaxEdge = PrevWall->GetTransform().TransformPosition(LocalMaxEdge); int ClosestIndex = 0; float ClosestDist = UE_MAX_FLT; for (int i = 0; i < RemainingWalls.Num(); i++) { const auto& WallAnchor = RemainingWalls[i]; FVector LocalMinEdge(0, WallAnchor->PlaneBounds.Min.X, 0); auto MinEdge = WallAnchor->GetTransform().TransformPosition(LocalMinEdge); const double Dist = FVector::Dist2D(MaxEdge, MinEdge); if (Dist < ClosestDist) { ClosestDist = Dist; ClosestIndex = i; } } ConnectedWalls.Push(RemainingWalls[ClosestIndex]); RemainingWalls.RemoveAt(ClosestIndex); } return ConnectedWalls; } TArray AMRUKRoom::SpawnInterior(const TMap& SpawnGroups, const TArray& CutHoleLabels, UMaterialInterface* ProceduralMaterial, bool ShouldFallbackToProcedural) { return SpawnInteriorFromStream(SpawnGroups, FRandomStream(NAME_None), CutHoleLabels, ProceduralMaterial, ShouldFallbackToProcedural); } TArray AMRUKRoom::SpawnInteriorFromStream(const TMap& SpawnGroups, const FRandomStream& RandomStream, const TArray& CutHoleLabels, UMaterialInterface* ProceduralMaterial, bool GlobalShouldFallbackToProcedural) { TArray InteriorActors; const auto ShouldFallbackToProcedural = [GlobalShouldFallbackToProcedural](const FMRUKSpawnGroup* Anchor) -> bool { check(Anchor); switch (Anchor->FallbackToProcedural) { case EMRUKFallbackToProceduralOverwrite::Default: return GlobalShouldFallbackToProcedural; case EMRUKFallbackToProceduralOverwrite::Fallback: return true; case EMRUKFallbackToProceduralOverwrite::NoFallback: return false; } return false; }; const float WorldToMeters = GetWorldSettings()->WorldToMeters; const auto WallFace = SpawnGroups.Find(FMRUKLabels::WallFace); if (!WallFace || (WallFace->Actors.IsEmpty() && ShouldFallbackToProcedural(WallFace))) { // If no wall mesh is given we want to spawn the walls procedural to make seamless UVs AttachProceduralMeshToWalls(CutHoleLabels, ProceduralMaterial); } const auto Floor = SpawnGroups.Find(FMRUKLabels::Floor); if (FloorAnchor && (!Floor || (Floor->Actors.IsEmpty() && ShouldFallbackToProcedural(Floor)))) { // Use metric scaling to match walls const FVector2D Scale = FloorAnchor->PlaneBounds.GetSize() / WorldToMeters; FloorAnchor->AttachProceduralMesh({ { FVector2D::ZeroVector, Scale } }, CutHoleLabels, true, ProceduralMaterial); } const auto Ceiling = SpawnGroups.Find(FMRUKLabels::Ceiling); if (CeilingAnchor && (!Ceiling || (Ceiling->Actors.IsEmpty() && ShouldFallbackToProcedural(Ceiling)))) { // Use metric scaling to match walls const FVector2D Scale = CeilingAnchor->PlaneBounds.GetSize() / WorldToMeters; CeilingAnchor->AttachProceduralMesh({ { FVector2D::ZeroVector, Scale } }, CutHoleLabels, true, ProceduralMaterial); } const auto Subsystem = GetGameInstance()->GetSubsystem(); for (const auto& Anchor : AllAnchors) { if (!Anchor) { continue; } if (Anchor->SemanticClassifications.IsEmpty()) { Anchor->AttachProceduralMesh(); continue; } bool SpawnProceduralMesh = true; for (const auto& SemanticClassification : Anchor->SemanticClassifications) { if (SemanticClassification == FMRUKLabels::WallFace && Anchor->SemanticClassifications.Contains(FMRUKLabels::InvisibleWallFace)) { // Treat anchors with WALL_FACE and INVISIBLE_WALL_FACE as anchors that only have INVISIBLE_WALL_FACE continue; } const auto SpawnGroup = SpawnGroups.Find(SemanticClassification); if (!SpawnGroup) { continue; } if (SpawnGroup->Actors.IsEmpty()) { if (!ShouldFallbackToProcedural(SpawnGroup)) { SpawnProceduralMesh = false; } continue; } SpawnProceduralMesh = false; int Index = 0; if (SpawnGroup->Actors.Num() > 1) { if (SpawnGroup->SelectionMode == EMRUKSpawnerSelectionMode::Random) { Index = RandomStream.RandRange(0, SpawnGroup->Actors.Num() - 1); } else if (SpawnGroup->SelectionMode == EMRUKSpawnerSelectionMode::ClosestSize) { if (Anchor->VolumeBounds.IsValid) { const double AnchorSize = FMath::Pow(Anchor->VolumeBounds.GetVolume(), 1.0 / 3.0); double ClosestSizeDifference = UE_BIG_NUMBER; for (int i = 0; i < SpawnGroup->Actors.Num(); ++i) { const auto& SpawnActor = SpawnGroup->Actors[i]; auto Bounds = Subsystem->GetActorClassBounds(SpawnActor.Actor); if (Bounds.IsValid) { const double SpawnActorSize = FMath::Pow(Bounds.GetVolume(), 1.0 / 3.0); const double SizeDifference = FMath::Abs(AnchorSize - SpawnActorSize); if (SizeDifference < ClosestSizeDifference) { ClosestSizeDifference = SizeDifference; Index = i; } } } } } } const auto& SpawnActor = SpawnGroup->Actors[Index]; if (SpawnActor.Actor) { auto InteriorActor = Anchor->SpawnInterior(SpawnActor.Actor, SpawnActor.MatchAspectRatio, SpawnActor.CalculateFacingDirection, SpawnActor.ScalingMode); InteriorActors.Push(InteriorActor); } else { UE_LOG(LogMRUK, Error, TEXT("Actor is nullptr for label %s."), *SemanticClassification); } break; } if (SpawnProceduralMesh) { Anchor->AttachProceduralMesh(CutHoleLabels, true, ProceduralMaterial); } } return InteriorActors; } bool AMRUKRoom::IsWallAnchor(AMRUKAnchor* Anchor) const { return WallAnchors.Contains(Anchor); } void AMRUKRoom::UpdateWorldLock(APawn* Pawn, const FVector& HeadWorldPosition) const { const auto& Anchor = FloorAnchor; if (!Anchor) { return; } FTransform AnchorTransform; FOculusXRAnchorLocationFlags AnchorFlags{}; if (Anchor->SpaceHandle && UOculusXRAnchorBPFunctionLibrary::TryGetAnchorTransformByHandle(Anchor->SpaceHandle, AnchorTransform, AnchorFlags, EOculusXRAnchorSpace::Tracking)) { const FTransform& Transform = Anchor->GetActorTransform(); const FTransform Adjustment = AnchorTransform.Inverse() * Transform; // Only use the Yaw component of the rotation, we don't want to introduce any errors with // pitch or roll. const double Yaw = Adjustment.Rotator().Yaw; Pawn->SetActorLocationAndRotation(Adjustment.GetLocation(), FRotator(0.0, Yaw, 0.0)); } } #undef LOCTEXT_NAMESPACE