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,53 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
using UnrealBuildTool;
public class MRUtilityKit : ModuleRules
{
public MRUtilityKit(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
bUseUnity = true;
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"RenderCore",
"Projects"
});
if (Target.Version.MajorVersion > 5 || (Target.Version.MajorVersion == 5 && Target.Version.MinorVersion >= 3))
{
PublicDependencyModuleNames.AddRange(
new string[]
{
"XRBase",
});
}
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
"OculusXRHMD",
"OculusXRAnchors",
"OculusXRScene",
"Json",
"ProceduralMeshComponent",
"HeadMountedDisplay",
"MRUtilityKitShared",
});
if (Target.bBuildEditor == true)
{
PrivateDependencyModuleNames.Add("UnrealEd");
}
}
}

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);
}

View File

@@ -0,0 +1,412 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "Modules/ModuleManager.h"
#include "GameFramework/Actor.h"
#include "MRUtilityKit.generated.h"
DECLARE_LOG_CATEGORY_EXTERN(LogMRUK, Log, All);
UENUM(BlueprintType)
enum class EMRUKInitStatus : uint8
{
/// Not Initialized.
None,
/// Is busy Initializing.
Busy,
/// Has finished Initializing.
Complete,
/// Failed to initialize.
Failed,
};
UENUM(BlueprintType)
enum class EMRUKCoordModeU : uint8
{
/// The texture coordinates start at 0 and increase by 1 unit every meter.
Metric,
/// The texture coordinates start at 0 and increase by 1 unit every meter but are adjusted to end on a whole number to avoid seams.
MetricSeamless,
/// The texture coordinates are adjusted to the other dimensions to ensure the aspect ratio is maintained.
MaintainAspectRatio,
/// The texture coordinates are adjusted to the other dimensions to ensure the aspect ratio is maintained but are adjusted to end on a whole number to avoid seams.
MaintainAspectRatioSeamless,
/// The texture coordinates range from 0 to 1.
Stretch,
};
UENUM(BlueprintType)
enum class EMRUKCoordModeV : uint8
{
/// The texture coordinates start at 0 and increase by 1 unit every meter.
Metric,
/// The texture coordinates are adjusted to the other dimensions to ensure the aspect ratio is maintained.
MaintainAspectRatio,
/// The texture coordinates range from 0 to 1.
Stretch,
};
UENUM(BlueprintType)
enum class EMRUKSpawnerSelectionMode : uint8
{
/// Pick one at random.
Random,
/// Pick the closest size.
ClosestSize,
/// Used in the AMRUKAnchorActorSpawner to use allow for a custom selection mode.
Custom,
};
UENUM(BlueprintType)
enum class EMRUKSpawnerScalingMode : uint8
{
/// Stretch each axis to exactly match the size of the Plane/Volume.
Stretch,
/// Scale each axis by the same amount to maintain the correct aspect ratio.
UniformScaling,
/// Scale the X and Y axes uniformly but the Z scale can be different.
UniformXYScale,
/// Don't perform any scaling.
NoScaling,
/// Used in the AMRUKAnchorActorSpawner to use allow for a custom scaling.
Custom,
};
UENUM(BlueprintType)
enum class EMRUKAlignMode : uint8
{
/// Do not perform any alignment
None,
/// Align the bottom of the bounding boxes and center the rest
Default,
/// Center the bounding box in the anchor bounding box
CenterOnCenter,
/// Align the bottom of the bounding boxes and center the rest
BottomOnBottom,
/// Align the top of the bounding boxes and center the rest
TopOnTop,
/// Align the left of the bounding boxes and center the rest
LeftOnLeft,
/// Align the right of the bounding boxes and center the rest
RightOnRight,
/// Align the front of the bounding boxes and center the rest
FrontOnFront,
/// Align the back of the bounding boxes and center the rest
BackOnBack,
/// Align the top to the bottom of the anchor bounding box and center the rest
BottomOnTop,
/// Align the bottom to the top of the anchor bounding box and center the rest
TopOnBottom,
/// Align the left to the right of the anchor bounding box and center the rest
LeftOnRight,
/// Align the right to the left of the anchor bounding box and center the rest
RightOnLeft,
/// Align the front to the back of the anchor bounding box and center the rest
FrontOnBack,
/// Align the back to the front of the anchor bounding box and center the rest
BackOnFront,
/// Use custom alignment mode
Custom,
};
/**
* This enum is used to specify the component type, scene anchors can either have plane or volume components associated with them or both.
*/
UENUM(meta = (Bitflags, UseEnumValuesAsMaskValuesInEditor = "true"))
enum class EMRUKComponentType
{
/// No component type.
None = 0 UMETA(Hidden),
/// Plane component type.
Plane = 1 << 0,
/// Volume component type.
Volume = 1 << 1,
/// Mesh component type.
Mesh = 1 << 2,
/// All component types.
All = Plane | Volume | Mesh UMETA(Hidden),
};
ENUM_CLASS_FLAGS(EMRUKComponentType);
/**
* Describes a Raycast hit in the MRUK (Mixed Reality Utility Kit). This structure is created by the AMRUKAnchor::Raycast and AMRUKAnchor::RaycastAll methods. You can read the position where the raycast hit, the normal of the surface that was hit, and the distance from the origin to the raycast hit position.
*/
USTRUCT(BlueprintType)
struct MRUTILITYKIT_API FMRUKHit
{
GENERATED_BODY()
/**
* The position where the raycast hit.
*/
UPROPERTY(BlueprintReadOnly, Category = "MR Utility Kit")
FVector HitPosition = FVector::ZeroVector;
/**
* The normal of the surface that was hit.
*/
UPROPERTY(BlueprintReadOnly, Category = "MR Utility Kit")
FVector HitNormal = FVector::ZeroVector;
/**
* The distance between the origin of the ray to the hit position.
*/
UPROPERTY(BlueprintReadOnly, Category = "MR Utility Kit")
float HitDistance = 0.0f;
};
/**
* Label filter to use in MRUK (Mixed Reality Utility Kit). You can use this to filter anchors by their labels.
* use the IncludedLabels and ExcludedLabels list to specify which labels to include and exclude.
*/
USTRUCT(BlueprintType)
struct MRUTILITYKIT_API FMRUKLabelFilter
{
GENERATED_BODY()
/**
* If included labels is not empty then the anchor must have at
* least one of the labels in this list.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
TArray<FString> IncludedLabels;
/**
* Anchors with any of the labels in this exclusion list
* will be ignored.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
TArray<FString> ExcludedLabels;
/**
* Enum flags representing component types to include, by default include all component types.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit", meta = (Bitmask, BitmaskEnum = "EMRUKComponentType"))
int32 ComponentTypes = static_cast<int32>(EMRUKComponentType::All);
/**
* Check if the labels pass the given label filter
* @param Labels The labels to check.
* @return Whether the filter passes or not.
*/
bool PassesFilter(const TArray<FString>& Labels) const;
};
/**
* Represents a configuration for adjusting the UV texture coordinates of a plane.
*
* It contains properties to specify an offset and scale to be applied to the UV texture coordinates.
*/
USTRUCT(BlueprintType)
struct MRUTILITYKIT_API FMRUKPlaneUV
{
GENERATED_BODY()
/**
* Offset applied to the UV texture coordinates.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
FVector2D Offset = FVector2D::ZeroVector;
/**
* Scale applied to the UV texture coordinates.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
FVector2D Scale = FVector2D::UnitVector;
};
/**
* Texture coordinate modes for MRUK (Mixed Reality Utility Kit). You can use this to specify the texture coordinate mode for the U and V directions.
*/
USTRUCT(BlueprintType)
struct MRUTILITYKIT_API FMRUKTexCoordModes
{
GENERATED_BODY()
/**
* Texture Coordinate mode for the U direction.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
EMRUKCoordModeU U = EMRUKCoordModeU::Metric;
/**
* Texture Coordinate mode for the V direction.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
EMRUKCoordModeV V = EMRUKCoordModeV::Metric;
};
/**
* This struct represents a configuration for spawning an actor in the scene.
*
* It contains properties to specify the class of the actor to spawn, whether to match the aspect ratio of the volume,
* whether to calculate the facing direction of the actor, and what scaling and alignment modes to apply to the actor.
*/
USTRUCT(BlueprintType)
struct MRUTILITYKIT_API FMRUKSpawnActor
{
GENERATED_BODY()
/**
* The class of actor to spawn.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
TSubclassOf<AActor> Actor;
/**
* When match aspect ratio is enabled then the actor will be rotated
* to try and match the aspect ratio of the volume as closely as possible.
* This is most useful for long and thin volumes, keep this disabled for
* objects with an aspect ratio close to 1:1. Only applies to volumes.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
bool MatchAspectRatio = false;
/**
* When calculate facing direction is enabled the actor will be rotated to
* face away from the closest wall. If match aspect ratio is also enabled
* then that will take precedence and it will be constrained to a choice
* between 2 directions only. Only applies to volumes.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
bool CalculateFacingDirection = false;
/**
* Set what scaling mode to apply to the actor. By default the actor will
* be stretched to fit the size of the plane/volume. But in some cases
* this may not be desirable and can be customized here.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
EMRUKSpawnerScalingMode ScalingMode = EMRUKSpawnerScalingMode::Stretch;
/**
* Set what alignment mode to apply to the actor. By default the actor will
* be aligned that its bounding box matches the one from the anchor.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
EMRUKAlignMode AlignMode = EMRUKAlignMode::Default;
};
/**
* This enum is used to specify the fallback behaviour when spawning an scene actor.
* Specify whether to fallback to a procedural mesh or not.
*/
UENUM(BlueprintType)
enum class EMRUKFallbackToProceduralOverwrite : uint8
{
/// Don't override the fallback to procedural standard behaviour.
Default,
/// Fallback to a procedural mesh.
Fallback,
/// Don't fallback to a procedural mesh.
NoFallback,
};
/**
* Holds a configuration for spawning a group of actors.
*
* It contains properties to specify a list of actors to choose from, the selection mode when multiple actors are specified,
* and whether to fall back to spawning a procedural mesh if no actor class has been specified for this label.
*/
USTRUCT(BlueprintType)
struct MRUTILITYKIT_API FMRUKSpawnGroup
{
GENERATED_BODY()
/**
* List of actors to choose from, multiple actors can be specified and
* the selection criteria will be determined by the SelectionMode option.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
TArray<FMRUKSpawnActor> Actors;
/**
* Set the selection mode when multiple different actors are specified.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
EMRUKSpawnerSelectionMode SelectionMode = EMRUKSpawnerSelectionMode::Random;
/**
* Control if there should happen a fallback to spawning a procedural mesh
* in case no actor class has been specified for this label. The global
* fallback behaviour can be specified in the AMRUKAnchorActorSpawner.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
EMRUKFallbackToProceduralOverwrite FallbackToProcedural = EMRUKFallbackToProceduralOverwrite::Default;
};
/**
* Implements the settings for the MRUtilityKit plugin. This is Unreal specific and not part of the MR Utility Kit library.
*/
UCLASS(config = Game, defaultconfig)
class MRUTILITYKIT_API UMRUKSettings : public UObject
{
GENERATED_BODY()
public:
UMRUKSettings(const FObjectInitializer& obj);
/**
* When world locking is enabled the position of the VR Pawn will be adjusted each frame to ensure
* the room anchors are where they should be relative to the camera position. This is necessary to
* ensure the position of the virtual objects in the world do not get out of sync with the real world.
*/
UPROPERTY(config, EditAnywhere, Category = "MR Utility Kit")
bool EnableWorldLock = true;
};
/**
* MRUK (Mixed Reality Utility Kit) labels. These are the labels that are used by the MR Utility Kit library.
* Those labels are used to identify the different types of objects in the scene, such as walls, floors, etc.
*
* Furthermore you also use those labels to filter, for queries and other tools such as the Raycast and RaycastAll methods.
*/
struct MRUTILITYKIT_API FMRUKLabels
{
static const FString Floor;
static const FString WallFace;
static const FString InvisibleWallFace;
static const FString Ceiling;
static const FString DoorFrame;
static const FString WindowFrame;
static const FString Couch;
static const FString Table;
static const FString Screen;
static const FString Bed;
static const FString Lamp;
static const FString Plant;
static const FString Storage;
static const FString WallArt;
static const FString GlobalMesh;
static const FString Other;
};
/**
* This spawnmode controls how the MR Utility Kit handles spawning actors in the scene, either for all rooms, only for the current room or not at all.
*/
UENUM(BlueprintType)
enum class EMRUKSpawnMode : uint8
{
/// Do not spawn anything on loading a scene or rooms.
None = 0,
/// Will only take the current room into account. This enables legacy single room behaviour. Keep in mind that if your
/// experience loads multiple rooms and you use that mode the behaviour might be undefined.
CurrentRoomOnly,
/// Spawn in every room and keep on spawning whenever a new room was discovered.
AllRooms
};
/**
* UE Module interface impelmentation
*/
class FMRUKModule : public IModuleInterface
{
public:
/** IModuleInterface implementation */
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};

View File

@@ -0,0 +1,281 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "GameFramework/Actor.h"
#include "Dom/JsonObject.h"
#include "MRUtilityKitAnchorActorSpawner.h"
#include "OculusXRAnchorTypes.h"
#include "ProceduralMeshComponent.h"
#include "MRUtilityKitAnchor.generated.h"
class AMRUKRoom;
class UMRUKAnchorData;
/**
* Represents an anchor in the Mixed Reality Utility Kit. This combines an Unreal actor with the scene anchor.
* The actor is placed at the position of the anchor and the actor's rotation is set to match the rotation of the anchor.
* Provides functions to check if a position is inside the volume or plane of the anchor, raycast against the anchor, etc...
* @see https://developer.oculus.com/documentation/unreal/unreal-spatial-anchors/
* for more information about anchors in the Mixed Reality Utility Kit.
*/
UCLASS(ClassGroup = MRUtilityKit, meta = (DisplayName = "MR Utility Kit Anchor"))
class MRUTILITYKIT_API AMRUKAnchor : public AActor
{
GENERATED_BODY()
public:
/**
* The space handle of this anchor
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
FOculusXRUInt64 SpaceHandle;
/**
* The anchors UUID
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
FOculusXRUUID AnchorUUID;
/**
* The semantic classification of the anchor, also sometimes refered to as labels for short.
* This can be for example FLOOR, COUCH, TABLE, SCREEN, BED, LAMP, etc...
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
TArray<FString> SemanticClassifications;
/**
* If the anchor has a plane attached to it, this represents the bounds of that plane in
* local coordinate space.
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
FBox2D PlaneBounds{ ForceInit };
/**
* If the anchor has a plane attached to it, this represents the boundary of it in
* local coordinate space. For rectangular boundaries this will be the same as the
* PlaneBounds.
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
TArray<FVector2D> PlaneBoundary2D;
/**
* If the anchor has a volume attached to it, this represents the bounds of that volume in
* local coordinate space.
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
FBox VolumeBounds{ ForceInit };
/**
* Procedural mesh that is generated from the anchor geometry.
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadWrite, Category = "MR Utility Kit")
TObjectPtr<UProceduralMeshComponent> ProceduralMeshComponent;
/**
* Pointer to the parent anchor, e.g. if this is a door or window frame the parent will
* be a wall. If this is a screen it could have a desk parent.
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
TObjectPtr<AMRUKAnchor> ParentAnchor;
/**
* Array of all children attached to it, e.g. if this is a wall, it could have an array
* of door/window frames. If this is a desk it could have an array of screens on it.
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
TArray<TObjectPtr<AMRUKAnchor>> ChildAnchors;
/**
* The room this anchor is placed in.
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
TObjectPtr<AMRUKRoom> Room;
/**
* Check if a 2D position is within the boundary of the plane. The position should be in
* the local coordinate system NOT world coordinates.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool IsPositionInBoundary(const FVector2D& Position);
/**
* Generate a uniform random position within the boundary of the plane.
* @return The random position in local coordinate space.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
FVector GenerateRandomPositionOnPlane();
/**
* Generate a uniform random position within the boundary of the plane from a random stream.
* @param RandomStream A random generator used to generate the position on the plane.
* @return The random position in local coordinate space.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
FVector GenerateRandomPositionOnPlaneFromStream(const FRandomStream& RandomStream);
/**
* Cast a ray and return the closest hit against the volume and plane bounds.
* @param Origin Origin The origin of the ray.
* @param Direction Direction The direction of the ray.
* @param MaxDist The maximum distance the ray should travel.
* @param OutHit The closest hit.
* @param ComponentTypes The component types to include in the raycast.
* @return Whether the ray hit anything
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool Raycast(const FVector& Origin, const FVector& Direction, float MaxDist, FMRUKHit& OutHit, UPARAM(meta = (Bitmask, BitmaskEnum = "EMRUKComponentType")) int32 ComponentTypes = 7 /* EMRUKComponentType::All */);
static_assert(static_cast<int32>(EMRUKComponentType::All) == 7, "If this changes, please update the hardcoded default parameter in the Raycast function above");
/**
* Cast a ray and collect hits against the volume and plane bounds. The order of the hits in the array is not specified.
* @param Origin Origin The origin of the ray.
* @param Direction Direction The direction of the ray.
* @param MaxDist The maximum distance the ray should travel.
* @param OutHits The hits the ray collected.
* @param ComponentTypes The component types to include in the raycast.
* @return Whether the ray hit anything
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool RaycastAll(const FVector& Origin, const FVector& Direction, float MaxDist, TArray<FMRUKHit>& OutHits, UPARAM(meta = (Bitmask, BitmaskEnum = "EMRUKComponentType")) int32 ComponentTypes = 7 /* EMRUKComponentType::All */);
static_assert(static_cast<int32>(EMRUKComponentType::All) == 7, "If this changes, please update the hardcoded default parameter in the RaycastAll function above");
/**
* Attach a procedural mesh to the anchor. The mesh will match the size, position and shape of the volume and/or plane
* if they are set.
* @param PlaneUVAdjustments Scale and offset to apply to the UV texture coordinates. If more than one is specified
* then multiple UV texture coordinates are created (up to 4) and adjustments applied to
* each. This can be left empty in which case a single set of UV texture coordinates are
* created in the range 0 to 1 for the plane.
* @param CutHoleLabels Labels for which the generated mesh should have holes. Only works with planes.
* @param GenerateCollision Whether to generate collision geometry or not
* @param ProceduralMaterial Material to use on the procedural generated mesh.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit", meta = (AutoCreateRefTerm = "PlaneUVAdjustments", DeprecatedFunction, DeprecationMessage = "Use GenerateProceduralMesh instead."))
void AttachProceduralMesh(TArray<FMRUKPlaneUV> PlaneUVAdjustments, const TArray<FString>& CutHoleLabels, bool GenerateCollision = true, UMaterialInterface* ProceduralMaterial = nullptr);
/**
* Generate a procedural mesh for the anchor. The mesh will match the size, position and shape of the volume and/or plane
* if they are set.
* @param ProceduralMesh The procedural mesh component that should be used to store the generated mesh.
* @param PlaneUVAdjustments Scale and offset to apply to the UV texture coordinates. If more than one is specified
* then multiple UV texture coordinates are created (up to 4) and adjustments applied to
* each. This can be left empty in which case a single set of UV texture coordinates are
* created in the range 0 to 1 for the plane.
* @param CutHoleLabels Labels for which the generated mesh should have holes. Only works with planes.
* @param GenerateCollision Whether to generate collision geometry or not
* @param Offset A offset to make the procedural mesh slightly bigger or smaller than the anchors volume/plane.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit", meta = (AutoCreateRefTerm = "PlaneUVAdjustments"))
void GenerateProceduralAnchorMesh(UProceduralMeshComponent* ProceduralMesh, const TArray<FMRUKPlaneUV>& PlaneUVAdjustments, const TArray<FString>& CutHoleLabels, bool PreferVolume = false, bool GenerateCollision = true, double Offset = 0.0);
/**
* Check if the anchor has the given label.
* @param Label The label to check.
* @return Whether the anchor has the given label.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool HasLabel(const FString& Label) const;
/**
* Check if the anchor has any of the given labels.
* @param Labels The labels to check.
* @return Whether the anchor has any of the given labels.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool HasAnyLabel(const TArray<FString>& Labels) const;
/**
* Check if the anchor passes the given label filter
* @param LabelFilter The labels to check.
* @return Whether the anchor has any of the given labels.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool PassesLabelFilter(const FMRUKLabelFilter& LabelFilter) const;
/**
* Calculate the closest surface position on this anchor.
* @param TestPosition The position in world space for which the closes surface position should be obtained.
* @param OutSurfacePosition The closest surface position
* @return The distance between TestPosition and OutSurfacePosition
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
double GetClosestSurfacePosition(const FVector& TestPosition, FVector& OutSurfacePosition);
/**
* Checks if the given position is on or inside the volume bounds.
* Floor, ceiling and wall anchors will be excluded from the search.
* @param Position The position in world space to check
* @param TestVerticalBounds Whether the vertical bounds should be checked or not
* @param Tolerance Tolerance
* @return The anchor the WorldPosition is in. A null pointer otherwise.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool IsPositionInVolumeBounds(const FVector& Position, bool TestVerticalBounds = true, double Tolerance = 0.0);
/**
* Gets a natural “forward” direction for anchors; for planes, this is always Z-forward.
* For volumes, its the X/Y cardinal axis that aligns best with the normal of the closest wall.
* @return The forward facing direction.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
FVector GetFacingDirection() const;
/**
* Spawn a mesh on the position of this anchor.
* The actor should have Z as up, Y as right and X as forward.
* @param ActorClass The Class to spawn at the anchors position.
* @param MatchAspectRatio If true the actor will be rotated to best match the aspect ratio of the volume (applies to volumes only).
* @param CalculateFacingDirection If true then actor will be rotated to face away from the closest wall (applies to volumes only).
* @param ScalingMode Sets how to scale the actor to fit the size of the volume/plane.
* @return The spawned actor or null if nothing was spawned.
*/
UFUNCTION(BlueprintCallable, meta = (DeprecatedFunction, DeprecationMessage = "Use AMRUKAnchorActorSpawner instead."), Category = "MR Utility Kit")
AActor* SpawnInterior(const TSubclassOf<class AActor>& ActorClass, bool MatchAspectRatio = false, bool CalculateFacingDirection = false, EMRUKSpawnerScalingMode ScalingMode = EMRUKSpawnerScalingMode::Stretch);
public:
AMRUKAnchor(const FObjectInitializer& ObjectInitializer);
/**
* Load the anchor from a MRUKAnchorData. This is used to load or update the anchor from device or from a JSON file.
*
* @param AnchorData The data to load from.
* @return true if the anchor was loaded successfully.
* @return false if the anchor could not be loaded.
*/
bool LoadFromData(UMRUKAnchorData* AnchorData);
/**
* Attach a procedural mesh to the anchor. The mesh will match the size, position and shape of the volume and/or plane.
*
* @param CutHoleLabels Labels for which the generated mesh should have holes. Only works with planes. Example values: "WindowFrame", "DoorFrame".
* @param GenerateCollision Whether to generate collision geometry or not.
* @param ProceduralMaterial Material to use on the procedural generated mesh.
*/
void AttachProceduralMesh(const TArray<FString>& CutHoleLabels = {}, bool GenerateCollision = true, UMaterialInterface* ProceduralMaterial = nullptr);
TSharedRef<FJsonObject> JsonSerialize();
protected:
void EndPlay(EEndPlayReason::Type Reason) override;
private:
bool RayCastPlane(const FRay& LocalRay, float MaxDist, FMRUKHit& OutHit);
bool RayCastVolume(const FRay& LocalRay, float MaxDist, FMRUKHit& OutHit);
struct TriangulatedMeshCache
{
TArray<FVector2D> Vertices;
TArray<int32> Triangles;
TArray<float> Areas;
float TotalArea;
void Clear();
};
UPROPERTY()
AActor* Interior = nullptr;
TOptional<TriangulatedMeshCache> CachedMesh;
};

View File

@@ -0,0 +1,322 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "GameFramework/Actor.h"
#include "MRUtilityKit.h"
#include "MRUtilityKitAnchorActorSpawner.generated.h"
extern const FName GMRUK_PROCEDURAL_ANCHOR_MESH_TAG;
class AMRUKAnchor;
/**
* Spawns meshes on anchor positions.
* If the out of the box functionality doesn't match your goals the AnchorActorSpawner provides way to inject
* custom spawning logic into every step of it's spawning process by overwriting certain functions.
* For this please take a look at SpawnAnchorActorsForRoom(), SpawnAnchorActorForLabel(), and SpawnAnchorActor().
*/
UCLASS(ClassGroup = MRUtilityKit, meta = (DisplayName = "MR Utility Kit Anchor Actor Spawner"))
class MRUTILITYKIT_API AMRUKAnchorActorSpawner : public AActor
{
GENERATED_BODY()
public:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInteriorSpawned, AMRUKRoom*, Room);
/**
* Event that gets fired when the interior spawner finished spawning actors.
*/
UPROPERTY(BlueprintAssignable, Category = "MR Utility Kit")
FOnInteriorSpawned OnActorsSpawned;
/**
* Seed to use for the random generator that decideds wich actor class to
* spawn if there a given multiple for a label.
* negative values will have the effect to initialize the random generator
* to a random seed.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
int AnchorRandomSpawnSeed = -1;
/**
* Whether actors should be spawned automatically after the mixed reality
* utility kit has been initialized. This should not be changed after the scene has been loaded.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
EMRUKSpawnMode SpawnMode = EMRUKSpawnMode::CurrentRoomOnly;
/**
* Material to use when falling back to procedural material.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
UMaterialInterface* ProceduralMaterial = nullptr;
/**
* Whether or not the spawner should fallback to procedural meshes in case no actor
* class has been defined for a label. This behaviour can be overwritten on the label
* basis in SpawnGroups.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
bool ShouldFallbackToProcedural = true;
/**
* Labels for which holes should be created in the parents plane mesh.
* E.g. if holes are needed in the walls where the windows and doors are, specify DOOR_FRAME and WINDOW_FRAME.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
TArray<FString> CutHoleLabels;
/**
* A map of Actor classes to spawn for the given label.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
TMap<FString, FMRUKSpawnGroup> SpawnGroups{
{ FMRUKLabels::Bed, {} },
{ FMRUKLabels::Ceiling, {} },
{ FMRUKLabels::Couch, {} },
{ FMRUKLabels::DoorFrame, {} },
{ FMRUKLabels::Floor, {} },
{ FMRUKLabels::Lamp, {} },
{ FMRUKLabels::Plant, {} },
{ FMRUKLabels::Screen, {} },
{ FMRUKLabels::Storage, {} },
{ FMRUKLabels::Table, {} },
{ FMRUKLabels::WallArt, {} },
{ FMRUKLabels::WallFace, {} },
{ FMRUKLabels::InvisibleWallFace, { {}, EMRUKSpawnerSelectionMode::Random, EMRUKFallbackToProceduralOverwrite::NoFallback } },
{ FMRUKLabels::WindowFrame, {} },
{ FMRUKLabels::Other, {} },
};
/**
* Spawns the meshes for the given labels above on the anchor positions in each room.
* There might be multiple actor classes for a give label. If thats the case a actor class will be chosen radomly.
* The seed for this random generator can be set by AnchorRandomSpawnSeed.
* This function will be called automatically after the mixed reality utility kit initialized unless
* the option SpawnOnStart is set to false.
* If there is no actor class specified for a label then a procedural mesh matching the anchors volume and plane
* will be generated.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void SpawnActors(AMRUKRoom* Room);
/**
* Return all spawned actors from the give room.
* @param Room The room from which the actors should be returned
* @param Actors The spawned actors.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void GetSpawnedActorsByRoom(AMRUKRoom* Room, TArray<AActor*>& Actors);
/**
* Return all spawned actors from all rooms.
* @param Actors The spawned actors.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void GetSpawnedActors(TArray<AActor*>& Actors);
protected:
/**
* This method gets called by the AnchorActorSpawner when it wants to spawn actors and procedural meshes in the room.
* It's possible to overwrite this function in Blueprint or C++ to implement custom spawning logic.
* The protected methods in the AnchorActorSpawner contain helper functions which can be useful when implementing
* a custom spawning logic. When implementing a custom spawning logic you may want to use SpawnAnchorActor() to spawn
* the actual actor and take care of it's orientation and scaling to match the anchors bounds.
* @param Room The room to spawn actors for.
* @param RandomStream A random stream to be used with the random selection mode.
* @return A list of all spawned actors.
*/
UFUNCTION(BlueprintNativeEvent, Category = "MR Utility Kit")
TArray<AActor*> SpawnAnchorActorsInRoom(AMRUKRoom* Room, const FRandomStream& RandomStream);
virtual TArray<AActor*> SpawnAnchorActorsInRoom_Implementation(AMRUKRoom* Room, const FRandomStream& RandomStream);
/**
* This method gets called by the default implementation of the SpawnAnchorActorsInRoom() for every label that should spawn a actor.
* By overwriting this function it is possible to inject custom spawning logic for actors on a per label basis.
* When implementing a custom spawning logic you may want to use SpawnAnchorActor() to spawn the actual actor and take care of it's
* orientation and scaling to match the anchors bounds.
* @param Anchor The anchor to spawn a actor for.
* @param Label The label to spawn a actor for.
* @param SpawnGroup Information on which actor should be spawned.
* @param RandomStream A random stream for implementing the random selection logic.
* @return The spawned actor.
*/
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "MR Utility Kit")
AActor* SpawnAnchorActorForLabel(AMRUKAnchor* Anchor, const FString& Label, const FMRUKSpawnGroup& SpawnGroup, const FRandomStream& RandomStream);
virtual AActor* SpawnAnchorActorForLabel_Implementation(AMRUKAnchor* Anchor, const FString& Label, const FMRUKSpawnGroup& SpawnGroup, const FRandomStream& RandomStream);
/**
* This method gets called by the default implementation of SpawnAnchorActorForLabel() to spawn the anchor and orient and scale
* it correct to the given anchor. If you are planning to implement a custom spawning logic you likely want to use this function
* in the end to actually spawn the actor as it takes care of orientation and scaling of the actor with regards to the anchor bounds.
* @param Anchor The anchor to spawn the actor for.
* @param SpawnActor Information on which actor should be spawned.
* @return The spawned actor.
*/
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "MR Utility Kit")
AActor* SpawnAnchorActor(AMRUKAnchor* Anchor, const FMRUKSpawnActor& SpawnActor);
virtual AActor* SpawnAnchorActor_Implementation(AMRUKAnchor* Anchor, const FMRUKSpawnActor& SpawnActor);
/**
* Override this method to inject custom scaling logic into the orientation process of an actor. The scale that this method returns
* gets used to scale the actor that will be spawned.
* @param Anchor The anchor for which the actor gets spawned.
* @param SpawnedActor The actor that gets spawned.
* @param StretchedScale The scale that would need to be applied to the actor to make it match with the bounding box of the anchor.
* In case it's a plane anchor only the X and Y component of the scale are relevant.
* @return The scale that should be applied to the actor. In case it's a plane anchor only the X and Y component are relevant.
*/
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "MR Utility Kit")
FVector ComputeCustomScaling(AMRUKAnchor* Anchor, AActor* SpawnedActor, const FVector& StretchedScale);
virtual FVector ComputeCustomScaling_Implementation(AMRUKAnchor* Anchor, AActor* SpawnedActor, const FVector& StretchedScale);
/**
* Override this method to inject custom actor selection logic. This will be called for every actor that gets spawned by the AMRUKAnchorActorSpawner.
* @param Anchor The anchor for which a actor should be spawned
* @param SpawnGroup The group of actors that can be used for decision making.
* @param RandomStream A random stream to randomize outputs if necessary.
* @param OutSpawnActor The actor which should be spawned.
* @return Whether the selection process was successful or not.
*/
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "MR Utility Kit")
bool SelectSpawnActorCustom(AMRUKAnchor* Anchor, const FMRUKSpawnGroup& SpawnGroup, const FRandomStream& RandomStream, FMRUKSpawnActor& OutSpawnActor);
virtual bool SelectSpawnActorCustom_Implementation(AMRUKAnchor* Anchor, const FMRUKSpawnGroup& SpawnGroup, const FRandomStream& RandomStream, FMRUKSpawnActor& OutSpawnActor);
/**
* Override this method to inject custom scaling logic into the orientation process of an actor. The scale that this method returns
* gets used to scale the actor that will be spawned.
* @param Anchor The anchor for which the actor gets spawned.
* @param Actor The actor that gets spawned.
* @param ChildBounds the rotated bounding box of the actor that should be spawned. For planes only X and Y components are relevant.
* @param Scale The scale that will be applied to the actor that will be spawned in place of the anchor. For planes only X and Y components are relevant.
* @return The offset that should be applied to the actor. In case it's a plane anchor only the X and Y component are relevant.
*/
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "MR Utility Kit")
FVector ComputeCustomAlign(AMRUKAnchor* Anchor, AActor* Actor, const FBox& ChildBounds, const FVector& Scale);
virtual FVector ComputeCustomAlign_Implementation(AMRUKAnchor* Anchor, AActor* Actor, const FBox& ChildBounds, const FVector& Scale);
/**
* Check if for the given SpawnGroup a procedural mesh should be spawned.
* @param SpawnGroup The spawn group to check
* @return Whether a procedural mesh should be spawned or not
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool ShouldAnchorFallbackToProceduralMesh(const FMRUKSpawnGroup& SpawnGroup) const;
/**
* Check if there should be spawned a actor for the given label. This function may return false in case
* the spawner should fallback to a procedural mesh.
* @param Anchor The anchor where the actor should be spawned
* @param Label The label of the anchor
* @param OutSpawnGroup Will be set in case a actor should be spawned
* @return Whether or not a actor should be spawned for the anchor
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool ShouldSpawnActorForAnchor(AMRUKAnchor* Anchor, const FString& Label, FMRUKSpawnGroup& OutSpawnGroup) const;
/**
* Spawn a procedural mesh for all walls if no wall actor is given to the spawner.
* This will take care of generating seamless UVs for the walls.
* @param Room The room to spawn in.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
TArray<AActor*> SpawnProceduralMeshesOnWallsIfNoWallActorGiven(AMRUKRoom* Room);
/**
* Spawn a procedural mesh for the floor if no floor actor is given to the spawner.
* @param Room The room to spawn in.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
AActor* SpawnProceduralMeshOnFloorIfNoFloorActorGiven(AMRUKRoom* Room);
/**
* Spawn a procedural mesh for the ceiling if no ceiling actor is given to the spawner.
* @param Room The room to spawn in.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
AActor* SpawnProceduralMeshOnCeilingIfNoCeilingActorGiven(AMRUKRoom* Room);
/**
* Spawn a procedural mesh for the given anchor if the settings on the AnchorActorSpawner say so.
* @param Anchor The anchor for which the procedural mesh should be spawned
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
AActor* SpawnProceduralMeshForAnchorIfNeeded(AMRUKAnchor* Anchor);
/**
* Spawn procedural meshes for every anchor that needs them. Including walls, ceiling and floor.
* The method determines if procedural mesh should be spawned or not based on the settings of the
* AnchorActorSpawner.
* @param Room The room to spawn in.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
TArray<AActor*> SpawnProceduralMeshesInRoom(AMRUKRoom* Room);
/**
* Select the SpawnActor based on the size that matches best the anchor bounds.
* @param Anchor The anchor for which a actor should be spawned.
* @param SpawnGroup The spawn group.
* @param OutSpawnActor The found spawn actor.
* @return True if a SpawnActor could be found. Otherwise, false.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool SelectSpawnActorClosestSize(AMRUKAnchor* Anchor, const FMRUKSpawnGroup& SpawnGroup, FMRUKSpawnActor& OutSpawnActor);
/**
* Select the SpawnActor randomly
* @param SpawnGroup The spawn group.
* @param RandomStream The random stream to use for the random selection.
* @param OutSpawnActor The found spawn actor.
* @return True if a SpawnActor could be found. Otherwise, false.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool SelectSpawnActorRandom(const FMRUKSpawnGroup& SpawnGroup, const FRandomStream& RandomStream, FMRUKSpawnActor& OutSpawnActor);
/**
* Select a SpawnActor from the SpawnGroup with respect to the given selection mode in SpawnGroup.
* @param Anchor The anchor for which the actor should be spawned.
* @param SpawnGroup The spawn group.
* @param RandomStream The random stream
* @param OutSpawnActor The found spawn actor
* @return True if a spawn actor has been found. Otherwise, false.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool SelectSpawnActorFromSpawnGroup(AMRUKAnchor* Anchor, const FMRUKSpawnGroup& SpawnGroup, const FRandomStream& RandomStream, FMRUKSpawnActor& OutSpawnActor);
/**
* Orient and scale the given actor to the anchors plane or volume bounds.
* @param Anchor The anchor
* @param Actor The actor which should be oriented and scaled to the given anchor.
* @param ScalingMode The scaling mode that should be used when doing the matching.
* @param bCalculateFacingDirection Whether or not the facing direction of the anchor should be calculated and used for the orientation process.
* @param bMatchAspectRatio Whether or not the aspect ratio of the anchor should be matched.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void AttachAndFitActorToAnchor(AMRUKAnchor* Anchor, AActor* Actor, EMRUKSpawnerScalingMode ScalingMode, EMRUKAlignMode AlignMode, bool bCalculateFacingDirection, bool bMatchAspectRatio);
void BeginPlay() override;
UFUNCTION()
void OnRoomCreated(AMRUKRoom* Room);
UFUNCTION()
void OnRoomUpdated(AMRUKRoom* Room);
UFUNCTION()
void OnRoomRemoved(AMRUKRoom* Room);
UFUNCTION()
void RemoveActors(AMRUKRoom* Room);
private:
// Room UUID to spawned actors in this room
TMap<AMRUKRoom*, TArray<AActor*>> SpawnedActors;
int32 LastSeed = -1;
};

View File

@@ -0,0 +1,173 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "OculusXRAnchorTypes.h"
#include "MRUtilityKitBPLibrary.generated.h"
USTRUCT(BlueprintType)
struct FMRUKMeshSegment
{
GENERATED_BODY()
TArray<FVector> Positions;
TArray<int32> Indices;
};
/**
* Load the scene async from device.
*/
UCLASS()
class MRUTILITYKIT_API UMRUKLoadFromDevice : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FMRUKLoaded);
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit", meta = (WorldContext = "WorldContext", BlueprintInternalUseOnly = "true"
))
static UMRUKLoadFromDevice* LoadSceneFromDeviceAsync(const UObject* WorldContext
);
virtual void Activate() override;
UPROPERTY(BlueprintAssignable)
FMRUKLoaded Success;
UPROPERTY(BlueprintAssignable)
FMRUKLoaded Failure;
private:
UFUNCTION(CallInEditor)
void OnSceneLoaded(bool Succeeded);
TWeakObjectPtr<UWorld> World = nullptr;
};
/**
* Mixed Reality Utility Kit Blueprint Function Library.
* See functions for further information.
*/
UCLASS()
class MRUTILITYKIT_API UMRUKBPLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Load the global mesh from the device.
* @param SpaceHandle Space handle of the room.
* @param OutProceduralMesh Procedural mesh to load the triangle data in.
* @param LoadCollision Whether to generate collision or not.
* @param WorldContext Context of the world.
* @return Whether the load was successful or not.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit", meta = (WorldContext = "WorldContext"))
static bool LoadGlobalMeshFromDevice(FOculusXRUInt64 SpaceHandle, UProceduralMeshComponent* OutProceduralMesh, bool LoadCollision, const UObject* WorldContext);
/**
* Load the global mesh from a JSON string.
* @param JsonString The string containing the JSON.
* @param AnchorUUID Anchor UUID of the room
* @param OutProceduralMesh Procedural mesh to load the triangle data in.
* @param LoadCollision Whether to generate collision or not
* @return Whether the load was successful or not.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
static bool LoadGlobalMeshFromJsonString(const FString& JsonString, FOculusXRUUID AnchorUUID, UProceduralMeshComponent* OutProceduralMesh, bool LoadCollision);
/**
* (Re)Calculate Normals and Tangents of the given procedural mesh.
* @param Mesh The procedural mesh.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
static void RecalculateProceduralMeshAndTangents(class UProceduralMeshComponent* Mesh);
/**
* Check if the current Unreal Engine is the fork of Meta.
* @return Whether its the fork or not.
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "MR Utility Kit")
static bool IsUnrealEngineMetaFork();
/**
* Compute the centroid of a polygon that is defined by the points.
* The centroid may be outside of the polygon in case the polygon is non convex.
* @param PolygonPoints Points that define the polygon.
* @return The centroid.
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "MR Utility Kit")
static FVector2D ComputeCentroid(const TArray<FVector2D>& PolygonPoints);
/**
* In Unreal Engine, scale is always applied in the local space to avoid any skew.
* This means that if you have a component which has a 90 degree rotation and is scaled, or any of its
* children are scaled then the scale axes will not be applied as you would expect. This is can make it
* very awkward to work with when trying to scale the actors to fit within the scene volumes. To work around
* this problem, this function will attempt to adjust the scale axes recursively to match the expected behaviour.
* This will only work reliably if the rotations involved are 90 degrees, if they are not then it will pick the closest axis.
* @param SceneComponent The component where the scale should be set
* @param UnRotatedScale The scale you would like to have without considering any rotations
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
static void SetScaleRecursivelyAdjustingForRotation(USceneComponent* SceneComponent, const FVector& UnRotatedScale);
/**
* Compute the direction that faces away from the closest wall of the given anchor.
* @param Anchor The anchor for which the direction should be computed.
* @param OutCardinalAxisIndex The index of the computed cardinal axis. Can be either 0, 1, 2 or 3
* @param ExcludedAxes Axes to exclude in the computation. Can contain 0, 1, 2, 3
* @return The direction
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
static FVector ComputeDirectionAwayFromClosestWall(const AMRUKAnchor* Anchor, int& OutCardinalAxisIndex, const TArray<int> ExcludedAxes);
/**
* Construct a 2D texture from a render target.
* @param RenderTarget2D The render target from which the texture should be created.
* @param Outer The (optional) outer object for the created texture.
* @param TexName Name for the new texture.
* @return The newly created texture.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
static UTexture2D* ConstructTexture2D(UTextureRenderTarget2D* RenderTarget2D, UObject* Outer, const FString& TexName);
/**
* Extract a column from a matrix.
* @param Matrix The matrix to use.
* @param Index The column index.
* @return The column of the matrix.
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "MR Utility Kit")
static FLinearColor GetMatrixColumn(const FMatrix& Matrix, int32 Index);
/**
* Compute a grid by taking into account the room box geometry. E.g. create evenly spaced points on ceiling, floor and walls.
* @param Room The room to use
* @param MaxPointsCount The maximum number of points
* @param PointsPerUnitX The density of points on the X axis
* @param PointsPerUnitY The density of points on the Y axis
* @param bIncludeFloor Whether or not to include the floor
* @param bIncludeCeiling Whether or not to include the ceiling
* @param bIncludeWalls Whether or not to include the walls
* @return The computed points
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
static TArray<FVector> ComputeRoomBoxGrid(const AMRUKRoom* Room, int32 MaxPointsCount, double PointsPerUnitX = 1.0, double PointsPerUnitY = 1.0);
/**
* Create mesh segments from the given mesh. This can be used for creating a destructible mesh system.
* @param MeshPositions The mesh positions that should be segmented
* @param MeshIndices The mesh indices that should be segmented
* @param SegmentationPoints A set of points that should be used to calculate the segments
* @param ReservedMin Reserved space from the lower part of the bound box
* @param ReservedMax Reserved space from the upper part of the bounding box
* @param OutSegments The segmented meshes that have been created from the given mesh
* @param OutReservedSegment
*/
static void CreateMeshSegmentation(const TArray<FVector>& MeshPositions, const TArray<uint32>& MeshIndices,
const TArray<FVector>& SegmentationPoints, const FVector& ReservedMin, const FVector& ReservedMax,
TArray<FMRUKMeshSegment>& OutSegments, FMRUKMeshSegment& OutReservedSegment);
};

