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