/*
* 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 System.Collections.Generic;
using System.Diagnostics;
using Meta.XR.Util;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Serialization;
using Debug = UnityEngine.Debug;
using Permission = UnityEngine.Android.Permission;
#if DEVELOPMENT_BUILD || UNITY_EDITOR
using System.Linq;
#endif
///
/// A manager for s created using the Room Setup feature.
///
///
/// "OVRSceneManager and associated classes are deprecated (v65), please use [MR Utility Kit](https://developer.oculus.com/documentation/unity/unity-mr-utility-kit-overview)" instead.
///
[HelpURL("https://developer.oculus.com/documentation/unity/unity-scene-use-scene-anchors/")]
[Obsolete(DeprecationMessage)]
[Feature(Feature.Scene)]
public class OVRSceneManager : MonoBehaviour
{
internal const string DeprecationMessage =
"OVRSceneManager and associated classes are deprecated (v65), please use MR Utility Kit instead (https://developer.oculus.com/documentation/unity/unity-mr-utility-kit-overview)";
///
/// A prefab that will be used to instantiate any Plane found
/// when querying the Scene model. If the anchor contains both
/// Volume and Plane elements, will
/// be used instead. If null, no object will be instantiated,
/// unless a prefab override is provided.
///
[FormerlySerializedAs("planePrefab")]
[Tooltip("A prefab that will be used to instantiate any Plane found " +
"when querying the Scene model. If the anchor contains both " +
"Volume and Plane elements, Volume will be used instead.")]
public OVRSceneAnchor PlanePrefab;
///
/// A prefab that will be used to instantiate any Volume found
/// when querying the Scene model. This anchor may also contain
/// Plane elements. If null, no object will be instantiated,
/// unless a prefab override is provided.
///
[FormerlySerializedAs("volumePrefab")]
[Tooltip("A prefab that will be used to instantiate any Volume found " +
"when querying the Scene model. This anchor may also contain " +
"Plane elements.")]
public OVRSceneAnchor VolumePrefab;
///
/// Overrides the instantiation of the generic Plane and Volume prefabs with specialized ones.
/// If null is provided, no object will be instantiated for that label.
///
[FormerlySerializedAs("prefabOverrides")]
[Tooltip("Overrides the instantiation of the generic Plane/Volume prefabs with specialized ones.")]
public List PrefabOverrides = new List();
///
/// When enabled, only rooms the user is currently in will be instantiated.
///
///
/// When `True`, will instantiate an and all of its child
/// scene anchors (walls, floor, ceiling, and furniture) only if the user is located in the room when
/// is called.
///
/// When `False`, the will instantiate an for each room,
/// regardless of the user's location.
///
/// The 2D boundary points of the room's floor are used to determine whether the user is inside a room.
///
/// If a room exists, but the user is not inside it, then the event is invoked as
/// if the user had not yet run Space Setup.
///
[Tooltip("Scene manager will only present the room(s) the user is currently in.")]
public bool ActiveRoomsOnly = true;
///
/// When true, verbose debug logs will be emitted.
///
[FormerlySerializedAs("verboseLogging")]
[Tooltip("When enabled, verbose debug logs will be emitted.")]
public bool VerboseLogging;
///
/// The maximum number of scene anchors that will be updated each frame.
///
[Tooltip("The maximum number of scene anchors that will be updated each frame.")]
public int MaxSceneAnchorUpdatesPerFrame = 3;
///
/// The parent transform to which each new or
/// will be parented upon instantiation.
///
///
/// if null, (s) instantiated by will have no parent, and
/// (s) will have either a as their parent or null, that is
/// they will be instantiated at the scene root. If non-null, (s) that do not
/// belong to any , and (s) along with its child
/// (s) will be parented to .
///
/// Changing this value does not affect existing (s) or (s).
///
public Transform InitialAnchorParent
{
get => _initialAnchorParent;
set => _initialAnchorParent = value;
}
[SerializeField]
[Tooltip("(Optional) The parent transform for each new scene anchor. " +
"Changing this value does not affect existing scene anchors. May be null.")]
internal Transform _initialAnchorParent;
#region Events
///
/// This event fires when the has instantiated prefabs
/// for the Scene Anchors in a Scene Model.
///
///
/// Wait until this event has been fired before accessing any s,
/// as this event waits for any additional initialization logic to be executed first.
/// Access s using
/// .
///
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 Setup in the space they are in.
///
public Action NoSceneModelToLoad;
///
/// Unable to load the scene model because the user has not granted permission to use Scene.
///
///
/// Apps that wish to use Scene must have "Spatial data" sharing permission. This is a runtime permission that
/// must be granted before loading the scene model. If the permission has not been granted, then calling
/// will result in this event.
///
/// The permission string is "com.oculus.permission.USE_SCENE". See Unity's
/// [Android Permission API](https://docs.unity3d.com/ScriptReference/Android.Permission.html) for more information
/// on interacting with permissions.
///
public event Action LoadSceneModelFailedPermissionNotGranted;
///
/// This event will fire after the Room Setup 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 Setup.
///
public Action UnexpectedErrorWithSceneCapture;
///
/// This event fires when the OVR Scene Manager detects a change in the room layout.
/// It indicates that the user performed Room Setup while the application was paused.
/// Upon receiving this event, user can call to reload the scene model.
///
public Action NewSceneModelAvailable;
#endregion
///
/// Represents the available classifications for each .
///
///
/// Classifications can be useful for functionally describing the contents of an .
/// By iterating all s and comparing their Classifications with those needed,
/// your application can determine whether a room meets its minimum requirements and/or can adjust its behavior
/// to fit the physical surroundings.
///
/// and associated classes are deprecated (v65), please use [MR Utility Kit](https://developer.oculus.com/documentation/unity/unity-mr-utility-kit-overview)" instead.
///
[Obsolete(DeprecationMessage)]
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.
/// This label has been deprecated in favor of .
///
[Obsolete("Deprecated. Use Table classification instead.")]
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";
///
/// Represents an that is classified as a storage (e.g., cabinet, shelf).
///
public const string Storage = "STORAGE";
///
/// Represents an that is classified as a bed.
///
public const string Bed = "BED";
///
/// Represents an that is classified as a screen (e.g., TV, computer monitor).
///
public const string Screen = "SCREEN";
///
/// Represents an that is classified as a lamp.
///
public const string Lamp = "LAMP";
///
/// Represents an that is classified as a plant.
///
public const string Plant = "PLANT";
///
/// Represents an that is classified as a table.
///
public const string Table = "TABLE";
///
/// Represents an that is classified as wall art.
///
public const string WallArt = "WALL_ART";
///
/// Represents an that is classified as an invisible wall face.
/// All invisible wall faces are also classified as a in order to
/// provide backwards compatibility for apps that expect closed rooms to only consist of
/// wall faces, instead of a sequence composed of either invisible wall faces or wall faces.
///
public const string InvisibleWallFace = "INVISIBLE_WALL_FACE";
///
/// Represents an that is classified as a global mesh.
///
public const string GlobalMesh = "GLOBAL_MESH";
///
/// The list of all possible semantic labels that can be applied to an .
///
///
/// exposes this list as a HashSet.
///
public static IReadOnlyList List { get; } = new[]
{
Floor,
Ceiling,
WallFace,
#pragma warning disable CS0618 // Type or member is obsolete
Desk,
#pragma warning restore CS0618 // Type or member is obsolete
Couch,
DoorFrame,
WindowFrame,
Other,
Storage,
Bed,
Screen,
Lamp,
Plant,
Table,
WallArt,
InvisibleWallFace,
GlobalMesh,
};
///
/// The set of possible semantic labels that can be applied to an .
///
///
/// This is the same as but allows for faster lookup.
///
public static HashSet Set { get; } = new(List);
}
///
/// A container for the set of s representing a room's Walls, Floor, and Ceiling as
/// s, which provide detailed geometry for each.
///
///
/// and associated classes are deprecated (v65), please use [MR Utility Kit](https://developer.oculus.com/documentation/unity/unity-mr-utility-kit-overview)" instead.
///
[Obsolete("RoomLayoutInformation is obsoleted. For each room's layout information " +
"(floor, ceiling, walls) see " + nameof(OVRSceneRoom) + ".", false)]
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();
}
///
/// Describes the room layout of a room in the scene model.
///
[Obsolete(
"RoomLayout is obsoleted. For each room's layout information (floor, ceiling, walls) see " +
nameof(OVRSceneRoom) +
".",
false)]
public RoomLayoutInformation RoomLayout;
#region Private Vars
// We use this to store the request id when attempting to load the scene
private ulong _sceneCaptureRequestId = ulong.MaxValue;
private OVRCameraRig _cameraRig;
private int _sceneAnchorUpdateIndex;
private bool _hasLoadBeenRequested;
#endregion
#region Logging
internal struct LogForwarder
{
public void Log(string context, string message, GameObject gameObject = null) =>
Debug.Log($"[{context}] {message}", gameObject);
public void LogWarning(string context, string message, GameObject gameObject = null) =>
Debug.LogWarning($"[{context}] {message}", gameObject);
public void LogError(string context, string message, GameObject gameObject = null) =>
Debug.LogError($"[{context}] {message}", gameObject);
}
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, GameObject gameObject = null) =>
Debug.Log($"[{context}] {message}", gameObject);
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
public static void LogWarning(string context, string message, GameObject gameObject = null) =>
Debug.LogWarning($"[{context}] {message}", gameObject);
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
public static void LogError(string context, string message, GameObject gameObject = null) =>
Debug.LogError($"[{context}] {message}", gameObject);
}
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
static void Log(string message, GameObject gameObject = null) =>
Development.Log(nameof(OVRSceneManager), message, gameObject);
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
static void LogWarning(string message, GameObject gameObject = null) =>
Development.LogWarning(nameof(OVRSceneManager), message, gameObject);
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
static void LogError(string message, GameObject gameObject = null) =>
Development.LogError(nameof(OVRSceneManager), message, gameObject);
#endregion
void Awake()
{
// Only allow one instance at runtime.
if (FindObjectsByType(FindObjectsSortMode.None).Length > 1)
{
new LogForwarder().LogError(nameof(OVRSceneManager),
$"Found multiple {nameof(OVRSceneManager)}s. Destroying '{name}'.");
enabled = false;
DestroyImmediate(this);
}
}
void Start()
{
OVRTelemetry.Start(OVRTelemetryConstants.Scene.MarkerId.UseOVRSceneManager)
.AddAnnotation(OVRTelemetryConstants.Scene.AnnotationType.UsingBasicPrefabs,
(PlanePrefab != null || VolumePrefab != null) ? "true" : "false")
.AddAnnotation(OVRTelemetryConstants.Scene.AnnotationType.UsingPrefabOverrides,
(PrefabOverrides.Count > 0) ? "true" : "false")
.AddAnnotation(OVRTelemetryConstants.Scene.AnnotationType.ActiveRoomsOnly,
ActiveRoomsOnly ? "true" : "false")
.Send();
}
private static void LogResult(OVRAnchor.FetchResult value)
{
if (((OVRPlugin.Result)value).IsSuccess())
{
Log($"xrDiscoverSpacesMETA completed successfully with result {value}.");
}
else
{
LogError($"xrDiscoverSpacesMETA failed with error {value}.");
}
}
internal static async OVRTask FetchAnchorsAsync(List anchors,
Action, int> incrementalResultsCallback = null) where T : struct, IOVRAnchorComponent
{
Log($"Fetching anchors of type {default(T).Type} using xrDiscoverSpacesMETA.");
var result = await OVRAnchor.FetchAnchorsAsync(anchors, new OVRAnchor.FetchOptions
{
SingleComponentType = typeof(T),
}, incrementalResultsCallback);
LogResult(result.Status);
return result.Success;
}
internal static async OVRTask FetchAnchorsAsync(IEnumerable uuids, List anchors)
{
Log($"Fetching {uuids.ToNonAlloc().GetCount()} anchors by UUID using xrDiscoverSpacesMETA.");
var result = await OVRAnchor.FetchAnchorsAsync(anchors, new OVRAnchor.FetchOptions
{
Uuids = uuids,
});
LogResult(result.Status);
return result.Success;
}
internal async void OnApplicationPause(bool isPaused)
{
// if we haven't loaded scene, we won't check anchor status
if (isPaused || !_hasLoadBeenRequested) return;
using (new OVRObjectPool.ListScope(out var anchors))
{
var success = await FetchAnchorsAsync(anchors);
if (!success)
{
Verbose?.Log(nameof(OVRSceneManager), "Failed to retrieve scene model information on resume.");
return;
}
// check whether room anchors have changed
foreach (var anchor in anchors)
{
if (!OVRSceneAnchor.SceneAnchors.ContainsKey(anchor.Uuid))
{
Verbose?.Log(nameof(OVRSceneManager),
$"Scene model changed. Invoking {nameof(NewSceneModelAvailable)} event.");
NewSceneModelAvailable?.Invoke();
break;
}
}
}
QueryForExistingAnchorsTransform();
}
private async void QueryForExistingAnchorsTransform()
{
using (new OVRObjectPool.ListScope(out var anchors))
using (new OVRObjectPool.ListScope(out var uuids))
{
foreach (var anchor in OVRSceneAnchor.SceneAnchorsList)
{
if (!anchor.Space.Valid || !anchor.IsTracked)
continue;
uuids.Add(anchor.Uuid);
}
if (uuids.Count > 0)
{
await FetchAnchorsAsync(uuids, anchors);
}
UpdateAllSceneAnchors();
}
}
///
/// Loads the scene model
///
///
/// The "scene model" consists of all the anchors (i.e., floor, ceiling, walls, and furniture) for all the rooms
/// defined during Space Setup.
///
/// 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 initiated.
public bool LoadSceneModel()
{
_hasLoadBeenRequested = true;
DestroyExistingAnchors();
var task = LoadSceneModelAsync();
if (!task.IsCompleted)
{
AwaitTask(task);
return true;
}
return InterpretResult(task.GetResult());
async void AwaitTask(OVRTask task) => InterpretResult(await task);
bool InterpretResult(LoadSceneModelResult result)
{
switch (result)
{
case LoadSceneModelResult.Success:
{
Log($"Scene model loaded successfully. Invoking {nameof(SceneModelLoadedSuccessfully)}");
SceneModelLoadedSuccessfully?.Invoke();
return true;
}
case LoadSceneModelResult.FailureScenePermissionNotGranted:
{
LogWarning($"Cannot retrieve anchors because {OVRPermissionsRequester.ScenePermission} has " +
$"not been granted. Invoking {nameof(LoadSceneModelFailedPermissionNotGranted)}",
gameObject);
LoadSceneModelFailedPermissionNotGranted?.Invoke();
// true because the query didn't fail
return true;
}
case LoadSceneModelResult.NoSceneModelToLoad:
{
LogWarning($"Although the app has {OVRPermissionsRequester.ScenePermission} permission, " +
$"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 true;
}
default: return false;
}
}
}
public enum LoadSceneModelResult
{
Success = 0,
NoSceneModelToLoad = 1,
FailureScenePermissionNotGranted = -1,
FailureUnexpectedError = -2,
}
internal struct Metrics
{
public int TotalRoomCount;
public int CandidateRoomCount;
public int Loaded;
public int Failed;
public int SkippedUserNotInRoom;
public int SkippedAlreadyInstantiated;
public static Metrics operator +(Metrics lhs, Metrics rhs) => new()
{
TotalRoomCount = lhs.TotalRoomCount + rhs.TotalRoomCount,
CandidateRoomCount = lhs.CandidateRoomCount + rhs.CandidateRoomCount,
Loaded = lhs.Loaded + rhs.Loaded,
Failed = lhs.Failed + rhs.Failed,
SkippedUserNotInRoom = lhs.SkippedUserNotInRoom + rhs.SkippedUserNotInRoom,
SkippedAlreadyInstantiated = lhs.SkippedAlreadyInstantiated + rhs.SkippedAlreadyInstantiated,
};
}
internal struct RoomLayoutUuids
{
public Guid Floor;
public Guid Ceiling;
public Guid[] Walls;
}
private async OVRTask ProcessBatch(List rooms, int startingIndex)
{
Log($"Processing batch [{startingIndex}..{rooms.Count - 1}]", gameObject);
var metrics = new Metrics
{
// Rooms in this batch
TotalRoomCount = rooms.Count - startingIndex,
};
using (new OVRObjectPool.ListScope(out var candidateRooms))
using (new OVRObjectPool.DictionaryScope(out var layoutUuids))
{
for (var i = startingIndex; i < rooms.Count; i++)
{
var room = rooms[i];
var layout = default(RoomLayoutUuids);
if (room.GetComponent()
.TryGetRoomLayout(out layout.Ceiling, out layout.Floor, out layout.Walls))
{
layoutUuids.Add(room, layout);
candidateRooms.Add(room);
}
else
{
LogError($"Unable to retrieve room layout information for {room.Uuid}. Ignoring.", gameObject);
metrics.Failed++;
}
}
metrics.CandidateRoomCount = candidateRooms.Count;
if (candidateRooms.Count == 0)
{
return metrics;
}
if (ActiveRoomsOnly)
{
Log($"Filtering inactive rooms.");
LoadSceneModelResult result;
(result, metrics.SkippedUserNotInRoom) = await FilterByActiveRoom(candidateRooms, layoutUuids);
if ((int)result < 0)
{
metrics.Failed += metrics.CandidateRoomCount;
return metrics;
}
if (metrics.SkippedUserNotInRoom > 0)
{
LogWarning($"{metrics.SkippedUserNotInRoom} of {metrics.CandidateRoomCount} candidate " +
$"room(s) were ignored because the user is not in them.");
}
// We must have filtered them all out
if (candidateRooms.Count == 0)
{
return metrics;
}
}
using (new OVRObjectPool.ListScope(out var taskResults))
using (new OVRObjectPool.ListScope>(out var tasks))
{
foreach (var room in candidateRooms)
{
// Skip pre-existing rooms
if (OVRSceneAnchor.SceneAnchors.TryGetValue(room.Uuid, out var sceneAnchor))
{
LogWarning($"Skipping {sceneAnchor.name} because it has already been instantiated.");
sceneAnchor.IsTracked = true;
metrics.SkippedAlreadyInstantiated++;
continue;
}
var layout = layoutUuids[room];
var roomGameObject = new GameObject($"Room {room.Uuid}");
roomGameObject.transform.parent = _initialAnchorParent;
sceneAnchor = roomGameObject.AddComponent();
sceneAnchor.Initialize(room);
var sceneRoom = roomGameObject.AddComponent();
tasks.Add(sceneRoom.LoadRoom(layout.Floor, layout.Ceiling, layout.Walls));
}
await OVRTask.WhenAll(tasks, taskResults);
foreach (var loadSuccessful in taskResults)
{
if (loadSuccessful)
{
metrics.Loaded++;
}
else
{
metrics.Failed++;
}
}
}
}
return metrics;
}
private async OVRTask LoadSceneModelAsync()
{
if (!Permission.HasUserAuthorizedPermission(OVRPermissionsRequester.ScenePermission))
{
return LoadSceneModelResult.FailureScenePermissionNotGranted;
}
using (new OVRObjectPool.ListScope(out var taskResults))
using (new OVRObjectPool.ListScope>(out var tasks))
using (new OVRObjectPool.ListScope(out var rooms))
{
var result = await FetchAnchorsAsync(rooms, (rooms, startingIndex) =>
{
tasks.Add(ProcessBatch(rooms, startingIndex));
});
// Wait for all batches to complete
await OVRTask.WhenAll(tasks, taskResults);
if (!result)
{
LogError($"Unable to query for rooms.", gameObject);
return LoadSceneModelResult.FailureUnexpectedError;
}
var combinedMetrics = default(Metrics);
foreach (var batchMetrics in taskResults)
{
combinedMetrics += batchMetrics;
}
Log($"{nameof(LoadSceneModelAsync)} Report:\n" +
$"\t{combinedMetrics.TotalRoomCount} total rooms\n" +
$"\t{combinedMetrics.CandidateRoomCount} candidate rooms\n" +
$"\t{combinedMetrics.Loaded} loaded\n" +
$"\t{combinedMetrics.Failed} failed\n" +
$"\t{combinedMetrics.SkippedAlreadyInstantiated} skipped because the room is already instantiated\n" +
$"\t{combinedMetrics.SkippedUserNotInRoom} skipped because the user is not in the room",
gameObject);
if (combinedMetrics.Loaded > 0)
{
#pragma warning disable CS0618 // Type or member is obsolete
RoomLayout = GetRoomLayoutInformation();
#pragma warning restore CS0618
Log($"{LoadSceneModelResult.Success}: Successfully loaded {combinedMetrics.Loaded} room(s)");
return LoadSceneModelResult.Success;
}
if (combinedMetrics.SkippedAlreadyInstantiated > 0)
{
Log($"{LoadSceneModelResult.Success}: Did not load any new rooms, but there are {combinedMetrics.SkippedAlreadyInstantiated} room(s) that were loaded previously.");
return LoadSceneModelResult.Success;
}
if (combinedMetrics.SkippedUserNotInRoom > 0)
{
LogWarning($" {LoadSceneModelResult.NoSceneModelToLoad}: There are {combinedMetrics.TotalRoomCount} room(s) but the user is not in any of them.");
return LoadSceneModelResult.NoSceneModelToLoad;
}
if (combinedMetrics.Failed > 0)
{
LogError($" {LoadSceneModelResult.FailureUnexpectedError}: Failed to load {combinedMetrics.Failed} room(s).");
return LoadSceneModelResult.FailureUnexpectedError;
}
// Nothing was loaded, skipped, or failed, and we have permission, so there must not be a scene model
Log($" {LoadSceneModelResult.NoSceneModelToLoad}: Query succeeded and {OVRPermissionsRequester.ScenePermission} permission has been granted, but there are no results.");
return LoadSceneModelResult.NoSceneModelToLoad;
}
}
private static async OVRTask<(LoadSceneModelResult, int)> FilterByActiveRoom(List rooms,
Dictionary layouts)
{
rooms.Clear();
var skipped = 0;
var userPosition = OVRPlugin.GetNodePose(OVRPlugin.Node.EyeCenter, OVRPlugin.Step.Render)
.Position
.FromVector3f();
using (new OVRObjectPool.ListScope(out var floorAndCeilingAnchors))
using (new OVRObjectPool.ListScope(out var floorAndCeilingUuids))
{
foreach (var layout in layouts.Values)
{
floorAndCeilingUuids.Add(layout.Ceiling);
floorAndCeilingUuids.Add(layout.Floor);
}
var result = await FetchAnchorsAsync(floorAndCeilingUuids, floorAndCeilingAnchors);
if (!result)
{
Development.LogError(nameof(OVRSceneManager), $"Unable to load floor and ceiling anchors.");
return (LoadSceneModelResult.FailureUnexpectedError, 0);
}
using (new OVRObjectPool.ListScope(out var results))
using (new OVRObjectPool.ListScope>(out var tasks))
{
foreach (var anchor in floorAndCeilingAnchors)
{
if (anchor.TryGetComponent(out var locatable))
{
tasks.Add(locatable.SetEnabledAsync(true));
}
}
await OVRTask.WhenAll(tasks, results);
}
using (new OVRObjectPool.DictionaryScope(out var uuidToAnchor))
{
foreach (var anchor in floorAndCeilingAnchors)
{
uuidToAnchor.Add(anchor.Uuid, anchor);
}
foreach (var (room, layout) in layouts)
{
if (uuidToAnchor.TryGetValue(layout.Floor, out var floor) &&
uuidToAnchor.TryGetValue(layout.Ceiling, out var ceiling) &&
IsUserInRoom(userPosition, floor: floor, ceiling: ceiling))
{
rooms.Add(room);
}
else
{
skipped++;
}
}
}
}
return (LoadSceneModelResult.Success, skipped);
}
#region Loading active room(s)
private static bool IsUserInRoom(Vector3 userPosition, OVRAnchor floor, OVRAnchor ceiling)
{
// Get floor anchor's pose
if (!OVRPlugin.TryLocateSpace(floor.Handle, OVRPlugin.GetTrackingOriginType(), out var floorPose))
{
Development.LogError(nameof(OVRSceneManager), $"Could not locate floor anchor {floor.Uuid}");
return false;
}
// Get ceiling anchor's pose
if (!OVRPlugin.TryLocateSpace(ceiling.Handle, OVRPlugin.GetTrackingOriginType(), out var ceilingPose))
{
Development.LogError(nameof(OVRSceneManager), $"Could not locate ceiling anchor {ceiling.Uuid}");
return false;
}
// Get room boundary vertices (assumes floor and ceiling have the same 2d boundary)
if (!OVRPlugin.GetSpaceBoundary2DCount(floor.Handle, out var count))
{
Development.LogWarning(nameof(OVRSceneManager), $"Could not get floor boundary {floor.Uuid}");
return false;
}
using var boundaryVertices = new NativeArray(count, Allocator.Temp);
if (!OVRPlugin.GetSpaceBoundary2D(floor.Handle, boundaryVertices))
return false;
if (userPosition.y < floorPose.Position.y)
return false;
if (userPosition.y > ceilingPose.Position.y)
return false;
// Perform location check
var offsetWithFloor = userPosition - floorPose.Position.FromVector3f();
var userPositionInRoom = Quaternion.Inverse(floorPose.Orientation.FromQuatf()) * offsetWithFloor;
return PointInPolygon2D(boundaryVertices, userPositionInRoom);
}
#endregion
private void DestroyExistingAnchors()
{
// Remove all the scene entities in memory. Update with scene entities from new query.
using (new OVRObjectPool.ListScope(out var anchors))
{
OVRSceneAnchor.GetSceneAnchors(anchors);
foreach (var sceneAnchor in anchors)
{
Destroy(sceneAnchor.gameObject);
}
}
#pragma warning disable CS0618 // Type or member is obsolete
RoomLayout = null;
#pragma warning restore CS0618
}
///
/// Requests scene capture from the Room Setup.
///
/// Returns true if scene capture succeeded, otherwise false.
public bool RequestSceneCapture()
{
#if !UNITY_EDITOR
bool result = OVRPlugin.RequestSceneCapture(out _sceneCaptureRequestId);
if (!result)
{
UnexpectedErrorWithSceneCapture?.Invoke();
}
// When a scene capture has been successfuly requested, silent fall through as it does not imply a successful scene capture
return result;
#else
Development.LogWarning(nameof(OVRSceneManager),
"Scene Capture does not work over Link.\n"
+ "Please capture a scene with the HMD in standalone mode, then access the scene model over Link.");
UnexpectedErrorWithSceneCapture?.Invoke();
return false;
#endif
}
///
/// Check if a room setup exists with specified anchors classifications.
///
/// Anchors classifications to check.
/// OVRTask that gives a boolean answer if the room setup exists upon completion.
public OVRTask DoesRoomSetupExist(IEnumerable requestedAnchorClassifications)
{
var task = OVRTask.FromGuid(Guid.NewGuid());
CheckIfClassificationsAreValid(requestedAnchorClassifications);
using (new OVRObjectPool.ListScope(out var roomAnchors))
{
var roomsTask = FetchAnchorsAsync(roomAnchors);
roomsTask.ContinueWith((result, anchors) => CheckClassificationsInRooms(result, anchors, requestedAnchorClassifications, task), roomAnchors);
}
return task;
}
private static void CheckIfClassificationsAreValid(IEnumerable requestedAnchorClassifications)
{
if (requestedAnchorClassifications == null)
{
throw new ArgumentNullException(nameof(requestedAnchorClassifications));
}
foreach (var classification in requestedAnchorClassifications)
{
if (!Classification.Set.Contains(classification))
{
throw new ArgumentException(
$"{nameof(requestedAnchorClassifications)} contains invalid anchor {nameof(Classification)} {classification}.");
}
}
}
private static void GetUuidsToQuery(OVRAnchor anchor, HashSet uuidsToQuery)
{
if (anchor.TryGetComponent(out var container))
{
foreach (var uuid in container.Uuids)
{
uuidsToQuery.Add(uuid);
}
}
}
private static void CheckClassificationsInRooms(bool success, List rooms, IEnumerable requestedAnchorClassifications, OVRTask task)
{
if (!success)
{
Development.Log(nameof(OVRSceneManager),
$"{nameof(OVRAnchor.FetchAnchorsAsync)} failed on {nameof(DoesRoomSetupExist)}() request to fetch room anchors.");
return;
}
using (new OVRObjectPool.HashSetScope(out var uuidsToQuery))
using (new OVRObjectPool.ListScope(out var anchorUuids))
{
for (int i = 0; i < rooms.Count; i++)
{
GetUuidsToQuery(rooms[i], uuidsToQuery);
anchorUuids.AddRange(uuidsToQuery);
uuidsToQuery.Clear();
}
using (new OVRObjectPool.ListScope(out var roomAnchors))
{
FetchAnchorsAsync(anchorUuids, roomAnchors)
.ContinueWith(result => CheckIfAnchorsContainClassifications(result, roomAnchors, requestedAnchorClassifications, task));
}
}
}
private static void CheckIfAnchorsContainClassifications(bool success, List roomAnchors, IEnumerable requestedAnchorClassifications, OVRTask task)
{
if (!success)
{
Development.Log(nameof(OVRSceneManager),
$"{nameof(OVRAnchor.FetchAnchorsAsync)} failed on {nameof(DoesRoomSetupExist)}() request to fetch anchors in rooms.");
return;
}
using (new OVRObjectPool.ListScope(out var labels))
{
CollectLabelsFromAnchors(roomAnchors, labels);
foreach (var classification in requestedAnchorClassifications)
{
var labelIndex = labels.IndexOf(classification);
if (labelIndex >= 0)
{
labels.RemoveAt(labelIndex);
}
else
{
task.SetResult(false);
return;
}
}
}
task.SetResult(true);
}
private static void CollectLabelsFromAnchors(List anchors, List labels)
{
for (int i = 0; i < anchors.Count; i++)
{
var anchor = anchors[i];
if (anchor.TryGetComponent(out var classification))
{
labels.AddRange(classification.Labels.Split(OVRSemanticClassification.LabelSeparator));
}
}
}
private static void OnTrackingSpaceChanged(Transform trackingSpace)
{
// Tracking space changed, update all scene anchors using their cache
UpdateAllSceneAnchors();
}
private void Update()
{
UpdateSomeSceneAnchors();
}
private static void UpdateAllSceneAnchors()
{
foreach (var sceneAnchor in OVRSceneAnchor.SceneAnchors.Values)
{
sceneAnchor.TryUpdateTransform(useCache: true);
if (sceneAnchor.TryGetComponent(out OVRScenePlane plane))
{
plane.UpdateTransform();
plane.RequestBoundary();
}
if (sceneAnchor.TryGetComponent(out OVRSceneVolume volume))
{
volume.UpdateTransform();
}
}
}
private void UpdateSomeSceneAnchors()
{
for (var i = 0; i < Math.Min(OVRSceneAnchor.SceneAnchorsList.Count, MaxSceneAnchorUpdatesPerFrame); i++)
{
_sceneAnchorUpdateIndex %= OVRSceneAnchor.SceneAnchorsList.Count;
var anchor = OVRSceneAnchor.SceneAnchorsList[_sceneAnchorUpdateIndex++];
anchor.TryUpdateTransform(useCache: false);
}
}
#pragma warning disable CS0618 // Type or member is obsolete
private RoomLayoutInformation GetRoomLayoutInformation()
{
var roomLayout = new RoomLayoutInformation();
#pragma warning restore CS0618 // Type or member is obsolete
if (OVRSceneRoom.SceneRoomsList.Count > 0)
{
roomLayout.Floor = OVRSceneRoom.SceneRoomsList[0].Floor;
roomLayout.Ceiling = OVRSceneRoom.SceneRoomsList[0].Ceiling;
roomLayout.Walls.Clear();
roomLayout.Walls.AddRange(OVRSceneRoom.SceneRoomsList[0].Walls);
}
return roomLayout;
}
private void OnEnable()
{
// Bind events
OVRManager.SceneCaptureComplete += OVRManager_SceneCaptureComplete;
if (OVRManager.display != null)
{
OVRManager.display.RecenteredPose += UpdateAllSceneAnchors;
}
if (!_cameraRig)
{
_cameraRig = FindAnyObjectByType();
}
if (_cameraRig)
{
_cameraRig.TrackingSpaceChanged += OnTrackingSpaceChanged;
}
}
private void OnDisable()
{
// Unbind events
OVRManager.SceneCaptureComplete -= OVRManager_SceneCaptureComplete;
if (OVRManager.display != null)
{
OVRManager.display.RecenteredPose -= UpdateAllSceneAnchors;
}
if (_cameraRig)
{
_cameraRig.TrackingSpaceChanged -= OnTrackingSpaceChanged;
}
}
///
/// Determines if a point is inside of a 2d polygon.
///
/// The vertices that make up the bounds of the polygon
/// The target point to test
/// True if the point is inside the polygon, false otherwise
internal static bool PointInPolygon2D(NativeArray boundaryVertices, Vector2 target)
{
if (boundaryVertices.Length < 3)
return false;
int collision = 0;
var x = target.x;
var y = target.y;
for (int i = 0; i < boundaryVertices.Length; i++)
{
var x1 = boundaryVertices[i].x;
var y1 = boundaryVertices[i].y;
var x2 = boundaryVertices[(i + 1) % boundaryVertices.Length].x;
var y2 = boundaryVertices[(i + 1) % boundaryVertices.Length].y;
if (y < y1 != y < y2 &&
x < x1 + ((y - y1) / (y2 - y1)) * (x2 - x1))
{
collision += (y1 < y2) ? 1 : -1;
}
}
return collision != 0;
}
#region Action callbacks
private void OVRManager_SceneCaptureComplete(UInt64 requestId, bool result)
{
if (requestId != _sceneCaptureRequestId)
{
Verbose?.LogWarning(nameof(OVRSceneManager),
$"Scene Room Setup 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 Setup returned without errors. Invoking {nameof(SceneCaptureReturnedWithoutError)}.");
SceneCaptureReturnedWithoutError?.Invoke();
}
else
{
Development.LogError(nameof(OVRSceneManager),
$"An error occurred when sending the user to the Room Setup. Invoking {nameof(UnexpectedErrorWithSceneCapture)}.");
UnexpectedErrorWithSceneCapture?.Invoke();
}
}
internal OVRSceneAnchor InstantiateSceneAnchor(OVRAnchor anchor, OVRSceneAnchor prefab)
{
var space = (OVRSpace)anchor.Handle;
var uuid = anchor.Uuid;
// 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.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, _initialAnchorParent);
sceneAnchor.gameObject.SetActive(true);
sceneAnchor.Initialize(anchor);
return sceneAnchor;
}
#endregion
}