View File

@@ -0,0 +1,72 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "CoreMinimal.h"
#include "Components/StaticMeshComponent.h"
#include "MRUtilityKitBlobShadowComponent.generated.h"
/**
* Adds a blob shadow below the actor.
* The blob shadow will position and resize itself automatically during runtime.
*/
UCLASS(ClassGroup = MRUtilityKit, Blueprintable, BlueprintType, meta = (BlueprintSpawnableComponent, DisplayName = "MR Utility Kit Blob Shadow Component"))
class MRUTILITYKIT_API UMRUKBlobShadowComponent : public UStaticMeshComponent
{
GENERATED_BODY()
public:
/**
* Controls the look of the blob shadow corners (0 = squared corners, 1 = rounded corners).
*/
UPROPERTY(Category = "MR Utility Kit|Aspect", EditAnywhere, BlueprintReadWrite, meta = (UIMin = "0", UIMax = "1"))
float Roundness = 1.0f;
/**
* Controls the look of the blob shadow alpha (0 = fully opaque, 1 = gradient from the center).
*/
UPROPERTY(Category = "MR Utility Kit|Aspect", EditAnywhere, BlueprintReadWrite, meta = (UIMin = "0", UIMax = "1"))
float Gradient = 0.544f;
/**
* Controls the curve of the blob shadow alpha gradient (only available if Gradient > 0).
*/
UPROPERTY(Category = "MR Utility Kit|Aspect", EditAnywhere, BlueprintReadWrite, meta = (EditCondition = "Gradient > 0"))
float GradientPower = 3.0f;
/**
* Increase or decrease the calculated blob shadow size by a fixed amount.
*/
UPROPERTY(Category = "MR Utility Kit", EditAnywhere, BlueprintReadWrite)
float ExtraExtent = -10.0f;
/**
* Maximum distance the actor can be away from the ground until the blob shadow is not shown anymore.
*/
UPROPERTY(Category = "MR Utility Kit", EditAnywhere, BlueprintReadWrite)
float MaxVerticalDistance = 100.f;
/**
* Distance from the ground until the blob shadow starts to fade.
*/
UPROPERTY(Category = "MR Utility Kit", EditAnywhere, BlueprintReadWrite)
float FadeDistance = 20.f;
/**
* Only callable in the editor from the scene, will update the blob shadow size, position and material parameters
* to give a preview how the blob shadow would look like.
*/
UFUNCTION(Category = "MR Utility Kit", CallInEditor)
void UpdatePlaneSizeAndPosition();
public:
UMRUKBlobShadowComponent();
void BeginPlay() override;
void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
void ComputeOwner2DBounds(FVector& Origin, FVector2D& Extent, double& Yaw) const;
protected:
UPROPERTY()
UMaterialInstanceDynamic* DynMaterial;
};

