1523 lines
45 KiB
C++
1523 lines
45 KiB
C++
// 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<USceneComponent>(TEXT("SceneComponent"));
|
|
}
|
|
|
|
void AMRUKRoom::EndPlay(EEndPlayReason::Type Reason)
|
|
{
|
|
for (const auto& Anchor : AllAnchors)
|
|
{
|
|
OnAnchorRemoved.Broadcast(Anchor);
|
|
Anchor->Destroy();
|
|
}
|
|
|
|
GetGameInstance()->GetSubsystem<UMRUKSubsystem>()->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<TObjectPtr<AMRUKAnchor>> AnchorsCreated;
|
|
TArray<TObjectPtr<AMRUKAnchor>> AnchorsUpdated;
|
|
|
|
for (const auto& AnchorData : RoomData->AnchorsData)
|
|
{
|
|
const auto AnchorFound = AnchorsToRemove.FindByPredicate([AnchorData](TObjectPtr<AMRUKAnchor> 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<FJsonObject> AMRUKRoom::JsonSerialize()
|
|
{
|
|
TSharedRef<FJsonObject> JsonObject = MakeShareable(new FJsonObject);
|
|
TArray<TSharedPtr<FJsonValue>> 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<UMRUKAnchorData> 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<AMRUKAnchor>(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<UMRUKSeatsComponent>();
|
|
if (!SeatsComponent)
|
|
{
|
|
SeatsComponent = NewObject<UMRUKSeatsComponent>(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<Surface> 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<EMRUKBoxSide>(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<FMRUKHit>& OutHits, TArray<AMRUKAnchor*>& 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<FString>& Labels)
|
|
{
|
|
if (Labels.IsEmpty())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
TArray<FString> 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<UMRUKSeatsComponent>();
|
|
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<AMRUKAnchor*> AMRUKRoom::GetAnchorsByLabel(const FString& Label) const
|
|
{
|
|
TArray<TObjectPtr<AMRUKAnchor>> 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<TObjectPtr<AMRUKAnchor>> 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<FString>& CutHoleLabels, UMaterialInterface* ProceduralMaterial)
|
|
{
|
|
AttachProceduralMeshToWalls({}, CutHoleLabels, ProceduralMaterial);
|
|
}
|
|
|
|
void AMRUKRoom::ComputeWallMeshUVAdjustments(const TArray<FMRUKTexCoordModes>& WallTextureCoordinateModes, TArray<FMRUKAnchorWithPlaneUVs>& OutAnchorsWithPlaneUVs)
|
|
{
|
|
TArray<TObjectPtr<AMRUKAnchor>> 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<FMRUKTexCoordModes>& TexCoordModes = WallTextureCoordinateModes.IsEmpty() ? TArray<FMRUKTexCoordModes>{ FMRUKTexCoordModes{} } : WallTextureCoordinateModes;
|
|
for (const auto& WallAnchor : ConnectedWalls)
|
|
{
|
|
const double WallWidth = WallAnchor->PlaneBounds.GetSize().X;
|
|
TArray<FMRUKPlaneUV> 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<UProceduralMeshComponent*> ProcMeshComponents;
|
|
GetComponents<UProceduralMeshComponent>(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<UProceduralMeshComponent>(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<FMRUKTexCoordModes>& WallTextureCoordinateModes, const TArray<FString>& CutHoleLabels, UMaterialInterface* ProceduralMaterial)
|
|
{
|
|
TArray<FMRUKAnchorWithPlaneUVs> AnchorsWithPlaneUVs;
|
|
ComputeWallMeshUVAdjustments(WallTextureCoordinateModes, AnchorsWithPlaneUVs);
|
|
for (const auto& AnchorWithPlaneUVs : AnchorsWithPlaneUVs)
|
|
{
|
|
AnchorWithPlaneUVs.Anchor->AttachProceduralMesh(AnchorWithPlaneUVs.PlaneUVs, CutHoleLabels, true, ProceduralMaterial);
|
|
}
|
|
}
|
|
|
|
TArray<TObjectPtr<AMRUKAnchor>> AMRUKRoom::ComputeConnectedWalls() const
|
|
{
|
|
if (WallAnchors.IsEmpty())
|
|
{
|
|
return {};
|
|
}
|
|
|
|
TArray<TObjectPtr<AMRUKAnchor>> ConnectedWalls;
|
|
TArray<TObjectPtr<AMRUKAnchor>> 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<AActor*> AMRUKRoom::SpawnInterior(const TMap<FString, FMRUKSpawnGroup>& SpawnGroups, const TArray<FString>& CutHoleLabels, UMaterialInterface* ProceduralMaterial, bool ShouldFallbackToProcedural)
|
|
{
|
|
return SpawnInteriorFromStream(SpawnGroups, FRandomStream(NAME_None), CutHoleLabels, ProceduralMaterial, ShouldFallbackToProcedural);
|
|
}
|
|
|
|
TArray<AActor*> AMRUKRoom::SpawnInteriorFromStream(const TMap<FString, FMRUKSpawnGroup>& SpawnGroups, const FRandomStream& RandomStream, const TArray<FString>& CutHoleLabels, UMaterialInterface* ProceduralMaterial, bool GlobalShouldFallbackToProcedural)
|
|
{
|
|
TArray<AActor*> 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<UMRUKSubsystem>();
|
|
|
|
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
|