Android build settings + metaxr
This commit is contained in:
53
Plugins/MetaXR/Source/MRUtilityKit/MRUtilityKit.Build.cs
Normal file
53
Plugins/MetaXR/Source/MRUtilityKit/MRUtilityKit.Build.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
84
Plugins/MetaXR/Source/MRUtilityKit/Private/MRUtilityKit.cpp
Normal file
84
Plugins/MetaXR/Source/MRUtilityKit/Private/MRUtilityKit.cpp
Normal 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)
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
355
Plugins/MetaXR/Source/MRUtilityKit/Private/MRUtilityKitData.cpp
Normal file
355
Plugins/MetaXR/Source/MRUtilityKit/Private/MRUtilityKitData.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1522
Plugins/MetaXR/Source/MRUtilityKit/Private/MRUtilityKitRoom.cpp
Normal file
1522
Plugins/MetaXR/Source/MRUtilityKit/Private/MRUtilityKitRoom.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
412
Plugins/MetaXR/Source/MRUtilityKit/Public/MRUtilityKit.h
Normal file
412
Plugins/MetaXR/Source/MRUtilityKit/Public/MRUtilityKit.h
Normal 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;
|
||||
};
|
||||
281
Plugins/MetaXR/Source/MRUtilityKit/Public/MRUtilityKitAnchor.h
Normal file
281
Plugins/MetaXR/Source/MRUtilityKit/Public/MRUtilityKitAnchor.h
Normal 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, it’s 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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
139
Plugins/MetaXR/Source/MRUtilityKit/Public/MRUtilityKitData.h
Normal file
139
Plugins/MetaXR/Source/MRUtilityKit/Public/MRUtilityKitData.h
Normal 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);
|
||||
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
481
Plugins/MetaXR/Source/MRUtilityKit/Public/MRUtilityKitRoom.h
Normal file
481
Plugins/MetaXR/Source/MRUtilityKit/Public/MRUtilityKitRoom.h
Normal 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;
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user