Android build settings + metaxr

This commit is contained in:
2025-05-14 14:00:02 +03:00
parent 6a2bb7475e
commit d5aa21f55c
594 changed files with 200530 additions and 2 deletions

View File

@@ -0,0 +1,386 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// @generated by `buck2 run //arvr/projects/mixedreality/libraries/mrutilitykit:build_and_deploy unreal`
#pragma once
#include <float.h>
#include <stddef.h>
#include <stdint.h>
#include "CoreTypes.h"
#include "Math/UnrealMath.h"
#include "Math/MathFwd.h"
struct MRUKShared
{
static MRUKShared* GetInstance() { return Instance; }
static void LoadMRUKSharedLibrary();
static void FreeMRUKSharedLibrary();
struct MrukSceneAnchor;
struct MrukRoomAnchor;
struct MrukUuid;
enum MrukSceneModel
{
MRUK_SCENE_MODEL_V2_FALLBACK_V1,
MRUK_SCENE_MODEL_V1,
MRUK_SCENE_MODEL_V2,
};
enum MrukLogLevel
{
MRUK_LOG_LEVEL_DEBUG,
MRUK_LOG_LEVEL_INFO,
MRUK_LOG_LEVEL_WARN,
MRUK_LOG_LEVEL_ERROR,
};
enum MrukResult
{
MRUK_SUCCESS,
MRUK_ERROR_INVALID_ARGS,
MRUK_ERROR_UNKNOWN,
MRUK_ERROR_INTERNAL,
MRUK_ERROR_DISCOVERY_ONGOING,
MRUK_ERROR_INVALID_JSON,
MRUK_ERROR_NO_ROOMS_FOUND,
MRUK_ERROR_INSUFFICIENT_RESOURCES,
MRUK_ERROR_STORAGE_AT_CAPACITY,
MRUK_ERROR_INSUFFICIENT_VIEW,
MRUK_ERROR_PERMISSION_INSUFFICIENT,
MRUK_ERROR_RATE_LIMITED,
MRUK_ERROR_TOO_DARK,
MRUK_ERROR_TOO_BRIGHT,
};
enum MrukSurfaceType
{
MRUK_SURFACE_TYPE_NONE,
MRUK_SURFACE_TYPE_PLANE,
MRUK_SURFACE_TYPE_VOLUME,
MRUK_SURFACE_TYPE_MESH,
MRUK_SURFACE_TYPE_ALL,
};
typedef void (*LogPrinter)(MrukLogLevel logLevel, const char* message);
typedef void (*MrukOnPreRoomAnchorAdded)(const MrukRoomAnchor* roomAnchor, void* userContext);
typedef void (*MrukOnRoomAnchorAdded)(const MrukRoomAnchor* roomAnchor, void* userContext);
typedef void (*MrukOnRoomAnchorUpdated)(const MrukRoomAnchor* roomAnchor, const MrukUuid* oldRoomAnchorUuid, void* userContext);
typedef void (*MrukOnRoomAnchorRemoved)(const MrukRoomAnchor* roomAnchor, void* userContext);
typedef void (*MrukOnSceneAnchorAdded)(const MrukSceneAnchor* sceneAnchor, void* userContext);
typedef void (*MrukOnSceneAnchorUpdated)(const MrukSceneAnchor* sceneAnchor, void* userContext);
typedef void (*MrukOnSceneAnchorRemoved)(const MrukSceneAnchor* sceneAnchor, void* userContext);
typedef void (*MrukOnDiscoveryFinished)(MrukResult result, void* userContext);
struct MrukQuatf
{
float x;
float y;
float z;
float w;
};
struct MrukPosef
{
FVector3f position;
MrukQuatf rotation;
};
struct MrukPolygon2f
{
const FVector2f* points;
uint32_t numPoints;
};
struct MrukMesh2f
{
FVector2f* vertices;
uint32_t numVertices;
uint32_t* indices;
uint32_t numIndices;
};
struct MrukMesh3f
{
FVector3f* vertices;
uint32_t numVertices;
uint32_t* indices;
uint32_t numIndices;
};
struct MrukUuid
{
uint64_t part1;
uint64_t part2;
};
struct MrukVolume
{
FVector3f min;
FVector3f max;
};
struct MrukPlane
{
float x;
float y;
float width;
float height;
};
struct MrukSceneAnchor
{
uint64_t space;
MrukUuid uuid;
MrukUuid roomUuid;
MrukPosef pose;
MrukVolume volume;
MrukPlane plane;
char** semanticLabels;
FVector2f* planeBoundary;
uint32_t* globalMeshIndices;
FVector3f* globalMeshPositions;
uint32_t semanticLabelsCount;
uint32_t planeBoundaryCount;
uint32_t globalMeshIndicesCount;
uint32_t globalMeshPositionsCount;
bool hasVolume;
bool hasPlane;
};
struct MrukRoomAnchor
{
uint64_t space;
MrukUuid uuid;
};
struct MrukEventListener
{
MrukOnPreRoomAnchorAdded onPreRoomAnchorAdded;
MrukOnRoomAnchorAdded onRoomAnchorAdded;
MrukOnRoomAnchorUpdated onRoomAnchorUpdated;
MrukOnRoomAnchorRemoved onRoomAnchorRemoved;
MrukOnSceneAnchorAdded onSceneAnchorAdded;
MrukOnSceneAnchorUpdated onSceneAnchorUpdated;
MrukOnSceneAnchorRemoved onSceneAnchorRemoved;
MrukOnDiscoveryFinished onDiscoveryFinished;
void* userContext;
};
struct MrukHit
{
MrukUuid roomAnchorUuid;
MrukUuid sceneAnchorUuid;
float hitDistance;
FVector3f hitPosition;
FVector3f hitNormal;
};
void (*SetLogPrinter)(LogPrinter printer);
/**
* Create the global anchor store with a external OpenXR instance and session.
* This should only be called once on application startup.
* Make sure to hook up the ContextOnOpenXrEvent() function as well.
* If the context is not needed anymore it should be destroyed with ContextDestroy() to free
* resources.
*/
MrukResult (*AnchorStoreCreate)(uint64_t xrInstance, uint64_t xrSession, void* xrInstanceProcAddrFunc, uint64_t baseSpace);
MrukResult (*AnchorStoreCreateWithoutOpenXr)();
/**
* Destroy the global anchor store
* This should only be called once on application shutdown.
*/
void (*AnchorStoreDestroy)();
/**
* If the base space changes after initialization, this function should be called to update the
* base space.
*/
void (*AnchorStoreSetBaseSpace)(uint64_t baseSpace);
/**
* Start anchor discovery in the anchor store
*/
MrukResult (*AnchorStoreStartDiscovery)(bool shouldRemoveMissingRooms, MrukSceneModel sceneModel);
/**
* Load the scene from a json string
*/
MrukResult (*AnchorStoreLoadSceneFromJson)(const char* jsonString, bool shouldRemoveMissingRooms, MrukSceneModel sceneModel);
/**
* Save the scene to a json string.
* @return The serialized JSON string. This string must be freed with FreeAnchorStoreJson after use!
*/
const char* (*AnchorStoreSaveSceneToJson)();
/**
* Free the json string returned by AnchorStoreSaveSceneToJson.
* @param[in] jsonString The JSON string to free.
*/
void (*AnchorStoreFreeJson)(const char* jsonString);
/**
* Clear and remove all rooms in the anchor store.
*/
void (*AnchorStoreClearRooms)();
/**
* Clear and remove the room that matches the given uuid.
*/
void (*AnchorStoreClearRoom)(MrukUuid roomUuid);
/**
* Allows to forward OpenXR events from the engine into the shared library
*/
void (*AnchorStoreOnOpenXrEvent)(void* baseEventHeader);
/**
* Needs to be called every tick by the engine.
*/
void (*AnchorStoreTick)(uint64_t nextPredictedDisplayTime);
void (*AnchorStoreRegisterEventListener)(MrukEventListener listener);
/**
* Cast a ray against all anchors in the room and return the first hit.
*/
bool (*AnchorStoreRaycastRoom)(MrukUuid roomUuid, FVector3f origin, FVector3f direction, float maxDistance, uint32_t surfaceType, MrukHit* outHit);
/**
* Cast a ray against all anchors in the room and return all hits along the ray.
*/
bool (*AnchorStoreRaycastRoomAll)(MrukUuid roomUuid, FVector3f origin, FVector3f direction, float maxDistance, uint32_t surfaceType, MrukHit* outHits, uint32_t* outHitsCount);
bool (*AnchorStoreIsDiscoveryRunning)();
/**
* Add two vectors together. This is implemented as a test to ensure the native shared
* library is working correctly.
*
* @param[in] a The first vector.
* @param[in] b The second vector.
* @return The sum of the two vectors.
*/
FVector3f (*AddVectors)(FVector3f a, FVector3f b);
/**
* Triangulate a polygon with holes, any winding order works. The first polyline defines the main
* polygon. Following polylines define holes. This function will allocate memory for the vertices
* and indices. You *MUST* call FreeMesh() when you are done with it or you will leak memory.
*
* @param[in] polygons The polygon to triangulate.
* @param[in] numPolygons The number of polygons in the array.
* @return mesh The triangulated mesh.
*/
MrukMesh2f (*TriangulatePolygon)(const MrukPolygon2f* polygons, uint32_t numPolygons);
/**
* Free the memory allocated by TriangulatePolygon.
*
* @param[in] mesh The mesh to free.
*/
void (*FreeMesh)(MrukMesh2f* mesh);
/**
* Compute the mesh segmentation for a given set of vertices, indices and segmentation points.
* You *MUST* call FreeMeshSegmentation() on the meshSegments array when you are done with it or you
* will leak memory.
*
* @param[in] vertices The mesh vertices.
* @param[in] numVertices The number of vertices in the mesh.
* @param[in] indices The mesh indices.
* @param[in] numIndices The number of indices in the mesh.
* @param[in] segmentationPoints The points that should be used to calculate the segments.
* @param[in] numSegmentationPoints The number of segmentation points.
* @param[in] reservedMin The minimum bounding box for the reserved segment.
* @param[in] reservedMax The maximum bounding box for the reserved segment.
* @param[out] meshSegments The resulting segments.
* @param[out] numSegments The number of segments in the resulting array.
* @param[out] reservedSegment The segment that is inside the reserved bounding box.
*/
MrukResult (*ComputeMeshSegmentation)(const FVector3f* vertices, uint32_t numVertices, const uint32_t* indices, uint32_t numIndices, const FVector3f* segmentationPoints, uint32_t numSegmentationPoints, FVector3f reservedMin, FVector3f reservedMax, MrukMesh3f** meshSegments, uint32_t* numSegments, MrukMesh3f* reservedSegment);
/**
* Free the memory allocated by ComputeMeshSegmentation.
*
* @param[in] meshSegments The array of segments to free.
* @param[in] numSegments The number of segments in the array.
* @param[in] reservedSegment The reserved segment to free.
*/
void (*FreeMeshSegmentation)(const MrukMesh3f* meshSegments, uint32_t numSegments, MrukMesh3f* reservedSegment);
private:
void LoadNativeFunctions()
{
SetLogPrinter = reinterpret_cast<decltype(SetLogPrinter)>(LoadFunction(TEXT("SetLogPrinter")));
AnchorStoreCreate = reinterpret_cast<decltype(AnchorStoreCreate)>(LoadFunction(TEXT("AnchorStoreCreate")));
AnchorStoreCreateWithoutOpenXr = reinterpret_cast<decltype(AnchorStoreCreateWithoutOpenXr)>(LoadFunction(TEXT("AnchorStoreCreateWithoutOpenXr")));
AnchorStoreDestroy = reinterpret_cast<decltype(AnchorStoreDestroy)>(LoadFunction(TEXT("AnchorStoreDestroy")));
AnchorStoreSetBaseSpace = reinterpret_cast<decltype(AnchorStoreSetBaseSpace)>(LoadFunction(TEXT("AnchorStoreSetBaseSpace")));
AnchorStoreStartDiscovery = reinterpret_cast<decltype(AnchorStoreStartDiscovery)>(LoadFunction(TEXT("AnchorStoreStartDiscovery")));
AnchorStoreLoadSceneFromJson = reinterpret_cast<decltype(AnchorStoreLoadSceneFromJson)>(LoadFunction(TEXT("AnchorStoreLoadSceneFromJson")));
AnchorStoreSaveSceneToJson = reinterpret_cast<decltype(AnchorStoreSaveSceneToJson)>(LoadFunction(TEXT("AnchorStoreSaveSceneToJson")));
AnchorStoreFreeJson = reinterpret_cast<decltype(AnchorStoreFreeJson)>(LoadFunction(TEXT("AnchorStoreFreeJson")));
AnchorStoreClearRooms = reinterpret_cast<decltype(AnchorStoreClearRooms)>(LoadFunction(TEXT("AnchorStoreClearRooms")));
AnchorStoreClearRoom = reinterpret_cast<decltype(AnchorStoreClearRoom)>(LoadFunction(TEXT("AnchorStoreClearRoom")));
AnchorStoreOnOpenXrEvent = reinterpret_cast<decltype(AnchorStoreOnOpenXrEvent)>(LoadFunction(TEXT("AnchorStoreOnOpenXrEvent")));
AnchorStoreTick = reinterpret_cast<decltype(AnchorStoreTick)>(LoadFunction(TEXT("AnchorStoreTick")));
AnchorStoreRegisterEventListener = reinterpret_cast<decltype(AnchorStoreRegisterEventListener)>(LoadFunction(TEXT("AnchorStoreRegisterEventListener")));
AnchorStoreRaycastRoom = reinterpret_cast<decltype(AnchorStoreRaycastRoom)>(LoadFunction(TEXT("AnchorStoreRaycastRoom")));
AnchorStoreRaycastRoomAll = reinterpret_cast<decltype(AnchorStoreRaycastRoomAll)>(LoadFunction(TEXT("AnchorStoreRaycastRoomAll")));
AnchorStoreIsDiscoveryRunning = reinterpret_cast<decltype(AnchorStoreIsDiscoveryRunning)>(LoadFunction(TEXT("AnchorStoreIsDiscoveryRunning")));
AddVectors = reinterpret_cast<decltype(AddVectors)>(LoadFunction(TEXT("AddVectors")));
TriangulatePolygon = reinterpret_cast<decltype(TriangulatePolygon)>(LoadFunction(TEXT("TriangulatePolygon")));
FreeMesh = reinterpret_cast<decltype(FreeMesh)>(LoadFunction(TEXT("FreeMesh")));
ComputeMeshSegmentation = reinterpret_cast<decltype(ComputeMeshSegmentation)>(LoadFunction(TEXT("ComputeMeshSegmentation")));
FreeMeshSegmentation = reinterpret_cast<decltype(FreeMeshSegmentation)>(LoadFunction(TEXT("FreeMeshSegmentation")));
}
void UnloadNativeFunctions()
{
SetLogPrinter = nullptr;
AnchorStoreCreate = nullptr;
AnchorStoreCreateWithoutOpenXr = nullptr;
AnchorStoreDestroy = nullptr;
AnchorStoreSetBaseSpace = nullptr;
AnchorStoreStartDiscovery = nullptr;
AnchorStoreLoadSceneFromJson = nullptr;
AnchorStoreSaveSceneToJson = nullptr;
AnchorStoreFreeJson = nullptr;
AnchorStoreClearRooms = nullptr;
AnchorStoreClearRoom = nullptr;
AnchorStoreOnOpenXrEvent = nullptr;
AnchorStoreTick = nullptr;
AnchorStoreRegisterEventListener = nullptr;
AnchorStoreRaycastRoom = nullptr;
AnchorStoreRaycastRoomAll = nullptr;
AnchorStoreIsDiscoveryRunning = nullptr;
AddVectors = nullptr;
TriangulatePolygon = nullptr;
FreeMesh = nullptr;
ComputeMeshSegmentation = nullptr;
FreeMeshSegmentation = nullptr;
}
void* LoadFunction(const TCHAR* ProcName);
static MRUKShared* Instance;
void* MRUKSharedHandle;
MRUKShared(void* handle);
~MRUKShared();
};

View File

@@ -0,0 +1,84 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "MRUtilityKit.h"
#include "Interfaces/IPluginManager.h"
#include "Misc/Paths.h"
#include "ShaderCore.h"
#if WITH_EDITOR
#include "ISettingsModule.h"
#endif // WITH_EDITOR
#define LOCTEXT_NAMESPACE "FMRUKModule"
DEFINE_LOG_CATEGORY(LogMRUK);
const FString FMRUKLabels::Floor("FLOOR");
const FString FMRUKLabels::WallFace("WALL_FACE");
const FString FMRUKLabels::InvisibleWallFace("INVISIBLE_WALL_FACE");
const FString FMRUKLabels::Ceiling("CEILING");
const FString FMRUKLabels::DoorFrame("DOOR_FRAME");
const FString FMRUKLabels::WindowFrame("WINDOW_FRAME");
const FString FMRUKLabels::Couch("COUCH");
const FString FMRUKLabels::Table("TABLE");
const FString FMRUKLabels::Screen("SCREEN");
const FString FMRUKLabels::Bed("BED");
const FString FMRUKLabels::Lamp("LAMP");
const FString FMRUKLabels::Plant("PLANT");
const FString FMRUKLabels::Storage("STORAGE");
const FString FMRUKLabels::WallArt("WALL_ART");
const FString FMRUKLabels::GlobalMesh("GLOBAL_MESH");
const FString FMRUKLabels::Other("OTHER");
bool FMRUKLabelFilter::PassesFilter(const TArray<FString>& Labels) const
{
for (const auto& ExcludedLabel : ExcludedLabels)
{
if (Labels.Contains(ExcludedLabel))
{
return false;
}
}
for (const auto& IncludedLabel : IncludedLabels)
{
if (Labels.Contains(IncludedLabel))
{
return true;
}
}
return IncludedLabels.IsEmpty();
}
UMRUKSettings::UMRUKSettings(const FObjectInitializer& obj)
{
}
void FMRUKModule::StartupModule()
{
// This code will execute after your module is loaded into memory; the exact timing is specified
// in the .uplugin file per-module
#if WITH_EDITOR
if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings"))
{
SettingsModule->RegisterSettings("Project", "Plugins", "MRUtilityKit",
LOCTEXT("RuntimeSettingsName", "Mixed Reality Utility Kit"), LOCTEXT("RuntimeSettingsDescription", "Configure the Mixed Reality Utility plugin"),
GetMutableDefault<UMRUKSettings>());
}
#endif // WITH_EDITOR
}
void FMRUKModule::ShutdownModule()
{
// This function may be called during shutdown to clean up your module. For modules that support
// dynamic reloading, we call this function before unloading the module.
#if WITH_EDITOR
if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings"))
{
SettingsModule->UnregisterSettings("Project", "Plugins", "MRUtilityKit");
}
#endif // WITH_EDITOR
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FMRUKModule, MRUtilityKit)

View File