View File

@@ -0,0 +1,139 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "GameFramework/Actor.h"
#include "OculusXRRoomLayoutManagerComponent.h"
#include "Dom/JsonValue.h"
#include "OculusXRAnchorsRequests.h"
#include "MRUtilityKitData.generated.h"
/**
* Actor to help finding the localization of actors.
* It gets a list of all anchor queries that should be localized
* and checks every tick if the anchor localization is there.
* When the localization is complete, it will emit the event OnComplete.
*
* NOTE: Normally this should be a async task. However, the anchor data
* can only be queried in game thread.
*/
UCLASS(ClassGroup = MRUtilityKit, Hidden)
class MRUTILITYKIT_API AMRUKLocalizer : public AActor
{
GENERATED_BODY()
public:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnComplete, bool, Success);
/**
* Event that gets fired when all anchors have been localized.
*/
UPROPERTY(BlueprintAssignable, Category = "MR Utility Kit")
FOnComplete OnComplete;
TArray<class UMRUKAnchorData*> AnchorsData;
AMRUKLocalizer();
void Tick(float DeltaTime) override;
};
/**
* A datastrcture to hold the data of a single anchor. It also provides functions to load the data from device or json.
*/
UCLASS(ClassGroup = MRUtilityKit, Hidden)
class MRUTILITYKIT_API UMRUKAnchorData : public UObject
{
GENERATED_BODY()
public:
FOculusXRAnchorsDiscoverResult SpaceQuery;
FTransform Transform;
FBox2D PlaneBounds;
FBox VolumeBounds;
TArray<FString> SemanticClassifications;
TArray<FVector2D> PlaneBoundary2D;
bool NeedAnchorLocalization = false;
void LoadFromDevice(const FOculusXRAnchorsDiscoverResult& AnchorsDiscoverResult);
void LoadFromJson(const FJsonValue& Value);
};
/**
* Load room data from device.
* When all room data has been loaded, the OnComplete event will be fired.
*/
UCLASS(ClassGroup = MRUtilityKit, Hidden)
class MRUTILITYKIT_API UMRUKRoomData : public UObject
{
GENERATED_BODY()
public:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnComplete, bool, Success);
/**
* Event that gets fired after all room data has been loaded.
*/
UPROPERTY(BlueprintAssignable, Category = "MR Utility Kit")
FOnComplete OnComplete;
FOculusXRAnchorsDiscoverResult SpaceQuery;
FOculusXRRoomLayout RoomLayout;
UPROPERTY()
TArray<TObjectPtr<UMRUKAnchorData>> AnchorsData;
UPROPERTY()
AMRUKLocalizer* LocalizationActor = nullptr;
class UMRUKSceneData* SceneData;
void LoadFromDevice(UMRUKSceneData* Data, const FOculusXRAnchorsDiscoverResult& AnchorsDiscoverResult);
void LoadFromJson(UMRUKSceneData* Data, const FJsonValue& Value);
private:
void FinishQuery(bool Success);
void RoomDataLoadedComplete(EOculusXRAnchorResult::Type Result);
void RoomDataLoadedIncrementalResults(const TArray<FOculusXRAnchorsDiscoverResult>& DiscoverResults);
UFUNCTION()
void AnchorsInitialized(bool Success);
};
/**
* Load scene data from device.
* When all scene data has been loaded, the OnComplete event will be fired.
*/
UCLASS(ClassGroup = MRUtilityKit, Hidden)
class MRUTILITYKIT_API UMRUKSceneData : public UObject
{
GENERATED_BODY()
public:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnComplete, bool, Success);
/**
* Event that gets fired after all scene data has been loaded.
*/
UPROPERTY(BlueprintAssignable, Category = "MR Utility Kit")
FOnComplete OnComplete;
UPROPERTY()
TArray<TObjectPtr<UMRUKRoomData>> RoomsData;
void LoadFromDevice();
void LoadFromJson(const FString& Json);
private:
int32 NumRoomsLeftToInitialize = 0;
bool AnyRoomFailed = false;
void FinishQuery(bool Success);
void SceneDataLoadedResult(EOculusXRAnchorResult::Type Result);
void SceneDataLoadedComplete(const TArray<FOculusXRAnchorsDiscoverResult>& DiscoverResults);
UFUNCTION()
void RoomQueryComplete(bool Success);
};

