/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * Licensed under the Oculus SDK License Agreement (the "License"); * you may not use the Oculus SDK except in compliance with the License, * which is provided at the time of installation or download, or which * otherwise accompanies this software in either electronic or hard copy form. * * You may obtain a copy of the License at * * https://developer.oculus.com/licenses/oculussdk/ * * Unless required by applicable law or agreed to in writing, the Oculus SDK * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using UnityEngine; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using UnityEngine.Serialization; using Debug = UnityEngine.Debug; /// /// A manager for s created using the Guardian's Room Capture feature. /// public class OVRSceneManager : MonoBehaviour { /// /// A prefab that will be used to instantiate any Plane found when querying the Scene model /// [FormerlySerializedAs("planePrefab")] [Tooltip("A prefab that will be used to instantiate any Plane found when querying the Scene model")] public OVRSceneAnchor PlanePrefab; /// /// A prefab that will be used to instantiate any Volume found when querying the Scene model /// [FormerlySerializedAs("volumePrefab")] [Tooltip("A prefab that will be used to instantiate any Volume found when querying the Scene model")] public OVRSceneAnchor VolumePrefab; /// /// Overrides the instantiation of the generic Plane and Volume prefabs with specialized ones /// [FormerlySerializedAs("prefabOverrides")] [Tooltip("Overrides the instantiation of the generic Plane/Volume prefabs with specialized ones")] public List PrefabOverrides = new List(); /// /// When true, verbose debug logs will be emitted. /// [FormerlySerializedAs("verboseLogging")] [Tooltip("When enabled, verbose debug logs will be emitted.")] public bool VerboseLogging; #region Events /// /// This event fires when the OVR Scene Manager has correctly loaded the scene definition and /// instantiated the prefabs for the planes and volumes. Trap it to know that the logic of the /// experience can now continue. /// public Action SceneModelLoadedSuccessfully; /// ///This event fires when a query load the Scene Model returns no result. It can indicate that the, /// user never used the Room Capture in the space they are in. /// public Action NoSceneModelToLoad; /// /// This event will fire after the Room Capture successfully returns. It can be trapped to load the /// scene Model. /// public Action SceneCaptureReturnedWithoutError; /// /// This event will fire if an error occurred while trying to send the user to Room Capture. /// public Action UnexpectedErrorWithSceneCapture; #endregion /// /// Represents the available classifications for each . /// public static class Classification { /// /// Represents an that is classified as a floor. /// public const string Floor = "FLOOR"; /// /// Represents an that is classified as a ceiling. /// public const string Ceiling = "CEILING"; /// /// Represents an that is classified as a wall face. /// public const string WallFace = "WALL_FACE"; /// /// Represents an that is classified as a desk. /// public const string Desk = "DESK"; /// /// Represents an that is classified as a couch. /// public const string Couch = "COUCH"; /// /// Represents an that is classified as a door frame. /// public const string DoorFrame = "DOOR_FRAME"; /// /// Represents an that is classified as a window frame. /// public const string WindowFrame = "WINDOW_FRAME"; /// /// Represents an that is classified as other. /// public const string Other = "OTHER"; /// /// The list of possible semantic labels. /// public static IReadOnlyList List { get; } = new[] { Floor, Ceiling, WallFace, Desk, Couch, DoorFrame, WindowFrame, Other }; } /// /// A container for the set of s representing a room. /// public class RoomLayoutInformation { /// /// The representing the floor of the room. /// public OVRScenePlane Floor; /// /// The representing the ceiling of the room. /// public OVRScenePlane Ceiling; /// /// The set of representing the walls of the room. /// public List Walls = new List(); } /// /// Describes the room layout stored by the Guardian. /// public RoomLayoutInformation RoomLayout; #region Private Vars private enum QueryMode { QueryAllAnchors, // Get entire Scene QueryByUuid, // Get a specific entity QueryAllBounded2DEnabled, // Get all planar entities QueryAllRoomLayoutEnabledForAllEntitiesInside, // Get Ceiling/Floor/Walls + other entities in Space Container. QueryAllRoomLayoutEnabledForRoomBox, // Get Ceiling/Floor/Walls only. } private readonly Dictionary _orderedRoomGuids = new Dictionary(); private Comparison _wallOrderComparer; // Maintain UUIDs to be used. private List _uuidsToQuery; private QueryMode _currentQueryMode = QueryMode.QueryAllAnchors; // We use this to store the request id when attempting to load the scene private UInt64 _sceneCaptureRequestId = UInt64.MaxValue; private HashSet _individualRequestIds = new HashSet(); // Spatial entities that we know about but are waiting for their "locatable" component to be enabled. private readonly Dictionary _pendingLocatable = new Dictionary(); #endregion internal struct LogForwarder { public void Log(string context, string message) => Debug.Log($"[{context}] {message}"); public void LogWarning(string context, string message) => Debug.LogWarning($"[{context}] {message}"); public void LogError(string context, string message) => Debug.LogError($"[{context}] {message}"); } internal LogForwarder? Verbose => VerboseLogging ? new LogForwarder() : (LogForwarder?)null; internal static class Development { [Conditional("DEVELOPMENT_BUILD")] [Conditional("UNITY_EDITOR")] public static void Log(string context, string message) => Debug.Log($"[{context}] {message}"); [Conditional("DEVELOPMENT_BUILD")] [Conditional("UNITY_EDITOR")] public static void LogWarning(string context, string message) => Debug.LogWarning($"[{context}] {message}"); [Conditional("DEVELOPMENT_BUILD")] [Conditional("UNITY_EDITOR")] public static void LogError(string context, string message) => Debug.LogError($"[{context}] {message}"); } void Awake() { _wallOrderComparer = (planeA, planeB) => { bool TryGetUuid(OVRScenePlane plane, out int index) { var guid = plane.GetComponent().Uuid; if (_orderedRoomGuids.TryGetValue(guid, out index)) return true; Development.LogWarning(nameof(OVRSceneManager), $"{nameof(OVRScenePlane)} {guid} does not belong to the current room layout."); return false; } if (!TryGetUuid(planeA, out var indexA)) return 0; if (!TryGetUuid(planeB, out var indexB)) return 0; return indexA.CompareTo(indexB); }; // Only allow one instance at runtime. if (FindObjectsOfType().Length > 1) { new LogForwarder().LogError(nameof(OVRSceneManager), $"Found multiple {nameof(OVRSceneManager)}s. Destroying '{name}'."); enabled = false; DestroyImmediate(this); } } /// /// Loads the scene model from the Guardian. /// /// /// When running on Quest, Scene is queried to retrieve the entities describing the Scene Model. In the Editor, /// the Scene Model is loaded over Link. /// /// Returns true if the query was successfully registered public bool LoadSceneModel() { _currentQueryMode = QueryMode.QueryAllRoomLayoutEnabledForAllEntitiesInside; return LoadSpatialEntities(); } /// /// Requests scene capture from the Guardian. /// /// Returns true if scene capture succeeded, otherwise false. public bool RequestSceneCapture() { #if !UNITY_EDITOR var requestString = ""; return OVRPlugin.RequestSceneCapture(requestString, out _sceneCaptureRequestId); #elif UNITY_EDITOR_WIN Development.LogWarning(nameof(OVRSceneManager), "Scene Capture does not work over Link. Please capture a scene with the HMD in standalone mode, then access the scene model over Link."); return false; #else return false; #endif } #region Private Methods private void OnEnable() { // Bind events OVRManager.SpaceQueryComplete += OVRManager_SpaceQueryComplete; OVRManager.SceneCaptureComplete += OVRManager_SceneCaptureComplete; OVRManager.SpaceSetComponentStatusComplete += OVRManager_SpaceSetComponentStatusComplete; } private void OnDisable() { // Unbind events OVRManager.SpaceQueryComplete -= OVRManager_SpaceQueryComplete; OVRManager.SceneCaptureComplete -= OVRManager_SceneCaptureComplete; OVRManager.SpaceSetComponentStatusComplete -= OVRManager_SpaceSetComponentStatusComplete; } private bool LoadSpatialEntities() { // Remove all the scene entities in memory. Update with scene entities from new query. var sceneAnchors = FindObjectsOfType(); foreach (var sceneAnchor in sceneAnchors) { Destroy(sceneAnchor.gameObject); } RoomLayout = new RoomLayoutInformation(); var queryInfo = new OVRPlugin.SpaceQueryInfo { QueryType = OVRPlugin.SpaceQueryType.Action, MaxQuerySpaces = 100, Timeout = 0, Location = OVRPlugin.SpaceStorageLocation.Local, ActionType = OVRPlugin.SpaceQueryActionType.Load, FilterType = OVRPlugin.SpaceQueryFilterType.None }; if (_currentQueryMode == QueryMode.QueryByUuid) { queryInfo.FilterType = OVRPlugin.SpaceQueryFilterType.Ids; queryInfo.IdInfo = new OVRPlugin.SpaceFilterInfoIds { NumIds = Math.Min(OVRPlugin.SpaceFilterInfoIdsMaxSize, _uuidsToQuery.Count), Ids = new Guid[OVRPlugin.SpaceFilterInfoIdsMaxSize] }; for (int i = 0; i < queryInfo.IdInfo.NumIds; ++i) { queryInfo.IdInfo.Ids[i] = _uuidsToQuery[i]; Verbose?.Log(nameof(OVRSceneManager), $"{nameof(LoadSpatialEntities)}() UUID to query [{_uuidsToQuery[i]}]"); } } else if (_currentQueryMode == QueryMode.QueryAllRoomLayoutEnabledForAllEntitiesInside || _currentQueryMode == QueryMode.QueryAllBounded2DEnabled || _currentQueryMode == QueryMode.QueryAllRoomLayoutEnabledForRoomBox) { queryInfo.FilterType = OVRPlugin.SpaceQueryFilterType.Components; queryInfo.ComponentsInfo = new OVRPlugin.SpaceFilterInfoComponents { Components = new OVRPlugin.SpaceComponentType[OVRPlugin.SpaceFilterInfoComponentsMaxSize], NumComponents = 1, }; if (_currentQueryMode == QueryMode.QueryAllRoomLayoutEnabledForAllEntitiesInside || _currentQueryMode == QueryMode.QueryAllRoomLayoutEnabledForRoomBox) { queryInfo.ComponentsInfo.Components[0] = OVRPlugin.SpaceComponentType.RoomLayout; } else { queryInfo.ComponentsInfo.Components[0] = OVRPlugin.SpaceComponentType.Bounded2D; } } if (OVRPlugin.QuerySpaces(queryInfo, out var requestId)) { // We save this request id to ensure that when we trap a SpaceQueryResults event // it's indeed one of our requests. _individualRequestIds.Add(requestId); Verbose?.Log(nameof(OVRSceneManager), $"{nameof(LoadSpatialEntities)}() calling {nameof(OVRPlugin)}.{nameof(OVRPlugin.QuerySpaces)}(). Request id [{requestId}] added to the request list."); return true; } Verbose?.LogWarning(nameof(OVRSceneManager), $"{nameof(LoadSpatialEntities)}() {nameof(OVRPlugin)}.{nameof(OVRPlugin.QuerySpaces)}() failed."); return false; } /// /// Tests whether is enabled and, if not, requests that it be enabled. /// /// Returns the current state of the component. private bool EnableComponentIfNecessary(OVRSpace space, Guid uuid, OVRPlugin.SpaceComponentType componentType) { OVRPlugin.GetSpaceComponentStatus(space, componentType, out bool componentEnabled, out _); if (componentEnabled) { Verbose?.Log(nameof(OVRSceneManager), $"[{uuid}] {nameof(EnableComponentIfNecessary)}() component [{componentType}] is already enabled."); return true; } double dTimeout = 10 * 1000f; OVRPlugin.SetSpaceComponentStatus(space, componentType, true, dTimeout, out var requestId); Verbose?.Log(nameof(OVRSceneManager), $"[{uuid}] {nameof(EnableComponentIfNecessary)}() component [{componentType}] requested with requestId [{requestId}]."); return false; } #endregion #region ActionFunctions private void OVRManager_SceneCaptureComplete(UInt64 requestId, bool result) { if (requestId != _sceneCaptureRequestId) { Verbose?.LogWarning(nameof(OVRSceneManager), $"Scene Room Capture with requestId: [{requestId}] was ignored, as it was not issued by this Scene Load request."); return; } Development.Log(nameof(OVRSceneManager), $"{nameof(OVRManager_SceneCaptureComplete)}() requestId: [{requestId}] result: [{result}]"); if (result) { // Either the user created a room, or they confirmed that the existing room is up to date. We can now load it. Development.Log(nameof(OVRSceneManager), $"The Room Capture returned without errors. Invoking {nameof(SceneCaptureReturnedWithoutError)}."); SceneCaptureReturnedWithoutError?.Invoke(); } else { Development.LogError(nameof(OVRSceneManager), $"An error occurred when sending the user to the Room Capture. Invoking {nameof(UnexpectedErrorWithSceneCapture)}."); UnexpectedErrorWithSceneCapture?.Invoke(); } } private static bool IsComponentEnabled(OVRSpace space, OVRPlugin.SpaceComponentType componentType) => OVRPlugin.GetSpaceComponentStatus(space, componentType, out var enabled, out _) && enabled; private OVRSceneAnchor InstantiateSceneAnchor(OVRSpace space, Guid uuid, OVRSceneAnchor prefab) { // Query for the semantic classification of the object var hasSemanticLabels = OVRPlugin.GetSpaceSemanticLabels(space, out var labelString); var labels = hasSemanticLabels ? labelString.Split(',') : Array.Empty(); // Search the prefab override for a matching label, and if found override the prefab if (PrefabOverrides.Count > 0) { foreach (var label in labels) { // Skip empty labels if (string.IsNullOrEmpty(label)) continue; // Search the prefab override for an entry matching the label foreach (var @override in PrefabOverrides) { if (@override.Prefab && @override.ClassificationLabel == label) { prefab = @override.Prefab; break; } } } } // This can occur if neither the prefab nor any matching override prefab is set in the inspector if(prefab == null) { Verbose?.Log(nameof(OVRSceneManager), $"No prefab was provided for space: [{space}]" + (labels.Length > 0 ? $" with semantic label {labels[0]}" : "")); return null; } var sceneAnchor = Instantiate(prefab, Vector3.zero, Quaternion.identity); sceneAnchor.Initialize(space, uuid); var plane = sceneAnchor.GetComponent(); if (plane) { // Populate RoomLayoutInformation foreach (var label in labels) { switch (label) { case Classification.Floor: RoomLayout.Floor = plane; break; case Classification.Ceiling: RoomLayout.Ceiling = plane; break; case Classification.WallFace: RoomLayout.Walls.Add(plane); break; } } } return sceneAnchor; } private void OVRManager_SpaceQueryComplete(UInt64 requestId, bool result) { // We should ignore any request that wasn't created by us if (!_individualRequestIds.Contains(requestId)) { Verbose?.LogWarning(nameof(OVRSceneManager), $"requestId: [{requestId}] was ignored as it's not part of the Scene Load requests."); return; } Verbose?.Log(nameof(OVRSceneManager), $"{nameof(OVRManager_SpaceQueryComplete)}() requestId: [{requestId}] result: [{result}]"); _individualRequestIds.Remove(requestId); if (!result) return; if (!OVRPlugin.RetrieveSpaceQueryResults(requestId, out var results)) { Development.LogError(nameof(OVRSceneManager), $"{nameof(OVRPlugin.RetrieveSpaceQueryResults)}() Could not retrieve results."); return; } if (results == null || results.Length == 0) { Development.LogWarning(nameof(OVRSceneManager), "Loading the Scene definition yielded no result. " + "Typically, this means the user has not captured the room they are in yet. " + "Alternatively, an internal error may be preventing this app from accessing scene. " + $"Invoking {nameof(NoSceneModelToLoad)}."); NoSceneModelToLoad?.Invoke(); return; } foreach (var queryResult in results) { ProcessQueryResult(queryResult); } CheckForCompletion(); } private void CheckForCompletion() { // Requests can be nested, so we have to wait for the last one to be complete before applying // any judgement on the final outcome. if (_individualRequestIds.Count == 0 && _pendingLocatable.Count == 0) { Development.Log(nameof(OVRSceneManager), $"Scene Model was loaded successfully. Invoking {nameof(SceneModelLoadedSuccessfully)}."); RoomLayout?.Walls.Sort(_wallOrderComparer); SceneModelLoadedSuccessfully?.Invoke(); } } private void OVRManager_SpaceSetComponentStatusComplete(UInt64 requestId, bool result, OVRSpace space, Guid uuid, OVRPlugin.SpaceComponentType componentType, bool isEnabled) { if (!result) { #if DEVELOPMENT_BUILD if (_pendingLocatable.ContainsKey(space)) { Development.LogError(nameof(OVRSceneManager), $"[{uuid}] {nameof(OVRManager)}.{nameof(OVRManager.SpaceSetComponentStatusComplete)} failed for component {componentType}."); } #endif return; } if (componentType == OVRPlugin.SpaceComponentType.Locatable && isEnabled && _pendingLocatable.TryGetValue(space, out var spaceQueryResult)) { Development.Log(nameof(OVRSceneManager), $"[{uuid}] is now locatable."); _pendingLocatable.Remove(space); ProcessQueryResult(spaceQueryResult); CheckForCompletion(); } } private void ProcessQueryResult(OVRPlugin.SpaceQueryResult queryResult) { var space = queryResult.space; var uuid = queryResult.uuid; OVRPlugin.GetSpaceComponentStatus(space, OVRPlugin.SpaceComponentType.Bounded3D, out bool bounded3dEnabled, out _); OVRPlugin.GetSpaceComponentStatus(space, OVRPlugin.SpaceComponentType.Bounded2D, out bool bounded2dEnabled, out _); OVRPlugin.GetSpaceComponentStatus(space, OVRPlugin.SpaceComponentType.RoomLayout, out bool roomLayoutEnabled, out _); IEnumerable EnabledComponents() { if (IsComponentEnabled(space, OVRPlugin.SpaceComponentType.Locatable)) yield return nameof(OVRPlugin.SpaceComponentType.Locatable); if (bounded2dEnabled) yield return nameof(OVRPlugin.SpaceComponentType.Bounded2D); if (bounded3dEnabled) yield return nameof(OVRPlugin.SpaceComponentType.Bounded3D); if (IsComponentEnabled(space, OVRPlugin.SpaceComponentType.SemanticLabels)) { if (OVRPlugin.GetSpaceSemanticLabels(space, out var labels)) { yield return $"{nameof(OVRPlugin.SpaceComponentType.SemanticLabels)} ({labels})"; } else { yield return $"{nameof(OVRPlugin.SpaceComponentType.SemanticLabels)} (none)"; } } if (roomLayoutEnabled) yield return nameof(OVRPlugin.SpaceComponentType.RoomLayout); } Verbose?.Log(nameof(OVRSceneManager), $"[{uuid}] {nameof(OVRManager_SpaceQueryComplete)}() Enabled components: {string.Join(", ", EnabledComponents())}"); if (bounded2dEnabled || bounded3dEnabled) { // Validate only allowed components are set if (roomLayoutEnabled) { Development.LogError(nameof(OVRSceneManager), $"[{uuid}] {nameof(OVRManager_SpaceQueryComplete)}() Anchor has incompatible components. {nameof(OVRPlugin.SpaceComponentType.RoomLayout)} should not be enabled with {nameof(OVRPlugin.SpaceComponentType.Bounded2D)} or {nameof(OVRPlugin.SpaceComponentType.Bounded3D)}."); return; } // Enable Locatable component, as it is not enabled when the space is loaded from storage for the first time. var locatableEnabled = EnableComponentIfNecessary(space, uuid, OVRPlugin.SpaceComponentType.Locatable); if (!locatableEnabled) { Development.Log(nameof(OVRSceneManager), $"[{uuid}] Waiting for spatial entity to become {nameof(OVRPlugin.SpaceComponentType.Locatable)}."); _pendingLocatable[queryResult.space] = queryResult; return; } InstantiateSceneAnchor(space, uuid, bounded2dEnabled ? PlanePrefab : VolumePrefab); } else if (roomLayoutEnabled) { bool roomLayoutSuccess = OVRPlugin.GetSpaceRoomLayout(space, out var roomLayout); if (!roomLayoutSuccess) { Development.LogError(nameof(OVRSceneManager), $"[{uuid}] has component {nameof(OVRPlugin.SpaceComponentType.RoomLayout)} but {nameof(OVRPlugin.GetSpaceRoomLayout)} failed. Ignoring room."); return; } var uuidSet = new HashSet(); if (!roomLayout.floorUuid.Equals(Guid.Empty)) { uuidSet.Add(roomLayout.floorUuid); Verbose?.Log(nameof(OVRSceneManager), $"{nameof(OVRPlugin.GetSpaceRoomLayout)}: floor [{roomLayout.floorUuid}]"); } if (!roomLayout.ceilingUuid.Equals(Guid.Empty)) { uuidSet.Add(roomLayout.ceilingUuid); Verbose?.Log(nameof(OVRSceneManager), $"{nameof(OVRPlugin.GetSpaceRoomLayout)}: ceiling [{roomLayout.ceilingUuid}]"); } _orderedRoomGuids.Clear(); int validWallsCount = 0; foreach (var wallUuid in roomLayout.wallUuids) { if (!wallUuid.Equals(Guid.Empty)) { uuidSet.Add(wallUuid); Verbose?.Log(nameof(OVRSceneManager), $"{nameof(OVRPlugin.GetSpaceRoomLayout)}: wall [{wallUuid}]"); _orderedRoomGuids[wallUuid] = validWallsCount++; } } Verbose?.Log(nameof(OVRSceneManager), $"{nameof(OVRPlugin.GetSpaceRoomLayout)}: wall count [{validWallsCount}]"); bool containerSuccess = OVRPlugin.GetSpaceContainer(space, out var containerUuids); Verbose?.Log(nameof(OVRSceneManager), $"{nameof(OVRPlugin.GetSpaceContainer)}: success [{containerSuccess}], count [{containerUuids.Length}]"); if (containerSuccess) { foreach (var containerUuid in containerUuids) { Verbose?.Log(nameof(OVRSceneManager), $"{nameof(OVRPlugin.GetSpaceContainer)}: UUID [{containerUuid}]"); if (!containerUuid.Equals(Guid.Empty)) { uuidSet.Add(containerUuid); } } } _uuidsToQuery = uuidSet.ToList(); _currentQueryMode = QueryMode.QueryByUuid; LoadSpatialEntities(); } } #endregion }