@@ -0,0 +1,889 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "MRUtilityKitAnchor.h"
#include "MRUtilityKit.h"
#include "MRUtilityKitBPLibrary.h"
#include "MRUtilityKitGeometry.h"
#include "MRUtilityKitSubsystem.h"
#include "MRUtilityKitSerializationHelpers.h"
#include "MRUtilityKitSeatsComponent.h"
#include "MRUtilityKitRoom.h"
#include "OculusXRAnchorTypes.h"
#include "Engine/World.h"
#define LOCTEXT_NAMESPACE "MRUKAnchor"
// #pragma optimize("", off)
AMRUKAnchor::AMRUKAnchor(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
// Create a scene component as root so we can attach spawned actors to it
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("SceneComponent"));
}
bool AMRUKAnchor::LoadFromData(UMRUKAnchorData* AnchorData)
{
check(AnchorData);
bool Changed = false;
if (const auto Seat = GetComponentByClass<UMRUKSeatsComponent>(); Seat && !HasLabel(FMRUKLabels::Couch))
{
Seat->UnregisterComponent();
Seat->DestroyComponent();
Changed = true;
}
AnchorUUID = AnchorData->SpaceQuery.UUID;
SpaceHandle = AnchorData->SpaceQuery.Space;
SetActorTransform(AnchorData->Transform, false, nullptr, ETeleportType::ResetPhysics);
const auto NewSemanticClassifications = AnchorData->SemanticClassifications;
if (NewSemanticClassifications != SemanticClassifications)
{
Changed = true;
}
SemanticClassifications = NewSemanticClassifications;
const FString Semantics = FString::Join(SemanticClassifications, TEXT("-"));
UE_LOG(LogMRUK, Log, TEXT("SpatialAnchor label is %s"), *Semantics);
if (PlaneBounds != AnchorData->PlaneBounds)
{
Changed = true;
}
PlaneBounds = AnchorData->PlaneBounds;
PlaneBoundary2D = AnchorData->PlaneBoundary2D;
if (VolumeBounds != AnchorData->VolumeBounds)
{
Changed = true;
}
VolumeBounds = AnchorData->VolumeBounds;
if (Changed)
{
if (ProceduralMeshComponent)
{
ProceduralMeshComponent->UnregisterComponent();
ProceduralMeshComponent->DestroyComponent();
ProceduralMeshComponent = nullptr;
}
if (CachedMesh.IsSet())
{
CachedMesh.GetValue().Clear();
}
}
return Changed;
}
bool AMRUKAnchor::IsPositionInBoundary(const FVector2D& Position)
{
if (PlaneBoundary2D.IsEmpty())
{
return false;
}
int Intersections = 0;
for (int i = 1; i <= PlaneBoundary2D.Num(); i++)
{
const FVector2D P1 = PlaneBoundary2D[i - 1];
const FVector2D P2 = PlaneBoundary2D[i % PlaneBoundary2D.Num()];
if (Position.Y > FMath::Min(P1.Y, P2.Y) && Position.Y <= FMath::Max(P1.Y, P2.Y))
{
if (Position.X <= FMath::Max(P1.X, P2.X))
{
if (P1.Y != P2.Y)
{
const auto Frac = (Position.Y - P1.Y) / (P2.Y - P1.Y);
const auto XIntersection = P1.X + Frac * (P2.X - P1.X);
if (P1.X == P2.X || Position.X <= XIntersection)
{
Intersections++;
}
}
}
}
}
return Intersections % 2 == 1;
}
FVector AMRUKAnchor::GenerateRandomPositionOnPlane()
{
return GenerateRandomPositionOnPlaneFromStream(FRandomStream(NAME_None));
}
FVector AMRUKAnchor::GenerateRandomPositionOnPlaneFromStream(const FRandomStream& RandomStream)
{
if (PlaneBoundary2D.IsEmpty())
{
return FVector::ZeroVector;
}
// Cache the mesh so that if the function is called multiple times it will re-use the previously triangulated mesh.
if (!CachedMesh.IsSet())
{
TriangulatedMeshCache Mesh;
TArray<FVector2f> PlaneBoundary;
PlaneBoundary.Reserve(PlaneBoundary2D.Num());
for (const auto Point : PlaneBoundary2D)
{
PlaneBoundary.Push(FVector2f(Point));
}
MRUKTriangulatePolygon({ PlaneBoundary }, Mesh.Vertices, Mesh.Triangles);
// Compute the area of each triangle and the total surface area of the mesh
Mesh.Areas.Reserve(Mesh.Triangles.Num() / 3);
Mesh.TotalArea = 0.0f;
for (int i = 0; i < Mesh.Triangles.Num(); i += 3)
{
const auto I0 = Mesh.Triangles[i];
const auto I1 = Mesh.Triangles[i + 1];
const auto I2 = Mesh.Triangles[i + 2];
auto V0 = Mesh.Vertices[I0];
auto V1 = Mesh.Vertices[I1];
auto V2 = Mesh.Vertices[I2];
const auto Cross = FVector2D::CrossProduct(V1 - V0, V2 - V0);
float Area = Cross * 0.5f;
Mesh.TotalArea += Area;
Mesh.Areas.Add(Area);
}
CachedMesh.Emplace(MoveTemp(Mesh));
}
const auto& [Vertices, Triangles, Areas, TotalArea] = CachedMesh.GetValue();
// Pick a random triangle weighted by surface area (triangles with larger surface
// area have more chance of being chosen)
auto Rand = RandomStream.FRandRange(0.0f, TotalArea);
int TriangleIndex = 0;
for (; TriangleIndex < Areas.Num() - 1; ++TriangleIndex)
{
Rand -= Areas[TriangleIndex];
if (Rand <= 0.0f)
{
break;
}
}
// Get the vertices of the chosen triangle
const auto I0 = Triangles[TriangleIndex * 3];
const auto I1 = Triangles[TriangleIndex * 3 + 1];
const auto I2 = Triangles[TriangleIndex * 3 + 2];
const auto V0 = FVector(0.0, Vertices[I0].X, Vertices[I0].Y);
const auto V1 = FVector(0.0, Vertices[I1].X, Vertices[I1].Y);
const auto V2 = FVector(0.0, Vertices[I2].X, Vertices[I2].Y);
// Calculate a random point on that triangle
float U = RandomStream.FRandRange(0.0f, 1.0f);
float V = RandomStream.FRandRange(0.0f, 1.0f);
if (U + V > 1.0f)
{
if (U > V)
{
U = 1.0f - U;
}
else
{
V = 1.0f - V;
}
}
return V0 + U * (V1 - V0) + V * (V2 - V0);
}
bool AMRUKAnchor::Raycast(const FVector& Origin, const FVector& Direction, float MaxDist, FMRUKHit& OutHit, int32 ComponentTypes)
{
// If this anchor is the global mesh test against it
if ((ComponentTypes & static_cast<int32>(EMRUKComponentType::Mesh)) != 0 && this == Room->GlobalMeshAnchor)
{
FHitResult GlobalMeshOutHit{};
float Dist = MaxDist;
if (MaxDist <= 0.0)
{
const float WorldToMeters = GetWorld()->GetWorldSettings()->WorldToMeters;
Dist = WorldToMeters * 1024; // 1024 m should cover every scene
}
if (ActorLineTraceSingle(GlobalMeshOutHit, Origin, Origin + Direction * Dist, ECollisionChannel::ECC_WorldDynamic, FCollisionQueryParams::DefaultQueryParam))
{
OutHit.HitPosition = GlobalMeshOutHit.Location;
OutHit.HitNormal = GlobalMeshOutHit.Normal;
OutHit.HitDistance = GlobalMeshOutHit.Distance;
return true;
}
return false;
}
auto Transform = GetTransform();
// Transform the ray into local space
auto InverseTransform = Transform.Inverse();
const auto OriginLocal = InverseTransform.TransformPositionNoScale(Origin);
const auto DirectionLocal = InverseTransform.TransformVectorNoScale(Direction);
FRay LocalRay = FRay(OriginLocal, DirectionLocal);
bool FoundHit = false;
// If this anchor has a plane, hit test against it
if ((ComponentTypes & static_cast<int32>(EMRUKComponentType::Plane)) != 0 && PlaneBounds.bIsValid && RayCastPlane(LocalRay, MaxDist, OutHit))
{
// Update max dist for the volume raycast
MaxDist = OutHit.HitDistance;
FoundHit = true;
}
// If this anchor has a volume, hit test against it
if ((ComponentTypes & static_cast<int32>(EMRUKComponentType::Volume)) != 0 && VolumeBounds.IsValid && RayCastVolume(LocalRay, MaxDist, OutHit))
{
MaxDist = OutHit.HitDistance;
FoundHit = true;
}
return FoundHit;
}
bool AMRUKAnchor::RaycastAll(const FVector& Origin, const FVector& Direction, float MaxDist, TArray<FMRUKHit>& OutHits, int32 ComponentTypes)
{
if ((ComponentTypes & static_cast<int32>(EMRUKComponentType::Mesh)) != 0 && this == Room->GlobalMeshAnchor)
{
FHitResult GlobalMeshOutHit{};
float Dist = MaxDist;
if (MaxDist <= 0.0)
{
const float WorldToMeters = GetWorld()->GetWorldSettings()->WorldToMeters;
Dist = WorldToMeters * 1024; // 1024 m should cover every scene
}
if (ActorLineTraceSingle(GlobalMeshOutHit, Origin, Origin + Direction * Dist, ECollisionChannel::ECC_WorldDynamic, FCollisionQueryParams::DefaultQueryParam))
{
FMRUKHit Hit{};
Hit.HitPosition = GlobalMeshOutHit.Location;
Hit.HitNormal = GlobalMeshOutHit.Normal;
Hit.HitDistance = GlobalMeshOutHit.Distance;
OutHits.Push(Hit);
return true;
}
return false;
}
auto Transform = GetTransform();
// Transform the ray into local space
auto InverseTransform = Transform.Inverse();
const auto OriginLocal = InverseTransform.TransformPositionNoScale(Origin);
const auto DirectionLocal = InverseTransform.TransformVectorNoScale(Direction);
FRay LocalRay = FRay(OriginLocal, DirectionLocal);
bool FoundHit = false;
// If this anchor has a plane, hit test against it
FMRUKHit Hit;
if ((ComponentTypes & static_cast<int32>(EMRUKComponentType::Plane)) != 0 && PlaneBounds.bIsValid && RayCastPlane(LocalRay, MaxDist, Hit))
{
OutHits.Push(Hit);
FoundHit = true;
}
// If this anchor has a volume, hit test against it
if ((ComponentTypes & static_cast<int32>(EMRUKComponentType::Volume)) != 0 && VolumeBounds.IsValid && RayCastVolume(LocalRay, MaxDist, Hit))
{
OutHits.Push(Hit);
FoundHit = true;
}
return FoundHit;
}
void AMRUKAnchor::AttachProceduralMesh(const TArray<FString>& CutHoleLabels, bool GenerateCollision, UMaterialInterface* ProceduralMaterial)
{
AttachProceduralMesh({}, CutHoleLabels, GenerateCollision, ProceduralMaterial);
}
void AMRUKAnchor::AttachProceduralMesh(TArray<FMRUKPlaneUV> PlaneUVAdjustments, const TArray<FString>& CutHoleLabels, bool GenerateCollision, UMaterialInterface* ProceduralMaterial)
{
if (ProceduralMeshComponent)
{
// Procedural mesh already attached
return;
}
ProceduralMeshComponent = NewObject<UProceduralMeshComponent>(this, TEXT("ProceduralMesh"));
ProceduralMeshComponent->SetupAttachment(RootComponent);
ProceduralMeshComponent->RegisterComponent();
GenerateProceduralAnchorMesh(ProceduralMeshComponent, PlaneUVAdjustments, CutHoleLabels, false, GenerateCollision);
for (int32 SectionIndex = 0; SectionIndex < ProceduralMeshComponent->GetNumSections(); ++SectionIndex)
{
ProceduralMeshComponent->SetMaterial(SectionIndex, ProceduralMaterial);
}
}
void AMRUKAnchor::GenerateProceduralAnchorMesh(UProceduralMeshComponent* ProceduralMesh, const TArray<FMRUKPlaneUV>& PlaneUVAdjustments, const TArray<FString>& CutHoleLabels, bool PreferVolume, bool GenerateCollision, double Offset)
{
int SectionIndex = 0;
if (VolumeBounds.IsValid)
{
TArray<FVector> Vertices;
TArray<int32> Triangles;
TArray<FVector> Normals;
TArray<FVector2D> UVs;
TArray<FLinearColor> Colors; // Currently unused
TArray<FProcMeshTangent> Tangents; // Currently unused
constexpr int32 NumVertices = 24;
constexpr int32 NumTriangles = 12;
Vertices.Reserve(NumVertices);
Triangles.Reserve(3 * NumTriangles);
Normals.Reserve(NumVertices);
UVs.Reserve(NumVertices);
FBox VolumeBoundsOffset(VolumeBounds.Min - Offset, VolumeBounds.Max + Offset);
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 2; j++)
{
FVector Normal = FVector::ZeroVector;
if (j == 0)
{
Normal[i] = -1.0f;
}
else
{
Normal[i] = 1.0f;
}
auto BaseIndex = Vertices.Num();
FVector Vertex;
Vertex[i] = VolumeBoundsOffset[j][i];
for (int k = 0; k < 2; k++)
{
for (int l = 0; l < 2; l++)
{
Vertex[(i + 1) % 3] = VolumeBoundsOffset[k][(i + 1) % 3];
Vertex[(i + 2) % 3] = VolumeBoundsOffset[l][(i + 2) % 3];
Vertices.Push(Vertex);
Normals.Push(Normal);
// The 4 side faces of the cube should have their 0, 0 at the top left corner
// when viewed from the outside.
// The top face should have UVs that are consistent with planes to avoid Z fighting
// in case a plane and volume overlap (e.g. in the case of the desk).
FVector2D UV;
switch (i)
{
case 0:
UV = FVector2D(1 - k, 1 - l);
break;
case 1:
UV = FVector2D(k, l);
break;
case 2:
UV = FVector2D(1 - l, k);
break;
default:
UV = FVector2D::Zero();
ensure(0);
}
if (j == 0)
{
UV.X = 1 - UV.X;
}
UVs.Push(UV);
}
}
if (j == 1)
{
Triangles.Push(BaseIndex);
Triangles.Push(BaseIndex + 1);
Triangles.Push(BaseIndex + 2);
Triangles.Push(BaseIndex + 2);
Triangles.Push(BaseIndex + 1);
Triangles.Push(BaseIndex + 3);
}
else
{
Triangles.Push(BaseIndex);
Triangles.Push(BaseIndex + 2);
Triangles.Push(BaseIndex + 1);
Triangles.Push(BaseIndex + 1);
Triangles.Push(BaseIndex + 2);
Triangles.Push(BaseIndex + 3);
}
}
}
ProceduralMesh->CreateMeshSection_LinearColor(SectionIndex++, Vertices, Triangles, Normals, UVs, Colors, Tangents, GenerateCollision);
}
if (PlaneBounds.bIsValid && !(VolumeBounds.IsValid && PreferVolume))
{
TArray<TArray<FVector2f>> Polygons;
TArray<FVector2f> PlaneBoundary;
PlaneBoundary.Reserve(PlaneBoundary2D.Num());
for (const auto Point : PlaneBoundary2D)
{
PlaneBoundary.Push(FVector2f(Point));
}
Polygons.Push(PlaneBoundary);
if (!CutHoleLabels.IsEmpty())
{
for (const auto& ChildAnchor : ChildAnchors)
{
if (!ChildAnchor->HasAnyLabel(CutHoleLabels))
{
continue;
}
if (!ChildAnchor->PlaneBounds.bIsValid)
{
UE_LOG(LogMRUK, Warning, TEXT("Can only cut holes with anchors that have a plane"));
continue;
}
const FVector ChildPositionLS = GetActorTransform().InverseTransformPosition(ChildAnchor->GetActorLocation());
TArray<FVector2f> HoleBoundary;
HoleBoundary.Reserve(ChildAnchor->PlaneBoundary2D.Num());
for (int32 I = ChildAnchor->PlaneBoundary2D.Num() - 1; I >= 0; --I)
{
HoleBoundary.Push(FVector2f(ChildPositionLS.Y, ChildPositionLS.Z) + FVector2f(ChildAnchor->PlaneBoundary2D[I]));
}
Polygons.Push(HoleBoundary);
}
}
TArray<FVector2D> MeshVertices;
TArray<int32> MeshIndices;
MRUKTriangulatePolygon(Polygons, MeshVertices, MeshIndices);
TArray<FVector> Vertices;
TArray<FVector> Normals;
TArray<FVector2D> UV0s;
TArray<FVector2D> UV1s;
TArray<FVector2D> UV2s;
TArray<FVector2D> UV3s;
TArray<FLinearColor> Colors; // Currently unused
TArray<FProcMeshTangent> Tangents;
const int32 NumVertices = MeshVertices.Num();
Normals.Reserve(NumVertices);
UV0s.Reserve(NumVertices);
UV1s.Reserve(NumVertices);
UV2s.Reserve(NumVertices);
UV3s.Reserve(NumVertices);
Tangents.Reserve(NumVertices);
static const FVector Normal = -FVector::XAxisVector;
const FVector NormalOffset = Normal * Offset;
auto BoundsSize = PlaneBounds.GetSize();
for (const auto& PlaneBoundaryVertex : MeshVertices)
{
const FVector Vertex = FVector(0, PlaneBoundaryVertex.X, PlaneBoundaryVertex.Y) + NormalOffset;
Vertices.Push(Vertex);
Normals.Push(Normal);
Tangents.Push(FProcMeshTangent(-FVector::YAxisVector, false));
auto U = (PlaneBoundaryVertex.X - PlaneBounds.Min.X) / BoundsSize.X;
auto V = 1 - (PlaneBoundaryVertex.Y - PlaneBounds.Min.Y) / BoundsSize.Y;
if (PlaneUVAdjustments.Num() == 0)
{
UV0s.Push(FVector2D(U, V));
}
if (PlaneUVAdjustments.Num() >= 1)
{
UV0s.Push(FVector2D(U, V) * PlaneUVAdjustments[0].Scale + PlaneUVAdjustments[0].Offset);
}
if (PlaneUVAdjustments.Num() >= 2)
{
UV1s.Push(FVector2D(U, V) * PlaneUVAdjustments[1].Scale + PlaneUVAdjustments[1].Offset);
}
if (PlaneUVAdjustments.Num() >= 3)
{
UV2s.Push(FVector2D(U, V) * PlaneUVAdjustments[2].Scale + PlaneUVAdjustments[2].Offset);
}
if (PlaneUVAdjustments.Num() >= 4)
{
UV3s.Push(FVector2D(U, V) * PlaneUVAdjustments[3].Scale + PlaneUVAdjustments[3].Offset);
}
}
ProceduralMesh->CreateMeshSection_LinearColor(SectionIndex++, Vertices, MeshIndices, Normals, UV0s, UV1s, UV2s, UV3s, Colors, Tangents, GenerateCollision);
}
}
bool AMRUKAnchor::HasLabel(const FString& Label) const
{
return SemanticClassifications.Contains(Label);
}
bool AMRUKAnchor::HasAnyLabel(const TArray<FString>& Labels) const
{
for (const auto& Label : Labels)
{
if (HasLabel(Label))
{
return true;
}
}
return false;
}
bool AMRUKAnchor::PassesLabelFilter(const FMRUKLabelFilter& LabelFilter) const
{
return LabelFilter.PassesFilter(SemanticClassifications);
}
double AMRUKAnchor::GetClosestSurfacePosition(const FVector& TestPosition, FVector& OutSurfacePosition)
{
const auto& Transform = GetActorTransform();
const auto TestPositionLocal = Transform.InverseTransformPosition(TestPosition);
double ClosestDistance = DBL_MAX;
FVector ClosestPoint = FVector::ZeroVector;
if (PlaneBounds.bIsValid)
{
const auto BestPoint2D = PlaneBounds.GetClosestPointTo(FVector2D(TestPositionLocal.Y, TestPositionLocal.Z));
const FVector BestPoint(0.0, BestPoint2D.X, BestPoint2D.Y);
const auto Distance = FVector::Distance(BestPoint, TestPositionLocal);
if (Distance < ClosestDistance)
{
ClosestPoint = BestPoint;
ClosestDistance = Distance;
}
}
if (VolumeBounds.IsValid)
{
const auto BestPoint = VolumeBounds.GetClosestPointTo(TestPositionLocal);
const auto Distance = FVector::Distance(BestPoint, TestPositionLocal);
if (Distance < ClosestDistance)
{
ClosestPoint = BestPoint;
ClosestDistance = Distance;
}
}
OutSurfacePosition = Transform.TransformPosition(ClosestPoint);
return ClosestDistance;
}
bool AMRUKAnchor::IsPositionInVolumeBounds(const FVector& Position, bool TestVerticalBounds, double Tolerance)
{
if (!VolumeBounds.IsValid)
{
return false;
}
const auto& LocalPosition = GetActorTransform().InverseTransformPosition(Position);
return ((TestVerticalBounds ? ((LocalPosition.X >= VolumeBounds.Min.X - Tolerance) && (LocalPosition.X <= VolumeBounds.Max.X + Tolerance)) : true)
&& (LocalPosition.Y >= VolumeBounds.Min.Y - Tolerance) && (LocalPosition.Y <= VolumeBounds.Max.Y + Tolerance)
&& (LocalPosition.Z >= VolumeBounds.Min.Z - Tolerance) && (LocalPosition.Z <= VolumeBounds.Max.Z + Tolerance));
}
FVector AMRUKAnchor::GetFacingDirection() const
{
if (Room == nullptr)
{
return {};
}
if (!VolumeBounds.IsValid)
{
return GetActorForwardVector();
}
int32 CardinalAxis = 0;
return UMRUKBPLibrary::ComputeDirectionAwayFromClosestWall(this, CardinalAxis, {});
}
AActor* AMRUKAnchor::SpawnInterior(const TSubclassOf<class AActor>& ActorClass, bool MatchAspectRatio, bool CalculateFacingDirection, EMRUKSpawnerScalingMode ScalingMode)
{
Interior = GetWorld()->SpawnActor(ActorClass);
auto InteriorRoot = Interior->GetRootComponent();
if (!InteriorRoot)
{
UE_LOG(LogMRUK, Error, TEXT("SpawnInterior Spawned actor does not have a root component."));
return nullptr;
}
InteriorRoot->SetMobility(EComponentMobility::Movable);
Interior->AttachToComponent(GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform);
Interior->SetActorRelativeScale3D(FVector::OneVector);
const auto ChildLocalBounds = Interior->CalculateComponentsBoundingBoxInLocalSpace(true);
FQuat Rotation = FQuat::Identity;
FVector Offset = FVector::ZeroVector;
FVector Scale = FVector::OneVector;
if (VolumeBounds.IsValid)
{
int CardinalAxisIndex = 0;
if (CalculateFacingDirection && !MatchAspectRatio)
{
// Pick rotation that is pointing away from the closest wall
// If we are also matching the aspect ratio then we only have a choice
// between 2 directions and first need to figure out what those 2 directions
// are before doing the ray casting.
UMRUKBPLibrary::ComputeDirectionAwayFromClosestWall(this, CardinalAxisIndex, {});
}
Rotation = FQuat::MakeFromEuler(FVector(90, -(CardinalAxisIndex + 1) * 90, 90));
FBox ChildBounds = ChildLocalBounds.TransformBy(FTransform(Rotation));
const FVector ChildSize1 = ChildBounds.GetSize();
Scale = VolumeBounds.GetSize() / ChildSize1;
if (MatchAspectRatio)
{
FVector ChildSize2 = ChildSize1;
Swap(ChildSize2.Y, ChildSize2.Z);
FVector Scale2 = VolumeBounds.GetSize() / ChildSize2;
float Distortion1 = FMath::Max(Scale.Y, Scale.Z) / FMath::Min(Scale.Y, Scale.Z);
float Distortion2 = FMath::Max(Scale2.Y, Scale2.Z) / FMath::Min(Scale2.Y, Scale2.Z);
bool FlipToMatchAspectRatio = Distortion1 > Distortion2;
if (FlipToMatchAspectRatio)
{
CardinalAxisIndex = 1;
Scale = Scale2;
}
if (CalculateFacingDirection)
{
UMRUKBPLibrary::ComputeDirectionAwayFromClosestWall(this, CardinalAxisIndex, FlipToMatchAspectRatio ? TArray<int>{ 0, 2 } : TArray<int>{ 1, 3 });
}
if (CardinalAxisIndex != 0)
{
// Update the rotation and child bounds if necessary
Rotation = FQuat::MakeFromEuler(FVector(90, -(CardinalAxisIndex + 1) * 90, 90));
ChildBounds = ChildLocalBounds.TransformBy(FTransform(Rotation));
}
}
switch (ScalingMode)
{
case EMRUKSpawnerScalingMode::UniformScaling:
Scale.X = Scale.Y = Scale.Z = FMath::Min3(Scale.X, Scale.Y, Scale.Z);
break;
case EMRUKSpawnerScalingMode::UniformXYScale:
Scale.Y = Scale.Z = FMath::Min(Scale.Y, Scale.Z);
break;
case EMRUKSpawnerScalingMode::NoScaling:
Scale = FVector::OneVector;
break;
case EMRUKSpawnerScalingMode::Stretch:
// Nothing to do
break;
}
// Calculate the offset between the base of the two bounding boxes. Note that the anchor is on the
// top of the volume and the X axis points downwards. So the base is at Max.X.
FVector VolumeBase = FVector(VolumeBounds.Max.X, 0.5 * (VolumeBounds.Min.Y + VolumeBounds.Max.Y), 0.5 * (VolumeBounds.Min.Z + VolumeBounds.Max.Z));
FVector ChildBase = FVector(ChildBounds.Max.X, 0.5 * (ChildBounds.Min.Y + ChildBounds.Max.Y), 0.5 * (ChildBounds.Min.Z + ChildBounds.Max.Z));
Offset = VolumeBase - ChildBase * Scale;
}
else if (PlaneBounds.bIsValid)
{
const auto XAxis = GetTransform().GetUnitAxis(EAxis::X);
// Adjust the rotation so that Z always points up. This enables assets to be authored in a more natural
// way and show up in the scene as expected.
if (XAxis.Z <= -UE_INV_SQRT_2)
{
// This is a floor or other surface facing upwards
Rotation = FQuat::MakeFromEuler(FVector(0, 90, 0));
}
else if (XAxis.Z >= UE_INV_SQRT_2)
{
// This is ceiling or other surface facing downwards.
Rotation = FQuat::MakeFromEuler(FVector(0, -90, 0));
}
else
{
// This is a wall or other upright surface.
Rotation = FQuat::MakeFromEuler(FVector(0, 0, 180));
}
const auto ChildBounds = ChildLocalBounds.TransformBy(FTransform(Rotation));
const auto ChildBounds2D = FBox2D(FVector2D(ChildBounds.Min.Y, ChildBounds.Min.Z), FVector2D(ChildBounds.Max.Y, ChildBounds.Max.Z));
auto Scale2D = PlaneBounds.GetSize() / ChildBounds2D.GetSize();
switch (ScalingMode)
{
case EMRUKSpawnerScalingMode::UniformScaling:
case EMRUKSpawnerScalingMode::UniformXYScale:
Scale2D.X = Scale2D.Y = FMath::Min(Scale2D.X, Scale2D.Y);
break;
case EMRUKSpawnerScalingMode::NoScaling:
Scale2D = FVector2D::UnitVector;
break;
case EMRUKSpawnerScalingMode::Stretch:
// Nothing to do
break;
}
const auto Offset2D = PlaneBounds.GetCenter() - ChildBounds2D.GetCenter() * Scale2D;
Offset = FVector(0.0, Offset2D.X, Offset2D.Y);
Scale = FVector(0.5 * (Scale2D.X + Scale2D.Y), Scale2D.X, Scale2D.Y);
}
Interior->SetActorRelativeRotation(Rotation);
Interior->SetActorRelativeLocation(Offset);
UMRUKBPLibrary::SetScaleRecursivelyAdjustingForRotation(InteriorRoot, Scale);
return Interior;
}
TSharedRef<FJsonObject> AMRUKAnchor::JsonSerialize()
{
TSharedRef<FJsonObject> JsonObject = MakeShareable(new FJsonObject);
JsonObject->SetField(TEXT("UUID"), MRUKSerialize(AnchorUUID));
JsonObject->SetField(TEXT("SemanticClassifications"), MRUKSerialize(SemanticClassifications));
JsonObject->SetField(TEXT("Transform"), MRUKSerialize(GetTransform()));
if (PlaneBounds.bIsValid)
{
JsonObject->SetField(TEXT("PlaneBounds"), MRUKSerialize(PlaneBounds));
}
if (!PlaneBoundary2D.IsEmpty())
{
JsonObject->SetField(TEXT("PlaneBoundary2D"), MRUKSerialize(PlaneBoundary2D));
}
if (VolumeBounds.IsValid)
{
JsonObject->SetField(TEXT("VolumeBounds"), MRUKSerialize(VolumeBounds));
}
if (this == Room->GlobalMeshAnchor)
{
TArray<UProceduralMeshComponent*> ProcMeshComponents;
GetComponents<UProceduralMeshComponent>(ProcMeshComponents);
for (const auto& Component : ProcMeshComponents)
{
const auto ProcMeshComponent = Cast<UProceduralMeshComponent>(Component);
if (ProcMeshComponent && ProcMeshComponent->ComponentHasTag("GlobalMesh"))
{
ensure(ProcMeshComponent->GetNumSections() == 1);
auto GlobalMeshJson = MakeShared<FJsonObject>();
GlobalMeshJson->SetField(TEXT("UUID"), MRUKSerialize(AnchorUUID));
const auto ProcMeshSection = ProcMeshComponent->GetProcMeshSection(0);
TArray<TSharedPtr<FJsonValue>> PositionsJson;
for (const auto& Vertex : ProcMeshSection->ProcVertexBuffer)
{
PositionsJson.Add(MRUKSerialize(Vertex.Position));
}
GlobalMeshJson->SetArrayField(TEXT("Positions"), PositionsJson);
TArray<TSharedPtr<FJsonValue>> IndicesJson;
for (const auto& Index : ProcMeshSection->ProcIndexBuffer)
{
IndicesJson.Add(MakeShared<FJsonValueNumber>(Index));
}
GlobalMeshJson->SetArrayField(TEXT("Indices"), IndicesJson);
JsonObject->SetObjectField(TEXT("GlobalMesh"), GlobalMeshJson);
}
}
}
return JsonObject;
}
void AMRUKAnchor::EndPlay(EEndPlayReason::Type Reason)
{
if (Interior)
{
Interior->Destroy();
}
Super::EndPlay(Reason);
}
bool AMRUKAnchor::RayCastPlane(const FRay& LocalRay, float MaxDist, FMRUKHit& OutHit)
{
// If the ray is behind or parallel to the anchor's plane then ignore it
if (LocalRay.Direction.X >= UE_KINDA_SMALL_NUMBER)
{
// Distance to the plane from the ray origin along the ray's direction
const float Dist = -LocalRay.Origin.X / LocalRay.Direction.X;
// If the distance is negative or less than the maximum distance then ignore it
if (Dist >= 0.0f && (MaxDist <= 0 || Dist < MaxDist))
{
const FVector HitPos = LocalRay.PointAt(Dist);
// Ensure the hit is within the plane extends and within the boundary
const FVector2D Pos2D(HitPos.Y, HitPos.Z);
if (PlaneBounds.IsInside(Pos2D) && IsPositionInBoundary(Pos2D))
{
// Transform the result back into world space
const auto Transform = GetTransform();
OutHit.HitPosition = Transform.TransformPositionNoScale(HitPos);
OutHit.HitNormal = Transform.TransformVectorNoScale(-FVector::XAxisVector);
OutHit.HitDistance = Dist;
return true;
}
}
}
return false;
}
bool AMRUKAnchor::RayCastVolume(const FRay& LocalRay, float MaxDist, FMRUKHit& OutHit)
{
// Use the slab method to determine if the ray intersects with the bounding box
// https://education.siggraph.org/static/HyperGraph/raytrace/rtinter3.htm
float DistNear = -UE_BIG_NUMBER, DistFar = UE_BIG_NUMBER;
int HitAxis = 0;
for (int i = 0; i < 3; ++i)
{
if (FMath::Abs(LocalRay.Direction.Component(i)) >= UE_KINDA_SMALL_NUMBER)
{
// Distance to the plane from the ray origin along the ray's direction
float Dist1 = (VolumeBounds.Min.Component(i) - LocalRay.Origin.Component(i)) / LocalRay.Direction.Component(i);
float Dist2 = (VolumeBounds.Max.Component(i) - LocalRay.Origin.Component(i)) / LocalRay.Direction.Component(i);
if (Dist1 > Dist2)
{
std::swap(Dist1, Dist2);
}
if (Dist1 > DistNear)
{
DistNear = Dist1;
HitAxis = i;
}
if (Dist2 < DistFar)
{
DistFar = Dist2;
}
}
else
{
// In this case there is no intersection because the ray is parallel to the plane
// Check that it is within bounds
if (LocalRay.Origin.Component(i) < VolumeBounds.Min.Component(i) || LocalRay.Origin.Component(i) > VolumeBounds.Max.Component(i))
{
// No intersection, set DistNear to a large number
DistNear = UE_BIG_NUMBER;
break;
}
}
}
if (DistNear >= 0 && DistNear <= DistFar && (MaxDist <= 0 || DistNear < MaxDist))
{
const FVector HitPos = LocalRay.PointAt(DistNear);
FVector HitNormal = FVector::ZeroVector;
HitNormal.Component(HitAxis) = LocalRay.Direction.Component(HitAxis) > 0 ? -1 : 1;
// Transform the result back into world space
const auto Transform = GetTransform();
OutHit.HitPosition = Transform.TransformPositionNoScale(HitPos);
OutHit.HitNormal = Transform.TransformVectorNoScale(HitNormal);
OutHit.HitDistance = DistNear;
return true;
}
return false;
}
void AMRUKAnchor::TriangulatedMeshCache::Clear()
{
Vertices.Empty();
Triangles.Empty();
Areas.Empty();
TotalArea = 0.0f;
}
// #pragma optimize("", on)
#undef LOCTEXT_NAMESPACE

View File