View File

@@ -0,0 +1,91 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "GameFramework/Actor.h"
#include "MRUtilityKitDebugComponent.generated.h"
/**
* Various debugging utilities for the scene.
* This component can for example attached to the player pawn. The various methods can
* then be called on input from the pawn.
*/
UCLASS(ClassGroup = MRUtilityKit, meta = (BlueprintSpawnableComponent, DisplayName = "MR Utility Kit Debug Component"))
class MRUTILITYKIT_API UMRUKDebugComponent : public UActorComponent
{
GENERATED_BODY()
public:
/**
* The gizmo to show when visualizing an anchor.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "MR Utility Kit")
TSubclassOf<AActor> GizmoActorClass = nullptr;
/**
* The text to show when visualizing an anchor.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "MR Utility Kit")
TSubclassOf<AActor> TextActorClass = nullptr;
/**
* The scale that should be applied to the gizmo before displaying it.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "MR Utility Kit")
FVector GizmoScale = FVector(0.1);
/**
* The scale that should be applied to the text before displaying it.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "MR Utility Kit")
FVector TextScale = FVector(0.5);
/**
* Shoot a ray and display the anchors coordinate system and labels that was hit by the ray if any.
* Call HideAnchor() to get rid of the displayed anchor.
* @param Origin The ray origin.
* @param Direction The ray direction.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void ShowAnchorAtRayHit(const FVector& Origin, const FVector& Direction);
/**
* Hide the current anchor. This method needs only to be called to hide the anchor
* that was displayed by ShowAnchorAtRayHit().
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void HideAnchor();
/**
* Shoot a ray and display the anchors space that was hit by the ray if any.
* Call HideAnchorSpace() to get rid of the displayed anchor space.
* @param Origin The ray origin.
* @param Direction The ray direction.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void ShowAnchorSpaceAtRayHit(const FVector& Origin, const FVector& Direction);
/**
* Hide the current anchor space actor. This method needs only to be called to hide the
* anchor space that was displayed by ShowAnchorAtRayHit().
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void HideAnchorSpace();
public:
UMRUKDebugComponent();
void BeginPlay() override;
void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
private:
UPROPERTY()
TObjectPtr<AActor> ActiveGizmoActor = nullptr;
UPROPERTY()
TObjectPtr<AActor> ActiveTextActor = nullptr;
UPROPERTY()
TObjectPtr<AActor> ActiveAnchorSpaceActor = nullptr;
void OrientTextActorToPlayer() const;
};

View File

@@ -0,0 +1,212 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "MRUtilityKit.h"
#include "MRUtilityKitBPLibrary.h"
#include "GameFramework/Actor.h"
#include "ProceduralMeshComponent.h"
#include "Tasks/Task.h"
#include "MRUtilityKitDestructibleMesh.generated.h"
/**
* Destructible mesh component. Creates mesh segments for the given geometry.
* The segments will be created async.
* In addition, its possible to define areas that are indestructible.
*/
UCLASS(ClassGroup = MRUtilityKit, Blueprintable, BlueprintType, meta = (BlueprintSpawnableComponent, DisplayName = "MR Utility Kit Destructible Mesh Component"))
class MRUTILITYKIT_API UMRUKDestructibleMeshComponent : public UProceduralMeshComponent
{
GENERATED_BODY()
public:
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnMeshesGenerated);
UMRUKDestructibleMeshComponent(const FObjectInitializer& ObjectInitializer);
UPROPERTY(BlueprintAssignable, Category = "MR Utility Kit")
FOnMeshesGenerated OnMeshesGenerated;
/**
* Material to display on the global mesh
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
UMaterialInterface* GlobalMeshMaterial;
/**
* Area on the top of the mesh that should be indestructible.
* The area is given in centimeters 1.0 == 1 cm.
* -1.0 means no reserved area.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
double ReservedTop = -1.0;
/**
* Area on the bottom of the mesh that should be indestructible.
* The area is given in centimeters 1.0 == 1 cm
* -1.0 means no reserved area.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
double ReservedBottom = 30.0;
/**
* Segment the given geometry into smaller chunks. For each chunk a procedural mesh component will be spawned and attached to the owning actor.
* @param MeshPositions Positions of the mesh to segment
* @param MeshIndices Indices of the mesh to segment
* @param SegmentationPoints Points to use to determine the segments.
*/
void SegmentMesh(const TArray<FVector>& MeshPositions, const TArray<uint32>& MeshIndices, const TArray<FVector>& SegmentationPoints);
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
private:
UE::Tasks::TTask<TPair<TArray<FMRUKMeshSegment>, FMRUKMeshSegment>> TaskResult;
};
/**
* Actor that constructs a destructible mesh for the given room
* The actor will automatically attach to the global mesh anchor of the given room to take it location and orientation.
*/
UCLASS(ClassGroup = MRUtilityKit, Blueprintable, BlueprintType, meta = (BlueprintSpawnableComponent, DisplayName = "MR Utility Kit Destructible Global Mesh"))
class MRUTILITYKIT_API AMRUKDestructibleGlobalMesh : public AActor
{
GENERATED_BODY()
public:
AMRUKDestructibleGlobalMesh();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
UMRUKDestructibleMeshComponent* DestructibleMeshComponent;
/**
* Density of mesh segments on the X axis.
* Increase this value to get smaller cracks in the global mesh.
* Decrease this value to get bigger cracks in the global mesh.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
double PointsPerUnitX = 1.0;
/**
* How many segmentation points should be created at a maximum.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
int MaxPointsCount = 256;
/**
* Density of mesh segments on the Y axis.
* Increase this value to get smaller cracks in the global mesh.
* Decrease this value to get bigger cracks in the global mesh.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
double PointsPerUnitY = 1.0;
/**
* Create a destructible mesh for the given room. If the global mesh has not yet been loaded
* this function will attempt to load the global mesh from the device.
* @param Room The room
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void CreateDestructibleMesh(AMRUKRoom* Room = nullptr);
/**
* Remove a segment of the global mesh. Takes care of not removing the reserved global mesh segment.
* @param Mesh The mesh to remove
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void RemoveGlobalMeshSegment(UPrimitiveComponent* Mesh);
};
/**
* The destructible global mesh spawner allows to spawn (automatically) destructible global meshes
* when new rooms are created.
* A destructible global mesh is a version of the global mesh that can be destructed during runtime.
* The bulk of the work is performed in UDestructibleMeshComponent. It will perform on start a one time
* preprocessing step to segment the given global mesh into smaller chunks. After that the chunks can be used
* during the game and removed (e.g. with ray casts) at any time during the game simulating as if the global
* mesh would crack down. To enhance the visual quality when cracking the (e.g. removing mesh chunks) global mesh
* a particle system could be used. The system allows to define areas that should be non destructible.
*/
UCLASS(ClassGroup = MRUtilityKit, Blueprintable, BlueprintType, meta = (BlueprintSpawnableComponent, DisplayName = "MR Utility Kit Destructible Global Mesh Spawner"))
class MRUTILITYKIT_API AMRUKDestructibleGlobalMeshSpawner : public AActor
{
GENERATED_BODY()
public:
/**
* Whether destructible meshes should be spawned automatically.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
EMRUKSpawnMode SpawnMode = EMRUKSpawnMode::CurrentRoomOnly;
/**
* Material to display on the global mesh
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
UMaterialInterface* GlobalMeshMaterial;
/**
* Density of mesh segments on the X axis.
* Increase this value to get smaller cracks in the global mesh.
* Decrease this value to get bigger cracks in the global mesh.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
double PointsPerUnitX = 1.0;
/**
* How many segmentation points should be created at a maximum.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
int MaxPointsCount = 256;
/**
* Density of mesh segments on the Y axis.
* Increase this value to get smaller cracks in the global mesh.
* Decrease this value to get bigger cracks in the global mesh.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
double PointsPerUnitY = 1.0;
/**
* Area on the top of the mesh that should be indestructible.
* The area is given in centimeters 1.0 == 1 cm
* -1.0 means no reserved area.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
double ReservedTop = -1.0;
/**
* Area on the bottom of the mesh that should be indestructible.
* The area is given in centimeters 1.0 == 1 cm
* -1.0 means no reserved area.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
double ReservedBottom = 30.0;
void BeginPlay() override;
/**
* Find the destructible mesh that has been spawned for the given room.
* @param Room Room to look for the destructible mesh
* @return The destructible mesh
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
AMRUKDestructibleGlobalMesh* FindDestructibleMeshForRoom(AMRUKRoom* Room);
/**
* Add new destructible mesh for the given room. A mesh will only get spawned if no
* destructible mesh has been spawned for the room yet.
* @param Room The room.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
AMRUKDestructibleGlobalMesh* AddDestructibleGlobalMesh(AMRUKRoom* Room);
private:
TMap<AMRUKRoom*, AMRUKDestructibleGlobalMesh*> SpawnedMeshes;
UFUNCTION()
void OnRoomCreated(AMRUKRoom* Room);
UFUNCTION()
void OnRoomRemoved(AMRUKRoom* Room);
};

View File

@@ -0,0 +1,197 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "MRUtilityKit.h"
#include "GameFramework/Actor.h"
#include "MRUtilityKitDistanceMapGenerator.generated.h"
const FName GMRUK_DISTANCE_MAP_ACTOR_TAG = TEXT("DistanceMapActor");
UENUM(BlueprintType)
enum class EMRUKDistanceMapGenerationMode : uint8
{
// Do not generate a distance map
None,
/// Generate distance map only for the free space. E.g. The floor inside the room.
FreeSpace,
/// Generate the distance map only for the occupied space. E.g. outside the room and inside scene objects.
OccupiedSpace,
/// Generate the distance map for free space and occupied space.
AllSpace,
};
/**
* Generates a distance map that can be used in materials to calculate the distance to various objects.
* This can enable interesting effects. With the distance map you can get the distance from scene objects
* or walls in a material shader.
*
* The Jump Flood Algorithm is used to generate the distance map. This is fast enough to regenerate
* every tick.
*
* To capture a distance map after a room has been loaded call CaptureDistanceMap().
* It will return a captured distance map. In case you already called CaptureDistanceMap()
* you can receive the last captured distance map with GetDistanceMap(). No other setup is required.
*
* This class will create procedural meshes for every anchor to create a mask. These meshes have their
* visibility set to scene capture only. That however means that if you place a scene capture component yourself
* that the meshes will show up in your scene capture component. The actors that have the procedural meshes
* attached are tagged with GMRUK_DISTANCE_MAP_ACTOR_TAG. In case you don't want them to show up in your
* scene capture you can hide them by receiving all these actors with the tag GMRUK_DISTANCE_MAP_ACTOR_TAG
* and add these to the scene captures hidden actors.
*/
UCLASS(ClassGroup = MRUtilityKit, meta = (DisplayName = "MR Utility Kit Distance Map Generator"))
class MRUTILITYKIT_API AMRUKDistanceMapGenerator : public AActor
{
GENERATED_BODY()
public:
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnReady);
UPROPERTY(BlueprintAssignable, Category = "MR Utility Kit")
FOnReady OnReady;
/**
* The mode in which the final distance map should be generated.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
EMRUKDistanceMapGenerationMode DistanceMapGenerationMode = EMRUKDistanceMapGenerationMode::FreeSpace;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
class USceneComponent* Root;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
class USceneCaptureComponent2D* SceneCapture2D;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
EMRUKSpawnMode SpawnMode = EMRUKSpawnMode::CurrentRoomOnly;
/**
* First render target for jump flood algorithm.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
class UCanvasRenderTarget2D* RenderTarget1;
/**
* Second render target for jump flood algorithm.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
class UCanvasRenderTarget2D* RenderTarget2;
/**
* Render target for the final distance map
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
class UCanvasRenderTarget2D* DistanceMapRenderTarget;
/**
* Material to render a mask that gets used to calculate the distance map.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
class UMaterialInterface* MaskMaterial;
/**
* Material that executes a pass of the jump flood algorithm.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
class UMaterialInterface* JFPassMaterial;
/**
* Material to render final distance map
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
class UMaterialInterface* DistanceMapFreeSpaceMaterial;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
class UMaterialInterface* DistanceMapOccupiedSpaceMaterial;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
class UMaterialInterface* DistanceMapAllSpaceMaterial;
/**
* Capture the distance map.
* @return The captured distance map.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
UTexture* CaptureDistanceMap();
/**
* Create mask meshes for the given room.
* These mask meshes are needed for the distance map to be rendered. It should only be called once before
* CaptureDistanceMap in case the SpawnMode has been set to None.
* The operation that this function executes is expensive. It only needs to be called after the room has been
* created or updated.
* @param Room The room for which the masked meshes should be created.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void CreateMaskMeshesForRoom(AMRUKRoom* Room);
/**
* Remove mask meshes for the given room.
* This function should only be executed when SpawnMode is set to None.
* It only needs to be called after a room has been removed.
* @param Room The room for which the masked meshes should be removed.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void RemoveMaskMeshesFromRoom(AMRUKRoom* Room);
/**
* Return the captured distance map. Be sure to call CaptureDistanceMap() before
* @return The captured distance map.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
UTexture* GetDistanceMap() const;
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
UCanvasRenderTarget2D* GetDistanceMapRenderTarget() const;
/**
* Retrieve the view info from the scene capture. This is useful for re projection of
* the distance map in a material.
* @return The view info.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
FMinimalViewInfo GetSceneCaptureView() const;
public:
AMRUKDistanceMapGenerator();
protected:
void BeginPlay() override;
private:
TMap<AMRUKRoom*, TArray<AActor*>> SpawnedMaskMeshes;
int32 DistanceMapRT = -1;
UPROPERTY()
class UMaterialInstanceDynamic* JFPassMaterialInstance = nullptr;
UPROPERTY()
class UMaterialInstanceDynamic* DistanceMapFreeSpaceMaterialInstance = nullptr;
UPROPERTY()
class UMaterialInstanceDynamic* DistanceMapOccupiedSpaceMaterialInstance = nullptr;
UPROPERTY()
class UMaterialInstanceDynamic* DistanceMapAllSpaceMaterialInstance = nullptr;
UPROPERTY()
UMaterialInterface* SceneObjectMaskMaterial;
UPROPERTY()
UMaterialInterface* FloorMaskMaterial;
void CaptureInitialSceneMask();
void RenderDistanceMap();
UFUNCTION()
void OnRoomCreated(AMRUKRoom* Room);
UFUNCTION()
void OnRoomUpdated(AMRUKRoom* Room);
UFUNCTION()
AActor* CreateMaskMeshOfAnchor(AMRUKAnchor* Anchor);
UFUNCTION()
AActor* UpdateMaskMeshOfAnchor(AMRUKAnchor* Anchor);
};

View File

@@ -0,0 +1,8 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "Containers/Array.h"
#include "Math/Vector2D.h"
MRUTILITYKIT_API void MRUKTriangulatePolygon(const TArray<TArray<FVector2f>>& Polygons, TArray<FVector2D>& Vertices, TArray<int32>& Indices);

View File

@@ -0,0 +1,191 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "CoreMinimal.h"
#include "Components/SceneComponent.h"
#include "MRUtilityKitGridSliceResizer.generated.h"
UENUM(BlueprintType, Meta = (Bitflags, UseEnumValuesAsMaskValuesInEditor = "true"))
enum class EMRUKScaleCenterMode : uint8
{
None = 0 UMETA(Hidden),
XAxis = 1,
YAxis = 2,
ZAxis = 4,
};
/**
* The GridSliceResizerComponent is a versatile tool designed to maintain the proportions of
* specific areas of 3D meshes while allowing others to stretch during scaling. This component
* should replace the static mesh component, rather than being used in conjunction with it.
*
* The concept of the GridSliceResizerComponent is similar to the popular 9-Slice-Scaling technique
* used in 2D graphics, which keeps the borders of sprites unstretched while the inner rectangle is
* stretched. In essence, the GridSliceResizerComponent is a 27-Slice-Scaler for 3D meshes.
*
* The component operates by dividing the bounding box of a 3D mesh into 27 cuboids, as illustrated below.
* Not all cuboids are visible in this picture. Only the once that are front facing:
*
* +-----+-----------+-----+
* /_____/___________/_____/|
* /_____/___________/_____/||
* / / / /|||
* +-----+-----------+-----+ |||
* | A | B | C |/|||
* +-----+-----------+-----+ |||
* | | | | |||
* | D | E | F | |||
* | | | |/||/
* +-----+-----------+-----+ |/
* | G | H | I | /
* +-----+-----+-----+-----+
*
* The scaling behaviour is as follows (assuming all other faces of the bounding box are divided as the
* front facing one):
*
* Center Cuboid (E): Vertices within this cuboid stretch on two axes (Y, Z).
* Corner Cuboids (A, C, G, I): These cuboids do not stretch on any axis.
* Middle Cuboids (B, H): These cuboids stretch horizontally but not vertically.
* Middle Cuboids (D, F): These cuboids stretch vertically but not horizontally.
*
* The slicing areas are defined by the SlicerPivotOffset and BorderXNegative, BorderXPositive, etc.
* These border values range from 0 to 1 and extend from the mesh's pivot (which may be offset by SlicerPivotOffset)
* to the maximum or minimum of the bounding box's axis.
* If all borders are set to 1, the mesh will stretch like a regular mesh during scaling. If set to 0, no stretching
* will occur. Typically, you'll want the pivot in the middle of the mesh and the borders set to around 0.8.
*
* You can visualize the borders and pivot in the Actor editor preview using bDebugDrawPivot, bDebugDrawBorderX, etc.
*
* This component is only compatible with static meshes that have CPU access enabled. Ensure you enable CPU
* access in the static mesh editor.
*/
UCLASS(ClassGroup = MRUtilityKit, Blueprintable, BlueprintType, meta = (BlueprintSpawnableComponent, DisplayName = "MR Utility Kit Grid Slice Resizer Component"))
class MRUTILITYKIT_API UMRUKGridSliceResizerComponent : public USceneComponent
{
GENERATED_BODY()
public:
/**
* The static mesh to slice. Make sure to enable CPU access on it.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
class UStaticMesh* Mesh;
/**
* Slice border for the negative X axis.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit|Slices", meta = (ClampMin = "0.0", ClampMax = "1.0"))
double BorderXNegative = 1.0;
/**
* Slice border for the positive X axis.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit|Slices", meta = (ClampMin = "0.0", ClampMax = "1.0"))
double BorderXPositive = 1.0;
/**
* Slice border for the negative Y axis.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit|Slices", meta = (ClampMin = "0.0", ClampMax = "1.0"))
double BorderYNegative = 1.0;
/**
* Slice border for the positive Y axis.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit|Slices", meta = (ClampMin = "0.0", ClampMax = "1.0"))
double BorderYPositive = 1.0;
/**
* Slice border for the negative Z axis.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit|Slices", meta = (ClampMin = "0.0", ClampMax = "1.0"))
double BorderZNegative = 1.0;
/**
* Slice border for the positive Z axis.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit|Slices", meta = (ClampMin = "0.0", ClampMax = "1.0"))
double BorderZPositive = 1.0;
/**
* How much the meshes pivot should be offset when applying the slice borders.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
FVector SlicerPivotOffset;
/**
* This parameter determines whether the center part of the object should be scaled.
* If set to false, the center vertices will remain stationary. This is particularly useful when
* you want to maintain the proportions of certain geometrical features in the center part, such
* as a doorknob. By keeping the center vertices in place, you can avoid unwanted stretching effects,
* resulting in a more visually appealing outcome.
* However, it's important to note that for a convincing visual effect, the texture applied to the object should also not stretch.
* If you encounter issues with texture stretching, consider adding an additional loop cut.
* This can help maintain the texture's proportions and prevent it from distorting.
* In case the mesh gets scaled down and some of the center vertices fall outside of the scaled down center
* all vertices that are inside the center will be scaled down uniformly.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit", meta = (Bitmask, BitmaskEnum = "/Script/MRUtilityKit.EMRUKScaleCenterMode"))
uint8 ScaleCenterMode = 0;
/**
* Whether or not a collision mesh should be created for the static mesh.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
bool bGenerateCollision = true;
#if WITH_EDITORONLY_DATA
/**
* Show the pivot of the mesh that gets used for the slice borders.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
bool bDebugDrawPivot = false;
/**
* Show the slice borders on the X axis.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
bool bDebugDrawBorderX = false;
/**
* Show the slice borders on the Y axis.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
bool bDebugDrawBorderY = false;
/**
* Show the slice borders on the Z axis.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
bool bDebugDrawBorderZ = false;
#endif
/**
* Slice the mesh. This gets automatically called whenever
* the scale of the owning Actor changes.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void SliceMesh();
public:
UMRUKGridSliceResizerComponent();
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
protected:
virtual void BeginPlay() override;
virtual void OnRegister() override;
#if WITH_EDITOR
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
#endif
private:
friend class FMRUKGridSliceResizerSpec;
UPROPERTY(Transient)
class UProceduralMeshComponent* ProcMesh;
FVector ResizerScale = FVector::OneVector;
};

View File

@@ -0,0 +1,37 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "MRUtilityKitRoom.h"
#include "GameFramework/Actor.h"
#include "ProceduralMeshComponent.h"
#include "MRUtilityKitGuardian.generated.h"
/**
* The Guardian is a procedural mesh that is generated from the anchor geometry and has the guardian material applied.
* It is used to show the player where the walls and furniture. It prevents the player from walking into walls or furniture.
* It uses TryGetClosestSurfacePosition to determine if the player is close to the walls or furniture.
* This can be beneficial if your application has a full VR mode.
*/
UCLASS(ClassGroup = MRUtilityKit, meta = (DisplayName = "MR Utility Kit Guardian Actor"))
class MRUTILITYKIT_API AMRUKGuardian : public AActor
{
GENERATED_BODY()
public:
/**
* Procedural mesh that is generated from the anchor geometry and has the guardian material applied.
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
TObjectPtr<UProceduralMeshComponent> GuardianMeshComponent;
/**
* Attaches the procedural mesh component to this actor.
* @param GuardianMesh The mesh to attach.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void CreateGuardian(UProceduralMeshComponent* GuardianMesh);
public:
AMRUKGuardian(const FObjectInitializer& ObjectInitializer);
};

View File

@@ -0,0 +1,107 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "MRUtilityKit.h"
#include "GameFramework/Actor.h"
#include "MRUtilityKitGuardian.h"
#include "MRUtilityKitGuardianSpawner.generated.h"
class AMRUKRoom;
/**
* This class helps with spawning a guardian if the player gets close to any furniture or walls. This is useful if your application has a full VR mode.
* It can spawn a guardian for each room in the scene. It can also spawn a guardian for the current room only.
* For details about the guardian see the AMRUKGuardian class.
*/
UCLASS(ClassGroup = MRUtilityKit, meta = (DisplayName = "MR Utility Kit Guardian"))
class MRUTILITYKIT_API AMRUKGuardianSpawner : public AActor
{
GENERATED_BODY()
public:
AMRUKGuardianSpawner();
/**
* Whether SpawnGuardian() should be called automatically after the mixed reality utility kit
* has been initialized.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
EMRUKSpawnMode SpawnMode = EMRUKSpawnMode::CurrentRoomOnly;
/**
* How close the camera needs to come to a surface before the guardian appears.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
double GuardianDistance = 0.75;
/**
* Whether the fading value should be calculated for the shader or not.
If fading is not needed this can save performance.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
bool EnableFade = true;
/**
* Spawn the guardian. This will get called automatically after the mixed reality utility kit has
* been initialized if SpawnMode is set to something other than None.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void SpawnGuardians(AMRUKRoom* Room);
/**
* Set the guardian material to a different one.
* @param Material The guardian material.
*/
UFUNCTION(BlueprintSetter, Category = "MR Utility Kit")
void SetGuardianMaterial(UMaterialInstance* Material);
/**
* Set the density of the grid.
* @param Density The grid density.
*/
UFUNCTION(BlueprintSetter, Category = "MR Utility Kit")
void SetGridDensity(double Density);
public:
void Tick(float DeltaSeconds) override;
protected:
/**
* The material to use for the guardian. It needs to have a scalar parameter Fade
* and a vector parameter WallScale. If this material is not set a default one
* will be used.
*/
UPROPERTY(EditAnywhere, BlueprintSetter = SetGuardianMaterial, Category = "MR Utility Kit")
TObjectPtr<UMaterialInstance> GuardianMaterial = nullptr;
/**
* How dense the grid should be.
*/
UPROPERTY(EditAnywhere, BlueprintSetter = SetGridDensity, Category = "MR Utility Kit")
double GridDensity = 2.0;
void BeginPlay() override;
#if WITH_EDITOR
void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
#endif
private:
// Room UUID to spawned actors in this room
TMap<AMRUKRoom*, TArray<AMRUKGuardian*>> SpawnedGuardians;
UPROPERTY()
TObjectPtr<UMaterialInstanceDynamic> DynamicGuardianMaterial = nullptr;
UFUNCTION()
void DestroyGuardians(AMRUKRoom* Room);
UFUNCTION()
void OnRoomCreated(AMRUKRoom* Room);
UFUNCTION()
void OnRoomUpdated(AMRUKRoom* Room);
UFUNCTION()
void OnRoomRemoved(AMRUKRoom* Room);
};

View File

@@ -0,0 +1,80 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MRUtilityKitLightDispatcher.generated.h"
/**
* If you want to have highlights from lights over passthrough use this actor to collect all point lights in the scene and send them to the M_Highlights material.
* It lights and sends them to a highlight material, which can be used to achieve highlights over Passthrough.
* The highlight effect is achieved by using a material parameter collection.
* See the PTRL Sample Project for an example of how to use this.
*/
UCLASS(ClassGroup = MRUtilityKit, meta = (DisplayName = "MR Utility Kit Light Dispatcher"))
class MRUTILITYKIT_API AMRUKLightDispatcher : public AActor
{
GENERATED_BODY()
public:
/**
* The material parameter collection in which to fill lights data.
* This parameter collection gets then send to the shader.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
class UMaterialParameterCollection* Collection;
/**
* Whether all point lights should be fetched automatically at BeginPlay().
* The automatic fetching only works for PointLightActors. Actors that have PointLightComponents
* attached to them will not be detected. These should be specified in AdditionalActorsToLookForPointLightComponents.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
bool ShouldFetchPointLightsAtBeginPlay = true;
/**
* List of actor(s) that contain a PointLightComponent that should contribute to the highlight effect.
* Use AddAdditionalPointLightActor to add actors during runtime.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
TArray<AActor*> AdditionalActorsToLookForPointLightComponents;
/**
* PointLightActors to use for the highlight effect (not available if "Fetch Point Lights At Begin Play" is true).
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (EditCondition = "!ShouldFetchPointLightsAtBeginPlay"), Category = "MR Utility Kit")
TArray<class APointLight*> ManualPointLights;
/**
* Add a actor to the AdditionalActorsToLookForPointLightComponents list.
* This should be used during runtime instead of adding actors directly to AdditionalActorsToLookForPointLightComponents.
* @param Actor Actor to add to AdditionalActorsToLookForPointLightComponents.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void AddAdditionalPointLightActor(AActor* Actor);
/**
* Only callable in the editor from the scene, will update the linked parameter collection with the info
* of the point lights in the scene (based on the parameters), updating the highlight effect in the process.
* This is meant to preview the effect in the editor.
*/
UFUNCTION(CallInEditor, Category = "MR Utility Kit")
void ForceUpdateCollection();
public:
AMRUKLightDispatcher();
void Tick(float DeltaSeconds) override;
void FillParameterCollection();
protected:
UPROPERTY(Transient)
TArray<class UPointLightComponent*> PointLightComponents;
void BeginPlay() override;
void FillPointLights();
void AddPointLightsFromActor(const AActor* Actor);
};

View File

@@ -0,0 +1,144 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "CoreMinimal.h"
#include "MRUtilityKitRoom.h"
#include "GameFramework/Actor.h"
#include "MRUtilityKitPositionGenerator.generated.h"
/**
* Holds the settings which are used for generating random positions. It offers several attributes to be configured, such as
* which room to use, what actor to spawn, scene labels to use and much more. This struct is used by the position generator.
* @see AMRUtilityKitPositionGenerator
*/
USTRUCT(BlueprintType)
struct FMRUKRandomSpawnSettings
{
GENERATED_BODY()
/**
* When the scene data is loaded, this controls what room(s) the position generator will be used in.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
EMRUKRoomFilter RoomFilter = EMRUKRoomFilter::CurrentRoomOnly;
/**
* When an actor instance is reference here, this actor will be moved around.
* If you'd need to spawn new actors, use ActorClass.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
AActor* ActorInstance = nullptr;
/**
* Reference the specific actor class for spawning.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
TSubclassOf<AActor> ActorClass;
/**
* How many instances to spawn at the random generated position per room.
* Note: If using an ActorInstance this property is ignored
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
int SpawnAmount = 8;
/**
* Maximum number of times to attempt spawning/moving an object before giving up.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
int MaxIterations = 1000;
/**
* The type of surface by which to limit the generation.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
EMRUKSpawnLocation SpawnLocations = EMRUKSpawnLocation::Floating;
/**
* The labels to include or exclude.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
FMRUKLabelFilter Labels;
/**
* If enabled then the spawn position will be checked to make sure there is no overlap with physics colliders including themselves.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
bool CheckOverlaps = true;
/**
* Required free space for the object.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
float OverrideBounds = -1;
/**
* The CollisionChannel to use.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
TEnumAsByte<ECollisionChannel> CollisionChannel = ECC_WorldStatic;
/**
* The clearance distance required in front of the surface in order for it to be considered a valid spawn position.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
float SurfaceClearanceDistance = 0.1f;
};
/**
* Position generator that can be used to generate random positions on the surface in a specific room or any room.
*
* It contains methods to generate random positions on the surface of a given spawn location,
* while ensuring that the generated positions are at least `MinDistanceToEdge` away from any edges,
* if it should run on start when MRUK initializes and follow the other settings specified in `SpawnSettings`.
*/
UCLASS()
class MRUTILITYKIT_API AMRUtilityKitPositionGenerator : public AActor
{
GENERATED_BODY()
public:
static bool CanSpawnBox(const UWorld* World, const FBox& Box, const FVector& SpawnPosition, const FQuat& SpawnRotation, const FCollisionQueryParams& QueryParams, ECollisionChannel CollisionChannel);
/**
* Generates a set of random positions on the surface of a given spawn location, while ensuring that the generated positions
* are at least `MinDistanceToEdge` away from any edges and follow the other settings specified in `SpawnSettings`.
* @param OutTransforms An array of transforms representing the generated positions.
* @return A boolean value indicating whether valid positions were found. If no valid positions could be found, `OutTransforms` will be empty.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool GenerateRandomPositionsOnSurface(TArray<FTransform>& OutTransforms);
/**
* Generates a set of random positions on the surface of a given spawn location, while ensuring that the generated positions
* are at least `MinDistanceToEdge` away from any edges and follow the other settings specified in `SpawnSettings` in the
* give room.
* @param Room The room where the positions should be generated in.
* @param OutTransforms An array of transforms representing the generated positions.
* @return A boolean value indicating whether valid positions were found. If no valid positions could be found, `OutTransforms` will be empty.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool GenerateRandomPositionsOnSurfaceInRoom(AMRUKRoom* Room, TArray<FTransform>& OutTransforms);
/**
* Whether GenerateRandomPositionsOnSurface() should be called automatically after the mixed reality utility kit has been initialized
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
bool RunOnStart = true;
/**
* Settings that should be used when generating random positions.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
FMRUKRandomSpawnSettings RandomSpawnSettings;
protected:
virtual void BeginPlay() override;
private:
virtual UWorld* GetTickableGameObjectWorld() const { return GetWorld(); }
UFUNCTION()
void SceneLoaded(bool Success);
};

View File

@@ -0,0 +1,481 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "GameFramework/Actor.h"
#include "Dom/JsonObject.h"
#include "MRUtilityKit.h"
#include "OculusXRAnchorTypes.h"
#include "MRUtilityKitRoom.generated.h"
class UMRUKRoomData;
UENUM(BlueprintType)
enum class EMRUKSpawnLocation : uint8
{
Floating UMETA(DisplayName = "Floating"), // Spawn somewhere floating in the free space within the room
AnySurface UMETA(DisplayName = "Any surface"), // Spawn on any surface (i.e. a combination of all 3 options below)
VerticalSurfaces UMETA(DisplayName = "Vertical surfaces"), // Spawn only on vertical surfaces such as walls, windows, wall art, doors, etc...
OnTopOfSurface UMETA(DisplayName = "On top of surfaces"), // Spawn on surfaces facing upwards such as ground, top of tables, beds, couches, etc...
HangingDown UMETA(DisplayName = "Hanging down") // Spawn on surfaces facing downwards such as the ceiling
};
enum class EMRUKBoxSide : uint8
{
XPos,
XNeg,
YPos,
YNeg,
ZPos,
ZNeg,
};
UENUM(BlueprintType)
enum class EMRUKRoomFilter : uint8
{
None,
CurrentRoomOnly,
AllRooms
};
/**
* Method to use when determining the position and rotation for the best pose.
*/
UENUM(BlueprintType)
enum class EMRUKPositioningMethod : uint8
{
/**
* Center the object on the surface.
*/
Center = 0,
/**
* Snap the object to edge which is closest to the user.
*/
Edge,
/**
* Use the location where the ray hit the object as the location.
* The rotation is dependent on the objects shape. For example for walls
* the hit normal from the raycast will be used. For floors the rotation
* will be towards the user and for volumes that got hit on the top the
* rotation will be towards the longest edge that is nearest to the player.
*/
Default,
};
/**
* Represents an anchor with its corresponding plane UVs in the Mixed Reality Utility Kit.
*/
USTRUCT(BlueprintType)
struct FMRUKAnchorWithPlaneUVs
{
GENERATED_BODY()
/**
* A readonly reference to the anchor.
*/
UPROPERTY(BlueprintReadOnly, Category = "MR Utility Kit")
TObjectPtr<AMRUKAnchor> Anchor;
/**
* An array of plane UVs that correspond to the anchor.
*/
UPROPERTY(BlueprintReadOnly, Category = "MR Utility Kit")
TArray<FMRUKPlaneUV> PlaneUVs;
};
/**
* Represents a room in the MRUK.
* A room holds (MRUK)Anchors as children for entities such as Desk, Floor, Ceiling, Walls, etc. Those entities are defined with their label.
* It also provides events which will be triggered when an anchor has been added, removed or updated from space setup.
*
* This room class calculates different helper properties such as Outline, Edges, Bounds
* and provides room functions as helpers such as determine if a point in space (XYZ) is inside the room, generating points on surfaces, generate points in room (floating), raycasts and more.
*/
UCLASS(ClassGroup = MRUtilityKit, meta = (DisplayName = "MR Utility Kit Room Actor"))
class MRUTILITYKIT_API AMRUKRoom : public AActor
{
GENERATED_BODY()
public:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAnchorUpdated, AMRUKAnchor*, Anchor);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAnchorCreated, AMRUKAnchor*, Anchor);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAnchorRemoved, AMRUKAnchor*, Anchor);
/**
* The space handle of this anchor
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
FOculusXRUInt64 SpaceHandle;
/**
* The anchors UUID
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
FOculusXRUUID AnchorUUID;
/**
* Event that gets fired if a anchor in this room was updated.
* E.g. volume or plane changed.
*/
UPROPERTY(BlueprintAssignable, Category = "MR Utility Kit")
FOnAnchorUpdated OnAnchorUpdated;
/**
* Event that gets fired if a new anchor was created in this room.
*/
UPROPERTY(BlueprintAssignable, Category = "MR Utility Kit")
FOnAnchorCreated OnAnchorCreated;
/**
* Event that gets fired if a anchor gets removed from this room.
*/
UPROPERTY(BlueprintAssignable, Category = "MR Utility Kit")
FOnAnchorRemoved OnAnchorRemoved;
/**
* Bounds of the room.
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
FBox RoomBounds;
/**
* Edges of the room.
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
TArray<FVector> RoomEdges;
/**
* The floor anchor of this room.
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
TObjectPtr<AMRUKAnchor> FloorAnchor;
/**
* The ceiling anchor of this room.
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
TObjectPtr<AMRUKAnchor> CeilingAnchor;
/**
* The wall anchors of this room.
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
TArray<TObjectPtr<AMRUKAnchor>> WallAnchors;
/**
* The global mesh anchor of this room.
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
TObjectPtr<AMRUKAnchor> GlobalMeshAnchor;
/**
* All anchors which are possible to sit on.
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
TArray<TObjectPtr<AMRUKAnchor>> SeatAnchors;
/**
* All anchors of this room.
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
TArray<TObjectPtr<AMRUKAnchor>> AllAnchors;
/**
* Check whether the position is inside the room or not.
* @param Position The position in world space to check.
* @param TestVerticalBounds Whether the room should be constrained by vertical bounds or not in the check.
* @return Whether the position is inside the room or not.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool IsPositionInRoom(const FVector& Position, bool TestVerticalBounds = true);
/**
* Generate a uniform random position within the room.
* @param OutPosition Contains the randomly generated position.
* @param MinDistanceToSurface The minimum distance between the generated position and the closest surface/volume.
* @param AvoidVolumes If true then the position will not be inside a volume and min distance away from it.
* @return Return true if success otherwise false. If this fails it can be because the min distance to surface is too large.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool GenerateRandomPositionInRoom(FVector& OutPosition, float MinDistanceToSurface = 0.0f, bool AvoidVolumes = false);
/**
* Generate a uniform random position within the room from a random stream.
* @param OutPosition Contains the randomly generated position.
* @param RandomStream A random generator used to generate the position on the plane.
* @param MinDistanceToSurface The minimum distance between the generated position and the closest surface/volume.
* @param AvoidVolumes If true then the position will not be inside a volume and min distance away from it.
* @return Return true if success otherwise false. If this fails it can be because the min distance to surface is too large.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool GenerateRandomPositionInRoomFromStream(FVector& OutPosition, const FRandomStream& RandomStream, float MinDistanceToSurface = 0.0f, bool AvoidVolumes = false);
/**
* Generates a random position on the surface of a given spawn location, while ensuring that the generated position is at least `MinDistanceToEdge` away from any edges. The `LabelFilter` parameter allows you to specify which types of surfaces should be considered for generating the random position.
*
* @param SpawnLocation The location where the random position should be generated.
* @param MinDistanceToEdge The minimum distance from the edge that the generated position must have.
* @param LabelFilter A filter that specifies which types of surfaces should be considered for generating the random position.
* @param OutPosition The generated position.
* @param OutNormal The normal vector of the generated position.
* @return A boolean value indicating whether a valid position was found. If no valid position could be found, both `OutPosition` and `OutNormal` will be set to zero vectors.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool GenerateRandomPositionOnSurface(EMRUKSpawnLocation SpawnLocation, float MinDistanceToEdge, FMRUKLabelFilter LabelFilter, FVector& OutPosition, FVector& OutNormal);
/**
* Cast a ray and return the closest hit anchor
* @param Origin Origin The origin of the ray.
* @param Direction Direction The direction of the ray.
* @param MaxDist The maximum distance the ray should travel.
* @param LabelFilter The label filter can be used to include/exclude certain labels from the search.
* @param OutHit The closest hit.
* @return The anchor that the ray hit.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit", meta = (AutoCreateRefTerm = "LabelFilter"))
AMRUKAnchor* Raycast(const FVector& Origin, const FVector& Direction, float MaxDist, const FMRUKLabelFilter& LabelFilter, FMRUKHit& OutHit);
/**
* Cast a ray and collect hits against the volume and plane bounds in this room. The order of the hits in the array is not specified.
* @param Origin Origin The origin of the ray.
* @param Direction Direction The direction of the ray.
* @param MaxDist The maximum distance the ray should travel.
* @param OutHits The hits the ray collected.
* @param LabelFilter The label filter can be used to include/exclude certain labels from the search.
* @param OutAnchors The anchors that were hit. Each anchor in this array corresponds to a entry at the same position in OutHits.
* @return Whether the ray hit anything
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit", meta = (AutoCreateRefTerm = "LabelFilter"))
bool RaycastAll(const FVector& Origin, const FVector& Direction, float MaxDist, const FMRUKLabelFilter& LabelFilter, TArray<FMRUKHit>& OutHits, TArray<AMRUKAnchor*>& OutAnchors);
/**
* Clear all anchors from the room.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void ClearRoom();
/**
* Check if the room does have any of the labels.
* @param Labels The labels to check.
* @return Whether the label was found in the room.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool DoesRoomHave(const TArray<FString>& Labels);
/**
* Get the position on the surface that is closest to the given position with respect to the distance.
* @param WorldPosition The position in world space from which the closest surface point should be found.
* @param OutSurfacePosition The closest position on the closest surface if any. Otherwise zero.
* @param OutSurfaceDistance The distance between WorldPosition and OutSurfacePosition.
* @param LabelFilter The label filter can be used to include/exclude certain labels from the search.
* @param MaxDistance The distance to which a closest surface position should be searched. Everything below or equal to zero will be treated as infinity.
* @return The Anchor on which the closest surface position was found or a null pointer otherwise.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit", meta = (AutoCreateRefTerm = "LabelFilter"))
AMRUKAnchor* TryGetClosestSurfacePosition(const FVector& WorldPosition, FVector& OutSurfacePosition, double& OutSurfaceDistance, const FMRUKLabelFilter& LabelFilter, double MaxDistance = 0.0);
/**
* Checks if the given position is on or inside of any scene volume in the room.
* Floor, ceiling and wall anchors will be excluded from the search.
* @param WorldPosition The position in world space to check
* @param TestVerticalBounds Whether the vertical bounds should be checked or not
* @param Tolerance Tolerance
* @return The anchor the WorldPosition is in. A null pointer otherwise.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
AMRUKAnchor* IsPositionInSceneVolume(const FVector& WorldPosition, bool TestVerticalBounds = true, double Tolerance = 0.0);
/**
* Finds the closest seat given a ray.
* @param RayOrigin The origin of the ray.
* @param RayDirection The direction of the ray.
* @param OutSeatTransform The seat pose.
* @return If any seat was found the Anchor that has seats available will be returned. Otherwise a null pointer.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
AMRUKAnchor* TryGetClosestSeatPose(const FVector& RayOrigin, const FVector& RayDirection, FTransform& OutSeatTransform);
/**
* Finds all anchors in this room that have the given label attached.
* @param Label The label to search for.
* @return An array off anchors with the given label.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
TArray<AMRUKAnchor*> GetAnchorsByLabel(const FString& Label) const;
/**
* Finds the first anchor in this room that has the given label attached.
* @param Label The label to search for.
* @return If found, the Anchor that has the label attached. Otherwise a null pointer.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
AMRUKAnchor* GetFirstAnchorByLabel(const FString& Label) const;
/**
* Get a suggested pose (position & rotation) from a raycast to place objects on surfaces in the scene.
* There are different positioning modes available. Default just uses the position where the raycast
* hit the object. Edge snaps the position to the edge that is nearest to the user and Center simply
* centers the position on top of the surface.
* @param RayOrigin The origin of the ray.
* @param RayDirection The direction of the ray.
* @param MaxDist The maximum distance the ray should travel.
* @param LabelFilter The label filter can be used to include/exclude certain labels from the search.
* @param OutPose The calculated pose.
* @param PositioningMethod The method that should be used for determining the position on the surface.
* @return The anchor that was hit by the ray if any. Otherwise a null pointer.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit", meta = (AutoCreateRefTerm = "LabelFilter"))
AMRUKAnchor* GetBestPoseFromRaycast(const FVector& RayOrigin, const FVector& RayDirection, double MaxDist, const FMRUKLabelFilter& LabelFilter, FTransform& OutPose, EMRUKPositioningMethod PositioningMethod = EMRUKPositioningMethod::Default);
/**
* Return the longest wall in the room that has no other walls behind it.
* @param Tolerance The tolerance to use when determining wall that are behind.
* @return The wall anchor that is the key wall in the room.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
AMRUKAnchor* GetKeyWall(double Tolerance = 0.1);
/**
* Return the largest surface for a given label.
* @param Label The label of the surfaces to search in.
* @return The anchor that has the largest surface if any. Otherwise, a null pointer.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
AMRUKAnchor* GetLargestSurface(const FString& Label);
/**
* Attach a procedural mesh to the walls. This is done at the room level to ensure the UV coordinates
* can be done in a seamless way if desired.
* @param WallTextureCoordinateModes Mode of the wall texture coordinates.
* @param CutHoleLabels Labels for which holes should be cut into the plane meshes
* @param ProceduralMaterial Material to apply on top of the procedural mesh.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit", meta = (AutoCreateRefTerm = "WallTextureCoordinateModes", DeprecatedFunction, DeprecationMessage = "Use GenerateProceduralMesh instead."))
void AttachProceduralMeshToWalls(const TArray<FMRUKTexCoordModes>& WallTextureCoordinateModes, const TArray<FString>& CutHoleLabels, UMaterialInterface* ProceduralMaterial = nullptr);
/**
* Spawn meshes on the position of the anchors of the room.
* The actors should have Z as up Y as right and X as forward.
* The pivot point should be in the bottom center.
* @param SpawnGroups A map which tells to spawn which actor to a given label.
* @param CutHoleLabels Labels for which the generated mesh should have holes. Only works with planes.
* @param ProceduralMaterial Material to apply on top of the procedural mesh if any.
* @param ShouldFallbackToProcedural Whether or not it should by default fallback to generating a procedural mesh if no actor class has been specified for a label.
* @return All spawned interior actors.
*/
UFUNCTION(BlueprintCallable, meta = (DeprecatedFunction, DeprecationMessage = "Use AMRUKAnchorActorSpawner instead."), Category = "MR Utility Kit")
TArray<AActor*> SpawnInterior(const TMap<FString, FMRUKSpawnGroup>& SpawnGroups, const TArray<FString>& CutHoleLabels, UMaterialInterface* ProceduralMaterial = nullptr, bool ShouldFallbackToProcedural = true);
/**
* Spawn meshes on the position of the anchors of the room from a random stream.
* The actors should have Z as up Y as right and X as forward.
* The pivot point should be in the bottom center.
* @param SpawnGroups A map wich tells to spawn which actor to a given label.
* @param CutHoleLabels Labels for which the generated mesh should have holes. Only works with planes.
* @param RandomStream A random generator to choose randomly between actor classes if there a multiple for one label.
* @param ProceduralMaterial Material to apply on top of the procedural mesh if any.
* @param ShouldFallbackToProcedural Whether or not it should by default fallback to generating a procedural mesh if no actor class has been specified for a label.
* @return All spawned interior actors.
*/
UFUNCTION(BlueprintCallable, meta = (DeprecatedFunction, DeprecationMessage = "Use AMRUKAnchorActorSpawner instead."), Category = "MR Utility Kit")
TArray<AActor*> SpawnInteriorFromStream(const TMap<FString, FMRUKSpawnGroup>& SpawnGroups, const FRandomStream& RandomStream, const TArray<FString>& CutHoleLabels, UMaterialInterface* ProceduralMaterial = nullptr, bool ShouldFallbackToProcedural = true);
/**
* Check if the given anchor is a wall anchor.
* @param Anchor The anchor to check.
* @return Whether the anchor is a wall anchor or not.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool IsWallAnchor(AMRUKAnchor* Anchor) const;
/**
* Compute the wall mesh texture coordinate adjustments that are needed to generate proper texture coordinates for the walls.
* @param WallTextureCoordinateModes The texture coordinate mode to use for the walls.
* @param OutAnchorsWithPlaneUVs The computed texture coordinate adjustment with the wall anchor.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void ComputeWallMeshUVAdjustments(const TArray<FMRUKTexCoordModes>& WallTextureCoordinateModes, TArray<FMRUKAnchorWithPlaneUVs>& OutAnchorsWithPlaneUVs);
/**
* Load the triangle mesh of the global mesh anchor if it's available.
* @param Material The Material to show if the global mesh is visible.
* @return On success true, otherwise false.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool LoadGlobalMeshFromDevice(UMaterialInterface* Material = nullptr);
/**
* Load the triangle mesh of the global mesh anchor. For this function to succeed you need to make
* sure to have a global mesh specified in the JSON file. Not every JSON file has a global mesh in it.
* @param JsonString The string with the JSON data.
* @param Material Material to apply on the global mesh.
* @return On Success true, otherwise false.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool LoadGlobalMeshFromJsonString(const FString& JsonString, UMaterialInterface* Material = nullptr);
/**
* Compute the centroid of the room by taking the points of the floor boundary.
* The centroid may be outside of the room for non convex rooms.
* The Z value determines the height of the resulting vectors and ranges from
* 0 to 1. A Z value of 1 corresponds to the ceiling positions Z, while a Z value
* of 0 corresponds to the floor positions Z. Any value between 0 and 1 will
* interpolate between the two values.
* In case the floor and ceiling anchors haven't been loaded yet a zero vector
* will be returned.
* @param Z Value used for interpolation of Z.
* @return The centroid.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
FVector ComputeCentroid(double Z = 0.5);
public:
AMRUKRoom(const FObjectInitializer& ObjectInitializer);
void EndPlay(EEndPlayReason::Type Reason) override;
void LoadFromData(UMRUKRoomData* RoomData);
void AttachProceduralMeshToWalls(const TArray<FString>& CutHoleLabels, UMaterialInterface* ProceduralMaterial = nullptr);
void UpdateWorldLock(APawn* Pawn, const FVector& HeadWorldPosition) const;
TSharedRef<FJsonObject> JsonSerialize();
bool Corresponds(UMRUKRoomData* RoomQuery) const;
private:
friend class FMRUKSpec;
AMRUKAnchor* SpawnAnchor();
void InitializeRoom();
void ComputeRoomBounds();
void ComputeAnchorHierarchy();
void ComputeSeats();
void ComputeRoomEdges();
UFUNCTION(CallInEditor)
void AddAnchorToRoom(AMRUKAnchor* Anchor);
class UProceduralMeshComponent* GetOrCreateGlobalMeshProceduralMeshComponent(bool& OutExistedAlready) const;
void SetupGlobalMeshProceduralMeshComponent(UProceduralMeshComponent& ProcMeshComponent, bool ExistedAlready, UMaterialInterface* Material) const;
/**
* Get the list of walls in an order such that each one wall shares an edge with the next
* one in the list.
*/
TArray<TObjectPtr<AMRUKAnchor>> ComputeConnectedWalls() const;
FOculusXRRoomLayout RoomLayout;
UPROPERTY()
AMRUKAnchor* KeyWallAnchor = nullptr;
struct Surface
{
AMRUKAnchor* Anchor;
float UsableArea;
bool IsPlane;
FBox2D Bounds;
EMRUKBoxSide Side;
};
};

View File

@@ -0,0 +1,76 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Engine/DataTable.h"
#include "MRUtilityKitSceneDataProvider.generated.h"
UCLASS(ClassGroup = MRUtilityKit, meta = (DisplayName = "MR Utility Kit Scene Data Provider"))
/*
* This actor is used to provide scene data to the MR Utility Kit when running in editor.
* You can also use it to not load a room from device.
* Use RandomRoom to load a random room from the list of rooms.
*/
class MRUTILITYKIT_API AMRUKSceneDataProvider : public AActor
{
GENERATED_BODY()
public:
/*
* This list holds the rooms that can be loaded, the key is the room type and the value is a data table that contains multiple rooms.
* Roomtypes such as Bedrooms, Livingrooms, etc.
*/
UPROPERTY(EditAnywhere, Category = "MR Utility Kit")
TMap<FString, UDataTable*> Rooms;
/*
* When this is true, a random room will be loaded from the list of rooms.
*/
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "MR Utility Kit")
bool bUseRandomRoom = true;
/*
* When this is true, a random room will be loaded a specific room class, defined in Rooms (Bedrooms, Offices, ..).
*/
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "MR Utility Kit", meta = (EditCondition = "!bUseRandomRoom", EditConditionHides))
bool bUseRandomRoomFromClass = false;
/*
* Use this property to define a specific room class to load, only visible when bUseRandomRoomFromClass is true.
* This can be a room class such as Bedrooms, Offices, ..
*/
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "MR Utility Kit", meta = (EditCondition = "bUseRandomRoomFromClass && !bUseRandomRoom", EditConditionHides))
FString SpecificRoomClass;
/*
* Define a specific room to load, only visible when bUseRandomRoom is false.
*/
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "MR Utility Kit", meta = (EditCondition = "!bUseRandomRoom && !bUseRandomRoomFromClass", EditConditionHides))
FString SpecificRoomName;
/*
* Gets you a room from the list of rooms, if bUseRandomRoom is true, a random room will be returned.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void GetRoom(FString& RoomJSON, FString& RoomName);
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
};
USTRUCT(Blueprintable, BlueprintType)
struct FJSONData : public FTableRowBase
{
GENERATED_USTRUCT_BODY()
public:
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "MR Utility Kit")
FString JSON;
};

View File

@@ -0,0 +1,31 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "MRUtilityKitSeatsComponent.generated.h"
/**
* This component gets attached to Anchors which have seats available.
* Seats can be used for example to spawn avatars in the correct locations.
*/
UCLASS(ClassGroup = MRUtilityKit)
class MRUTILITYKIT_API UMRUKSeatsComponent : public UActorComponent
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "MR Utility Kit")
TArray<FTransform> SeatPoses;
/**
* Calculate the seats poses that are available on the actor.
* This gets called automatically after the room has been loaded.
* However, it's okay to call this function again with a different SeatWidth.
* The seat poses will then get recalculated.
* @param SeatWidth The width of each seat.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void CalculateSeatPoses(double SeatWidth = 60.0);
};

View File

@@ -0,0 +1,204 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "Dom/JsonObject.h"
#include "OculusXRAnchorTypes.h"
#include "OculusXRRoomLayoutManagerComponent.h"
TSharedPtr<FJsonValue> MRUKSerialize(const FString& String);
void MRUKDeserialize(const FJsonValue& Value, FString& String);
TSharedPtr<FJsonValue> MRUKSerialize(const FOculusXRUUID& UUID);
void MRUKDeserialize(const FJsonValue& Value, FOculusXRUUID& UUID);
TSharedPtr<FJsonValue> MRUKSerialize(const double& Number);
void MRUKDeserialize(const FJsonValue& Value, double& Number);
TSharedPtr<FJsonValue> MRUKSerialize(const FOculusXRRoomLayout& RoomLayout);
void MRUKDeserialize(const FJsonValue& Value, FOculusXRRoomLayout& RoomLayout);
template <typename T>
TSharedPtr<FJsonValue> MRUKSerialize(const UE::Math::TVector2<T>& Vector)
{
return MakeShareable(new FJsonValueArray({ MakeShareable(new FJsonValueNumber(Vector.X)), MakeShareable(new FJsonValueNumber(Vector.Y)) }));
}
template <typename T>
void MRUKDeserialize(const FJsonValue& Value, UE::Math::TVector2<T>& Vector)
{
if (auto Array = Value.AsArray(); Array.Num() == 2)
{
MRUKDeserialize(*Array[0], Vector.X);
MRUKDeserialize(*Array[1], Vector.Y);
}
else
{
UE_LOG(LogJson, Error, TEXT("Json Array is of length %d (expected 2) when deserializing TVector2"), Array.Num());
Vector = UE::Math::TVector2<T>::ZeroVector;
}
}
template <typename T>
TSharedPtr<FJsonValue> MRUKSerialize(const UE::Math::TVector<T>& Vector)
{
return MakeShareable(new FJsonValueArray({ MakeShareable(new FJsonValueNumber(Vector.X)), MakeShareable(new FJsonValueNumber(Vector.Y)), MakeShareable(new FJsonValueNumber(Vector.Z)) }));
}
template <typename T>
void MRUKDeserialize(const FJsonValue& Value, UE::Math::TVector<T>& Vector)
{
auto Array = Value.AsArray();
if (Array.Num() == 3)
{
MRUKDeserialize(*Array[0], Vector.X);
MRUKDeserialize(*Array[1], Vector.Y);
MRUKDeserialize(*Array[2], Vector.Z);
}
else
{
UE_LOG(LogJson, Error, TEXT("Json Array is of length %d (expected 3) when deserializing TVector"), Array.Num());
Vector = UE::Math::TVector<T>::ZeroVector;
}
}
template <typename T>
TSharedPtr<FJsonValue> MRUKSerialize(const UE::Math::TRotator<T>& Rotation)
{
return MakeShareable(new FJsonValueArray({ MakeShareable(new FJsonValueNumber(Rotation.Pitch)), MakeShareable(new FJsonValueNumber(Rotation.Yaw)), MakeShareable(new FJsonValueNumber(Rotation.Roll)) }));
}
template <typename T>
void MRUKDeserialize(const FJsonValue& Value, UE::Math::TRotator<T>& Rotation)
{
auto Array = Value.AsArray();
if (Array.Num() == 3)
{
MRUKDeserialize(*Array[0], Rotation.Pitch);
MRUKDeserialize(*Array[1], Rotation.Yaw);
MRUKDeserialize(*Array[2], Rotation.Roll);
}
else
{
UE_LOG(LogJson, Error, TEXT("Json Array is of length %d (expected 3) when deserializing TRotator"), Array.Num());
Rotation = UE::Math::TRotator<T>::ZeroRotator;
}
}
template <typename T>
TSharedPtr<FJsonValue> MRUKSerialize(const UE::Math::TBox2<T>& Box)
{
if (Box.bIsValid)
{
const TSharedRef<FJsonObject> JsonObject = MakeShareable(new FJsonObject);
JsonObject->SetField(TEXT("Min"), MRUKSerialize(Box.Min));
JsonObject->SetField(TEXT("Max"), MRUKSerialize(Box.Max));
return MakeShareable(new FJsonValueObject(JsonObject));
}
else
{
return MakeShareable(new FJsonValueNull());
}
}
template <typename T>
void MRUKDeserialize(const FJsonValue& Value, UE::Math::TBox2<T>& Box)
{
if (Value.IsNull())
{
Box.Init();
}
else
{
const auto Object = Value.AsObject();
MRUKDeserialize(*Object->GetField<EJson::None>(TEXT("Min")), Box.Min);
MRUKDeserialize(*Object->GetField<EJson::None>(TEXT("Max")), Box.Max);
Box.bIsValid = true;
}
}
template <typename T>
TSharedPtr<FJsonValue> MRUKSerialize(const UE::Math::TBox<T>& Box)
{
if (Box.IsValid)
{
const TSharedRef<FJsonObject> JsonObject = MakeShareable(new FJsonObject);
JsonObject->SetField(TEXT("Min"), MRUKSerialize(Box.Min));
JsonObject->SetField(TEXT("Max"), MRUKSerialize(Box.Max));
return MakeShareable(new FJsonValueObject(JsonObject));
}
else
{
return MakeShareable(new FJsonValueNull());
}
}
template <typename T>
void MRUKDeserialize(const FJsonValue& Value, UE::Math::TBox<T>& Box)
{
if (Value.IsNull())
{
Box.Init();
}
else
{
const auto Object = Value.AsObject();
MRUKDeserialize(*Object->GetField<EJson::None>(TEXT("Min")), Box.Min);
MRUKDeserialize(*Object->GetField<EJson::None>(TEXT("Max")), Box.Max);
Box.IsValid = 1;
}
}
template <typename T>
TSharedPtr<FJsonValue> MRUKSerialize(const UE::Math::TTransform<T>& Transform)
{
const TSharedRef<FJsonObject> JsonObject = MakeShareable(new FJsonObject);
JsonObject->SetField(TEXT("Translation"), MRUKSerialize(Transform.GetTranslation()));
JsonObject->SetField(TEXT("Rotation"), MRUKSerialize(Transform.Rotator()));
JsonObject->SetField(TEXT("Scale"), MRUKSerialize(Transform.GetScale3D()));
return MakeShareable(new FJsonValueObject(JsonObject));
}
template <typename T>
void MRUKDeserialize(const FJsonValue& Value, UE::Math::TTransform<T>& Transform)
{
const auto Object = Value.AsObject();
UE::Math::TVector<T> Translation;
UE::Math::TRotator<T> Rotation;
UE::Math::TVector<T> Scale;
MRUKDeserialize(*Object->GetField<EJson::None>(TEXT("Translation")), Translation);
MRUKDeserialize(*Object->GetField<EJson::None>(TEXT("Rotation")), Rotation);
MRUKDeserialize(*Object->GetField<EJson::None>(TEXT("Scale")), Scale);
Transform.SetComponents(UE::Math::TQuat<T>(Rotation), Translation, Scale);
}
template <typename T>
TSharedPtr<FJsonValue> MRUKSerialize(const TArray<T>& Array)
{
TArray<TSharedPtr<FJsonValue>> JsonArray;
JsonArray.Reserve(Array.Num());
for (const auto& Item : Array)
{
JsonArray.Add(MRUKSerialize(Item));
}
return MakeShareable(new FJsonValueArray(JsonArray));
}
template <typename T>
void MRUKDeserialize(const FJsonValue& Value, TArray<T>& OutArray)
{
auto Array = Value.AsArray();
OutArray.Empty();
OutArray.Reserve(Array.Num());
for (const auto& Item : Array)
{
T ItemDeserialized;
MRUKDeserialize(*Item, ItemDeserialized);
OutArray.Push(ItemDeserialized);
}
}

View File

@@ -0,0 +1,309 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "Dom/JsonObject.h"
#include "GameFramework/Actor.h"
#include "GameFramework/WorldSettings.h"
#include "MRUtilityKitRoom.h"
#include "MRUtilityKit.h"
#include "MRUtilityKitData.h"
#include "OculusXRAnchorsRequests.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "Tickable.h"
#include "MRUtilityKitSubsystem.generated.h"
/**
* The Mixed Reality Utility Kit subsystem.
*
* This subsystem acts as a container for scene/anchor data. It has methods to load
* the scene data from the device or a JSON file. After the scene data has been loaded
* it will be stored inside the subsystem to make it possible to query the data from
* everywhere. In addition, it offers methods to fulfill queries on the scene data
* like ray casts or simple content placement.
*
* The subsystem only contains core functionality that is useful for most cases.
* More specific functionality is part of actors. For example, if your goal is to spawn
* meshes in the place of scene anchors you can place the AMRUKAnchorActorSpawner in the
* level to do this. When a level loads you would first load the anchor data from the
* device with this subsystem by calling LoadSceneFromDevice() and then the AMRUKAnchorActorSpawner
* will listen for the subsystem to load the scene data and then spawn the actors accordingly.
*
* You can expect methods in this subsystem to take all loaded rooms into consideration when computing.
* If you want to use a method only on a single specific room, there is most of the time a method
* with the same name on the AMRUKRoom.
*/
UCLASS(ClassGroup = MRUtilityKit)
class MRUTILITYKIT_API UMRUKSubsystem : public UGameInstanceSubsystem, public FTickableGameObject
{
GENERATED_BODY()
public:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnLoaded, bool, Success);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCaptureComplete, bool, Success);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRoomCreated, AMRUKRoom*, Room);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRoomUpdated, AMRUKRoom*, Room);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRoomRemoved, AMRUKRoom*, Room);
/**
* The status of the scene loading. When loading from device this is an asynchronous process
* so will be in the Busy state until it moves to Complete or Failed.
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
EMRUKInitStatus SceneLoadStatus = EMRUKInitStatus::None;
/**
* An event that will trigger when a scene is loaded either from Device or from JSON.
* The Success parameter indicates whether the scene was loaded successfully or not.
*/
UPROPERTY(BlueprintAssignable, Category = "MR Utility Kit")
FOnLoaded OnSceneLoaded;
/**
* An event that gets fired after a room has been created.
*/
UPROPERTY(BlueprintAssignable, Category = "MR Utility Kit")
FOnRoomCreated OnRoomCreated;
/**
* An event that gets fired after a room has been updated.
*/
UPROPERTY(BlueprintAssignable, Category = "MR Utility Kit")
FOnRoomUpdated OnRoomUpdated;
/**
* An event that gets fired when a room gets removed.
*/
UPROPERTY(BlueprintAssignable, Category = "MR Utility Kit")
FOnRoomRemoved OnRoomRemoved;
/**
* An event that will trigger when the capture flow completed.
* The Success parameter indicates whether the scene was captured successfully or not.
*/
UPROPERTY(BlueprintAssignable, Category = "MR Utility Kit")
FOnCaptureComplete OnCaptureComplete;
/**
* Contains a list of rooms that are tracked by the mixed reality utility kit subsystem.
*/
UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
TArray<TObjectPtr<AMRUKRoom>> Rooms;
/**
* When world locking is enabled the position of the VR Pawn will be adjusted each frame to ensure
* the room anchors are where they should be relative to the camera position. This is necessary to
* ensure the position of the virtual objects in the world do not get out of sync with the real world.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
bool EnableWorldLock = true;
/**
* Cast a ray and return the closest hit anchor in the scene.
* @param Origin Origin The origin of the ray.
* @param Direction Direction The direction of the ray.
* @param MaxDist The maximum distance the ray should travel.
* @param LabelFilter The label filter can be used to include/exclude certain labels from the search.
* @param OutHit The closest hit.
* @return The anchor that the ray hit
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit", meta = (AutoCreateRefTerm = "LabelFilter"))
AMRUKAnchor* Raycast(const FVector& Origin, const FVector& Direction, float MaxDist, const FMRUKLabelFilter& LabelFilter, FMRUKHit& OutHit);
/**
* Cast a ray and collect hits against the volumes and plane bounds in every room in the scene.
* The order of the hits in the array is not specified.
* @param Origin Origin The origin of the ray.
* @param Direction Direction The direction of the ray.
* @param MaxDist The maximum distance the ray should travel.
* @param LabelFilter The label filter can be used to include/exclude certain labels from the search.
* @param OutHits The hits the ray collected.
* @param OutAnchors The anchors that were hit. Each anchor in this array corresponds to a entry at the same position in OutHits.
* @return Whether the ray hit anything
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit", meta = (AutoCreateRefTerm = "LabelFilter"))
bool RaycastAll(const FVector& Origin, const FVector& Direction, float MaxDist, const FMRUKLabelFilter& LabelFilter, TArray<FMRUKHit>& OutHits, TArray<AMRUKAnchor*>& OutAnchors);
/**
* Return the room that the headset is currently in. If the headset is not in any given room
* then it will return the room the headset was last in when this function was called.
* If the headset hasn't been in a valid room yet then return the first room in the list.
* If no rooms have been loaded yet then return null.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
AMRUKRoom* GetCurrentRoom() const;
/**
* Save all rooms and anchors to JSON. This JSON representation can than later be used by
* LoadSceneFromJsonString() to load the scene again.
* @return the JSON string.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
FString SaveSceneToJsonString();
/**
* Load rooms and anchors from a JSON representation.
* If the scene is already loaded the scene will be updated with the changes.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void LoadSceneFromJsonString(const FString& String);
/**
* Load rooms and anchors from the device.
* If the scene is already loaded the scene will be updated with the changes.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void LoadSceneFromDevice();
/**
* Removes and clears every room.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
void ClearScene();
/**
* Get the position on the surface that is closest to the given position with respect to the distance in all rooms.
* @param WorldPosition The position in world space from which the closest surface point should be found.
* @param OutSurfacePosition The closest position on the closest surface if any. Otherwise zero.
* @param LabelFilter The label filter can be used to include/exclude certain labels from the search.
* @param MaxDistance The distance to which a closest surface position should be searched. Everything below or equal to zero will be treated as infinity.
* @return The Anchor on which the closest surface position was found or a null pointer otherwise.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit", meta = (AutoCreateRefTerm = "LabelFilter"))
AMRUKAnchor* TryGetClosestSurfacePosition(const FVector& WorldPosition, FVector& OutSurfacePosition, const FMRUKLabelFilter& LabelFilter, double MaxDistance = 0.0);
/**
* Finds the closest seat given a ray.
* @param RayOrigin The origin of the ray.
* @param RayDirection The direction of the ray.
* @param OutSeatTransform The seat pose.
* @return If any seat was found the Anchor that has seats available will be returned. Otherwise a null pointer.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
AMRUKAnchor* TryGetClosestSeatPose(const FVector& RayOrigin, const FVector& RayDirection, FTransform& OutSeatTransform);
/**
* Get a suggested pose (position & rotation) from a raycast to place objects on surfaces in the scene.
* There are different positioning modes available. Default just uses the position where the raycast
* hit the object. Edge snaps the position to the edge that is nearest to the user and Center simply
* centers the position on top of the surface.
* @param RayOrigin The origin of the ray.
* @param RayDirection The direction of the ray.
* @param MaxDist The maximum distance the ray should travel.
* @param LabelFilter The label filter can be used to include/exclude certain labels from the search.
* @param OutPose The calculated pose.
* @param PositioningMethod The method that should be used for determining the position on the surface.
* @return The anchor that was hit by the ray if any. Otherwise a null pointer.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit", meta = (AutoCreateRefTerm = "LabelFilter"))
AMRUKAnchor* GetBestPoseFromRaycast(const FVector& RayOrigin, const FVector& RayDirection, double MaxDist, const FMRUKLabelFilter& LabelFilter, FTransform& OutPose, EMRUKPositioningMethod PositioningMethod = EMRUKPositioningMethod::Default);
/**
* Return the longest wall in the current room that has no other walls behind it.
* @param Tolerance The tolerance to use when determining wall that are behind.
* @return The wall anchor that is the key wall in the room.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
AMRUKAnchor* GetKeyWall(double Tolerance = 0.1);
/**
* Return the largest surface for a given label in the current room.
* @param Label The label of the surfaces to search in.
* @return The anchor that has the largest surface if any. Otherwise, a null pointer.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
AMRUKAnchor* GetLargestSurface(const FString& Label);
/**
* Checks if the given position is on or inside of any scene volume in the rooms.
* All rooms will be checked and the first anchors scene volume that has the point on or inside it will be returned.
* @param WorldPosition The position in world space to check
* @param TestVerticalBounds Whether the vertical bounds should be checked or not
* @param Tolerance Tolerance
* @return The anchor the WorldPosition is in. A null pointer otherwise.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
AMRUKAnchor* IsPositionInSceneVolume(const FVector& WorldPosition, bool TestVerticalBounds = true, double Tolerance = 0.0);
/**
* Spawn meshes on the position of the anchors of each room.
* The actors should have Z as up Y as right and X as forward.
* The pivot point should be in the bottom center.
* @param SpawnGroups A map which tells to spawn which actor to a given label.
* @param ProceduralMaterial Material to apply on top of the procedural mesh if any.
* @param CutHoleLabels Labels for which the generated mesh should have holes. Only works with planes.
* @param ShouldFallbackToProcedural Whether or not it should by default fallback to generating a procedural mesh if no actor class has been specified for a label.
* @return The spawned actors.
*/
UFUNCTION(BlueprintCallable, meta = (DeprecatedFunction, DeprecationMessage = "Use AMRUKAnchorActorSpawner instead."), Category = "MR Utility Kit")
TArray<AActor*> SpawnInterior(const TMap<FString, FMRUKSpawnGroup>& SpawnGroups, const TArray<FString>& CutHoleLabels, UMaterialInterface* ProceduralMaterial = nullptr, bool ShouldFallbackToProcedural = true);
/**
* Spawn meshes on the position of the anchors of each room from a random stream.
* The actors should have Z as up Y as right and X as forward.
* The pivot point should be in the bottom center.
* @param SpawnGroups A map which tells to spawn which actor to a given label.
* @param RandomStream A random generator to choose randomly between actor classes if there a multiple for one label.
* @param CutHoleLabels Labels for which the generated mesh should have holes. Only works with planes.
* @param ProceduralMaterial Material to apply on top of the procedural mesh if any.
* @param ShouldFallbackToProcedural Whether or not it should by default fallback to generating a procedural mesh if no actor class has been specified for a label.
* @return The spawned actors.
*/
UFUNCTION(BlueprintCallable, meta = (DeprecatedFunction, DeprecationMessage = "Use AMRUKAnchorActorSpawner instead."), Category = "MR Utility Kit")
TArray<AActor*> SpawnInteriorFromStream(const TMap<FString, FMRUKSpawnGroup>& SpawnGroups, const FRandomStream& RandomStream, const TArray<FString>& CutHoleLabels, UMaterialInterface* ProceduralMaterial = nullptr, bool ShouldFallbackToProcedural = true);
/**
* Launch the scene capture. After a successful capture the scene should be updated.
* @return Whether the capture was successful.
*/
UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
bool LaunchSceneCapture();
public:
void Initialize(FSubsystemCollectionBase& Collection) override;
void Deinitialize() override;
TSharedRef<FJsonObject> JsonSerialize();
void UnregisterRoom(AMRUKRoom* Room);
// Calculate the bounds of an Actor class and return it, the result is saved in a cache for faster lookup.
FBox GetActorClassBounds(TSubclassOf<AActor> Actor);
UOculusXRRoomLayoutManagerComponent* GetRoomLayoutManager();
private:
AMRUKRoom* SpawnRoom();
void FinishedLoading(bool Success);
// FTickableGameObject interface
virtual void Tick(float DeltaTime) override;
virtual bool IsTickable() const override;
virtual ETickableTickType GetTickableTickType() const override { return (HasAnyFlags(RF_ClassDefaultObject) ? ETickableTickType::Never : ETickableTickType::Conditional); }
virtual TStatId GetStatId() const override { RETURN_QUICK_DECLARE_CYCLE_STAT(UMRUKSubsystem, STATGROUP_Tickables); }
virtual UWorld* GetTickableGameObjectWorld() const override { return GetWorld(); }
// ~FTickableGameObject interface
UFUNCTION()
void SceneDataLoadedComplete(bool Success);
UFUNCTION()
void UpdatedSceneDataLoadedComplete(bool Success);
UFUNCTION()
void SceneCaptureComplete(FOculusXRUInt64 RequestId, bool bSuccess);
UPROPERTY()
TObjectPtr<UMRUKSceneData> SceneData = nullptr;
UPROPERTY()
AActor* RoomLayoutManagerActor = nullptr;
UPROPERTY()
UOculusXRRoomLayoutManagerComponent* RoomLayoutManager = nullptr;
UPROPERTY()
mutable AMRUKRoom* CachedCurrentRoom = nullptr;
mutable int64 CachedCurrentRoomFrame = 0;
UPROPERTY()
AActor* PositionGenerator = nullptr;
TMap<TSubclassOf<AActor>, FBox> ActorClassBoundsCache;
};

View File

@@ -0,0 +1,18 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
#pragma once
#include "OculusXRTelemetry.h"
namespace MRUKTelemetry
{
using FLoadGuardianMarker = OculusXRTelemetry::TMarker<257237531>;
using FLoadBlobShadowMarker = OculusXRTelemetry::TMarker<257244458>;
using FLoadLightDispatcherMarker = OculusXRTelemetry::TMarker<257234454>;
using FLoadDebugComponentMarker = OculusXRTelemetry::TMarker<257232584>;
using FLoadAnchorActorSpawnerMarker = OculusXRTelemetry::TMarker<257232670>;
using FLoadSceneFromDeviceMarker = OculusXRTelemetry::TMarker<257235234>;
using FLoadSceneFromJsonMarker = OculusXRTelemetry::TMarker<257237876>;
using FLoadGridSliceResizerMarker = OculusXRTelemetry::TMarker<257238248>;
using FLoadDestructibleGlobalMeshSpawner = OculusXRTelemetry::TMarker<257232038>;
} // namespace MRUKTelemetry