Android build settings + metaxr
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user