@@ -0,0 +1,738 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "MRUtilityKitAnchorActorSpawner.h"
#include "MRUtilityKitAnchor.h"
#include "MRUtilityKitTelemetry.h"
#include "MRUtilityKitSubsystem.h"
#include "MRUtilityKitBPLibrary.h"
#include "GameFramework/WorldSettings.h"
#include "Engine/GameInstance.h"
const FName GMRUK_PROCEDURAL_ANCHOR_MESH_TAG = TEXT("MRUKProceduralAnchorMesh");
namespace
{
AActor* SpawnProceduralMesh(AMRUKAnchor* Anchor, const TArray<FMRUKPlaneUV>& PlaneUVAdjustments, const TArray<FString>& CutHoleLabels, UMaterialInterface* Material)
{
AActor* Actor = Anchor->GetWorld()->SpawnActor<AActor>();
Actor->SetOwner(Anchor);
Actor->Tags.AddUnique(GMRUK_PROCEDURAL_ANCHOR_MESH_TAG);
Actor->SetRootComponent(NewObject<USceneComponent>(Actor, TEXT("Root")));
Actor->GetRootComponent()->SetMobility(EComponentMobility::Movable);
Actor->AttachToComponent(Anchor->GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform);
Actor->SetActorRelativeScale3D(FVector::OneVector);
UProceduralMeshComponent* ProceduralMeshComponent = NewObject<UProceduralMeshComponent>(Actor, TEXT("ProceduralMesh"));
ProceduralMeshComponent->SetupAttachment(Actor->GetRootComponent());
ProceduralMeshComponent->RegisterComponent();
Actor->AddInstanceComponent(ProceduralMeshComponent);
Anchor->GenerateProceduralAnchorMesh(ProceduralMeshComponent, PlaneUVAdjustments, CutHoleLabels, false, true);
for (int32 SectionIndex = 0; SectionIndex < ProceduralMeshComponent->GetNumSections(); ++SectionIndex)
{
ProceduralMeshComponent->SetMaterial(SectionIndex, Material);
}
return Actor;
}
} // namespace
void AMRUKAnchorActorSpawner::BeginPlay()
{
Super::BeginPlay();
OculusXRTelemetry::TScopedMarker<MRUKTelemetry::FLoadAnchorActorSpawnerMarker> Event(static_cast<int>(GetTypeHash(this)));
if (SpawnMode == EMRUKSpawnMode::CurrentRoomOnly)
{
const auto Subsystem = GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
if (Subsystem->SceneLoadStatus == EMRUKInitStatus::Complete)
{
SpawnActors(Subsystem->GetCurrentRoom());
}
else
{
// Only listen for the room created event in case no current room was available yet
Subsystem->OnRoomCreated.AddUniqueDynamic(this, &AMRUKAnchorActorSpawner::OnRoomCreated);
}
}
else if (SpawnMode == EMRUKSpawnMode::AllRooms)
{
const auto Subsystem = GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
for (auto Room : Subsystem->Rooms)
{
SpawnActors(Room);
}
// Listen for new rooms that get created
Subsystem->OnRoomCreated.AddUniqueDynamic(this, &AMRUKAnchorActorSpawner::OnRoomCreated);
}
}
void AMRUKAnchorActorSpawner::OnRoomCreated(AMRUKRoom* Room)
{
if (SpawnMode == EMRUKSpawnMode::CurrentRoomOnly && GetGameInstance()->GetSubsystem<UMRUKSubsystem>()->GetCurrentRoom() != Room)
{
// Skip this room if it is not the current room
return;
}
SpawnActors(Room);
}
void AMRUKAnchorActorSpawner::OnRoomUpdated(AMRUKRoom* Room)
{
if (!SpawnedActors.Find(Room))
{
// A room was updated that we don't care about. If we are in current room only mode
// we only want to update the one room we created
return;
}
SpawnActors(Room);
}
void AMRUKAnchorActorSpawner::OnRoomRemoved(AMRUKRoom* Room)
{
RemoveActors(Room);
}
void AMRUKAnchorActorSpawner::RemoveActors(AMRUKRoom* Room)
{
if (!IsValid(Room))
{
UE_LOG(LogMRUK, Warning, TEXT("Can not remove actors from room that is a nullptr"));
return;
}
if (TArray<AActor*>* Actors = SpawnedActors.Find(Room))
{
for (AActor* Actor : *Actors)
{
if (IsValid(Actor))
{
Actor->Destroy();
}
}
Actors->Empty();
SpawnedActors.Remove(Room);
}
}
bool AMRUKAnchorActorSpawner::ShouldAnchorFallbackToProceduralMesh(const FMRUKSpawnGroup& SpawnGroup) const
{
switch (SpawnGroup.FallbackToProcedural)
{
case EMRUKFallbackToProceduralOverwrite::Default:
return ShouldFallbackToProcedural;
case EMRUKFallbackToProceduralOverwrite::Fallback:
return true;
case EMRUKFallbackToProceduralOverwrite::NoFallback:
return false;
}
return false;
}
TArray<AActor*> AMRUKAnchorActorSpawner::SpawnProceduralMeshesOnWallsIfNoWallActorGiven(AMRUKRoom* Room)
{
TArray<AActor*> Actors;
const auto WallFace = SpawnGroups.Find(FMRUKLabels::WallFace);
if (!WallFace || (WallFace->Actors.IsEmpty() && ShouldAnchorFallbackToProceduralMesh(*WallFace)))
{
// If no wall mesh is given we want to spawn the walls procedural to make seamless UVs
TArray<FMRUKAnchorWithPlaneUVs> AnchorsWithPlaneUVs;
Room->ComputeWallMeshUVAdjustments({}, AnchorsWithPlaneUVs);
for (const auto& AnchorWithPlaneUVs : AnchorsWithPlaneUVs)
{
Actors.Push(SpawnProceduralMesh(AnchorWithPlaneUVs.Anchor, AnchorWithPlaneUVs.PlaneUVs, CutHoleLabels, ProceduralMaterial));
}
}
return Actors;
}
AActor* AMRUKAnchorActorSpawner::SpawnProceduralMeshOnFloorIfNoFloorActorGiven(AMRUKRoom* Room)
{
const auto Floor = SpawnGroups.Find(FMRUKLabels::Floor);
if (Room->FloorAnchor && (!Floor || (Floor->Actors.IsEmpty() && ShouldAnchorFallbackToProceduralMesh(*Floor))))
{
// Use metric scaling to match walls
const float WorldToMeters = GetWorldSettings()->WorldToMeters;
const FVector2D Scale = Room->FloorAnchor->PlaneBounds.GetSize() / WorldToMeters;
const TArray<FMRUKPlaneUV> PlaneUVAdj = { { FVector2D::ZeroVector, Scale } };
return SpawnProceduralMesh(Room->FloorAnchor, PlaneUVAdj, CutHoleLabels, ProceduralMaterial);
}
return nullptr;
}
AActor* AMRUKAnchorActorSpawner::SpawnProceduralMeshOnCeilingIfNoCeilingActorGiven(AMRUKRoom* Room)
{
const auto Ceiling = SpawnGroups.Find(FMRUKLabels::Ceiling);
if (Room->CeilingAnchor && (!Ceiling || (Ceiling->Actors.IsEmpty() && ShouldAnchorFallbackToProceduralMesh(*Ceiling))))
{
// Use metric scaling to match walls
const float WorldToMeters = GetWorldSettings()->WorldToMeters;
const FVector2D Scale = Room->CeilingAnchor->PlaneBounds.GetSize() / WorldToMeters;
const TArray<FMRUKPlaneUV> PlaneUVAdj = { { FVector2D::ZeroVector, Scale } };
return SpawnProceduralMesh(Room->CeilingAnchor, PlaneUVAdj, CutHoleLabels, ProceduralMaterial);
}
return nullptr;
}
AActor* AMRUKAnchorActorSpawner::SpawnProceduralMeshForAnchorIfNeeded(AMRUKAnchor* Anchor)
{
if (!IsValid(Anchor))
{
return nullptr;
}
if (Anchor->SemanticClassifications.IsEmpty())
{
// For unknown scene objects spawn a procedural mesh (should not happen in practice)
return SpawnProceduralMesh(Anchor, {}, CutHoleLabels, ProceduralMaterial);
}
for (const FString& Label : Anchor->SemanticClassifications)
{
if (Label == 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 FMRUKSpawnGroup* SpawnGroup = SpawnGroups.Find(Label);
if (SpawnGroup && SpawnGroup->Actors.IsEmpty() && ShouldAnchorFallbackToProceduralMesh(*SpawnGroup))
{
return SpawnProceduralMesh(Anchor, {}, CutHoleLabels, ProceduralMaterial);
}
}
return nullptr;
}
TArray<AActor*> AMRUKAnchorActorSpawner::SpawnProceduralMeshesInRoom(AMRUKRoom* Room)
{
TArray<AActor*> Actors;
const TArray<AActor*> WallActors = SpawnProceduralMeshesOnWallsIfNoWallActorGiven(Room);
if (!WallActors.IsEmpty())
{
Actors.Append(WallActors);
}
AActor* Actor = nullptr;
Actor = SpawnProceduralMeshOnFloorIfNoFloorActorGiven(Room);
if (Actor)
{
Actors.Push(Actor);
}
Actor = SpawnProceduralMeshOnCeilingIfNoCeilingActorGiven(Room);
if (Actor)
{
Actors.Push(Actor);
}
for (const auto& Anchor : Room->AllAnchors)
{
if (Anchor->HasLabel(FMRUKLabels::Floor) || Anchor->HasLabel(FMRUKLabels::Ceiling) || Anchor->HasLabel(FMRUKLabels::WallFace))
{
// These have already been spawned above in case it was necessary
continue;
}
Actor = SpawnProceduralMeshForAnchorIfNeeded(Anchor);
if (Actor)
{
Actors.Push(Actor);
}
}
return Actors;
}
bool AMRUKAnchorActorSpawner::SelectSpawnActorClosestSize(AMRUKAnchor* Anchor, const FMRUKSpawnGroup& SpawnGroup, FMRUKSpawnActor& OutSpawnActor)
{
if (SpawnGroup.Actors.IsEmpty())
{
return false;
}
int Index = 0;
if (SpawnGroup.Actors.Num() > 1)
{
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];
const auto Subsystem = GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
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;
}
}
}
}
}
OutSpawnActor = SpawnGroup.Actors[Index];
return true;
}
bool AMRUKAnchorActorSpawner::SelectSpawnActorRandom(const FMRUKSpawnGroup& SpawnGroup, const FRandomStream& RandomStream, FMRUKSpawnActor& OutSpawnActor)
{
if (SpawnGroup.Actors.IsEmpty())
{
return false;
}
const int Index = RandomStream.RandRange(0, SpawnGroup.Actors.Num() - 1);
OutSpawnActor = SpawnGroup.Actors[Index];
return true;
}
bool AMRUKAnchorActorSpawner::SelectSpawnActorFromSpawnGroup(AMRUKAnchor* Anchor, const FMRUKSpawnGroup& SpawnGroup, const FRandomStream& RandomStream, FMRUKSpawnActor& OutSpawnActor)
{
if (SpawnGroup.Actors.IsEmpty())
{
return false;
}
if (SpawnGroup.SelectionMode == EMRUKSpawnerSelectionMode::Random)
{
return SelectSpawnActorRandom(SpawnGroup, RandomStream, OutSpawnActor);
}
if (SpawnGroup.SelectionMode == EMRUKSpawnerSelectionMode::ClosestSize)
{
return SelectSpawnActorClosestSize(Anchor, SpawnGroup, OutSpawnActor);
}
if (SpawnGroup.SelectionMode == EMRUKSpawnerSelectionMode::Custom)
{
return SelectSpawnActorCustom(Anchor, SpawnGroup, RandomStream, OutSpawnActor);
}
OutSpawnActor = SpawnGroup.Actors[0];
return true;
}
void AMRUKAnchorActorSpawner::AttachAndFitActorToAnchor(AMRUKAnchor* Anchor, AActor* Actor, EMRUKSpawnerScalingMode ScalingMode, EMRUKAlignMode AlignMode, bool bCalculateFacingDirection, bool bMatchAspectRatio)
{
auto ActorRoot = Actor->GetRootComponent();
if (!ActorRoot)
{
UE_LOG(LogMRUK, Error, TEXT("Spawned actor does not have a root component."));
return;
}
ActorRoot->SetMobility(EComponentMobility::Movable);
Actor->AttachToComponent(Anchor->GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform);
Actor->SetActorRelativeScale3D(FVector::OneVector);
const auto ChildLocalBounds = Actor->CalculateComponentsBoundingBoxInLocalSpace(true);
FQuat Rotation = FQuat::Identity;
FVector Offset = FVector::ZeroVector;
FVector Scale = FVector::OneVector;
if (Anchor->VolumeBounds.IsValid)
{
int CardinalAxisIndex = 0;
if (bCalculateFacingDirection && !bMatchAspectRatio)
{
// Pick rotation that is pointing away from the closest wall
// If we are also matching the aspect ratio then we only have a choice
// between 2 directions and first need to figure out what those 2 directions
// are before doing the ray casting.
UMRUKBPLibrary::ComputeDirectionAwayFromClosestWall(Anchor, CardinalAxisIndex, {});
}
Rotation = FQuat::MakeFromEuler(FVector(90, -(CardinalAxisIndex + 1) * 90, 90));
FBox ChildBounds = ChildLocalBounds.TransformBy(FTransform(Rotation));
const FVector ChildSize1 = ChildBounds.GetSize();
Scale = Anchor->VolumeBounds.GetSize() / ChildSize1;
if (bMatchAspectRatio)
{
FVector ChildSize2 = ChildSize1;
Swap(ChildSize2.Y, ChildSize2.Z);
FVector Scale2 = Anchor->VolumeBounds.GetSize() / ChildSize2;
float Distortion1 = FMath::Max(Scale.Y, Scale.Z) / FMath::Min(Scale.Y, Scale.Z);
float Distortion2 = FMath::Max(Scale2.Y, Scale2.Z) / FMath::Min(Scale2.Y, Scale2.Z);
bool FlipToMatchAspectRatio = Distortion1 > Distortion2;
if (FlipToMatchAspectRatio)
{
CardinalAxisIndex = 1;
Scale = Scale2;
}
if (bCalculateFacingDirection)
{
UMRUKBPLibrary::ComputeDirectionAwayFromClosestWall(Anchor, CardinalAxisIndex, FlipToMatchAspectRatio ? TArray<int>{ 0, 2 } : TArray<int>{ 1, 3 });
}
if (CardinalAxisIndex != 0)
{
// Update the rotation and child bounds if necessary
Rotation = FQuat::MakeFromEuler(FVector(90, -(CardinalAxisIndex + 1) * 90, 90));
ChildBounds = ChildLocalBounds.TransformBy(FTransform(Rotation));
}
}
switch (ScalingMode)
{
case EMRUKSpawnerScalingMode::UniformScaling:
Scale.X = Scale.Y = Scale.Z = FMath::Min3(Scale.X, Scale.Y, Scale.Z);
break;
case EMRUKSpawnerScalingMode::UniformXYScale:
Scale.Y = Scale.Z = FMath::Min(Scale.Y, Scale.Z);
break;
case EMRUKSpawnerScalingMode::NoScaling:
Scale = FVector::OneVector;
break;
case EMRUKSpawnerScalingMode::Stretch:
// Nothing to do
break;
case EMRUKSpawnerScalingMode::Custom:
Scale = ComputeCustomScaling(Anchor, Actor, Scale);
break;
}
if (AlignMode == EMRUKAlignMode::Custom)
{
Offset = ComputeCustomAlign(Anchor, Actor, ChildBounds, Scale);
}
else if (AlignMode != EMRUKAlignMode::None)
{
FVector ChildBase;
FVector VolumeBase;
switch (AlignMode)
{
case EMRUKAlignMode::CenterOnCenter:
ChildBase = FVector(0.5 * (ChildBounds.Min.X + ChildBounds.Max.X), 0.5 * (ChildBounds.Min.Y + ChildBounds.Max.Y), 0.5 * (ChildBounds.Min.Z + ChildBounds.Max.Z));
break;
case EMRUKAlignMode::TopOnTop:
case EMRUKAlignMode::TopOnBottom:
ChildBase = FVector(ChildBounds.Min.X, 0.5 * (ChildBounds.Min.Y + ChildBounds.Max.Y), 0.5 * (ChildBounds.Min.Z + ChildBounds.Max.Z));
break;
case EMRUKAlignMode::Default:
case EMRUKAlignMode::BottomOnBottom:
case EMRUKAlignMode::BottomOnTop:
ChildBase = FVector(ChildBounds.Max.X, 0.5 * (ChildBounds.Min.Y + ChildBounds.Max.Y), 0.5 * (ChildBounds.Min.Z + ChildBounds.Max.Z));
break;
case EMRUKAlignMode::LeftOnLeft:
case EMRUKAlignMode::LeftOnRight:
ChildBase = FVector(0.5 * (ChildBounds.Min.X + ChildBounds.Max.X), 0.5 * (ChildBounds.Min.Y + ChildBounds.Max.Y), ChildBounds.Max.Z);
break;
case EMRUKAlignMode::RightOnRight:
case EMRUKAlignMode::RightOnLeft:
ChildBase = FVector(0.5 * (ChildBounds.Min.X + ChildBounds.Max.X), 0.5 * (ChildBounds.Min.Y + ChildBounds.Max.Y), ChildBounds.Min.Z);
break;
case EMRUKAlignMode::FrontOnFront:
case EMRUKAlignMode::FrontOnBack:
ChildBase = FVector(0.5 * (ChildBounds.Min.X + ChildBounds.Max.X), ChildBounds.Max.Y, 0.5 * (ChildBounds.Min.Z + ChildBounds.Max.Z));
break;
case EMRUKAlignMode::BackOnBack:
case EMRUKAlignMode::BackOnFront:
ChildBase = FVector(0.5 * (ChildBounds.Min.X + ChildBounds.Max.X), ChildBounds.Min.Y, 0.5 * (ChildBounds.Min.Z + ChildBounds.Max.Z));
break;
}
switch (AlignMode)
{
case EMRUKAlignMode::CenterOnCenter:
VolumeBase = FVector(0.5 * (Anchor->VolumeBounds.Min.X + Anchor->VolumeBounds.Max.X), 0.5 * (Anchor->VolumeBounds.Min.Y + Anchor->VolumeBounds.Max.Y), 0.5 * (Anchor->VolumeBounds.Min.Z + Anchor->VolumeBounds.Max.Z));
break;
case EMRUKAlignMode::TopOnTop:
case EMRUKAlignMode::BottomOnTop:
VolumeBase = FVector(Anchor->VolumeBounds.Min.X, 0.5 * (Anchor->VolumeBounds.Min.Y + Anchor->VolumeBounds.Max.Y), 0.5 * (Anchor->VolumeBounds.Min.Z + Anchor->VolumeBounds.Max.Z));
break;
case EMRUKAlignMode::Default:
case EMRUKAlignMode::BottomOnBottom:
case EMRUKAlignMode::TopOnBottom:
VolumeBase = FVector(Anchor->VolumeBounds.Max.X, 0.5 * (Anchor->VolumeBounds.Min.Y + Anchor->VolumeBounds.Max.Y), 0.5 * (Anchor->VolumeBounds.Min.Z + Anchor->VolumeBounds.Max.Z));
break;
case EMRUKAlignMode::LeftOnLeft:
case EMRUKAlignMode::RightOnLeft:
VolumeBase = FVector(0.5 * (Anchor->VolumeBounds.Min.X + Anchor->VolumeBounds.Max.X), 0.5 * (Anchor->VolumeBounds.Min.Y + Anchor->VolumeBounds.Max.Y), Anchor->VolumeBounds.Max.Z);
break;
case EMRUKAlignMode::RightOnRight:
case EMRUKAlignMode::LeftOnRight:
VolumeBase = FVector(0.5 * (Anchor->VolumeBounds.Min.X + Anchor->VolumeBounds.Max.X), 0.5 * (Anchor->VolumeBounds.Min.Y + Anchor->VolumeBounds.Max.Y), Anchor->VolumeBounds.Min.Z);
break;
case EMRUKAlignMode::FrontOnFront:
case EMRUKAlignMode::BackOnFront:
VolumeBase = FVector(0.5 * (Anchor->VolumeBounds.Min.X + Anchor->VolumeBounds.Max.X), Anchor->VolumeBounds.Max.Y, 0.5 * (Anchor->VolumeBounds.Min.Z + Anchor->VolumeBounds.Max.Z));
break;
case EMRUKAlignMode::BackOnBack:
case EMRUKAlignMode::FrontOnBack:
VolumeBase = FVector(0.5 * (Anchor->VolumeBounds.Min.X + Anchor->VolumeBounds.Max.X), Anchor->VolumeBounds.Min.Y, 0.5 * (Anchor->VolumeBounds.Min.Z + Anchor->VolumeBounds.Max.Z));
break;
}
Offset = VolumeBase - ChildBase * Scale;
}
}
else if (Anchor->PlaneBounds.bIsValid)
{
const auto XAxis = Anchor->GetTransform().GetUnitAxis(EAxis::X);
// Adjust the rotation so that Z always points up. This enables assets to be authored in a more natural
// way and show up in the scene as expected.
if (XAxis.Z <= -UE_INV_SQRT_2)
{
// This is a floor or other surface facing upwards
Rotation = FQuat::MakeFromEuler(FVector(0, 90, 0));
}
else if (XAxis.Z >= UE_INV_SQRT_2)
{
// This is ceiling or other surface facing downwards.
Rotation = FQuat::MakeFromEuler(FVector(0, -90, 0));
}
else
{
// This is a wall or other upright surface.
Rotation = FQuat::MakeFromEuler(FVector(0, 0, 180));
}
const auto ChildBounds = ChildLocalBounds.TransformBy(FTransform(Rotation));
const auto ChildBounds2D = FBox2D(FVector2D(ChildBounds.Min.Y, ChildBounds.Min.Z), FVector2D(ChildBounds.Max.Y, ChildBounds.Max.Z));
auto Scale2D = Anchor->PlaneBounds.GetSize() / ChildBounds2D.GetSize();
switch (ScalingMode)
{
case EMRUKSpawnerScalingMode::UniformScaling:
case EMRUKSpawnerScalingMode::UniformXYScale:
Scale2D.X = Scale2D.Y = FMath::Min(Scale2D.X, Scale2D.Y);
break;
case EMRUKSpawnerScalingMode::NoScaling:
Scale2D = FVector2D::UnitVector;
break;
case EMRUKSpawnerScalingMode::Stretch:
// Nothing to do
break;
case EMRUKSpawnerScalingMode::Custom:
const FVector S = ComputeCustomScaling(Anchor, Actor, FVector(Scale2D.X, Scale2D.Y, 0.0));
Scale2D.X = S.X;
Scale2D.Y = S.Y;
break;
}
FVector2D Offset2D = FVector2D::ZeroVector;
switch (AlignMode)
{
case EMRUKAlignMode::None:
case EMRUKAlignMode::BackOnBack:
case EMRUKAlignMode::FrontOnFront:
case EMRUKAlignMode::FrontOnBack:
case EMRUKAlignMode::BackOnFront:
Offset = FVector::ZeroVector;
break;
case EMRUKAlignMode::Default:
case EMRUKAlignMode::CenterOnCenter:
Offset2D = Anchor->PlaneBounds.GetCenter() - ChildBounds2D.GetCenter() * Scale2D;
break;
case EMRUKAlignMode::BottomOnBottom:
Offset2D = FVector2D(Anchor->PlaneBounds.GetCenter().X, Anchor->PlaneBounds.Min.Y) - FVector2D(ChildBounds2D.GetCenter().X, ChildBounds2D.Min.Y) * Scale2D;
break;
case EMRUKAlignMode::TopOnTop:
Offset2D = FVector2D(Anchor->PlaneBounds.GetCenter().X, Anchor->PlaneBounds.Max.Y) - FVector2D(ChildBounds2D.GetCenter().X, ChildBounds2D.Max.Y) * Scale2D;
break;
case EMRUKAlignMode::LeftOnLeft:
Offset2D = FVector2D(Anchor->PlaneBounds.Max.X, Anchor->PlaneBounds.GetCenter().Y) - FVector2D(ChildBounds2D.Max.X, ChildBounds2D.GetCenter().Y) * Scale2D;
break;
case EMRUKAlignMode::RightOnRight:
Offset2D = FVector2D(Anchor->PlaneBounds.Min.X, Anchor->PlaneBounds.GetCenter().Y) - FVector2D(ChildBounds2D.Min.X, ChildBounds2D.GetCenter().Y) * Scale2D;
break;
case EMRUKAlignMode::BottomOnTop:
Offset2D = FVector2D(Anchor->PlaneBounds.GetCenter().X, Anchor->PlaneBounds.Max.Y) - FVector2D(ChildBounds2D.GetCenter().X, ChildBounds2D.Min.Y) * Scale2D;
break;
case EMRUKAlignMode::TopOnBottom:
Offset2D = FVector2D(Anchor->PlaneBounds.GetCenter().X, Anchor->PlaneBounds.Min.Y) - FVector2D(ChildBounds2D.GetCenter().X, ChildBounds2D.Max.Y) * Scale2D;
break;
case EMRUKAlignMode::LeftOnRight:
Offset2D = FVector2D(Anchor->PlaneBounds.Min.X, Anchor->PlaneBounds.GetCenter().Y) - FVector2D(ChildBounds2D.Max.X, ChildBounds2D.GetCenter().Y) * Scale2D;
break;
case EMRUKAlignMode::RightOnLeft:
Offset2D = FVector2D(Anchor->PlaneBounds.Max.X, Anchor->PlaneBounds.GetCenter().Y) - FVector2D(ChildBounds2D.Min.X, ChildBounds2D.GetCenter().Y) * Scale2D;
break;
case EMRUKAlignMode::Custom:
Offset = ComputeCustomAlign(Anchor, Actor, FBox(FVector(ChildBounds2D.Min, 0.0), FVector(ChildBounds2D.Max, 0.0)), FVector(Scale2D.X, Scale2D.Y, 0.0));
Offset2D = FVector2D(Offset.X, Offset.Y);
break;
}
Offset = FVector(0.0, Offset2D.X, Offset2D.Y);
Scale = FVector(0.5 * (Scale2D.X + Scale2D.Y), Scale2D.X, Scale2D.Y);
}
Actor->SetActorRelativeRotation(Rotation);
Actor->SetActorRelativeLocation(Offset);
UMRUKBPLibrary::SetScaleRecursivelyAdjustingForRotation(ActorRoot, Scale);
}
AActor* AMRUKAnchorActorSpawner::SpawnAnchorActor_Implementation(AMRUKAnchor* Anchor, const FMRUKSpawnActor& SpawnActor)
{
AActor* SpawnedActor = GetWorld()->SpawnActor(SpawnActor.Actor);
AttachAndFitActorToAnchor(Anchor, SpawnedActor, SpawnActor.ScalingMode, SpawnActor.AlignMode, SpawnActor.CalculateFacingDirection, SpawnActor.MatchAspectRatio);
return SpawnedActor;
}
FVector AMRUKAnchorActorSpawner::ComputeCustomScaling_Implementation(AMRUKAnchor* Anchor, AActor* SpawnedActor, const FVector& StretchedScale)
{
UE_LOG(LogMRUK, Warning, TEXT("Custom scaling mode selected but default implementation used. Please override ComputeCustomScaling() to define custom scaling"));
return StretchedScale;
}
bool AMRUKAnchorActorSpawner::SelectSpawnActorCustom_Implementation(AMRUKAnchor* Anchor, const FMRUKSpawnGroup& SpawnGroup, const FRandomStream& RandomStream, FMRUKSpawnActor& OutSpawnActor)
{
UE_LOG(LogMRUK, Warning, TEXT("Custom selection mode specified, but custom selection logic was not overwritten. Please overwrite SelectSpawnActorCustom() to define custom selection logic"));
return SelectSpawnActorRandom(SpawnGroup, RandomStream, OutSpawnActor);
}
FVector AMRUKAnchorActorSpawner::ComputeCustomAlign_Implementation(AMRUKAnchor* Anchor, AActor* Actor, const FBox& ChildBounds, const FVector& Scale)
{
UE_LOG(LogMRUK, Warning, TEXT("Custom align mode selected but default implementation used. Please override ComputeCustomAlign() to define custom align"));
return FVector::ZeroVector;
}
bool AMRUKAnchorActorSpawner::ShouldSpawnActorForAnchor(AMRUKAnchor* Anchor, const FString& Label, FMRUKSpawnGroup& OutSpawnGroup) const
{
if (Label == FMRUKLabels::WallFace && Anchor->SemanticClassifications.Contains(FMRUKLabels::InvisibleWallFace))
{
// Treat anchors with WALL_FACE and INVISIBLE_WALL_FACE as anchors that only have INVISIBLE_WALL_FACE
return false;
}
const auto SpawnGroup = SpawnGroups.Find(Label);
if (!SpawnGroup)
{
return false;
}
if (SpawnGroup->Actors.IsEmpty() && ShouldAnchorFallbackToProceduralMesh(*SpawnGroup))
{
return false;
}
OutSpawnGroup = *SpawnGroup;
return true;
}
AActor* AMRUKAnchorActorSpawner::SpawnAnchorActorForLabel_Implementation(AMRUKAnchor* Anchor, const FString& Label, const FMRUKSpawnGroup& SpawnGroup, const FRandomStream& RandomStream)
{
FMRUKSpawnActor SpawnActor{};
if (SelectSpawnActorFromSpawnGroup(Anchor, SpawnGroup, RandomStream, SpawnActor))
{
if (!SpawnActor.Actor)
{
UE_LOG(LogMRUK, Error, TEXT("Actor to spawn is a nullptr for label %s. Skipping it."), *Label);
return nullptr;
}
return SpawnAnchorActor(Anchor, SpawnActor);
}
UE_LOG(LogMRUK, Error, TEXT("Actor is nullptr for label %s."), *Label);
return nullptr;
}
TArray<AActor*> AMRUKAnchorActorSpawner::SpawnAnchorActorsInRoom_Implementation(AMRUKRoom* Room, const FRandomStream& RandomStream)
{
TArray<AActor*> SpawnedActorsInRoom;
SpawnedActorsInRoom.Append(SpawnProceduralMeshesInRoom(Room));
for (const auto& Anchor : Room->AllAnchors)
{
if (!IsValid(Anchor))
{
continue;
}
for (const FString& Label : Anchor->SemanticClassifications)
{
FMRUKSpawnGroup SpawnGroup{};
if (!ShouldSpawnActorForAnchor(Anchor, Label, SpawnGroup))
{
continue;
}
if (AActor* SpawnedActor = SpawnAnchorActorForLabel(Anchor, Label, SpawnGroup, RandomStream))
{
SpawnedActorsInRoom.Push(SpawnedActor);
}
}
}
return SpawnedActorsInRoom;
}
void AMRUKAnchorActorSpawner::SpawnActors(AMRUKRoom* Room)
{
if (!IsValid(Room))
{
UE_LOG(LogMRUK, Warning, TEXT("Can not spawn actors in Room that is a nullptr"));
return;
}
RemoveActors(Room);
// Use last seed if possible to keep spawning deterministic after the first spawn.
// In case the anchor random spawn seed has been changed it will be used instead
// of the last seed.
int32 Seed = -1;
if (LastSeed >= 0)
{
if ((AnchorRandomSpawnSeed >= 0) && (LastSeed != AnchorRandomSpawnSeed))
{
Seed = AnchorRandomSpawnSeed;
}
else
{
Seed = LastSeed;
}
}
else if (AnchorRandomSpawnSeed >= 0)
{
Seed = AnchorRandomSpawnSeed;
}
FRandomStream RandomStream(Seed);
if (Seed < 0)
{
RandomStream.GenerateNewSeed();
}
LastSeed = RandomStream.GetCurrentSeed();
const TArray<AActor*>& Actors = SpawnAnchorActorsInRoom(Room, RandomStream);
SpawnedActors.Add(Room, Actors);
const auto Subsystem = GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
Subsystem->OnRoomUpdated.AddUniqueDynamic(this, &AMRUKAnchorActorSpawner::OnRoomUpdated);
Subsystem->OnRoomRemoved.AddUniqueDynamic(this, &AMRUKAnchorActorSpawner::OnRoomRemoved);
OnActorsSpawned.Broadcast(Room);
}
void AMRUKAnchorActorSpawner::GetSpawnedActorsByRoom(AMRUKRoom* Room, TArray<AActor*>& Actors)
{
if (const TArray<AActor*>* A = SpawnedActors.Find(Room))
{
Actors.Append(*A);
}
}
void AMRUKAnchorActorSpawner::GetSpawnedActors(TArray<AActor*>& Actors)
{
for (const auto& KeyValue : SpawnedActors)
{
Actors.Append(KeyValue.Value);
}
}

View File

@@ -0,0 +1,591 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "MRUtilityKitBPLibrary.h"
#include "MRUtilityKit.h"
#include "Generated/MRUtilityKitShared.h"
#include "MRUtilityKitAnchor.h"
#include "MRUtilityKitSubsystem.h"
#include "MRUtilityKitSerializationHelpers.h"
#include "ProceduralMeshComponent.h"
#include "VectorUtil.h"
#include "Engine/World.h"
#include "Engine/GameInstance.h"
#include "Engine/Engine.h"
#include "TextureResource.h"
#include "Engine/TextureRenderTarget2D.h"
#include "Engine/Texture2D.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
namespace
{
TArray<FVector> RecalculateNormals(const TArray<FVector>& Vertices, const TArray<uint32>& Triangles)
{
TArray<FVector> Normals;
// Initialize the normals array with zero vectors
Normals.Init(FVector::ZeroVector, Vertices.Num());
// Iterate through each triangle
for (int32 TriIndex = 0; TriIndex < Triangles.Num(); TriIndex += 3)
{
// Get the vertices of the triangle
FVector VertexA = Vertices[Triangles[TriIndex]];
FVector VertexB = Vertices[Triangles[TriIndex + 1]];
FVector VertexC = Vertices[Triangles[TriIndex + 2]];
// Calculate the triangle's normal
const FVector TriangleNormal = FVector::CrossProduct(VertexC - VertexA, VertexB - VertexA).GetSafeNormal();
// Add the triangle's normal to each of its vertices' normals
Normals[Triangles[TriIndex]] += TriangleNormal;
Normals[Triangles[TriIndex + 1]] += TriangleNormal;
Normals[Triangles[TriIndex + 2]] += TriangleNormal;
}
// Normalize the vertex normals
for (FVector& Normal : Normals)
{
if (!Normal.IsNearlyZero())
{
Normal.Normalize();
}
else
{
Normal = FVector::UpVector;
}
}
return Normals;
}
TArray<FProcMeshTangent> RecalculateTangents(const TArray<FVector>& Normals)
{
TArray<FProcMeshTangent> Tangents;
// Initialize the tangents array with zero tangents
Tangents.Init(FProcMeshTangent(0.f, 0.f, 0.f), Normals.Num());
// Iterate through each normal
for (int32 NormalIndex = 0; NormalIndex < Normals.Num(); NormalIndex++)
{
const FVector& Normal = Normals[NormalIndex];
// Calculate a tangent based on the normal
FVector TangentX = FVector(1.0f, 0.0f, 0.0f);
// Gram-Schmidt orthogonalization
TangentX -= Normal * FVector::DotProduct(TangentX, Normal);
if (!TangentX.IsNearlyZero())
{
TangentX.Normalize();
}
else
{
TangentX = FVector::UpVector;
}
// Store the tangent in the array
Tangents[NormalIndex] = FProcMeshTangent(TangentX, false);
}
return Tangents;
}
void SetScaleRecursivelyAdjustingForRotationInternal(USceneComponent* SceneComponent, const FVector& UnRotatedScale, const FQuat& AccumulatedRotation, const FVector& ParentReciprocalScale)
{
if (SceneComponent)
{
const auto RelativeRotation = SceneComponent->GetRelativeRotationCache().RotatorToQuat(SceneComponent->GetRelativeRotation());
const auto Rotation = AccumulatedRotation * RelativeRotation;
const FVector RotatedXAxis = Rotation.GetAxisX();
const FVector RotatedYAxis = Rotation.GetAxisY();
const FVector RotatedZAxis = Rotation.GetAxisZ();
FVector RotatedScale;
if (FMath::Abs(RotatedXAxis.X) >= UE_INV_SQRT_2)
{
RotatedScale.X = UnRotatedScale.X;
}
else if (FMath::Abs(RotatedXAxis.Y) >= UE_INV_SQRT_2)
{
RotatedScale.X = UnRotatedScale.Y;
}
else
{
RotatedScale.X = UnRotatedScale.Z;
}
if (FMath::Abs(RotatedYAxis.X) >= UE_INV_SQRT_2)
{
RotatedScale.Y = UnRotatedScale.X;
}
else if (FMath::Abs(RotatedYAxis.Y) >= UE_INV_SQRT_2)
{
RotatedScale.Y = UnRotatedScale.Y;
}
else
{
RotatedScale.Y = UnRotatedScale.Z;
}
if (FMath::Abs(RotatedZAxis.X) >= UE_INV_SQRT_2)
{
RotatedScale.Z = UnRotatedScale.X;
}
else if (FMath::Abs(RotatedZAxis.Y) >= UE_INV_SQRT_2)
{
RotatedScale.Z = UnRotatedScale.Y;
}
else
{
RotatedScale.Z = UnRotatedScale.Z;
}
const FVector OldScale = SceneComponent->GetRelativeScale3D();
const FVector NewScale = ParentReciprocalScale * RotatedScale * OldScale;
SceneComponent->SetRelativeScale3D(NewScale);
const FVector NewParentReciprocalScale = ParentReciprocalScale * (OldScale / NewScale);
for (auto Child : SceneComponent->GetAttachChildren())
{
if (Child)
{
SetScaleRecursivelyAdjustingForRotationInternal(Child, UnRotatedScale, Rotation, NewParentReciprocalScale);
}
}
}
}
TArray<FVector> GeneratePoints(const FTransform& Plane, const FBox2D& PlaneBounds, double PointsPerUnitX, double PointsPerUnitY, double WorldToMeters = 100.0)
{
const FVector PlaneRight = Plane.GetRotation().GetRightVector();
const FVector PlaneUp = Plane.GetRotation().GetUpVector();
const FVector PlaneSize = FVector(PlaneBounds.GetSize().X, PlaneBounds.GetSize().Y, 0.0);
const FVector PlaneBottomLeft = Plane.GetLocation() - PlaneRight * PlaneSize.X * 0.5f - PlaneUp * PlaneSize.Y * 0.5f;
const int32 PointsX = FMath::Max(FMathf::Ceil(PointsPerUnitX * PlaneSize.X) / WorldToMeters, 1);
const int32 PointsY = FMath::Max(FMathf::Ceil(PointsPerUnitY * PlaneSize.Y) / WorldToMeters, 1);
const FVector2D Stride{ PlaneSize.X / (PointsX + 1), PlaneSize.Y / (PointsY + 1) };
TArray<FVector> Points;
Points.SetNum(PointsX * PointsY);
for (int Iy = 0; Iy < PointsY; ++Iy)
{
for (int Ix = 0; Ix < PointsX; ++Ix)
{
const float Dx = (Ix + 1) * Stride.X;
const float Dy = (Iy + 1) * Stride.Y;
const FVector Point = PlaneBottomLeft + Dx * PlaneRight + Dy * PlaneUp;
Points[Ix + Iy * PointsX] = Point;
}
}
return Points;
}
} // namespace
UMRUKLoadFromDevice* UMRUKLoadFromDevice::LoadSceneFromDeviceAsync(const UObject* WorldContext
)
{
// We must have a valid contextual world for this action, so we don't even make it
// unless we can resolve the UWorld from WorldContext.
UWorld* World = GEngine->GetWorldFromContextObject(WorldContext, EGetWorldErrorMode::ReturnNull);
if (!ensureAlwaysMsgf(IsValid(WorldContext), TEXT("World Context was not valid.")))
{
return nullptr;
}
// Create a new UMyDelayAsyncAction, and store function arguments in it.
UMRUKLoadFromDevice* NewAction = NewObject<UMRUKLoadFromDevice>();
NewAction->World = World;
NewAction->RegisterWithGameInstance(World->GetGameInstance());
return NewAction;
}
void UMRUKLoadFromDevice::Activate()
{
const auto Subsystem = World->GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
Subsystem->OnSceneLoaded.AddDynamic(this, &UMRUKLoadFromDevice::OnSceneLoaded);
{
Subsystem->LoadSceneFromDevice();
}
}
void UMRUKLoadFromDevice::OnSceneLoaded(bool Succeeded)
{
const auto Subsystem = World->GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
Subsystem->OnSceneLoaded.RemoveDynamic(this, &UMRUKLoadFromDevice::OnSceneLoaded);
if (Succeeded)
{
Success.Broadcast();
}
else
{
Failure.Broadcast();
}
SetReadyToDestroy();
}
bool UMRUKBPLibrary::LoadGlobalMeshFromDevice(FOculusXRUInt64 SpaceHandle, UProceduralMeshComponent* OutProceduralMesh, bool LoadCollision, const UObject* WorldContext)
{
ensure(OutProceduralMesh);
const UWorld* World = GEngine->GetWorldFromContextObject(WorldContext, EGetWorldErrorMode::ReturnNull);
if (!ensureAlwaysMsgf(IsValid(WorldContext), TEXT("World Context was not valid.")))
{
return false;
}
const auto RoomLayoutManager = World->GetGameInstance()->GetSubsystem<UMRUKSubsystem>()->GetRoomLayoutManager();
const bool LoadResult = RoomLayoutManager->LoadTriangleMesh(SpaceHandle.Value, OutProceduralMesh, LoadCollision);
if (!LoadResult)
{
UE_LOG(LogMRUK, Warning, TEXT("Could not load triangle mesh from layout manager"));
return false;
}
return true;
}
bool UMRUKBPLibrary::LoadGlobalMeshFromJsonString(const FString& JsonString, FOculusXRUUID AnchorUUID, UProceduralMeshComponent* OutProceduralMesh, bool LoadCollision)
{
ensure(OutProceduralMesh);
TSharedPtr<FJsonValue> JsonValue;
auto JsonReader = TJsonReaderFactory<>::Create(JsonString);
if (!FJsonSerializer::Deserialize(JsonReader, JsonValue))
{
UE_LOG(LogMRUK, Warning, TEXT("Could not deserialize global mesh JSON data"));
return false;
}
auto JsonObject = JsonValue->AsObject();
// Find room
auto RoomsJson = JsonObject->GetArrayField(TEXT("Rooms"));
for (const auto& RoomJson : RoomsJson)
{
auto RoomObject = RoomJson->AsObject();
FOculusXRUUID RoomUUID;
MRUKDeserialize(*RoomObject->GetField<EJson::None>(TEXT("UUID")), RoomUUID);
if (RoomUUID == AnchorUUID)
{
// Find global mesh anchor
auto AnchorsJson = RoomObject->GetArrayField(TEXT("Anchors"));
for (const auto& AnchorJson : AnchorsJson)
{
auto AnchorObject = AnchorJson->AsObject();
if (AnchorObject->HasField(TEXT("GlobalMesh")))
{
auto GlobalMeshObject = AnchorObject->GetField<EJson::Object>(TEXT("GlobalMesh"))->AsObject();
auto PositionsJson = GlobalMeshObject->GetArrayField(TEXT("Positions"));
TArray<FVector> Positions;
Positions.Reserve(PositionsJson.Num());
for (const auto& PositionJson : PositionsJson)
{
FVector Position;
MRUKDeserialize(*PositionJson, Position);
Positions.Push(Position);
}
auto IndicesJson = GlobalMeshObject->GetArrayField(TEXT("Indices"));
TArray<int32> Indices;
Indices.Reserve(IndicesJson.Num());
for (const auto& IndexJson : IndicesJson)
{
double Index;
MRUKDeserialize(*IndexJson, Index);
Indices.Push((int32)Index);
}
TArray<FVector> EmptyNormals;
TArray<FVector2D> EmptyUV;
TArray<FColor> EmptyVertexColors;
TArray<FProcMeshTangent> EmptyTangents;
OutProceduralMesh->CreateMeshSection(0, Positions, Indices, EmptyNormals, EmptyUV, EmptyVertexColors, EmptyTangents, LoadCollision);
return true;
}
}
break;
}
}
UE_LOG(LogMRUK, Warning, TEXT("Could not find global mesh in room"));
return false;
}
void UMRUKBPLibrary::RecalculateProceduralMeshAndTangents(UProceduralMeshComponent* Mesh)
{
if (!IsValid(Mesh))
return;
for (int s = 0; s < Mesh->GetNumSections(); ++s)
{
FProcMeshSection* Section = Mesh->GetProcMeshSection(s);
// Get vertices of the section
TArray<FVector> Vertices;
for (FProcMeshVertex Vertex : Section->ProcVertexBuffer)
{
Vertices.Add(Vertex.Position);
}
// Calculate normals and tangents
TArray<FVector> Normals = RecalculateNormals(Vertices, Section->ProcIndexBuffer);
TArray<FProcMeshTangent> Tangents = RecalculateTangents(Normals);
TArray<FVector2D> EmptyUV;
TArray<FColor> EmptyVertexColors;
// Update mesh section
Mesh->UpdateMeshSection(s, Vertices, Normals, EmptyUV, EmptyVertexColors, Tangents);
}
}
bool UMRUKBPLibrary::IsUnrealEngineMetaFork()
{
#if defined(WITH_OCULUS_BRANCH)
return true;
#else
return false;
#endif
}
FVector2D UMRUKBPLibrary::ComputeCentroid(const TArray<FVector2D>& PolygonPoints)
{
FVector2D Centroid = FVector2D::ZeroVector;
double SignedArea = 0.0;
for (int32 I = 0; I < PolygonPoints.Num(); ++I)
{
const double X0 = PolygonPoints[I].X;
const double Y0 = PolygonPoints[I].Y;
const double X1 = PolygonPoints[(I + 1) % PolygonPoints.Num()].X;
const double Y1 = PolygonPoints[(I + 1) % PolygonPoints.Num()].Y;
const double A = X0 * Y1 - X1 * Y0;
SignedArea += A;
Centroid.X += (X0 + X1) * A;
Centroid.Y += (Y0 + Y1) * A;
}
return Centroid / (6.0 * (SignedArea * 0.5));
}
void UMRUKBPLibrary::SetScaleRecursivelyAdjustingForRotation(USceneComponent* SceneComponent, const FVector& UnRotatedScale)
{
SetScaleRecursivelyAdjustingForRotationInternal(SceneComponent, UnRotatedScale, FQuat::Identity, FVector::OneVector);
}
FVector UMRUKBPLibrary::ComputeDirectionAwayFromClosestWall(const AMRUKAnchor* Anchor, int& OutCardinalAxisIndex, const TArray<int> ExcludedAxes)
{
double ClosestWallDistance = DBL_MAX;
FVector AwayFromWall{};
for (int i = 0; i < 4; ++i)
{
if (ExcludedAxes.Contains(i))
{
continue;
}
// Shoot a ray along the cardinal directions
// The "Up" (i.e. Z axis) for anchors typically points away from the facing direction, but it depends
// entirely on how the user defined the volume in scene capture.
const auto CardinalAxis = (FQuat::MakeFromEuler({ 0.0, 0.0, 90.0 * i }).RotateVector(Anchor->GetActorUpVector()));
for (const auto& WallAnchor : Anchor->Room->WallAnchors)
{
if (!WallAnchor)
{
continue;
}
FMRUKHit Hit{};
if (!WallAnchor->Raycast(Anchor->GetActorLocation(), CardinalAxis, 0.0, Hit))
{
continue;
}
const auto DistToWall = FVector::Distance(Hit.HitPosition, Anchor->GetActorLocation());
if (DistToWall < ClosestWallDistance)
{
ClosestWallDistance = DistToWall;
AwayFromWall = -CardinalAxis;
OutCardinalAxisIndex = i;
}
}
}
return AwayFromWall;
}
UTexture2D* UMRUKBPLibrary::ConstructTexture2D(UTextureRenderTarget2D* RenderTarget2D, UObject* Outer, const FString& TexName)
{
const auto SizeX = RenderTarget2D->SizeX;
const auto SizeY = RenderTarget2D->SizeY;
const auto Tex = UTexture2D::CreateTransient(SizeX, SizeY, RenderTarget2D->GetFormat());
Tex->AddToRoot();
Tex->Filter = TF_Bilinear;
Tex->CompressionSettings = TC_Default;
Tex->SRGB = 0;
Tex->UpdateResource();
FTextureRenderTargetResource* RenderTargetResource = RenderTarget2D->GameThread_GetRenderTargetResource();
FReadSurfaceDataFlags ReadSurfaceDataFlags;
ReadSurfaceDataFlags.SetLinearToGamma(false);
TArray<FColor> OutBMP;
RenderTargetResource->ReadPixels(OutBMP, ReadSurfaceDataFlags);
FTexture2DMipMap& Mip = Tex->GetPlatformData()->Mips[0];
void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);
FMemory::Memcpy(Data, OutBMP.GetData(), SizeX * SizeY * 4);
Mip.BulkData.Unlock();
Tex->UpdateResource();
return Tex;
}
FLinearColor UMRUKBPLibrary::GetMatrixColumn(const FMatrix& Matrix, int32 Index)
{
ensure(0 <= Index && Index < 4);
FLinearColor V;
V.R = Matrix.M[0][Index];
V.G = Matrix.M[1][Index];
V.B = Matrix.M[2][Index];
V.A = Matrix.M[3][Index];
return V;
}
TArray<FVector> UMRUKBPLibrary::ComputeRoomBoxGrid(const AMRUKRoom* Room, int32 MaxPointsCount, double PointsPerUnitX, double PointsPerUnitY)
{
TArray<FVector> AllPoints;
const double WorldToMeters = Room->GetWorld()->GetWorldSettings()->WorldToMeters;
for (const AMRUKAnchor* WallAnchor : Room->WallAnchors)
{
const auto Points = GeneratePoints(WallAnchor->GetTransform(), WallAnchor->PlaneBounds, PointsPerUnitX, PointsPerUnitY, WorldToMeters);
AllPoints.Append(Points);
}
// Generate points between floor and ceiling
const float DistFloorCeiling = Room->CeilingAnchor->GetTransform().GetLocation().Z - Room->FloorAnchor->GetTransform().GetLocation().Z;
const int32 PlanesCount = FMath::Max(FMathf::Ceil(PointsPerUnitY * DistFloorCeiling) / WorldToMeters, 1);
const int32 SpaceBetweenPlanes = DistFloorCeiling / PlanesCount;
for (int i = 1; i < PlanesCount; ++i)
{
FTransform Transform = Room->CeilingAnchor->GetTransform();
Transform.SetLocation(FVector(Transform.GetLocation().X, Transform.GetLocation().Y, Transform.GetLocation().Z - (SpaceBetweenPlanes * i)));
const auto Points = GeneratePoints(Transform, Room->CeilingAnchor->PlaneBounds, PointsPerUnitX, PointsPerUnitY, WorldToMeters);
AllPoints.Append(Points);
}
const auto CeilingPoints = GeneratePoints(Room->CeilingAnchor->GetTransform(), Room->CeilingAnchor->PlaneBounds, PointsPerUnitX, PointsPerUnitY, WorldToMeters);
AllPoints.Append(CeilingPoints);
const auto FloorPoints = GeneratePoints(Room->FloorAnchor->GetTransform(), Room->FloorAnchor->PlaneBounds, PointsPerUnitX, PointsPerUnitY, WorldToMeters);
AllPoints.Append(FloorPoints);
if (AllPoints.Num() > MaxPointsCount)
{
// Shuffle the array
AllPoints.Sort([](const FVector& /*Item1*/, const FVector& /*Item2*/) {
return FMath::FRand() < 0.5f;
});
// Randomly remove some points
int32 PointsToRemoveCount = AllPoints.Num() - MaxPointsCount;
while (PointsToRemoveCount > 0)
{
AllPoints.Pop();
--PointsToRemoveCount;
}
}
return AllPoints;
}
void UMRUKBPLibrary::CreateMeshSegmentation(const TArray<FVector>& MeshPositions, const TArray<uint32>& MeshIndices,
const TArray<FVector>& SegmentationPoints, const FVector& ReservedMin, const FVector& ReservedMax,
TArray<FMRUKMeshSegment>& OutSegments, FMRUKMeshSegment& OutReservedSegment)
{
if (!MRUKShared::GetInstance())
{
UE_LOG(LogMRUK, Error, TEXT("MRUK shared library is not available. To use this functionality make sure the library is included"));
return;
}
TArray<FVector3f> MeshPositionsF;
MeshPositionsF.Reserve(MeshPositions.Num());
for (const FVector& V : MeshPositions)
{
MeshPositionsF.Add(FVector3f(V));
}
TArray<FVector3f> SegmentationPointsF;
SegmentationPointsF.Reserve(SegmentationPoints.Num());
for (const FVector& V : SegmentationPoints)
{
SegmentationPointsF.Add(FVector3f(V));
}
MRUKShared::MrukMesh3f* MeshSegmentsF = nullptr;
uint32_t MeshSegmentsCount = 0;
MRUKShared::MrukMesh3f ReservedMeshSegmentF{};
const FVector3f ReservedMinF(ReservedMin);
const FVector3f ReservedMaxF(ReservedMax);
MRUKShared::GetInstance()->ComputeMeshSegmentation(MeshPositionsF.GetData(), MeshPositionsF.Num(), MeshIndices.GetData(),
MeshIndices.Num(), SegmentationPointsF.GetData(), SegmentationPointsF.Num(), ReservedMinF, ReservedMaxF, &MeshSegmentsF,
&MeshSegmentsCount, &ReservedMeshSegmentF);
OutSegments.Reserve(MeshSegmentsCount);
for (uint32_t i = 0; i < MeshSegmentsCount; ++i)
{
const MRUKShared::MrukMesh3f& SegmentF = MeshSegmentsF[i];
if (SegmentF.numIndices == 0)
{
continue;
}
FMRUKMeshSegment MeshSegment{};
MeshSegment.Indices.Reserve(SegmentF.numIndices);
MeshSegment.Positions.Reserve(SegmentF.numVertices);
for (uint32_t j = 0; j < SegmentF.numIndices; ++j)
{
MeshSegment.Indices.Add(SegmentF.indices[j]);
}
for (uint32_t j = 0; j < SegmentF.numVertices; ++j)
{
const FVector3f& V = SegmentF.vertices[j];
MeshSegment.Positions.Add({ V.X, V.Y, V.Z });
}
OutSegments.Emplace(MoveTemp(MeshSegment));
}
if (ReservedMeshSegmentF.numIndices && ReservedMeshSegmentF.numVertices)
{
OutReservedSegment.Indices.Reserve(ReservedMeshSegmentF.numIndices);
OutReservedSegment.Positions.Reserve(ReservedMeshSegmentF.numVertices);
for (uint32_t j = 0; j < ReservedMeshSegmentF.numIndices; ++j)
{
OutReservedSegment.Indices.Add(ReservedMeshSegmentF.indices[j]);
}
for (uint32_t j = 0; j < ReservedMeshSegmentF.numVertices; ++j)
{
const FVector3f& V = ReservedMeshSegmentF.vertices[j];
OutReservedSegment.Positions.Add({ V.X, V.Y, V.Z });
}
}
MRUKShared::GetInstance()->FreeMeshSegmentation(MeshSegmentsF, MeshSegmentsCount, &ReservedMeshSegmentF);
}

View File

@@ -0,0 +1,145 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "MRUtilityKitBlobShadowComponent.h"
#include "MRUtilityKitTelemetry.h"
#include "MRUtilityKit.h"
#include "Kismet/KismetSystemLibrary.h"
#include "UObject/ConstructorHelpers.h"
#include "Materials/MaterialInstance.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Engine/StaticMesh.h"
UMRUKBlobShadowComponent::UMRUKBlobShadowComponent()
{
const ConstructorHelpers::FObjectFinder<UStaticMesh> PlaneAsset(TEXT("/Engine/BasicShapes/Plane"));
if (PlaneAsset.Succeeded())
{
SetStaticMesh(PlaneAsset.Object);
}
else
{
UE_LOG(LogMRUK, Log, TEXT("Blob shadow couldn't find plane mesh in /Engine/BasicShapes/Plane"));
}
const ConstructorHelpers::FObjectFinder<UMaterialInstance> BlobShadowMaterialAsset(TEXT("/OculusXR/Materials/MI_BlobShadow"));
if (BlobShadowMaterialAsset.Succeeded())
{
SetMaterial(0, BlobShadowMaterialAsset.Object);
}
else
{
UE_LOG(LogMRUK, Log, TEXT("Blob shadow couldn't find blob shadow material in /OculusXR/Materials/MI_BlobShadow"));
}
// Prevent sorting issue with transparent ground
SetTranslucentSortPriority(1);
// We don't want any collision
SetCollisionProfileName("NoCollision");
// Need tick to be enabled
SetComponentTickEnabled(true);
PrimaryComponentTick.bCanEverTick = true;
bAutoActivate = true;
}
void UMRUKBlobShadowComponent::BeginPlay()
{
Super::BeginPlay();
OculusXRTelemetry::TScopedMarker<MRUKTelemetry::FLoadBlobShadowMarker> Event(static_cast<int>(GetTypeHash(this)));
// Create dynamic material (for roundness and gradient settings)
DynMaterial = CreateAndSetMaterialInstanceDynamic(0);
// Since we're updating the component size and position every frame it's better to not be influenced by parent
SetUsingAbsoluteLocation(true);
SetUsingAbsoluteRotation(true);
SetUsingAbsoluteScale(true);
// Compute size and position once
UpdatePlaneSizeAndPosition();
}
void UMRUKBlobShadowComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// Update component size and position every frame
UpdatePlaneSizeAndPosition();
}
void UMRUKBlobShadowComponent::UpdatePlaneSizeAndPosition()
{
FVector Origin;
FVector2D Extent;
double Yaw;
ComputeOwner2DBounds(Origin, Extent, Yaw);
Extent += FVector2D::UnitVector * ExtraExtent; // Additional extent
SetWorldScale3D(FVector(Extent * 0.02f, 1.f)); // Plane mesh is 100x100, multiplying by 0.02f to match the correct size when scaling
SetWorldRotation(FRotator(0.f, Yaw, 0.f));
// Sphere trace to the ground
FHitResult Hit;
TArray<AActor*> ActorsToIgnore;
ActorsToIgnore.Add(GetOwner());
const bool bHasHit = UKismetSystemLibrary::SphereTraceSingle(this, Origin, Origin + FVector::DownVector * MaxVerticalDistance, Extent.Length() * 0.5f, TraceTypeQuery1,
true, ActorsToIgnore, EDrawDebugTrace::None, Hit, true);
float Opacity = 0.f;
if (bHasHit)
{
SetHiddenInGame(false); // Make plane visible
SetWorldLocation(Hit.ImpactPoint + FVector::UpVector * 0.02f); // Impact + some offset to avoid Z-fighting
Opacity = FMath::GetMappedRangeValueClamped(
FVector2D(MaxVerticalDistance - FadeDistance, MaxVerticalDistance),
FVector2D(1.f, 0.f),
Hit.Distance); // Set opacity based on distance to ground
}
else
SetHiddenInGame(true); // Hide plane
// Update material's parameters
if (DynMaterial)
{
DynMaterial->SetScalarParameterValue("CornerWorldSize", FMath::Min(Extent.X, Extent.Y) * Roundness);
DynMaterial->SetScalarParameterValue("Gradient", Gradient);
DynMaterial->SetScalarParameterValue("GradientPower", GradientPower);
DynMaterial->SetScalarParameterValue("Opacity", Opacity);
}
else // In case DynMaterial doesn't exist (e.g. in editor), update values directly on the mesh
{
SetScalarParameterValueOnMaterials("CornerWorldSize", FMath::Min(Extent.X, Extent.Y) * Roundness);
SetScalarParameterValueOnMaterials("Gradient", Gradient);
SetScalarParameterValueOnMaterials("GradientPower", GradientPower);
SetScalarParameterValueOnMaterials("Opacity", Opacity);
}
}
void UMRUKBlobShadowComponent::ComputeOwner2DBounds(FVector& Origin, FVector2D& Extent, double& Yaw) const
{
const AActor* Actor = GetOwner();
// Calculate local space BoundingBox from all components, but keep yaw to have a correct 2D bounding box at the end
FBox Box(ForceInit);
const FRotator YawOnly = FRotator(0.f, Actor->GetActorRotation().Yaw, 0.f);
const FTransform ActorToWorld = FTransform(YawOnly.Quaternion());
const FTransform WorldToActor = ActorToWorld.Inverse();
Actor->ForEachComponent<UPrimitiveComponent>(true, [&](const UPrimitiveComponent* InPrimComp) {
// Ignore editor & blob shadow components
if (InPrimComp->IsRegistered() && !InPrimComp->IsEditorOnly() && !InPrimComp->bUseAttachParentBound && !InPrimComp->IsA<UMRUKBlobShadowComponent>())
{
const FTransform ComponentToActor = InPrimComp->GetComponentTransform() * WorldToActor;
Box += InPrimComp->CalcBounds(ComponentToActor).GetBox();
}
});
const FTransform Transform = Actor->GetTransform();
// Project 3D extent to 2D
const FVector ProjectedExtent = FVector::VectorPlaneProject(Box.GetExtent(), FVector::UpVector);
Origin = ActorToWorld.TransformPosition(Box.GetCenter());
Extent = FVector2D(ProjectedExtent);
Yaw = Transform.GetRotation().Rotator().Yaw;
}

View File

@@ -0,0 +1,355 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "MRUtilityKitData.h"
#include "MRUtilityKitSubsystem.h"
#include "MRUtilityKitSerializationHelpers.h"
#include "MRUtilityKitTelemetry.h"
#include "OculusXRAnchorBPFunctionLibrary.h"
#include "OculusXRScene.h"
#include "Engine/World.h"
#include "Engine/GameInstance.h"
#include "GameFramework/WorldSettings.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
AMRUKLocalizer::AMRUKLocalizer()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;
}
void AMRUKLocalizer::Tick(float DeltaTime)
{
for (int i = 0; i < AnchorsData.Num(); ++i)
{
const auto Query = AnchorsData[i];
if (UOculusXRAnchorBPFunctionLibrary::GetAnchorTransformByHandle(Query->SpaceQuery.Space, Query->Transform))
{
Query->NeedAnchorLocalization = false;
if (Query->SemanticClassifications.IsEmpty())
{
UE_LOG(LogMRUK, Log, TEXT("Localized anchor %s"), *Query->SpaceQuery.UUID.ToString());
}
else
{
UE_LOG(LogMRUK, Log, TEXT("Localized anchor %s - %s"), *Query->SpaceQuery.UUID.ToString(), *Query->SemanticClassifications[0]);
}
AnchorsData.RemoveAt(i);
--i;
}
}
if (AnchorsData.IsEmpty())
{
UE_LOG(LogMRUK, Log, TEXT("All anchors localized"));
OnComplete.Broadcast(true);
}
}
void UMRUKAnchorData::LoadFromDevice(const FOculusXRAnchorsDiscoverResult& AnchorsDiscoverResult)
{
SpaceQuery = AnchorsDiscoverResult;
Transform = FTransform::Identity;
NeedAnchorLocalization = false;
if (!UOculusXRAnchorBPFunctionLibrary::GetAnchorTransformByHandle(SpaceQuery.Space, Transform))
{
UE_LOG(LogMRUK, Log, TEXT("Anchor %s is not localized yet. Localize it async."), *SpaceQuery.UUID.ToString());
NeedAnchorLocalization = true;
}
EOculusXRAnchorResult::Type Result = OculusXRScene::FOculusXRScene::GetSemanticClassification(SpaceQuery.Space.Value, SemanticClassifications);
if (!UOculusXRAnchorBPFunctionLibrary::IsAnchorResultSuccess(Result))
{
UE_LOG(LogMRUK, Error, TEXT("Failed to get semantic classification space for %s."), *SpaceQuery.UUID.ToString());
}
const UWorld* World = GetWorld();
const float WorldToMeters = World ? World->GetWorldSettings()->WorldToMeters : 100.0;
FVector ScenePlanePos;
FVector ScenePlaneSize;
Result = OculusXRScene::FOculusXRScene::GetScenePlane(SpaceQuery.Space, ScenePlanePos, ScenePlaneSize);
if (UOculusXRAnchorBPFunctionLibrary::IsAnchorResultSuccess(Result))
{
const FVector2D PlanePos = FVector2D(ScenePlanePos.Y, ScenePlanePos.Z) * WorldToMeters;
const FVector2D PlaneSize = FVector2D(ScenePlaneSize.Y, ScenePlaneSize.Z) * WorldToMeters;
PlaneBounds = FBox2D(PlanePos, PlanePos + PlaneSize);
TArray<FVector2f> SpaceBoundary2D;
Result = OculusXRScene::FOculusXRScene::GetBoundary2D(SpaceQuery.Space, SpaceBoundary2D);
if (UOculusXRAnchorBPFunctionLibrary::IsAnchorResultSuccess(Result))
{
PlaneBoundary2D.Reserve(SpaceBoundary2D.Num());
for (int i = 0; i < SpaceBoundary2D.Num(); ++i)
{
PlaneBoundary2D.Push(FVector2D(SpaceBoundary2D[i].X * WorldToMeters, SpaceBoundary2D[i].Y * WorldToMeters));
}
}
}
FVector SceneVolumePos;
FVector SceneVolumeSize;
Result = OculusXRScene::FOculusXRScene::GetSceneVolume(SpaceQuery.Space, SceneVolumePos, SceneVolumeSize);
if (UOculusXRAnchorBPFunctionLibrary::IsAnchorResultSuccess(Result))
{
const FVector VolumePos = SceneVolumePos * WorldToMeters;
const FVector VolumeSize = SceneVolumeSize * WorldToMeters;
VolumeBounds = FBox(VolumePos, VolumePos + VolumeSize);
}
}
void UMRUKAnchorData::LoadFromJson(const FJsonValue& Value)
{
const auto Object = Value.AsObject();
MRUKDeserialize(*Object->GetField<EJson::None>(TEXT("UUID")), SpaceQuery.UUID);
MRUKDeserialize(*Object->GetField<EJson::None>(TEXT("SemanticClassifications")), SemanticClassifications);
MRUKDeserialize(*Object->GetField<EJson::None>(TEXT("Transform")), Transform);
if (const auto JsonValue = Object->TryGetField(TEXT("PlaneBounds")))
{
MRUKDeserialize(*JsonValue, PlaneBounds);
}
if (const auto JsonValue = Object->TryGetField(TEXT("PlaneBoundary2D")))
{
MRUKDeserialize(*JsonValue, PlaneBoundary2D);
}
if (const auto JsonValue = Object->TryGetField(TEXT("VolumeBounds")))
{
MRUKDeserialize(*JsonValue, VolumeBounds);
}
NeedAnchorLocalization = false;
}
void UMRUKRoomData::LoadFromDevice(UMRUKSceneData* Data, const FOculusXRAnchorsDiscoverResult& AnchorsDiscoverResult)
{
SceneData = Data;
SpaceQuery = AnchorsDiscoverResult;
const auto Subsystem = GetWorld()->GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
if (!Subsystem->GetRoomLayoutManager()->GetRoomLayout(SpaceQuery.Space.Value, RoomLayout))
{
UE_LOG(LogMRUK, Error, TEXT("Could not query room layout"));
FinishQuery(false);
return;
}
EOculusXRAnchorResult::Type Result{};
const auto Filter = NewObject<UOculusXRSpaceDiscoveryIdsFilter>(this);
Filter->Uuids = RoomLayout.RoomObjectUUIDs;
FOculusXRSpaceDiscoveryInfo DiscoveryInfo{};
DiscoveryInfo.Filters.Push(Filter);
OculusXRAnchors::FOculusXRAnchors::DiscoverAnchors(DiscoveryInfo, FOculusXRDiscoverAnchorsResultsDelegate::CreateUObject(this, &UMRUKRoomData::RoomDataLoadedIncrementalResults), FOculusXRDiscoverAnchorsCompleteDelegate::CreateUObject(this, &UMRUKRoomData::RoomDataLoadedComplete), Result);
if (Result != EOculusXRAnchorResult::Success)
{
UE_LOG(LogMRUK, Error, TEXT("Failed to discover anchors"));
FinishQuery(false);
}
}
void UMRUKRoomData::LoadFromJson(UMRUKSceneData* Data, const FJsonValue& Value)
{
SceneData = Data;
const auto Object = Value.AsObject();
MRUKDeserialize(*Object->GetField<EJson::None>(TEXT("UUID")), SpaceQuery.UUID);
MRUKDeserialize(*Object->GetField<EJson::None>(TEXT("RoomLayout")), RoomLayout);
auto AnchorsJson = Object->GetArrayField(TEXT("Anchors"));
for (const auto& AnchorJson : AnchorsJson)
{
auto AnchorQuery = NewObject<UMRUKAnchorData>(this);
AnchorsData.Push(AnchorQuery);
RoomLayout.RoomObjectUUIDs.Add(AnchorQuery->SpaceQuery.UUID);
AnchorQuery->LoadFromJson(*AnchorJson);
}
FinishQuery(true);
}
void UMRUKRoomData::FinishQuery(bool Success)
{
OnComplete.Broadcast(Success);
}
void UMRUKRoomData::RoomDataLoadedComplete(EOculusXRAnchorResult::Type Result)
{
if (!UOculusXRAnchorBPFunctionLibrary::IsAnchorResultSuccess(Result))
{
UE_LOG(LogMRUK, Error, TEXT("Discovering room data failed"));
FinishQuery(false);
return;
}
if (AnchorsData.Num() == 0)
{
UE_LOG(LogMRUK, Warning, TEXT("Discovered room which doesn't contain any anchors. Skip that room"));
SceneData->RoomsData.Remove(this);
AnchorsInitialized(true);
return;
}
TArray<UMRUKAnchorData*> AnchorQueriesLocalization;
for (auto& AnchorQuery : AnchorsData)
{
if (AnchorQuery->NeedAnchorLocalization)
{
AnchorQueriesLocalization.Push(AnchorQuery);
}
}
if (!AnchorQueriesLocalization.IsEmpty())
{
UE_LOG(LogMRUK, Log, TEXT("Could not localize all anchors. Going to localize them async"));
FActorSpawnParameters ActorSpawnParams;
ActorSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
LocalizationActor = GetWorld()->SpawnActor<AMRUKLocalizer>(ActorSpawnParams);
LocalizationActor->AnchorsData = AnchorQueriesLocalization;
LocalizationActor->OnComplete.AddDynamic(this, &UMRUKRoomData::AnchorsInitialized);
}
else
{
AnchorsInitialized(true);
}
}
void UMRUKRoomData::RoomDataLoadedIncrementalResults(const TArray<FOculusXRAnchorsDiscoverResult>& DiscoverResults)
{
// NOTE: This function may be called multiple times in batches. E.g. if there are 18 anchors in a room, this may
// be called once with 10 anchors and a second time with 8 anchors in DiscoverResults.
UE_LOG(LogMRUK, Log, TEXT("Received %d anchors from device"), DiscoverResults.Num());
for (auto& DiscoverResult : DiscoverResults)
{
auto AnchorQuery = NewObject<UMRUKAnchorData>(this);
AnchorQuery->LoadFromDevice(DiscoverResult);
AnchorsData.Push(AnchorQuery);
}
}
void UMRUKRoomData::AnchorsInitialized(bool Success)
{
UE_LOG(LogMRUK, Log, TEXT("Anchors data initialized Success==%d"), Success);
if (IsValid(LocalizationActor))
{
LocalizationActor->Destroy();
LocalizationActor = nullptr;
}
FinishQuery(Success);
}
void UMRUKSceneData::LoadFromDevice()
{
NumRoomsLeftToInitialize = 0;
EOculusXRAnchorResult::Type Result{};
const auto Filter = NewObject<UOculusXRSpaceDiscoveryComponentsFilter>(this);
Filter->ComponentType = EOculusXRSpaceComponentType::RoomLayout;
FOculusXRSpaceDiscoveryInfo DiscoveryInfo{};
DiscoveryInfo.Filters.Push(Filter);
OculusXRAnchors::FOculusXRAnchors::DiscoverAnchors(DiscoveryInfo, FOculusXRDiscoverAnchorsResultsDelegate::CreateUObject(this, &UMRUKSceneData::SceneDataLoadedComplete), FOculusXRDiscoverAnchorsCompleteDelegate::CreateUObject(this, &UMRUKSceneData::SceneDataLoadedResult), Result);
if (Result != EOculusXRAnchorResult::Success)
{
UE_LOG(LogMRUK, Error, TEXT("Failed to discover room layouts"));
FinishQuery(false);
}
}
void UMRUKSceneData::LoadFromJson(const FString& Json)
{
TSharedPtr<FJsonValue> Value;
const TSharedRef<TJsonReader<>> JsonReader = TJsonReaderFactory<>::Create(Json);
if (!FJsonSerializer::Deserialize(JsonReader, Value))
{
UE_LOG(LogMRUK, Warning, TEXT("Could not deserialize JSON scene data: %s"), *JsonReader->GetErrorMessage());
FinishQuery(false);
return;
}
const auto Object = Value->AsObject();
auto RoomsJson = Object->GetArrayField(TEXT("Rooms"));
OculusXRTelemetry::TScopedMarker<MRUKTelemetry::FLoadSceneFromJsonMarker> Event(static_cast<int>(GetTypeHash(this)));
Event.AddAnnotation("NumRooms", TCHAR_TO_ANSI(*FString::FromInt(RoomsJson.Num())));
Event.SetResult(RoomsJson.Num() > 0 ? OculusXRTelemetry::EAction::Success : OculusXRTelemetry::EAction::Fail);
if (RoomsJson.IsEmpty())
{
UE_LOG(LogMRUK, Warning, TEXT("Could not find Rooms in JSON"));
FinishQuery(false);
return;
}
NumRoomsLeftToInitialize = RoomsJson.Num();
UE_LOG(LogMRUK, Log, TEXT("Found %d rooms in JSON"), NumRoomsLeftToInitialize);
for (const auto& RoomJson : RoomsJson)
{
auto RoomQuery = NewObject<UMRUKRoomData>(this);
RoomsData.Push(RoomQuery);
RoomQuery->OnComplete.AddDynamic(this, &UMRUKSceneData::RoomQueryComplete);
RoomQuery->LoadFromJson(this, *RoomJson);
}
}
void UMRUKSceneData::FinishQuery(bool Success)
{
if (!Success)
{
AnyRoomFailed = true;
}
--NumRoomsLeftToInitialize;
if (NumRoomsLeftToInitialize <= 0)
{
OnComplete.Broadcast(!AnyRoomFailed);
}
}
void UMRUKSceneData::SceneDataLoadedResult(EOculusXRAnchorResult::Type Result)
{
if (!UOculusXRAnchorBPFunctionLibrary::IsAnchorResultSuccess(Result) || RoomsData.IsEmpty())
{
UE_LOG(LogMRUK, Error, TEXT("Discovering room layouts failed"));
FinishQuery(false);
}
}
void UMRUKSceneData::SceneDataLoadedComplete(const TArray<FOculusXRAnchorsDiscoverResult>& DiscoverResults)
{
NumRoomsLeftToInitialize = DiscoverResults.Num();
UE_LOG(LogMRUK, Log, TEXT("Found on %d rooms on the device"), NumRoomsLeftToInitialize);
OculusXRTelemetry::TScopedMarker<MRUKTelemetry::FLoadSceneFromDeviceMarker> Event(static_cast<int>(GetTypeHash(this)));
Event.AddAnnotation("NumRooms", TCHAR_TO_ANSI(*FString::FromInt(DiscoverResults.Num())));
Event.SetResult(DiscoverResults.Num() > 0 ? OculusXRTelemetry::EAction::Success : OculusXRTelemetry::EAction::Fail);
if (NumRoomsLeftToInitialize == 0)
{
UE_LOG(LogMRUK, Error, TEXT("No room layouts discovered"));
FinishQuery(false);
return;
}
for (auto& DiscoverResult : DiscoverResults)
{
auto RoomQuery = NewObject<UMRUKRoomData>(this);
RoomsData.Push(RoomQuery);
RoomQuery->OnComplete.AddDynamic(this, &UMRUKSceneData::RoomQueryComplete);
RoomQuery->LoadFromDevice(this, DiscoverResult);
}
}
void UMRUKSceneData::RoomQueryComplete(bool Success)
{
if (!Success)
{
AnyRoomFailed = true;
}
--NumRoomsLeftToInitialize;
if (NumRoomsLeftToInitialize == 0)
{
FinishQuery(!AnyRoomFailed);
}
}

View File

@@ -0,0 +1,208 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "MRUtilityKitDebugComponent.h"
#include "MRUtilityKitTelemetry.h"
#include "MRUtilityKitSubsystem.h"
#include "MRUtilityKitAnchor.h"
#include "Kismet/KismetMathLibrary.h"
#include "IXRTrackingSystem.h"
#include "TextRenderComponent.h"
#include "Engine/World.h"
#include "Engine/GameInstance.h"
#include "Engine/Engine.h"
UMRUKDebugComponent::UMRUKDebugComponent()
{
PrimaryComponentTick.bCanEverTick = true;
}
void UMRUKDebugComponent::ShowAnchorAtRayHit(const FVector& Origin, const FVector& Direction)
{
if (!GizmoActorClass)
{
UE_LOG(LogMRUK, Warning, TEXT("Can not show anchor because no gizmo actor is set"));
return;
}
HideAnchor();
const auto Subsystem = GetOwner()->GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
if (!Subsystem)
{
UE_LOG(LogMRUK, Warning, TEXT("Can not show anchor because there is no MRUtilityKit subsystem"));
return;
}
FMRUKHit Hit{};
FMRUKLabelFilter LabelFilter{};
auto Anchor = Subsystem->Raycast(Origin, Direction, 0.0, LabelFilter, Hit);
if (!Anchor)
{
return;
}
// Spawn Gizmo
if (!ActiveGizmoActor)
{
FActorSpawnParameters ActorSpawnParams;
ActorSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
ActorSpawnParams.Owner = GetOwner();
ActiveGizmoActor = GetWorld()->SpawnActor(GizmoActorClass, nullptr, ActorSpawnParams);
}
else
{
ActiveGizmoActor->SetActorHiddenInGame(false);
}
ActiveGizmoActor->SetActorLocation(Hit.HitPosition);
ActiveGizmoActor->SetActorScale3D(GizmoScale);
ActiveGizmoActor->SetActorRotation(Anchor->GetActorRotation());
if (!TextActorClass)
{
UE_LOG(LogMRUK, Warning, TEXT("Can not show text at anchor because no text actor is set"));
return;
}
// Spawn Text
if (!ActiveTextActor)
{
FActorSpawnParameters ActorSpawnParams;
ActorSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
ActorSpawnParams.Owner = GetOwner();
ActiveTextActor = GetWorld()->SpawnActor(TextActorClass, nullptr, ActorSpawnParams);
}
else
{
ActiveTextActor->SetActorHiddenInGame(false);
}
auto TextRenderComponent = ActiveTextActor->GetComponentByClass<UTextRenderComponent>();
FString Text;
for (int i = 0; i < Anchor->SemanticClassifications.Num(); ++i)
{
if (i != 0)
{
Text += ", ";
}
Text += Anchor->SemanticClassifications[i];
}
TextRenderComponent->SetText(FText::FromString(Text));
ActiveTextActor->SetActorLocation(Hit.HitPosition + (Hit.HitNormal * 20.0));
ActiveTextActor->SetActorScale3D(TextScale);
OrientTextActorToPlayer();
SetComponentTickEnabled(true);
}
void UMRUKDebugComponent::HideAnchor()
{
if (ActiveGizmoActor)
{
ActiveGizmoActor->SetActorHiddenInGame(true);
}
if (ActiveTextActor)
{
ActiveTextActor->SetActorHiddenInGame(true);
}
SetComponentTickEnabled(false);
}
void UMRUKDebugComponent::ShowAnchorSpaceAtRayHit(const FVector& Origin, const FVector& Direction)
{
const auto Subsystem = GetOwner()->GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
if (!Subsystem)
{
UE_LOG(LogMRUK, Warning, TEXT("Can not show anchor because there is no MRUtilityKit subsystem"));
return;
}
FMRUKHit Hit{};
const auto Anchor = Subsystem->Raycast(Origin, Direction, 0.0, {}, Hit);
if (!Anchor)
{
return;
}
if (!ActiveAnchorSpaceActor || (ActiveAnchorSpaceActor && ActiveAnchorSpaceActor->GetParentActor() != Anchor))
{
static constexpr double DebugSpaceOffset = 0.5;
HideAnchorSpace();
FActorSpawnParameters ActorSpawnParams;
ActorSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
ActorSpawnParams.Owner = Anchor;
ActiveAnchorSpaceActor = GetWorld()->SpawnActor<AActor>(ActorSpawnParams);
ActiveAnchorSpaceActor->SetRootComponent(NewObject<USceneComponent>(ActiveAnchorSpaceActor, TEXT("SceneComponent")));
ActiveAnchorSpaceActor->AttachToActor(Anchor, FAttachmentTransformRules::KeepRelativeTransform);
ActiveAnchorSpaceActor->GetRootComponent()->SetMobility(EComponentMobility::Movable);
const auto ProceduralMesh = NewObject<UProceduralMeshComponent>(ActiveAnchorSpaceActor, TEXT("DebugVolumePlane"));
Anchor->GenerateProceduralAnchorMesh(ProceduralMesh, {}, {}, true, false, DebugSpaceOffset);
ActiveAnchorSpaceActor->AddInstanceComponent(ProceduralMesh);
ProceduralMesh->SetupAttachment(ActiveAnchorSpaceActor->GetRootComponent());
ProceduralMesh->RegisterComponent();
}
}
void UMRUKDebugComponent::HideAnchorSpace()
{
if (ActiveAnchorSpaceActor)
{
ActiveAnchorSpaceActor->Destroy();
ActiveAnchorSpaceActor = nullptr;
}
}
void UMRUKDebugComponent::BeginPlay()
{
Super::BeginPlay();
OculusXRTelemetry::TScopedMarker<MRUKTelemetry::FLoadDebugComponentMarker> Event(static_cast<int>(GetTypeHash(this)));
SetComponentTickEnabled(false);
}
void UMRUKDebugComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
UE_LOG(LogTemp, Warning, TEXT("Ticking enabled"));
OrientTextActorToPlayer();
}
void UMRUKDebugComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
if (ActiveGizmoActor)
{
ActiveGizmoActor->Destroy();
ActiveGizmoActor = nullptr;
}
if (ActiveTextActor)
{
ActiveTextActor->Destroy();
ActiveTextActor = nullptr;
}
if (ActiveAnchorSpaceActor)
{
ActiveAnchorSpaceActor->Destroy();
ActiveAnchorSpaceActor = nullptr;
}
}
void UMRUKDebugComponent::OrientTextActorToPlayer() const
{
if (ActiveTextActor)
{
FQuat Orientation;
FVector Position(0.0);
GEngine->XRSystem->GetCurrentPose(IXRTrackingSystem::HMDDeviceId, Orientation, Position);
const auto TextForward = (Position - ActiveTextActor->GetActorLocation()).GetSafeNormal();
const auto TextUp = FVector::UpVector;
const auto TextRot = UKismetMathLibrary::MakeRotFromXZ(TextForward, TextUp);
ActiveTextActor->SetActorRotation(TextRot);
}
}

View File

@@ -0,0 +1,248 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "MRUtilityKitDestructibleMesh.h"
#include "Engine/GameInstance.h"
#include "MRUtilityKitBPLibrary.h"
#include "MRUtilityKitSubsystem.h"
#include "MRUtilityKitAnchor.h"
#include "MRUtilityKitRoom.h"
#include "MRUtilityKitTelemetry.h"
#include "OculusXRTelemetry.h"
#include "Tasks/Task.h"
constexpr const char* RESERVED_MESH_SEGMENT_TAG = "ReservedMeshSegment";
UMRUKDestructibleMeshComponent::UMRUKDestructibleMeshComponent(const FObjectInitializer& ObjectInitializer)
: UProceduralMeshComponent(ObjectInitializer)
{
PrimaryComponentTick.bCanEverTick = true;
}
void UMRUKDestructibleMeshComponent::SegmentMesh(const TArray<FVector>& MeshPositions, const TArray<uint32>& MeshIndices, const TArray<FVector>& SegmentationPoints)
{
TaskResult = UE::Tasks::Launch(UE_SOURCE_LOCATION, [this, MeshPositions, MeshIndices, SegmentationPoints]() {
TArray<FMRUKMeshSegment> Segments;
FMRUKMeshSegment ReservedMeshSegment;
const FVector ReservedMin(ReservedTop, -1.0, -1.0);
const FVector ReservedMax(ReservedBottom, -1.0, -1.0);
UMRUKBPLibrary::CreateMeshSegmentation(MeshPositions, MeshIndices, SegmentationPoints, ReservedMin, ReservedMax, Segments, ReservedMeshSegment);
return TPair<TArray<FMRUKMeshSegment>, FMRUKMeshSegment>{ MoveTemp(Segments), MoveTemp(ReservedMeshSegment) };
});
SetComponentTickEnabled(true);
}
void UMRUKDestructibleMeshComponent::BeginPlay()
{
Super::BeginPlay();
SetComponentTickEnabled(false);
}
void UMRUKDestructibleMeshComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (!TaskResult.IsCompleted())
{
return;
}
const auto& [MeshSegments, ReservedMeshSegment] = TaskResult.GetResult();
for (int32 i = 0; i < MeshSegments.Num(); ++i)
{
const auto& [Positions, Indices] = MeshSegments[i];
const FString ProcMeshName = FString::Printf(TEXT("DestructibleMeshSegment%d"), i);
const auto ProcMesh = NewObject<UProceduralMeshComponent>(GetOwner(), *ProcMeshName);
const FAttachmentTransformRules TransformRules{ EAttachmentRule::KeepRelative, false };
ProcMesh->AttachToComponent(GetOwner()->GetRootComponent(), TransformRules);
ProcMesh->RegisterComponent();
ProcMesh->ComponentTags.AddUnique(TEXT("DestructibleMeshSegment"));
GetOwner()->AddInstanceComponent(ProcMesh);
ProcMesh->CreateMeshSection(0, Positions, Indices, {}, {}, {}, {}, true);
if (GlobalMeshMaterial)
{
ProcMesh->SetMaterial(0, GlobalMeshMaterial);
}
}
if (ReservedMeshSegment.Indices.Num() > 0)
{
const auto ProcMesh = NewObject<UProceduralMeshComponent>(GetOwner(), TEXT("ReservedMeshSegment"));
const FAttachmentTransformRules TransformRules{ EAttachmentRule::KeepRelative, false };
ProcMesh->AttachToComponent(GetOwner()->GetRootComponent(), TransformRules);
ProcMesh->RegisterComponent();
ProcMesh->ComponentTags.AddUnique(RESERVED_MESH_SEGMENT_TAG);
GetOwner()->AddInstanceComponent(ProcMesh);
ProcMesh->CreateMeshSection(0, ReservedMeshSegment.Positions, ReservedMeshSegment.Indices, {}, {}, {}, {}, true);
if (GlobalMeshMaterial)
{
ProcMesh->SetMaterial(0, GlobalMeshMaterial);
}
}
SetComponentTickEnabled(false);
OnMeshesGenerated.Broadcast();
}
AMRUKDestructibleGlobalMesh::AMRUKDestructibleGlobalMesh()
{
DestructibleMeshComponent = CreateDefaultSubobject<UMRUKDestructibleMeshComponent>(TEXT("DestructibleMesh"));
SetRootComponent(DestructibleMeshComponent);
}
void AMRUKDestructibleGlobalMesh::CreateDestructibleMesh(AMRUKRoom* Room)
{
const UMRUKSubsystem* MRUKSubsystem = GetWorld()->GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
if (!Room)
{
Room = MRUKSubsystem->GetCurrentRoom();
}
if (!Room)
{
UE_LOG(LogMRUK, Warning, TEXT("Could not find a room for the destructible mesh"));
return;
}
if (!Room->GlobalMeshAnchor)
{
UE_LOG(LogMRUK, Warning, TEXT("No global mesh available for creating a destructible mesh"));
return;
}
const AMRUKAnchor* GlobalMesh = Room->GlobalMeshAnchor;
UProceduralMeshComponent* GlobalProcMesh = Cast<UProceduralMeshComponent>(GlobalMesh->GetComponentByClass(UProceduralMeshComponent::StaticClass()));
if (!GlobalProcMesh)
{
Room->LoadGlobalMeshFromDevice();
}
if (!GlobalProcMesh)
{
UE_LOG(LogMRUK, Warning, TEXT("Could not load a triangle mesh from the global mesh"));
return;
}
// Attach to the global mesh
const FAttachmentTransformRules AttachmentTransformRules{ EAttachmentRule::KeepRelative, false };
AttachToActor(Room->GlobalMeshAnchor, AttachmentTransformRules);
// Get global mesh data
ensure(GlobalProcMesh);
ensure(GlobalProcMesh->ComponentHasTag("GlobalMesh"));
FProcMeshSection* ProcMeshSection = GlobalProcMesh->GetProcMeshSection(0);
TArray<FVector> MeshPositions;
MeshPositions.SetNum(ProcMeshSection->ProcVertexBuffer.Num());
for (int32 i = 0; i < ProcMeshSection->ProcVertexBuffer.Num(); ++i)
{
MeshPositions[i] = ProcMeshSection->ProcVertexBuffer[i].Position * GetWorldSettings()->WorldToMeters;
}
const TArray<uint32>& MeshIndices = ProcMeshSection->ProcIndexBuffer;
TArray<FVector> SegmentationPointsWS = UMRUKBPLibrary::ComputeRoomBoxGrid(Room, MaxPointsCount, PointsPerUnitX, PointsPerUnitY);
TArray<FVector> SegmentationPointsLS;
SegmentationPointsLS.SetNum(SegmentationPointsWS.Num());
const FTransform T = GlobalMesh->GetActorTransform().Inverse();
for (int32 i = 0; i < SegmentationPointsWS.Num(); ++i)
{
SegmentationPointsLS[i] = T.TransformPosition(SegmentationPointsWS[i]);
}
DestructibleMeshComponent->SegmentMesh(MeshPositions, MeshIndices, SegmentationPointsLS);
}
void AMRUKDestructibleGlobalMesh::RemoveGlobalMeshSegment(UPrimitiveComponent* Mesh)
{
if (!Mesh->ComponentTags.Contains(RESERVED_MESH_SEGMENT_TAG))
{
// Only remove mesh segments that are allowed to be destroyed
Mesh->DestroyComponent();
}
}
void AMRUKDestructibleGlobalMeshSpawner::BeginPlay()
{
Super::BeginPlay();
OculusXRTelemetry::TScopedMarker<MRUKTelemetry::FLoadDestructibleGlobalMeshSpawner> Event(static_cast<int>(GetTypeHash(this)));
if (SpawnMode == EMRUKSpawnMode::CurrentRoomOnly)
{
const auto Subsystem = GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
if (Subsystem->SceneLoadStatus == EMRUKInitStatus::Complete)
{
AddDestructibleGlobalMesh(Subsystem->GetCurrentRoom());
}
else
{
// Only listen for the room created event in case no current room was available yet
Subsystem->OnRoomCreated.AddUniqueDynamic(this, &AMRUKDestructibleGlobalMeshSpawner::OnRoomCreated);
// Remove destructible meshes as soon as the room gets removed
Subsystem->OnRoomRemoved.AddUniqueDynamic(this, &AMRUKDestructibleGlobalMeshSpawner::OnRoomRemoved);
}
}
else if (SpawnMode == EMRUKSpawnMode::AllRooms)
{
const auto Subsystem = GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
for (auto Room : Subsystem->Rooms)
{
AddDestructibleGlobalMesh(Room);
}
// Listen for new rooms that get created
Subsystem->OnRoomCreated.AddUniqueDynamic(this, &AMRUKDestructibleGlobalMeshSpawner::OnRoomCreated);
// Remove destructible meshes as soon as the room gets removed
Subsystem->OnRoomRemoved.AddUniqueDynamic(this, &AMRUKDestructibleGlobalMeshSpawner::OnRoomRemoved);
}
}
void AMRUKDestructibleGlobalMeshSpawner::OnRoomCreated(AMRUKRoom* Room)
{
if (SpawnMode == EMRUKSpawnMode::CurrentRoomOnly && GetGameInstance()->GetSubsystem<UMRUKSubsystem>()->GetCurrentRoom() != Room)
{
// Skip this room if it is not the current room
return;
}
AddDestructibleGlobalMesh(Room);
}
void AMRUKDestructibleGlobalMeshSpawner::OnRoomRemoved(AMRUKRoom* Room)
{
if (!IsValid(Room))
{
return;
}
if (AMRUKDestructibleGlobalMesh** Mesh = SpawnedMeshes.Find(Room))
{
(*Mesh)->Destroy();
SpawnedMeshes.Remove(Room);
}
}
AMRUKDestructibleGlobalMesh* AMRUKDestructibleGlobalMeshSpawner::FindDestructibleMeshForRoom(AMRUKRoom* Room)
{
if (AMRUKDestructibleGlobalMesh** Mesh = SpawnedMeshes.Find(Room))
{
return *Mesh;
}
return nullptr;
}
AMRUKDestructibleGlobalMesh* AMRUKDestructibleGlobalMeshSpawner::AddDestructibleGlobalMesh(AMRUKRoom* Room)
{
if (SpawnedMeshes.Contains(Room))
{
return SpawnedMeshes[Room];
}
AMRUKDestructibleGlobalMesh* Mesh = GetWorld()->SpawnActor<AMRUKDestructibleGlobalMesh>();
Mesh->PointsPerUnitX = PointsPerUnitX;
Mesh->PointsPerUnitY = PointsPerUnitY;
Mesh->MaxPointsCount = MaxPointsCount;
Mesh->DestructibleMeshComponent->GlobalMeshMaterial = GlobalMeshMaterial;
Mesh->DestructibleMeshComponent->ReservedBottom = ReservedBottom;
Mesh->DestructibleMeshComponent->ReservedTop = ReservedTop;
Mesh->CreateDestructibleMesh();
SpawnedMeshes.Add(Room, Mesh);
return Mesh;
}

View File

@@ -0,0 +1,427 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "MRUtilityKitDistanceMapGenerator.h"
#include "MRUtilityKitSubsystem.h"
#include "MRUtilityKit.h"
#include "MRUtilityKitRoom.h"
#include "MRUtilityKitAnchor.h"
#include "Components/SceneCaptureComponent2D.h"
#include "Engine/CanvasRenderTarget2D.h"
#include "Engine/Canvas.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "Kismet/KismetRenderingLibrary.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Materials/MaterialInterface.h"
#include "UObject/ConstructorHelpers.h"
AMRUKDistanceMapGenerator::AMRUKDistanceMapGenerator()
{
// Create components
Root = CreateDefaultSubobject<USceneComponent>(TEXT("DefaultSceneRoot"));
SceneCapture2D = CreateDefaultSubobject<USceneCaptureComponent2D>(TEXT("SceneCapture2D"));
RootComponent = Root;
// Setup components
SceneCapture2D->SetupAttachment(Root);
SceneCapture2D->ProjectionType = ECameraProjectionMode::Orthographic;
SceneCapture2D->OrthoWidth = 512.0f;
SceneCapture2D->CaptureSource = ESceneCaptureSource::SCS_SceneColorHDR;
SceneCapture2D->PrimitiveRenderMode = ESceneCapturePrimitiveRenderMode::PRM_UseShowOnlyList;
SceneCapture2D->bCaptureEveryFrame = false;
SceneCapture2D->bCaptureOnMovement = false;
const ConstructorHelpers::FObjectFinder<UCanvasRenderTarget2D> RT1Finder(TEXT("/OculusXR/Textures/CRT_JumpFlood1"));
if (RT1Finder.Succeeded())
{
RenderTarget1 = RT1Finder.Object;
}
const ConstructorHelpers::FObjectFinder<UCanvasRenderTarget2D> RT2Finder(TEXT("/OculusXR/Textures/CRT_JumpFlood2"));
if (RT2Finder.Succeeded())
{
RenderTarget2 = RT2Finder.Object;
}
const ConstructorHelpers::FObjectFinder<UCanvasRenderTarget2D> RTMaskFinder(TEXT("/OculusXR/Textures/CRT_Mask"));
if (RTMaskFinder.Succeeded())
{
SceneCapture2D->TextureTarget = RTMaskFinder.Object;
}
const ConstructorHelpers::FObjectFinder<UCanvasRenderTarget2D> RTDistanceMapFinder(TEXT("/OculusXR/Textures/CRT_DistanceMap"));
if (RTDistanceMapFinder.Succeeded())
{
DistanceMapRenderTarget = RTDistanceMapFinder.Object;
}
const ConstructorHelpers::FObjectFinder<UMaterialInterface> MaskMaterialFinder(TEXT("/OculusXR/Materials/M_CreateMask"));
if (MaskMaterialFinder.Succeeded())
{
MaskMaterial = MaskMaterialFinder.Object;
}
const ConstructorHelpers::FObjectFinder<UMaterialInterface> JFPassMaterialFinder(TEXT("/OculusXR/Materials/M_JFAPass"));
if (JFPassMaterialFinder.Succeeded())
{
JFPassMaterial = JFPassMaterialFinder.Object;
}
const ConstructorHelpers::FObjectFinder<UMaterialInterface> SceneObjectMaskMaterialFinder(TEXT("/OculusXR/Materials/M_SceneObjectMask"));
if (SceneObjectMaskMaterialFinder.Succeeded())
{
SceneObjectMaskMaterial = SceneObjectMaskMaterialFinder.Object;
}
const ConstructorHelpers::FObjectFinder<UMaterialInterface> FloorMaskMaterialFinder(TEXT("/OculusXR/Materials/M_FloorMask"));
if (FloorMaskMaterialFinder.Succeeded())
{
FloorMaskMaterial = FloorMaskMaterialFinder.Object;
}
const ConstructorHelpers::FObjectFinder<UMaterialInterface> DistanceMapFreeSpaceMaterialFinder(TEXT("/OculusXR/Materials/M_DistanceMapFree"));
if (DistanceMapFreeSpaceMaterialFinder.Succeeded())
{
DistanceMapFreeSpaceMaterial = DistanceMapFreeSpaceMaterialFinder.Object;
}
const ConstructorHelpers::FObjectFinder<UMaterialInterface> DistanceMapOccupiedSpaceMaterialFinder(TEXT("/OculusXR/Materials/M_DistanceMapOccupied"));
if (DistanceMapOccupiedSpaceMaterialFinder.Succeeded())
{
DistanceMapOccupiedSpaceMaterial = DistanceMapOccupiedSpaceMaterialFinder.Object;
}
const ConstructorHelpers::FObjectFinder<UMaterialInterface> DistanceMapAllSpaceMaterialFinder(TEXT("/OculusXR/Materials/M_DistanceMapAll"));
if (DistanceMapAllSpaceMaterialFinder.Succeeded())
{
DistanceMapAllSpaceMaterial = DistanceMapAllSpaceMaterialFinder.Object;
}
}
void AMRUKDistanceMapGenerator::BeginPlay()
{
Super::BeginPlay();
SceneObjectMaskMaterial->EnsureIsComplete();
FloorMaskMaterial->EnsureIsComplete();
if (SpawnMode == EMRUKSpawnMode::CurrentRoomOnly)
{
const auto Subsystem = GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
if (AMRUKRoom* CurrentRoom = Subsystem->GetCurrentRoom())
{
CreateMaskMeshesForRoom(CurrentRoom);
}
else
{
Subsystem->OnRoomCreated.AddUniqueDynamic(this, &AMRUKDistanceMapGenerator::OnRoomCreated);
}
}
else if (SpawnMode == EMRUKSpawnMode::AllRooms)
{
const auto Subsystem = GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
for (auto& Room : Subsystem->Rooms)
{
CreateMaskMeshesForRoom(Room);
}
Subsystem->OnRoomCreated.AddUniqueDynamic(this, &AMRUKDistanceMapGenerator::OnRoomCreated);
}
}
UTexture* AMRUKDistanceMapGenerator::CaptureDistanceMap()
{
CaptureInitialSceneMask();
RenderDistanceMap();
return GetDistanceMap();
}
void AMRUKDistanceMapGenerator::CaptureInitialSceneMask()
{
if (!JFPassMaterialInstance)
{
JFPassMaterialInstance = UMaterialInstanceDynamic::Create(JFPassMaterial, this);
}
if (DistanceMapFreeSpaceMaterial && !DistanceMapFreeSpaceMaterialInstance)
{
DistanceMapFreeSpaceMaterialInstance = UMaterialInstanceDynamic::Create(DistanceMapFreeSpaceMaterial, this);
}
if (DistanceMapOccupiedSpaceMaterial && !DistanceMapOccupiedSpaceMaterialInstance)
{
DistanceMapOccupiedSpaceMaterialInstance = UMaterialInstanceDynamic::Create(DistanceMapOccupiedSpaceMaterial, this);
}
if (DistanceMapAllSpaceMaterial && !DistanceMapAllSpaceMaterialInstance)
{
DistanceMapAllSpaceMaterialInstance = UMaterialInstanceDynamic::Create(DistanceMapAllSpaceMaterial, this);
}
check(SceneCapture2D->TextureTarget->SizeX == SceneCapture2D->TextureTarget->SizeY);
SceneCapture2D->CaptureScene();
// Renders the texture that was captured by the scene capture component into a mask that can then be used further down
UKismetRenderingLibrary::ClearRenderTarget2D(GetWorld(), RenderTarget1, FLinearColor::Black);
UCanvas* Canvas{};
FVector2D Size{};
FDrawToRenderTargetContext RenderTargetContext{};
UKismetRenderingLibrary::BeginDrawCanvasToRenderTarget(GetWorld(), RenderTarget1, Canvas, Size, RenderTargetContext);
Canvas->K2_DrawMaterial(MaskMaterial, FVector2D::ZeroVector, Size, FVector2D::ZeroVector);
UKismetRenderingLibrary::EndDrawCanvasToRenderTarget(GetWorld(), RenderTargetContext);
}
void AMRUKDistanceMapGenerator::RenderDistanceMap()
{
UCanvasRenderTarget2D* RTs[2] = { RenderTarget1, RenderTarget2 };
int32 RTIndex = 0;
const double TextureSize = SceneCapture2D->TextureTarget->SizeX;
check(TextureSize == RenderTarget1->SizeX);
check(TextureSize == RenderTarget1->SizeY);
check(TextureSize == RenderTarget2->SizeX);
check(TextureSize == RenderTarget2->SizeY);
const int32 LastIndex = static_cast<int32>(FMath::Log2(TextureSize / 2.0));
// Play buffer ping pong and execute the jump flood algorithm on each step
for (int32 I = 1; I <= LastIndex; ++I)
{
// Read from the render target that we have written before
JFPassMaterialInstance->SetTextureParameterValue(FName("RT"), RTs[RTIndex]);
const double Step = 1.0 / FMath::Pow(2.0, static_cast<double>(I));
JFPassMaterialInstance->SetScalarParameterValue(FName("Step"), Step);
// Make sure to render to the other render target
RTIndex = (RTIndex + 1) % 2;
UCanvasRenderTarget2D* RT = RTs[RTIndex];
UKismetRenderingLibrary::ClearRenderTarget2D(GetWorld(), RT, FLinearColor::Black);
UCanvas* Canvas{};
FVector2D Size{};
FDrawToRenderTargetContext RenderTargetContext{};
UKismetRenderingLibrary::BeginDrawCanvasToRenderTarget(GetWorld(), RT, Canvas, Size, RenderTargetContext);
Canvas->K2_DrawMaterial(JFPassMaterialInstance, FVector2D::ZeroVector, Size, FVector2D::ZeroVector);
UKismetRenderingLibrary::EndDrawCanvasToRenderTarget(GetWorld(), RenderTargetContext);
}
DistanceMapRT = RTIndex;
if (DistanceMapGenerationMode != EMRUKDistanceMapGenerationMode::None)
{
UMaterialInstanceDynamic* RenderMaterial = nullptr;
UKismetRenderingLibrary::ClearRenderTarget2D(GetWorld(), DistanceMapRenderTarget);
UCanvas* Canvas{};
FVector2D Size{};
FDrawToRenderTargetContext RenderTargetContext{};
UKismetRenderingLibrary::BeginDrawCanvasToRenderTarget(GetWorld(), DistanceMapRenderTarget, Canvas, Size, RenderTargetContext);
switch (DistanceMapGenerationMode)
{
case EMRUKDistanceMapGenerationMode::FreeSpace:
RenderMaterial = DistanceMapFreeSpaceMaterialInstance;
break;
case EMRUKDistanceMapGenerationMode::OccupiedSpace:
RenderMaterial = DistanceMapOccupiedSpaceMaterialInstance;
break;
case EMRUKDistanceMapGenerationMode::AllSpace:
RenderMaterial = DistanceMapAllSpaceMaterialInstance;
break;
case EMRUKDistanceMapGenerationMode::None:
RenderMaterial = nullptr;
break;
}
if (RenderMaterial)
{
RenderMaterial->SetTextureParameterValue(FName("RT"), GetDistanceMapRenderTarget());
Canvas->K2_DrawMaterial(RenderMaterial, FVector2D::ZeroVector, Size, FVector2D::ZeroVector);
}
UKismetRenderingLibrary::EndDrawCanvasToRenderTarget(GetWorld(), RenderTargetContext);
}
}
void AMRUKDistanceMapGenerator::OnRoomCreated(AMRUKRoom* Room)
{
if (SpawnMode == EMRUKSpawnMode::CurrentRoomOnly && GetGameInstance()->GetSubsystem<UMRUKSubsystem>()->GetCurrentRoom() != Room)
{
// Skip this room if it is not the current room
return;
}
CreateMaskMeshesForRoom(Room);
}
void AMRUKDistanceMapGenerator::OnRoomUpdated(AMRUKRoom* Room)
{
if (!SpawnedMaskMeshes.Find(Room))
{
// A room was updated that we don't care about. If we are in current room only mode
// we only want to update the one room we created
return;
}
CreateMaskMeshesForRoom(Room);
}
void AMRUKDistanceMapGenerator::CreateMaskMeshesForRoom(AMRUKRoom* Room)
{
if (!Room)
{
UE_LOG(LogMRUK, Warning, TEXT("Can not create masked meshes for room that is a nullptr"));
return;
}
if (TArray<AActor*>* Actors = SpawnedMaskMeshes.Find(Room))
{
for (AActor* Actor : *Actors)
{
Actor->Destroy();
}
Actors->Empty();
SpawnedMaskMeshes.Remove(Room);
}
// Create for each anchor a mesh with a material to use as a mask
// to initialize the jump flood algorithm.
TArray<AActor*> SpawnedActors;
for (auto& Anchor : Room->AllAnchors)
{
if (!Anchor->VolumeBounds.IsValid)
{
continue;
}
SpawnedActors.Push(CreateMaskMeshOfAnchor(Anchor));
}
if (Room->FloorAnchor)
{
SpawnedActors.Push(CreateMaskMeshOfAnchor(Room->FloorAnchor));
}
SpawnedMaskMeshes.Add(Room, SpawnedActors);
const auto Subsystem = GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
Subsystem->OnRoomRemoved.AddUniqueDynamic(this, &AMRUKDistanceMapGenerator::RemoveMaskMeshesFromRoom);
Subsystem->OnRoomUpdated.AddUniqueDynamic(this, &AMRUKDistanceMapGenerator::OnRoomUpdated);
OnReady.Broadcast();
}
AActor* AMRUKDistanceMapGenerator::CreateMaskMeshOfAnchor(AMRUKAnchor* Anchor)
{
check(Anchor);
FActorSpawnParameters ActorSpawnParams{};
ActorSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
ActorSpawnParams.Owner = Anchor;
AActor* Actor = GetWorld()->SpawnActor<AActor>(ActorSpawnParams);
Actor->Tags.Push(GMRUK_DISTANCE_MAP_ACTOR_TAG);
const auto R = NewObject<USceneComponent>(Actor, TEXT("Root"));
Actor->SetRootComponent(R);
R->RegisterComponent();
Actor->AddInstanceComponent(R);
Actor->AttachToActor(Anchor, FAttachmentTransformRules::KeepRelativeTransform);
const auto ProceduralMesh = NewObject<UProceduralMeshComponent>(Actor, TEXT("DistanceMapMesh"));
Anchor->GenerateProceduralAnchorMesh(ProceduralMesh, {}, {}, true, false);
// Set a material depending if the anchor is the floor or a scene object.
// The different materials have different colors. These colors will be used to create different
// initialization masks for the jump flood algorithm.
if (Anchor == Anchor->Room->FloorAnchor)
{
ProceduralMesh->SetMaterial(0, FloorMaskMaterial);
}
else
{
ProceduralMesh->SetMaterial(0, SceneObjectMaskMaterial);
}
ProceduralMesh->SetupAttachment(Actor->GetRootComponent());
ProceduralMesh->RegisterComponent();
Actor->AddInstanceComponent(ProceduralMesh);
// The created meshes will be only used to create a mask for jump flood.
// Therefore we don't want them to show up in the normal camera.
// This unfortunate means that the meshes will show up in other scene captures the user may place as well.
ProceduralMesh->SetVisibleInSceneCaptureOnly(true);
SceneCapture2D->ShowOnlyActors.Push(Actor);
return Actor;
}
AActor* AMRUKDistanceMapGenerator::UpdateMaskMeshOfAnchor(AMRUKAnchor* Anchor)
{
TArray<AActor*> ChildActors;
Anchor->GetAllChildActors(ChildActors);
for (auto Child : ChildActors)
{
if (Child->ActorHasTag(GMRUK_DISTANCE_MAP_ACTOR_TAG))
{
// Remove existing distance map actor
SceneCapture2D->ShowOnlyActors.Remove(Child);
Child->Destroy();
}
}
return CreateMaskMeshOfAnchor(Anchor);
}
UTexture* AMRUKDistanceMapGenerator::GetDistanceMap() const
{
return GetDistanceMapRenderTarget();
}
UCanvasRenderTarget2D* AMRUKDistanceMapGenerator::GetDistanceMapRenderTarget() const
{
if (DistanceMapRT == -1)
{
UE_LOG(LogMRUK, Warning, TEXT("Make sure to first render the distance map by calling CaptureDistanceMap()"));
return nullptr;
}
check(DistanceMapRT >= 0);
UCanvasRenderTarget2D* RTs[2] = { RenderTarget1, RenderTarget2 };
return RTs[DistanceMapRT];
}
FMinimalViewInfo AMRUKDistanceMapGenerator::GetSceneCaptureView() const
{
FMinimalViewInfo Info = {};
SceneCapture2D->GetCameraView(1.0f, Info);
return Info;
}
void AMRUKDistanceMapGenerator::RemoveMaskMeshesFromRoom(AMRUKRoom* Room)
{
if (!Room)
{
UE_LOG(LogMRUK, Warning, TEXT("Can not remove masked meshes for room that is a nullptr"));
return;
}
if (TArray<AActor*>* Actors = SpawnedMaskMeshes.Find(Room))
{
for (AActor* Actor : *Actors)
{
Actor->Destroy();
}
Actors->Empty();
SpawnedMaskMeshes.Remove(Room);
}
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "MRUtilityKitGeometry.h"
#include "MRUtilityKit.h"
#include "Generated/MRUtilityKitShared.h"
void MRUKTriangulatePolygon(const TArray<TArray<FVector2f>>& Polygons, TArray<FVector2D>& Vertices, TArray<int32>& Indices)
{
Vertices.Empty();
Indices.Empty();
auto MRUKShared = MRUKShared::GetInstance();
if (!MRUKShared)
{
UE_LOG(LogMRUK, Error, TEXT("MRUK shared library is not available. To use this functionality make sure the library is included"));
return;
}
TArray<MRUKShared::MrukPolygon2f> ConvertedPolygons;
ConvertedPolygons.Reserve(Polygons.Num());
for (const auto& Polygon : Polygons)
{
ConvertedPolygons.Push({ Polygon.GetData(), static_cast<uint32_t>(Polygon.Num()) });
}
auto Mesh = MRUKShared->TriangulatePolygon(ConvertedPolygons.GetData(), ConvertedPolygons.Num());
Vertices.Reserve(Mesh.numVertices);
Indices.Reserve(Mesh.numIndices);
for (uint32_t i = 0; i < Mesh.numVertices; ++i)
{
Vertices.Push(FVector2D(Mesh.vertices[i]));
}
for (uint32_t i = 0; i < Mesh.numIndices; ++i)
{
Indices.Push(Mesh.indices[i]);
}
MRUKShared->FreeMesh(&Mesh);
}

View File

@@ -0,0 +1,281 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "MRUtilityKitGridSliceResizer.h"
#include "MRUtilityKit.h"
#include "MRUtilityKitTelemetry.h"
#include "OculusXRTelemetry.h"
#include "Engine/StaticMesh.h"
#include "ProceduralMeshComponent.h"
#include "StaticMeshResources.h"
UMRUKGridSliceResizerComponent::UMRUKGridSliceResizerComponent()
{
PrimaryComponentTick.bCanEverTick = true;
ProcMesh = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("ProcMesh"));
ProcMesh->SetupAttachment(this);
}
void UMRUKGridSliceResizerComponent::BeginPlay()
{
Super::BeginPlay();
OculusXRTelemetry::TScopedMarker<MRUKTelemetry::FLoadGridSliceResizerMarker> Event(static_cast<int>(GetTypeHash(this)));
SliceMesh();
}
void UMRUKGridSliceResizerComponent::OnRegister()
{
Super::OnRegister();
SliceMesh();
}
void UMRUKGridSliceResizerComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
const FVector ActorScale = GetOwner() ? GetOwner()->GetActorScale() : FVector::OneVector;
if (Mesh && ActorScale != ResizerScale)
{
ResizerScale = ActorScale;
SliceMesh();
}
}
#if WITH_EDITOR
void UMRUKGridSliceResizerComponent::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
if (PropertyChangedEvent.Property->GetOwner<AActor>() == GetOwner())
{
return;
}
const FName PropertyName = (PropertyChangedEvent.Property != nullptr) ? PropertyChangedEvent.Property->GetFName() : NAME_None;
if (PropertyName == GET_MEMBER_NAME_CHECKED(UMRUKGridSliceResizerComponent, BorderXNegative)
|| PropertyName == GET_MEMBER_NAME_CHECKED(UMRUKGridSliceResizerComponent, BorderXPositive)
|| PropertyName == GET_MEMBER_NAME_CHECKED(UMRUKGridSliceResizerComponent, BorderYNegative)
|| PropertyName == GET_MEMBER_NAME_CHECKED(UMRUKGridSliceResizerComponent, BorderYPositive)
|| PropertyName == GET_MEMBER_NAME_CHECKED(UMRUKGridSliceResizerComponent, BorderZNegative)
|| PropertyName == GET_MEMBER_NAME_CHECKED(UMRUKGridSliceResizerComponent, BorderZPositive)
|| PropertyName == GET_MEMBER_NAME_CHECKED(UMRUKGridSliceResizerComponent, SlicerPivotOffset))
{
SliceMesh();
}
}
#endif
void UMRUKGridSliceResizerComponent::SliceMesh()
{
if (!Mesh)
{
return;
}
if (!Mesh->bAllowCPUAccess)
{
UE_LOG(LogMRUK, Error, TEXT("Can not slice a mesh that has no CPU access. Make sure you enable CPU access on the static mesh asset."));
return;
}
TArray<FVector> Positions;
TArray<FVector> Normals;
TArray<FVector2D> UVs;
TArray<FColor> Colors;
TArray<int32> Triangles;
const FStaticMeshLODResources& LODResources = Mesh->GetRenderData()->LODResources[0];
const FStaticMeshVertexBuffers& VertexBuffers = LODResources.VertexBuffers;
const FRawStaticIndexBuffer& IndexBuffer = LODResources.IndexBuffer;
Positions.SetNum(LODResources.GetNumVertices());
Normals.SetNum(LODResources.GetNumVertices());
UVs.SetNum(LODResources.GetNumVertices());
Colors.SetNum(LODResources.GetNumVertices());
const FVector ActorScale = GetOwner() ? GetOwner()->GetActorScale() : FVector::OneVector;
const FVector ActorScaleInv = FVector(1.0 / ActorScale.X, 1.0 / ActorScale.Y, 1.0 / ActorScale.Z);
const FVector Size = ActorScale;
// Slicing
FTransform PivotTransform;
PivotTransform.SetLocation(-SlicerPivotOffset);
FTransform ScaledInvPivotTransform;
ScaledInvPivotTransform.SetLocation(Size * SlicerPivotOffset);
// The bounding box of the mesh to resize
FBox BBox = Mesh->GetBoundingBox();
BBox = FBox(PivotTransform.TransformPosition(BBox.Min), PivotTransform.TransformPosition(BBox.Max));
// The bounding box of the mesh to resize scaled by the size
const FBox BBoxScaled = FBox(BBox.Min * Size, BBox.Max * Size);
// The bounding box of the mesh to resize scaled including the pivot point
// This may be a bigger box as ScaledBBox in case the pivot is outside of the scaled bounding box.
const FBox BBoxScaledPivot = FBox(
FVector(FMath::Min(BBox.Min.X, SlicerPivotOffset.X), FMath::Min(BBox.Min.Y, SlicerPivotOffset.Y), FMath::Min(BBox.Min.Z, SlicerPivotOffset.Z)),
FVector(FMath::Max(BBox.Max.X, SlicerPivotOffset.X), FMath::Max(BBox.Max.Y, SlicerPivotOffset.Y), FMath::Max(BBox.Max.Z, SlicerPivotOffset.Z)));
// Locations of the border slices between 0 - 1
FVector BorderPos = FVector(BorderXPositive, BorderYPositive, BorderZPositive);
FVector BorderNeg = FVector(BorderXNegative, BorderYNegative, BorderZNegative);
// Locations of the border slices for the X,Y,Z axis in local space
FVector BorderPosLS;
FVector BorderNegLS;
// Distance from the Border[Pos|Neg]LS to the outer maximum/minimum of the BBox
FVector StubPos;
FVector StubNeg;
// The inner bounding box that should be stretched in all directions
FVector BBoxInnerMax;
FVector BBoxInnerMin;
// The expected bounding box of the inner bounding box when its scaled up by the size
FVector BBoxInnerScaledMax;
FVector BBoxInnerScaledMin;
// The ratio between the inner bounding box and the scaled bounding box
FVector InnerBoxScaleRatioMax;
FVector InnerBoxScaleRatioMin;
// The ratio to use for downscaling in case it's needed
FVector DownscaleMax;
FVector DownscaleMin;
for (int32 I = 0; I < 3; ++I)
{
// We don't want to have division by zero further down the line
BorderPos[I] = FMath::Clamp(BorderPos[I], DBL_EPSILON, 1.0);
BorderNeg[I] = FMath::Clamp(BorderNeg[I], DBL_EPSILON, 1.0);
BorderPosLS[I] = BBoxScaledPivot.Max[I] - (1.0 - BorderPos[I]) * FMath::Abs(BBoxScaledPivot.Max[I]);
BorderNegLS[I] = BBoxScaledPivot.Min[I] + (1.0 - BorderNeg[I]) * FMath::Abs(BBoxScaledPivot.Min[I]);
StubPos[I] = FMath::Abs(BBox.Max[I] - BorderPosLS[I]);
StubNeg[I] = FMath::Abs(BBox.Min[I] - BorderNegLS[I]);
BBoxInnerMax[I] = BBox.Max[I] - StubPos[I];
BBoxInnerMin[I] = BBox.Min[I] + StubNeg[I];
// Max may be negative and Min may be positive in case the stubs are greater than
// the scaled down bounding box and therefore don't fit the scaled bounding box.
// This case gets treated special down below.
BBoxInnerScaledMax[I] = BBoxScaled.Max[I] - StubPos[I];
BBoxInnerScaledMin[I] = BBoxScaled.Min[I] + StubNeg[I];
InnerBoxScaleRatioMax[I] = FMath::Max(0.0, BBoxInnerScaledMax[I] / BBoxInnerMax[I]);
InnerBoxScaleRatioMin[I] = FMath::Max(0.0, BBoxInnerScaledMin[I] / BBoxInnerMin[I]);
// When Downscale[Min/Max] needs to be applied the temporary bounding box is
// Max == StubPos, Min == StubNeg. Therefore get the ratio between it and the
// expected scaled down bounding box to calculate the scale that needs
// to be applied
DownscaleMax[I] = BBoxScaled.Max[I] / StubPos[I];
DownscaleMin[I] = BBoxScaled.Min[I] / StubNeg[I];
}
// Process vertices
// If the center shouldn't be scaled we need to take care of the case when the original
// center vertices would be outside of the expected downscaled bounding box. Therefore, iterate
// through all vertices and check if the center vertices are outside. If they are outside we need
// to scale down the center part as usually.
// This unfortunately has to be done in a separate first pass.
bool ScaleDownCenter[3] = { false, false, false };
const int32 VertexCount = LODResources.GetNumVertices();
for (int32 I = 0; I < VertexCount; ++I)
{
const FVector3f& Normal = VertexBuffers.StaticMeshVertexBuffer.VertexTangentZ(I);
Normals[I] = FVector(Normal.X, Normal.Y, Normal.Z);
const FVector2f& UV = VertexBuffers.StaticMeshVertexBuffer.GetVertexUV(I, 0);
UVs[I] = FVector2D(UV.X, UV.Y);
const FVector3f& P = VertexBuffers.PositionVertexBuffer.VertexPosition(I);
// Apply pivot offset
Positions[I] = PivotTransform.TransformPosition(FVector(P.X, P.Y, P.Z));
const FVector& Position = Positions[I];
for (int32 A = 0; A < 3; ++A)
{
if ((0.0 <= Position[A] && Position[A] <= BorderPosLS[A]) && (Position[A] > BBoxInnerScaledMax[A]))
{
ScaleDownCenter[A] = true;
}
else if ((BorderNegLS[A] <= Position[A] && Position[A] <= 0.0) && (Position[A] < BBoxInnerScaledMin[A]))
{
ScaleDownCenter[A] = true;
}
}
}
bool bScaleCenter[3] = {};
bScaleCenter[0] = ScaleCenterMode & static_cast<uint8>(EMRUKScaleCenterMode::XAxis) ? true : false;
bScaleCenter[1] = ScaleCenterMode & static_cast<uint8>(EMRUKScaleCenterMode::YAxis) ? true : false;
bScaleCenter[2] = ScaleCenterMode & static_cast<uint8>(EMRUKScaleCenterMode::ZAxis) ? true : false;
for (FVector& Position : Positions)
{
// Apply computations on each axis
for (int32 A = 0; A < 3; ++A)
{
if ((bScaleCenter[A] || ScaleDownCenter[A]) && (0.0 <= Position[A] && Position[A] <= BorderPosLS[A]))
{
// Vertex is inside the inner distance and should be stretched
Position[A] *= InnerBoxScaleRatioMax[A];
}
else if ((bScaleCenter[A] || ScaleDownCenter[A]) && (BorderNegLS[A] <= Position[A] && Position[A] <= 0.0))
{
// Vertex is inside the inner distance and should be stretched
Position[A] *= InnerBoxScaleRatioMin[A];
}
else if (BorderPosLS[A] < Position[A])
{
// Vertex is inside the outer stub and should not be stretched
// Perform linear transform of vertices into their expect position
Position[A] = BorderPosLS[A] * InnerBoxScaleRatioMax[A] + (Position[A] - BorderPosLS[A]);
if (BBoxInnerScaledMax[A] < 0.0)
{
// The mesh that would result from the linear transform above is still not small enough to
// fit into the expected scaled down bounding box. This means the stubs need to be scaled down
// to make them fit.
Position[A] *= DownscaleMax[A];
}
}
else if (Position[A] < BorderNegLS[A])
{
// Vertex is inside the outer stub and should not be stretched
// Perform linear transform of vertices into their expect position
Position[A] = BorderNegLS[A] * InnerBoxScaleRatioMin[A] - (BorderNegLS[A] - Position[A]);
if (BBoxInnerScaledMin[A] > 0.0)
{
// The mesh that would result from the linear transform above is still not small enough to
// fit into the expected scaled down bounding box. This means the stubs need to be scaled down
// to make them fit.
Position[A] *= -DownscaleMin[A];
}
}
}
// Undo pivot offset
Position = ActorScaleInv * ScaledInvPivotTransform.TransformPosition(Position);
}
Triangles.SetNum(IndexBuffer.GetNumIndices());
for (int32 I = 0; I < IndexBuffer.GetNumIndices(); ++I)
{
Triangles[I] = IndexBuffer.GetIndex(I);
}
ProcMesh->ClearMeshSection(0);
ProcMesh->CreateMeshSection(0, Positions, Triangles, Normals, UVs, Colors, {}, bGenerateCollision);
ProcMesh->SetMaterial(0, Mesh->GetMaterial(0));
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "MRUtilityKitGuardian.h"
AMRUKGuardian::AMRUKGuardian(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 AMRUKGuardian::CreateGuardian(UProceduralMeshComponent* GuardianMesh)
{
GuardianMesh->SetupAttachment(RootComponent);
GuardianMesh->RegisterComponent();
GuardianMeshComponent = GuardianMesh;
}

View File

@@ -0,0 +1,235 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "MRUtilityKitGuardianSpawner.h"
#include "Engine/GameInstance.h"
#include "Engine/GameEngine.h"
#include "IXRTrackingSystem.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "MRUtilityKitAnchor.h"
#include "MRUtilityKitGuardian.h"
#include "MRUtilityKitRoom.h"
#include "MRUtilityKitSubsystem.h"
#include "MRUtilityKitTelemetry.h"
AMRUKGuardianSpawner::AMRUKGuardianSpawner()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bTickEvenWhenPaused = true;
PrimaryActorTick.TickGroup = TG_PrePhysics;
}
void AMRUKGuardianSpawner::SetGuardianMaterial(UMaterialInstance* Material)
{
if (!Material)
{
return;
}
GuardianMaterial = Material;
DynamicGuardianMaterial = UMaterialInstanceDynamic::Create(GuardianMaterial, this);
DynamicGuardianMaterial->SetVectorParameterValue(TEXT("WallScale"), FVector(GridDensity));
// Recreate guardian meshes
TArray<AMRUKRoom*> Rooms;
SpawnedGuardians.GetKeys(Rooms);
for (AMRUKRoom* Room : Rooms)
{
SpawnGuardians(Room);
}
}
void AMRUKGuardianSpawner::SpawnGuardians(AMRUKRoom* Room)
{
if (!IsValid(Room))
{
UE_LOG(LogMRUK, Warning, TEXT("Can not spawn Guardians for a room that is a nullptr"));
return;
}
// Remove guardians that are already in this room
DestroyGuardians(Room);
const auto Subsystem = GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
Subsystem->OnRoomUpdated.AddUniqueDynamic(this, &AMRUKGuardianSpawner::OnRoomUpdated);
Subsystem->OnRoomRemoved.AddUniqueDynamic(this, &AMRUKGuardianSpawner::OnRoomRemoved);
const auto SpawnGuardian = [this](AMRUKAnchor* Anchor, const TArray<FMRUKPlaneUV>& PlaneUVAdjustments) {
// Create guardian actor
const auto GuardianActor = GetWorld()->SpawnActor<AMRUKGuardian>();
GuardianActor->AttachToComponent(Anchor->GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform);
GuardianActor->SetActorHiddenInGame(IsHidden());
// Generate procedural mesh
const auto ProceduralMesh = NewObject<UProceduralMeshComponent>(GuardianActor, TEXT("GuardianMesh"));
Anchor->GenerateProceduralAnchorMesh(ProceduralMesh, PlaneUVAdjustments, {}, true, false, 0.01);
ProceduralMesh->SetMaterial(0, DynamicGuardianMaterial);
GuardianActor->CreateGuardian(ProceduralMesh);
return GuardianActor;
};
TArray<AMRUKGuardian*> SpawnedActors;
// Attach procedural meshes to the walls first because they are connected.
TArray<FMRUKAnchorWithPlaneUVs> AnchorsWithPlaneUVs;
const TArray<FMRUKTexCoordModes> WallTextureCoordinateModes = { { EMRUKCoordModeU::Metric, EMRUKCoordModeV::Metric } };
Room->ComputeWallMeshUVAdjustments(WallTextureCoordinateModes, AnchorsWithPlaneUVs);
for (const auto& [Anchor, PlaneUVs] : AnchorsWithPlaneUVs)
{
SpawnedActors.Push(SpawnGuardian(Anchor, PlaneUVs));
}
// Attach procedural meshes to the rest of the anchors. The walls have already meshes applied
// because of the first step and will therefore be ignored by this code automatically.
for (const auto& Anchor : Room->AllAnchors)
{
if (!Anchor || Anchor == Room->FloorAnchor || Anchor == Room->CeilingAnchor || Room->IsWallAnchor(Anchor))
{
continue;
}
SpawnedActors.Push(SpawnGuardian(Anchor, {}));
}
SpawnedGuardians.Add(Room, SpawnedActors);
}
void AMRUKGuardianSpawner::SetGridDensity(double Density)
{
GridDensity = Density;
if (DynamicGuardianMaterial)
{
DynamicGuardianMaterial->SetVectorParameterValue(TEXT("WallScale"), FVector(GridDensity));
}
}
void AMRUKGuardianSpawner::Tick(float DeltaSeconds)
{
if (!DynamicGuardianMaterial)
{
return;
}
if (EnableFade)
{
const auto Subsystem = GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
const auto CurrentRoom = Subsystem->GetCurrentRoom();
if (!CurrentRoom)
{
return;
}
FQuat HeadsetOrientation;
FVector HeadsetPosition(0.f);
GEngine->XRSystem->GetCurrentPose(IXRTrackingSystem::HMDDeviceId, HeadsetOrientation, HeadsetPosition);
FVector SurfacePosition = FVector::ZeroVector;
double SurfaceDistance = 0.0;
FMRUKLabelFilter LabelFilter;
LabelFilter.ExcludedLabels = { FMRUKLabels::Ceiling, FMRUKLabels::Floor };
CurrentRoom->TryGetClosestSurfacePosition(HeadsetPosition, SurfacePosition, SurfaceDistance, LabelFilter);
const auto WorldToMeters = GetWorldSettings()->WorldToMeters;
const auto GuardianFade = FMath::Clamp(1.0 - ((SurfaceDistance / WorldToMeters) / GuardianDistance), 0.0, 1.0);
DynamicGuardianMaterial->SetScalarParameterValue(TEXT("Fade"), GuardianFade);
}
}
void AMRUKGuardianSpawner::BeginPlay()
{
Super::BeginPlay();
SetGuardianMaterial(GuardianMaterial);
OculusXRTelemetry::TScopedMarker<MRUKTelemetry::FLoadGuardianMarker> Event(static_cast<int>(GetTypeHash(this)));
if (SpawnMode == EMRUKSpawnMode::CurrentRoomOnly)
{
const auto Subsystem = GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
if (Subsystem->SceneLoadStatus == EMRUKInitStatus::Complete)
{
if (AMRUKRoom* CurrentRoom = Subsystem->GetCurrentRoom())
{
SpawnGuardians(CurrentRoom);
}
}
else
{
// Only listen for the room created event in case no current room was available yet
Subsystem->OnRoomCreated.AddUniqueDynamic(this, &AMRUKGuardianSpawner::OnRoomCreated);
}
}
else if (SpawnMode == EMRUKSpawnMode::AllRooms)
{
const auto Subsystem = GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
for (auto Room : Subsystem->Rooms)
{
SpawnGuardians(Room);
}
// Listen for new rooms that get created
Subsystem->OnRoomCreated.AddUniqueDynamic(this, &AMRUKGuardianSpawner::OnRoomCreated);
}
}
#if WITH_EDITOR
void AMRUKGuardianSpawner::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
const auto PropertyName = (PropertyChangedEvent.Property != nullptr) ? PropertyChangedEvent.Property->GetFName() : NAME_None;
if (PropertyName == GET_MEMBER_NAME_CHECKED(AMRUKGuardianSpawner, GridDensity))
{
SetGridDensity(GridDensity);
}
else if (PropertyName == GET_MEMBER_NAME_CHECKED(AMRUKGuardianSpawner, GuardianMaterial))
{
SetGuardianMaterial(GuardianMaterial);
}
Super::PostEditChangeProperty(PropertyChangedEvent);
}
#endif
void AMRUKGuardianSpawner::OnRoomCreated(AMRUKRoom* Room)
{
if (SpawnMode == EMRUKSpawnMode::CurrentRoomOnly && GetGameInstance()->GetSubsystem<UMRUKSubsystem>()->GetCurrentRoom() != Room)
{
// Skip this room if it is not the current room
return;
}
SpawnGuardians(Room);
}
void AMRUKGuardianSpawner::OnRoomUpdated(AMRUKRoom* Room)
{
if (!SpawnedGuardians.Find(Room))
{
// A room was updated that we don't care about. If we are in current room only mode
// we only want to update the one room we created
return;
}
SpawnGuardians(Room);
}
void AMRUKGuardianSpawner::OnRoomRemoved(AMRUKRoom* Room)
{
DestroyGuardians(Room);
}
void AMRUKGuardianSpawner::DestroyGuardians(AMRUKRoom* Room)
{
if (TArray<AMRUKGuardian*>* Actors = SpawnedGuardians.Find(Room))
{
for (AActor* Actor : *Actors)
{
if (IsValid(Actor))
{
Actor->Destroy();
}
}
Actors->Empty();
SpawnedGuardians.Remove(Room);
}
}

View File

@@ -0,0 +1,161 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "MRUtilityKitLightDispatcher.h"
#include "MRUtilityKit.h"
#include "MRUtilityKitTelemetry.h"
#include "Components/PointLightComponent.h"
#include "Engine/PointLight.h"
#include "Engine/World.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetMaterialLibrary.h"
#include "Materials/MaterialParameterCollection.h"
#include "Materials/MaterialParameterCollectionInstance.h"
#include "UObject/ConstructorHelpers.h"
AMRUKLightDispatcher::AMRUKLightDispatcher()
{
PrimaryActorTick.bCanEverTick = true;
const ConstructorHelpers::FObjectFinder<UMaterialParameterCollection> MpcAsset(TEXT("/OculusXR/Materials/MPC_Highlights"));
if (MpcAsset.Succeeded())
{
Collection = MpcAsset.Object;
}
else
{
UE_LOG(LogMRUK, Log, TEXT("Light dispatcher couldn't find material parameter collection in /OculusXR/Materials/MPC_Highlights"));
}
}
void AMRUKLightDispatcher::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
FillParameterCollection();
}
void AMRUKLightDispatcher::FillParameterCollection()
{
if (!Collection || PointLightComponents.IsEmpty())
{
return;
}
UMaterialParameterCollectionInstance* Instance = GetWorld()->GetParameterCollectionInstance(Collection);
for (int i = 0; i < PointLightComponents.Num(); i++)
{
const UPointLightComponent* Light = PointLightComponents[i];
if (!IsValid(Light))
{
continue;
}
const int Step = i * 3;
// It's not possible to expand the amount of parameters in collection at runtime,
// in case we exceed the count of existing parameters break the loop
if (Collection->VectorParameters.Num() < Step + 3)
{
break;
}
// Prepare parameters
FCollectionVectorParameter PositionParam, DataParam, ColorParam;
PositionParam.ParameterName = FName("PointLightPosition" + FString::FromInt(i));
DataParam.ParameterName = FName("PointLightData" + FString::FromInt(i));
ColorParam.ParameterName = FName("PointLightColor" + FString::FromInt(i));
PositionParam.DefaultValue = FLinearColor(Light->GetComponentLocation());
DataParam.DefaultValue = FLinearColor(1.f / Light->AttenuationRadius, Light->ComputeLightBrightness(), Light->LightFalloffExponent, Light->bUseInverseSquaredFalloff);
ColorParam.DefaultValue = Light->GetLightColor();
// Fill collection's vector parameters
Collection->VectorParameters[Step] = PositionParam;
Collection->VectorParameters[Step + 1] = DataParam;
Collection->VectorParameters[Step + 2] = ColorParam;
}
// Send count of lights
Collection->ScalarParameters[0].DefaultValue = PointLightComponents.Num();
UKismetMaterialLibrary::SetScalarParameterValue(GetWorld(), Collection, "TotalLights", PointLightComponents.Num());
// Update instance
Instance->UpdateRenderState(false);
}
void AMRUKLightDispatcher::AddAdditionalPointLightActor(AActor* Actor)
{
AdditionalActorsToLookForPointLightComponents.AddUnique(Actor);
AddPointLightsFromActor(Actor);
}
void AMRUKLightDispatcher::ForceUpdateCollection()
{
FillPointLights();
FillParameterCollection();
PointLightComponents.Empty();
}
void AMRUKLightDispatcher::BeginPlay()
{
Super::BeginPlay();
OculusXRTelemetry::TScopedMarker<MRUKTelemetry::FLoadLightDispatcherMarker> Event(static_cast<int>(GetTypeHash(this)));
FillPointLights();
}
void AMRUKLightDispatcher::FillPointLights()
{
// Make sure we don't have duplicates in the array
PointLightComponents.Empty();
if (ShouldFetchPointLightsAtBeginPlay)
{
// Fetch all point light actors from the level
TArray<AActor*> PointLightActors;
UGameplayStatics::GetAllActorsOfClass(this, APointLight::StaticClass(), PointLightActors);
for (AActor* Actor : PointLightActors)
{
const APointLight* PointLightActor = Cast<APointLight>(Actor);
PointLightComponents.Add(PointLightActor->PointLightComponent);
}
}
else
{
// Only use the point lights that have been specified in ManualPointLights
for (AActor* Actor : ManualPointLights)
{
if (!IsValid(Actor))
{
continue;
}
const APointLight* PointLightActor = Cast<APointLight>(Actor);
PointLightComponents.Add(PointLightActor->PointLightComponent);
}
}
// Check the additional added actors for point lights and add them in case they have
// PointLightComponents attached
for (const AActor* Actor : AdditionalActorsToLookForPointLightComponents)
{
if (!IsValid(Actor))
{
continue;
}
AddPointLightsFromActor(Actor);
}
}
void AMRUKLightDispatcher::AddPointLightsFromActor(const AActor* Actor)
{
TArray<UPointLightComponent*> LightComponents;
Actor->GetComponents(LightComponents, false);
PointLightComponents.Append(LightComponents);
}

View File

@@ -0,0 +1,220 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "MRUtilityKitPositionGenerator.h"
#include "MRUtilityKitSubsystem.h"
#include "Engine/OverlapResult.h"
#include "Engine/World.h"
#include "Engine/GameInstance.h"
#include "CollisionShape.h"
bool AMRUtilityKitPositionGenerator::CanSpawnBox(const UWorld* World, const FBox& Box, const FVector& SpawnPosition, const FQuat& SpawnRotation, const FCollisionQueryParams& QueryParams, const ECollisionChannel CollisionChannel)
{
TArray<FOverlapResult> OutOverlaps;
const bool bHasOverlap = World->OverlapMultiByChannel(OutOverlaps, SpawnPosition, SpawnRotation, CollisionChannel, FCollisionShape::MakeBox(Box.GetExtent()), QueryParams);
return !bHasOverlap;
}
void AMRUtilityKitPositionGenerator::BeginPlay()
{
Super::BeginPlay();
if (RunOnStart)
{
const auto Subsystem = GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
if (Subsystem->SceneLoadStatus == EMRUKInitStatus::Complete)
{
SceneLoaded(true);
}
Subsystem->OnSceneLoaded.AddUniqueDynamic(this, &AMRUtilityKitPositionGenerator::SceneLoaded);
}
}
bool AMRUtilityKitPositionGenerator::GenerateRandomPositionsOnSurface(TArray<FTransform>& OutTransforms)
{
const auto Subsystem = GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
bool bSuccess = true;
bool bAnyFailure = false;
switch (RandomSpawnSettings.RoomFilter)
{
case EMRUKRoomFilter::None:
break;
case EMRUKRoomFilter::CurrentRoomOnly:
{
const auto Room = Subsystem->GetCurrentRoom();
bSuccess = GenerateRandomPositionsOnSurfaceInRoom(Room, OutTransforms);
break;
}
case EMRUKRoomFilter::AllRooms:
{
for (auto& Room : Subsystem->Rooms)
{
if (!GenerateRandomPositionsOnSurfaceInRoom(Room, OutTransforms))
{
bAnyFailure = true;
}
}
bSuccess = !bAnyFailure;
break;
}
default:;
}
return bSuccess;
}
bool AMRUtilityKitPositionGenerator::GenerateRandomPositionsOnSurfaceInRoom(AMRUKRoom* Room, TArray<FTransform>& OutTransforms)
{
bool bInitializedAnchor = IsValid(RandomSpawnSettings.ActorInstance);
if (bInitializedAnchor && RandomSpawnSettings.ActorClass != nullptr)
{
UE_LOG(LogMRUK, Error, TEXT("Cannot use an initialized Actor AND a defined ActorClass together. Use one of the options"));
return false;
}
if (!bInitializedAnchor && RandomSpawnSettings.ActorClass == nullptr)
{
UE_LOG(LogMRUK, Error, TEXT("Please define ActorClass."));
return false;
}
const auto Subsystem = GetGameInstance()->GetSubsystem<UMRUKSubsystem>();
auto Bounds = bInitializedAnchor ? RandomSpawnSettings.ActorInstance->CalculateComponentsBoundingBoxInLocalSpace() : Subsystem->GetActorClassBounds(RandomSpawnSettings.ActorClass);
float MinRadius = 0.0f;
float CenterOffset = (Bounds.GetCenter().Z != 0) ? Bounds.GetCenter().Z : 0.0f;
float BaseOffset = (Bounds.Min.Z != 0) ? -Bounds.Min.Z : 0.0f;
FBox AdjustedBounds;
TArray<FBox> SpawnedBounds;
if (Bounds.IsValid)
{
constexpr float ClearanceDistance = 0.01f;
CenterOffset = Bounds.GetCenter().Z;
MinRadius = FMath::Min(FMath::Min(-Bounds.Min.X, -Bounds.Min.Y), FMath::Min(Bounds.Max.X, Bounds.Max.Y));
if (MinRadius < 0.0f)
{
MinRadius = 0.0f;
}
FVector Min = Bounds.Min;
FVector Max = Bounds.Max;
Min.Z += ClearanceDistance;
if (Max.Z < Min.Z)
{
Max.Z = Min.Z;
}
AdjustedBounds = FBox(Min, Max);
if (RandomSpawnSettings.OverrideBounds > 0)
{
FVector Center = FVector(0.0f, 0.0f, ClearanceDistance);
FVector Extents = FVector((RandomSpawnSettings.OverrideBounds), (RandomSpawnSettings.OverrideBounds), ClearanceDistance);
AdjustedBounds = FBox(Center - Extents, Center + Extents);
}
}
int FoundPositions = 0;
for (int i = 0; i < RandomSpawnSettings.SpawnAmount; ++i)
{
for (int j = 0; j < RandomSpawnSettings.MaxIterations; ++j)
{
FVector SpawnPosition = FVector::ZeroVector;
FVector SpawnNormal = FVector::ZeroVector;
bool FoundSpawnPos = false;
if (RandomSpawnSettings.SpawnLocations == EMRUKSpawnLocation::Floating)
{
FVector OutPos;
if (auto bRandomPos = Room->GenerateRandomPositionInRoom(OutPos, MinRadius, true); !bRandomPos)
{
break;
}
SpawnPosition = OutPos;
FoundSpawnPos = true;
}
else
{
if (FVector Normal, Pos; Room->GenerateRandomPositionOnSurface(RandomSpawnSettings.SpawnLocations, MinRadius, RandomSpawnSettings.Labels, Pos, Normal))
{
SpawnPosition = Pos + Normal * BaseOffset;
SpawnNormal = Normal;
auto Center = SpawnPosition + Normal * CenterOffset;
if (auto bInRoom = Room->IsPositionInRoom(Center); !bInRoom)
{
continue;
}
if (Room->IsPositionInSceneVolume(Center))
{
continue;
}
if (FMRUKHit Hit{}; Room->Raycast(SpawnPosition, Normal, RandomSpawnSettings.SurfaceClearanceDistance, RandomSpawnSettings.Labels, Hit))
{
continue;
}
FoundSpawnPos = true;
}
}
FQuat SpawnRotation = FQuat::Identity;
if (!SpawnNormal.IsNearlyZero())
{
SpawnNormal.Normalize();
SpawnRotation = FQuat::FindBetweenNormals(FVector::UpVector, SpawnNormal);
}
if (RandomSpawnSettings.CheckOverlaps && Bounds.IsValid && FoundSpawnPos)
{
FBox WorldBounds(AdjustedBounds.Min + SpawnPosition - AdjustedBounds.GetCenter(), AdjustedBounds.Max + SpawnPosition - AdjustedBounds.GetCenter());
FVector AdjustedSpawnPos = SpawnPosition + SpawnRotation * AdjustedBounds.GetCenter();
// check against world
if (!CanSpawnBox(GetTickableGameObjectWorld(), WorldBounds, AdjustedSpawnPos, SpawnRotation, FCollisionQueryParams::DefaultQueryParam, RandomSpawnSettings.CollisionChannel))
{
continue;
}
}
if (bInitializedAnchor && FoundSpawnPos)
{
RandomSpawnSettings.ActorInstance->SetActorLocationAndRotation(SpawnPosition, SpawnRotation);
// ignore SpawnAmount once we have a successful move of existing object in the scene
return true;
}
if (FoundSpawnPos)
{
OutTransforms.Add(FTransform(SpawnRotation, SpawnPosition, FVector::OneVector));
FoundPositions++;
break;
}
}
}
return FoundPositions == RandomSpawnSettings.SpawnAmount;
}
void AMRUtilityKitPositionGenerator::SceneLoaded(bool Success)
{
if (Success)
{
TArray<FTransform> OutTransforms;
const bool bSuccess = GenerateRandomPositionsOnSurface(OutTransforms);
if (!bSuccess)
{
UE_LOG(LogMRUK, Warning, TEXT("Generate Random Positions on Surface not successful"));
return;
}
if (RandomSpawnSettings.ActorClass != nullptr)
{
for (auto Transform : OutTransforms)
{
FActorSpawnParameters Params{};
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
GetWorld()->SpawnActor(RandomSpawnSettings.ActorClass, &Transform, Params);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,87 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "MRUtilityKitSceneDataProvider.h"
#include "UObject/ConstructorHelpers.h"
#include "MRUtilityKit.h"
void AMRUKSceneDataProvider::GetRoom(FString& RoomJSON, FString& RoomName)
{
if (!bUseRandomRoom)
{
if (!SpecificRoomName.IsEmpty())
{
for (const auto& Room : Rooms)
{
const auto RoomDT = Room.Value;
const auto TmpJSON = RoomDT->FindRow<FJSONData>(FName(SpecificRoomName), "", false);
if (TmpJSON != nullptr)
{
RoomJSON = TmpJSON->JSON;
RoomName = SpecificRoomName;
return;
}
}
UE_LOG(LogMRUK, Warning, TEXT("Specific room name not found, using random room."));
}
else
{
UE_LOG(LogMRUK, Warning, TEXT("Specific room name not defined, using random room."));
}
}
if (bUseRandomRoomFromClass)
{
if (!SpecificRoomClass.IsEmpty())
{
const auto RoomDT = *Rooms.Find(SpecificRoomClass);
if (RoomDT != nullptr)
{
TArray<FJSONData*> TmpArray;
RoomDT->GetAllRows("", TmpArray);
auto TmpRowNames = RoomDT->GetRowNames();
const auto Num = TmpArray.Num() - 1;
const auto Idx = FMath::RandRange(0, Num);
RoomJSON = TmpArray[Idx]->JSON;
RoomName = TmpRowNames[Idx].ToString();
return;
}
UE_LOG(LogMRUK, Warning, TEXT("Specific room class not found, using random room."));
}
else
{
UE_LOG(LogMRUK, Warning, TEXT("Specific room class not defined, using random room."));
}
}
auto Num = Rooms.Num() - 1;
auto Idx = FMath::RandRange(0, Num);
TArray<UDataTable*> ChildArray;
Rooms.GenerateValueArray(ChildArray);
const auto Room = ChildArray[Idx];
Num = Room->GetRowMap().Num() - 1;
Idx = FMath::RandRange(0, Num);
TArray<FJSONData*> RandomRoomRows;
auto RandomRoomRowNames = Room->GetRowNames();
Room->GetAllRows("", RandomRoomRows);
RoomJSON = RandomRoomRows[Idx]->JSON;
RoomName = RandomRoomRowNames[Idx].ToString();
}
// Called when the game starts or when spawned
void AMRUKSceneDataProvider::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void AMRUKSceneDataProvider::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}

View File

@@ -0,0 +1,54 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "MRUtilityKitSeatsComponent.h"
#include "MRUtilityKitAnchor.h"
#include "MRUtilityKitRoom.h"
#include "Kismet/KismetMathLibrary.h"
void UMRUKSeatsComponent::CalculateSeatPoses(double SeatWidth)
{
const auto Anchor = Cast<AMRUKAnchor>(GetOwner());
if (!Anchor)
{
return;
}
SeatPoses.Empty();
const auto SurfaceDimensions = Anchor->PlaneBounds.GetExtent();
const auto SurfaceRatio = SurfaceDimensions.X / SurfaceDimensions.Y;
const auto SeatForward = Anchor->GetFacingDirection();
const auto SeatUp = FVector::UpVector;
const auto SeatRotation = UKismetMathLibrary::MakeRotFromXZ(SeatForward, SeatUp).Quaternion();
if (SurfaceRatio < 2.0 && SurfaceRatio > 0.5)
{
// If the surface dimensions are mostly square (likely a chair), just have one centered seat.
FTransform SeatPose{};
SeatPose.SetLocation(Anchor->GetActorLocation());
SeatPose.SetRotation(SeatRotation);
SeatPoses.Add(SeatPose);
}
else
{
const auto XLong = SurfaceDimensions.X > SurfaceDimensions.Y;
const auto LongestDimension = XLong ? SurfaceDimensions.X : SurfaceDimensions.Y;
const auto NumSeats = FMath::Floor(LongestDimension / SeatWidth);
const auto SeatPadding = (LongestDimension - (NumSeats * SeatWidth)) / NumSeats;
const auto FirstSeatOffset = (-LongestDimension + SeatPadding + SeatWidth) * 0.5;
for (int i = 0; i < NumSeats; ++i)
{
const auto SeatRight = XLong ? Anchor->GetActorRightVector() : Anchor->GetActorUpVector();
const auto Offset = FirstSeatOffset + (SeatWidth + SeatPadding) * i;
const auto SeatPosition = Anchor->GetActorLocation() + SeatRight * Offset;
FTransform SeatPose{};
SeatPose.SetLocation(SeatPosition);
SeatPose.SetRotation(SeatRotation);
SeatPoses.Add(SeatPose);
}
}
}

View File

@@ -0,0 +1,61 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "MRUtilityKitSerializationHelpers.h"
TSharedPtr<FJsonValue> MRUKSerialize(const FString& String)
{
return MakeShareable(new FJsonValueString(String));
}
void MRUKDeserialize(const FJsonValue& Value, FString& String)
{
String = Value.AsString();
}
TSharedPtr<FJsonValue> MRUKSerialize(const FOculusXRUUID& UUID)
{
return MakeShareable(new FJsonValueString(UUID.ToString()));
}
void MRUKDeserialize(const FJsonValue& Value, FOculusXRUUID& UUID)
{
const auto Hex = Value.AsString();
if (Hex.Len() == OCULUSXR_UUID_SIZE * 2)
{
HexToBytes(Hex, UUID.UUIDBytes);
}
else
{
UE_LOG(LogJson, Error, TEXT("Json String '%s' is not of expected length %d when deserializing FOculusXRUUID"), *Hex, OCULUSXR_UUID_SIZE * 2);
UUID = FOculusXRUUID();
}
}
TSharedPtr<FJsonValue> MRUKSerialize(const double& Number)
{
return MakeShareable(new FJsonValueNumber(Number));
}
void MRUKDeserialize(const FJsonValue& Value, double& Number)
{
Number = Value.AsNumber();
}
TSharedPtr<FJsonValue> MRUKSerialize(const FOculusXRRoomLayout& RoomLayout)
{
const TSharedRef<FJsonObject> JsonObject = MakeShareable(new FJsonObject);
// Note: No need to serialize the list of room object UUIDs since it is just the list of
// all anchors in the room
JsonObject->SetField(TEXT("FloorUuid"), MRUKSerialize(RoomLayout.FloorUuid));
JsonObject->SetField(TEXT("CeilingUuid"), MRUKSerialize(RoomLayout.CeilingUuid));
JsonObject->SetField(TEXT("WallsUuid"), MRUKSerialize(RoomLayout.WallsUuid));
return MakeShareable(new FJsonValueObject(JsonObject));
}
void MRUKDeserialize(const FJsonValue& Value, FOculusXRRoomLayout& RoomLayout)
{
const auto Object = Value.AsObject();
MRUKDeserialize(*Object->GetField<EJson::None>(TEXT("FloorUuid")), RoomLayout.FloorUuid);
MRUKDeserialize(*Object->GetField<EJson::None>(TEXT("CeilingUuid")), RoomLayout.CeilingUuid);
MRUKDeserialize(*Object->GetField<EJson::None>(TEXT("WallsUuid")), RoomLayout.WallsUuid);
}

View File

@@ -0,0 +1,70 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "Generated/MRUtilityKitShared.h"
#include "MRUtilityKit.h"
#include "Misc/Paths.h"
#include "HAL/PlatformProcess.h"
#include "Interfaces/IPluginManager.h"
MRUKShared* MRUKShared::Instance;
MRUKShared::MRUKShared(void* handle)
: MRUKSharedHandle(handle)
{
LoadNativeFunctions();
}
MRUKShared::~MRUKShared()
{
UnloadNativeFunctions();
FPlatformProcess::FreeDllHandle(MRUKSharedHandle);
MRUKSharedHandle = nullptr;
}
void MRUKShared::LoadMRUKSharedLibrary()
{
if (Instance != nullptr)
{
return;
}
// Load
UE_LOG(LogMRUK, Log, TEXT("Loading MR Utility Kit Shared library"));
#if PLATFORM_WINDOWS
const FString BinariesPath = FPaths::Combine(IPluginManager::Get().FindPlugin(TEXT("OculusXR"))->GetBaseDir(), TEXT("/Source/Thirdparty/MRUtilityKitShared/Lib/Win64"));
FPlatformProcess::PushDllDirectory(*BinariesPath);
void* handle = FPlatformProcess::GetDllHandle(TEXT("mrutilitykitshared.dll"));
FPlatformProcess::PopDllDirectory(*BinariesPath);
#elif PLATFORM_ANDROID
void* handle = FPlatformProcess::GetDllHandle(TEXT("libmrutilitykitshared.so"));
#endif // PLATFORM_ANDROID
if (handle == nullptr)
{
UE_LOG(LogMRUK, Error, TEXT("Failed to load MR Utility Kit Shared library"));
return;
}
Instance = new MRUKShared(handle);
}
void MRUKShared::FreeMRUKSharedLibrary()
{
if (Instance == nullptr)
{
return;
}
delete Instance;
Instance = nullptr;
}
void* MRUKShared::LoadFunction(const TCHAR* ProcName)
{
auto func = FPlatformProcess::GetDllExport(MRUKSharedHandle, ProcName);
if (func == nullptr)
{
UE_LOG(LogMRUK, Error, TEXT("Failed to load native function: %s"), ProcName);
}
return func;
}

View File

@@ -0,0 +1,574 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#include "MRUtilityKitSubsystem.h"
#include "MRUtilityKitAnchor.h"
#include "Kismet/GameplayStatics.h"
#include "HeadMountedDisplayFunctionLibrary.h"
#include "MRUtilityKitPositionGenerator.h"
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"
#include "GameFramework/Pawn.h"
#include "OculusXRRoomLayoutManagerComponent.h"
#include "OculusXRSceneEventDelegates.h"
#include "OculusXRSceneFunctionLibrary.h"
#include "Engine/Engine.h"
#if WITH_EDITOR
#include "Editor.h"
#endif // WITH_EDITOR
#include "Generated/MRUtilityKitShared.h"
AMRUKAnchor* UMRUKSubsystem::Raycast(const FVector& Origin, const FVector& Direction, float MaxDist, const FMRUKLabelFilter& LabelFilter, FMRUKHit& OutHit)
{
AMRUKAnchor* HitComponent = nullptr;
for (const auto& Room : Rooms)
{
FMRUKHit HitResult;
if (!Room)
{
continue;
}
if (AMRUKAnchor* Anchor = Room->Raycast(Origin, Direction, MaxDist, LabelFilter, HitResult))
{
// Prevent further hits which are further away from being found
MaxDist = HitResult.HitDistance;
OutHit = HitResult;
HitComponent = Anchor;
}
}
return HitComponent;
}
bool UMRUKSubsystem::RaycastAll(const FVector& Origin, const FVector& Direction, float MaxDist, const FMRUKLabelFilter& LabelFilter, TArray<FMRUKHit>& OutHits, TArray<AMRUKAnchor*>& OutAnchors)
{
bool HitAnything = false;
for (const auto& Room : Rooms)
{
if (!Room)
{
continue;
}
if (Room->RaycastAll(Origin, Direction, MaxDist, LabelFilter, OutHits, OutAnchors))
{
HitAnything = true;
}
}
return HitAnything;
}
void UMRUKSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
const UMRUKSettings* Settings = GetMutableDefault<UMRUKSettings>();
EnableWorldLock = Settings->EnableWorldLock;
MRUKShared::LoadMRUKSharedLibrary();
}
void UMRUKSubsystem::Deinitialize()
{
MRUKShared::FreeMRUKSharedLibrary();
}
TSharedRef<FJsonObject> UMRUKSubsystem::JsonSerialize()
{
TSharedRef<FJsonObject> JsonObject = MakeShareable(new FJsonObject);
TArray<TSharedPtr<FJsonValue>> RoomsArray;
for (const auto& Room : Rooms)
{
if (Room)
{
RoomsArray.Add(MakeShareable(new FJsonValueObject(Room->JsonSerialize())));
}
}
JsonObject->SetArrayField(TEXT("Rooms"), RoomsArray);
return JsonObject;
}
void UMRUKSubsystem::UnregisterRoom(AMRUKRoom* Room)
{
Rooms.Remove(Room);
}
AMRUKRoom* UMRUKSubsystem::GetCurrentRoom() const
{
// This is a rather expensive operation, we should only do it at most once per frame.
if (CachedCurrentRoomFrame != GFrameCounter)
{
if (const APlayerController* PlayerController = UGameplayStatics::GetPlayerController(this, 0))
{
if (APawn* Pawn = PlayerController->GetPawn())
{
const auto& PawnTransform = Pawn->GetActorTransform();
FVector HeadPosition;
FRotator Unused;
// Get the position and rotation of the VR headset
UHeadMountedDisplayFunctionLibrary::GetOrientationAndPosition(Unused, HeadPosition);
HeadPosition = PawnTransform.TransformPosition(HeadPosition);
for (const auto& Room : Rooms)
{
if (IsValid(Room) && Room->IsPositionInRoom(HeadPosition))
{
CachedCurrentRoom = Room;
CachedCurrentRoomFrame = GFrameCounter;
return Room;
}
}
}
}
}
if (IsValid(CachedCurrentRoom))
{
return CachedCurrentRoom;
}
for (const auto& Room : Rooms)
{
if (IsValid(Room))
{
return Room;
}
}
return nullptr;
}
FString UMRUKSubsystem::SaveSceneToJsonString()
{
FString Json;
const TSharedRef<TJsonWriter<>> JsonWriter = TJsonWriterFactory<>::Create(&Json, 0);
FJsonSerializer::Serialize(JsonSerialize(), JsonWriter);
return Json;
}
void UMRUKSubsystem::LoadSceneFromJsonString(const FString& String)
{
if (SceneData || SceneLoadStatus == EMRUKInitStatus::Busy)
{
UE_LOG(LogMRUK, Error, TEXT("Can't start loading a scene from JSON while the scene is already loading"));
return;
}
SceneData = NewObject<UMRUKSceneData>(this);
if (SceneLoadStatus == EMRUKInitStatus::Complete)
{
// Update the scene
UE_LOG(LogMRUK, Log, TEXT("Update scene from JSON"));
SceneData->OnComplete.AddDynamic(this, &UMRUKSubsystem::UpdatedSceneDataLoadedComplete);
}
else
{
UE_LOG(LogMRUK, Log, TEXT("Load scene from JSON"));
SceneData->OnComplete.AddDynamic(this, &UMRUKSubsystem::SceneDataLoadedComplete);
}
SceneLoadStatus = EMRUKInitStatus::Busy;
SceneData->LoadFromJson(String);
}
void UMRUKSubsystem::LoadSceneFromDevice()
{
if (SceneData || SceneLoadStatus == EMRUKInitStatus::Busy)
{
UE_LOG(LogMRUK, Error, TEXT("Can't start loading a scene from device while the scene is already loading"));
if (SceneData)
{
UE_LOG(LogMRUK, Error, TEXT("Ongoing scene data query"));
}
return;
}
SceneData = NewObject<UMRUKSceneData>(this);
if (!Rooms.IsEmpty())
{
// Update the scene
UE_LOG(LogMRUK, Log, TEXT("Update scene from device"));
SceneData->OnComplete.AddDynamic(this, &UMRUKSubsystem::UpdatedSceneDataLoadedComplete);
}
else
{
UE_LOG(LogMRUK, Log, TEXT("Load scene from device"));
SceneData->OnComplete.AddDynamic(this, &UMRUKSubsystem::SceneDataLoadedComplete);
}
SceneLoadStatus = EMRUKInitStatus::Busy;
#if WITH_EDITOR
if (GetWorld()->WorldType == EWorldType::PIE && GEditor->IsSimulateInEditorInProgress())
{
// LoadFromDevice sometimes doesn't broadcast failure when running in simulate mode. We can skip trying and just fail immediately in this case.
SceneData->OnComplete.Broadcast(false);
}
else
#endif // WITH_EDITOR
{
SceneData->LoadFromDevice();
}
}
void UMRUKSubsystem::SceneDataLoadedComplete(bool Success)
{
UE_LOG(LogMRUK, Log, TEXT("Loaded scene data. Success==%d"), Success);
if (!SceneData)
{
UE_LOG(LogMRUK, Warning, TEXT("Can't process scene data if it's not loaded"));
FinishedLoading(false);
return;
}
if (SceneData->RoomsData.IsEmpty())
{
UE_LOG(LogMRUK, Warning, TEXT("No room data found"));
FinishedLoading(false);
return;
}
if (Success)
{
UE_LOG(LogMRUK, Log, TEXT("Spawn rooms from scene data"));
for (const auto& RoomData : SceneData->RoomsData)
{
AMRUKRoom* Room = SpawnRoom();
Room->LoadFromData(RoomData);
}
}
FinishedLoading(Success);
for (const auto& Room : Rooms)
{
OnRoomCreated.Broadcast(Room);
}
}
void UMRUKSubsystem::UpdatedSceneDataLoadedComplete(bool Success)
{
UE_LOG(LogMRUK, Log, TEXT("Loaded updated scene data from device. Sucess==%d"), Success);
TArray<TObjectPtr<AMRUKRoom>> RoomsCreated;
TArray<TObjectPtr<AMRUKRoom>> RoomsUpdated;
if (Success)
{
UE_LOG(LogMRUK, Log, TEXT("Update found %d rooms"), SceneData->RoomsData.Num());
TArray<TObjectPtr<AMRUKRoom>> RoomsToRemove = Rooms;
Rooms.Empty();
for (int i = 0; i < SceneData->RoomsData.Num(); ++i)
{
UMRUKRoomData* RoomData = SceneData->RoomsData[i];
const TObjectPtr<AMRUKRoom>* RoomFound = RoomsToRemove.FindByPredicate([RoomData](TObjectPtr<AMRUKRoom> Room) {
return Room->Corresponds(RoomData);
});
TObjectPtr<AMRUKRoom> Room = nullptr;
if (RoomFound)
{
Room = *RoomFound;
UE_LOG(LogMRUK, Log, TEXT("Update room from query"));
Rooms.Push(Room);
RoomsToRemove.Remove(Room);
RoomsUpdated.Push(Room);
}
else
{
UE_LOG(LogMRUK, Log, TEXT("Spawn room from query"));
Room = SpawnRoom();
RoomsCreated.Push(Room);
}
Room->LoadFromData(RoomData);
}
UE_LOG(LogMRUK, Log, TEXT("Destroy %d old rooms"), RoomsToRemove.Num());
for (const auto& Room : RoomsToRemove)
{
OnRoomRemoved.Broadcast(Room);
Room->Destroy();
}
}
FinishedLoading(Success);
for (const auto& Room : RoomsUpdated)
{
OnRoomUpdated.Broadcast(Room);
}
for (const auto& Room : RoomsCreated)
{
OnRoomCreated.Broadcast(Room);
}
}
void UMRUKSubsystem::ClearScene()
{
if (SceneLoadStatus == EMRUKInitStatus::Busy)
{
UE_LOG(LogMRUK, Error, TEXT("Cannot clear scene while scene is loading"));
return;
}
SceneLoadStatus = EMRUKInitStatus::None;
// No ranged for loop because rooms may remove themselves from the array during destruction
for (int32 I = Rooms.Num() - 1; I >= 0; --I)
{
AMRUKRoom* Room = Rooms[I];
if (IsValid(Room))
{
Room->Destroy();
}
}
Rooms.Empty();
}
AMRUKAnchor* UMRUKSubsystem::TryGetClosestSurfacePosition(const FVector& WorldPosition, FVector& OutSurfacePosition, const FMRUKLabelFilter& LabelFilter, double MaxDistance)
{
AMRUKAnchor* ClosestAnchor = nullptr;
for (const auto& Room : Rooms)
{
if (!Room)
{
continue;
}
double SurfaceDistance{};
FVector SurfacePos{};
if (const auto Anchor = Room->TryGetClosestSurfacePosition(WorldPosition, SurfacePos, SurfaceDistance, LabelFilter, MaxDistance))
{
ClosestAnchor = Anchor;
OutSurfacePosition = SurfacePos;
MaxDistance = SurfaceDistance;
}
}
return ClosestAnchor;
}
AMRUKAnchor* UMRUKSubsystem::TryGetClosestSeatPose(const FVector& RayOrigin, const FVector& RayDirection, FTransform& OutSeatTransform)
{
AMRUKAnchor* ClosestAnchor = nullptr;
double ClosestSeatDistanceSq = DBL_MAX;
for (const auto& Room : Rooms)
{
if (!Room)
{
continue;
}
FTransform SeatTransform{};
if (AMRUKAnchor* Anchor = Room->TryGetClosestSeatPose(RayOrigin, RayDirection, SeatTransform))
{
const double SeatDistanceSq = (RayOrigin - Anchor->GetActorTransform().GetTranslation()).SquaredLength();
if (SeatDistanceSq < ClosestSeatDistanceSq)
{
ClosestAnchor = Anchor;
ClosestSeatDistanceSq = SeatDistanceSq;
OutSeatTransform = SeatTransform;
}
}
}
return ClosestAnchor;
}
AMRUKAnchor* UMRUKSubsystem::GetBestPoseFromRaycast(const FVector& RayOrigin, const FVector& RayDirection, double MaxDist, const FMRUKLabelFilter& LabelFilter, FTransform& OutPose, EMRUKPositioningMethod PositioningMethod)
{
AMRUKAnchor* ClosestAnchor = nullptr;
double ClosestPoseDistanceSq = DBL_MAX;
for (const auto& Room : Rooms)
{
if (!Room)
{
continue;
}
FTransform Pose{};
AMRUKAnchor* Anchor = Room->GetBestPoseFromRaycast(RayOrigin, RayDirection, MaxDist, LabelFilter, Pose, PositioningMethod);
if (Anchor)
{
const double PoseDistanceSq = (RayOrigin - OutPose.GetTranslation()).SquaredLength();
if (PoseDistanceSq < ClosestPoseDistanceSq)
{
ClosestAnchor = Anchor;
ClosestPoseDistanceSq = PoseDistanceSq;
OutPose = Pose;
}
}
}
return ClosestAnchor;
}
AMRUKAnchor* UMRUKSubsystem::GetKeyWall(double Tolerance)
{
if (AMRUKRoom* CurrentRoom = GetCurrentRoom())
{
return CurrentRoom->GetKeyWall(Tolerance);
}
return nullptr;
}
AMRUKAnchor* UMRUKSubsystem::GetLargestSurface(const FString& Label)
{
if (AMRUKRoom* CurrentRoom = GetCurrentRoom())
{
return CurrentRoom->GetLargestSurface(Label);
}
return nullptr;
}
AMRUKAnchor* UMRUKSubsystem::IsPositionInSceneVolume(const FVector& WorldPosition, bool TestVerticalBounds, double Tolerance)
{
for (const auto& Room : Rooms)
{
if (!Room)
{
continue;
}
if (const auto Anchor = Room->IsPositionInSceneVolume(WorldPosition, TestVerticalBounds, Tolerance))
{
return Anchor;
}
}
return nullptr;
}
TArray<AActor*> UMRUKSubsystem::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*> UMRUKSubsystem::SpawnInteriorFromStream(const TMap<FString, FMRUKSpawnGroup>& SpawnGroups, const FRandomStream& RandomStream, const TArray<FString>& CutHoleLabels, UMaterialInterface* ProceduralMaterial, bool ShouldFallbackToProcedural)
{
TArray<AActor*> AllInteriorActors;
for (const auto& Room : Rooms)
{
if (!Room)
{
continue;
}
auto InteriorActors = Room->SpawnInteriorFromStream(SpawnGroups, RandomStream, CutHoleLabels, ProceduralMaterial, ShouldFallbackToProcedural);
AllInteriorActors.Append(InteriorActors);
}
return AllInteriorActors;
}
bool UMRUKSubsystem::LaunchSceneCapture()
{
const bool Success = GetRoomLayoutManager()->LaunchCaptureFlow();
if (Success)
{
UE_LOG(LogMRUK, Log, TEXT("Capture flow launched with success"));
}
else
{
UE_LOG(LogMRUK, Error, TEXT("Launching capture flow failed!"));
}
return Success;
}
FBox UMRUKSubsystem::GetActorClassBounds(TSubclassOf<AActor> Actor)
{
if (const auto Entry = ActorClassBoundsCache.Find(Actor))
{
return *Entry;
}
const auto TempActor = GetWorld()->SpawnActor(Actor);
const auto Bounds = TempActor->CalculateComponentsBoundingBoxInLocalSpace(true);
TempActor->Destroy();
ActorClassBoundsCache.Add(Actor, Bounds);
return Bounds;
}
void UMRUKSubsystem::SceneCaptureComplete(FOculusXRUInt64 RequestId, bool bSuccess)
{
UE_LOG(LogMRUK, Log, TEXT("Scene capture complete Success==%d"), bSuccess);
OnCaptureComplete.Broadcast(bSuccess);
}
UOculusXRRoomLayoutManagerComponent* UMRUKSubsystem::GetRoomLayoutManager()
{
if (!RoomLayoutManager)
{
FActorSpawnParameters Params{};
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
Params.Owner = nullptr;
RoomLayoutManagerActor = GetWorld()->SpawnActor<AActor>(Params);
RoomLayoutManagerActor->SetRootComponent(NewObject<USceneComponent>(RoomLayoutManagerActor, TEXT("SceneComponent")));
RoomLayoutManagerActor->AddComponentByClass(UOculusXRRoomLayoutManagerComponent::StaticClass(), false, FTransform::Identity, false);
RoomLayoutManager = RoomLayoutManagerActor->GetComponentByClass<UOculusXRRoomLayoutManagerComponent>();
RoomLayoutManager->OculusXRRoomLayoutSceneCaptureComplete.AddDynamic(this, &UMRUKSubsystem::SceneCaptureComplete);
}
return RoomLayoutManager;
}
AMRUKRoom* UMRUKSubsystem::SpawnRoom()
{
FActorSpawnParameters ActorSpawnParams;
ActorSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
AMRUKRoom* Room = GetWorld()->SpawnActor<AMRUKRoom>(ActorSpawnParams);
#if WITH_EDITOR
Room->SetActorLabel(TEXT("ROOM"));
#endif
Rooms.Push(Room);
return Room;
}
void UMRUKSubsystem::FinishedLoading(bool Success)
{
UE_LOG(LogMRUK, Log, TEXT("Finished loading: Success==%d"), Success);
if (SceneData)
{
SceneData->MarkAsGarbage();
SceneData = nullptr;
}
if (Success)
{
SceneLoadStatus = EMRUKInitStatus::Complete;
}
else
{
SceneLoadStatus = EMRUKInitStatus::Failed;
}
OnSceneLoaded.Broadcast(Success);
}
void UMRUKSubsystem::Tick(float DeltaTime)
{
if (EnableWorldLock)
{
if (const auto Room = GetCurrentRoom())
{
if (const APlayerController* PlayerController = UGameplayStatics::GetPlayerController(this, 0))
{
if (APawn* Pawn = PlayerController->GetPawn())
{
const auto& PawnTransform = Pawn->GetActorTransform();
FVector HeadPosition;
FRotator Unused;
// Get the position and rotation of the VR headset
UHeadMountedDisplayFunctionLibrary::GetOrientationAndPosition(Unused, HeadPosition);
HeadPosition = PawnTransform.TransformPosition(HeadPosition);
Room->UpdateWorldLock(Pawn, HeadPosition);
}
}
}
}
}
bool UMRUKSubsystem::IsTickable() const
{
return !HasAnyFlags(RF_BeginDestroyed) && IsValidChecked(this) && (EnableWorldLock);
}