mixedreality/com.microsoft.mixedreality..../Providers/WindowsMixedReality/XR2018/WindowsMixedRealitySpatialM...

733 lines
29 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.MixedReality.Toolkit.SpatialAwareness;
using Microsoft.MixedReality.Toolkit.Utilities;
using Microsoft.MixedReality.Toolkit.Windows.Utilities;
using System.Collections.Generic;
using Unity.Profiling;
using UnityEngine;
#if UNITY_WSA
using UnityEngine.XR;
using UnityEngine.XR.WSA;
#endif // UNITY_WSA
#if WINDOWS_UWP
using WindowsSpatialSurfaces = global::Windows.Perception.Spatial.Surfaces;
#endif // WINDOWS_UWP
namespace Microsoft.MixedReality.Toolkit.WindowsMixedReality.SpatialAwareness
{
[MixedRealityDataProvider(
typeof(IMixedRealitySpatialAwarenessSystem),
SupportedPlatforms.WindowsUniversal,
"Windows Mixed Reality Spatial Mesh Observer",
"Profiles/DefaultMixedRealitySpatialAwarenessMeshObserverProfile.asset",
"MixedRealityToolkit.SDK",
true,
SupportedUnityXRPipelines.LegacyXR)]
[HelpURL("https://docs.microsoft.com/windows/mixed-reality/mrtk-unity/features/spatial-awareness/spatial-awareness-getting-started")]
public class WindowsMixedRealitySpatialMeshObserver :
BaseSpatialMeshObserver,
IMixedRealityCapabilityCheck
{
/// <summary>
/// Constructor.
/// </summary>
/// <param name="registrar">The <see cref="IMixedRealityServiceRegistrar"/> instance that loaded the service.</param>
/// <param name="spatialAwarenessSystem">The service instance that receives data from this provider.</param>
/// <param name="name">Friendly name of the service.</param>
/// <param name="priority">Service priority. Used to determine order of instantiation.</param>
/// <param name="profile">The service's configuration profile.</param>
[System.Obsolete("This constructor is obsolete (registrar parameter is no longer required) and will be removed in a future version of the Microsoft Mixed Reality Toolkit.")]
public WindowsMixedRealitySpatialMeshObserver(
IMixedRealityServiceRegistrar registrar,
IMixedRealitySpatialAwarenessSystem spatialAwarenessSystem,
string name = null,
uint priority = DefaultPriority,
BaseMixedRealityProfile profile = null) : this(spatialAwarenessSystem, name, priority, profile)
{
Registrar = registrar;
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="spatialAwarenessSystem">The service instance that receives data from this provider.</param>
/// <param name="name">Friendly name of the service.</param>
/// <param name="priority">Service priority. Used to determine order of instantiation.</param>
/// <param name="profile">The service's configuration profile.</param>
public WindowsMixedRealitySpatialMeshObserver(
IMixedRealitySpatialAwarenessSystem spatialAwarenessSystem,
string name = null,
uint priority = DefaultPriority,
BaseMixedRealityProfile profile = null) : base(spatialAwarenessSystem, name, priority, profile)
{ }
#region BaseSpatialObserver Implementation
/// <summary>
/// Creates the surface observer and handles the desired startup behavior.
/// </summary>
protected override void CreateObserver()
{
if (Service == null) { return; }
#if UNITY_WSA
if (observer == null)
{
observer = new SurfaceObserver();
ConfigureObserverVolume();
if (StartupBehavior == AutoStartBehavior.AutoStart)
{
Resume();
}
}
#endif // UNITY_WSA
}
/// <summary>
/// Implements proper cleanup of the SurfaceObserver.
/// </summary>
protected override void CleanupObserver()
{
if (IsRunning)
{
Suspend();
}
#if UNITY_WSA
if (observer != null)
{
observer.Dispose();
observer = null;
}
#endif // UNITY_WSA
}
#endregion BaseSpatialObserver Implementation
#region BaseSpatialMeshObserver Implementation
/// <inheritdoc />
protected override int LookupTriangleDensity(SpatialAwarenessMeshLevelOfDetail levelOfDetail)
{
int triangleDensity;
switch (levelOfDetail)
{
case SpatialAwarenessMeshLevelOfDetail.Coarse:
triangleDensity = 0;
break;
case SpatialAwarenessMeshLevelOfDetail.Medium:
triangleDensity = 400;
break;
case SpatialAwarenessMeshLevelOfDetail.Fine:
case SpatialAwarenessMeshLevelOfDetail.Unlimited:
triangleDensity = 2000;
break;
default:
Debug.LogWarning($"There is no triangle density lookup for {levelOfDetail}, defaulting to Coarse");
triangleDensity = 0;
break;
}
return triangleDensity;
}
#endregion BaseSpatialMeshObserver Implementation
#region IMixedRealityCapabilityCheck Implementation
/// <inheritdoc />
public bool CheckCapability(MixedRealityCapability capability)
{
if (WindowsApiChecker.IsMethodAvailable(
"Windows.Perception.Spatial.Surfaces",
"SpatialSurfaceObserver",
"IsSupported"))
{
#if WINDOWS_UWP
return (capability == MixedRealityCapability.SpatialAwarenessMesh) && WindowsSpatialSurfaces.SpatialSurfaceObserver.IsSupported();
#endif // WINDOWS_UWP
}
return false;
}
#endregion IMixedRealityCapabilityCheck Implementation
#region IMixedRealityDataProvider Implementation
#if UNITY_WSA
/// <summary>
/// Creates and configures the spatial observer, as well as
/// setting the required SpatialPerception capability.
/// </summary>
public override void Initialize()
{
base.Initialize();
#if UNITY_EDITOR && UNITY_WSA
Utilities.Editor.UWPCapabilityUtility.RequireCapability(
UnityEditor.PlayerSettings.WSACapability.SpatialPerception,
this.GetType());
#endif
// If we aren't using a HoloLens or there isn't an XR device present, return.
if (observer == null || HolographicSettings.IsDisplayOpaque || !XRDevice.isPresent) { return; }
if (RuntimeSpatialMeshPrefab != null)
{
AddRuntimeSpatialMeshPrefabToHierarchy();
}
}
private static readonly ProfilerMarker UpdatePerfMarker = new ProfilerMarker("[MRTK] WindowsMixedRealitySpatialMeshObserver.Update");
/// <inheritdoc />
public override void Update()
{
using (UpdatePerfMarker.Auto())
{
base.Update();
UpdateObserver();
}
}
#endif // UNITY_WSA
#endregion IMixedRealityDataProvider Implementation
#region IMixedRealitySpatialAwarenessObserver Implementation
#if UNITY_WSA
/// <summary>
/// The surface observer providing the spatial data.
/// </summary>
private SurfaceObserver observer = null;
/// <summary>
/// A queue of <see cref="SurfaceId"/> that need their meshes created (or updated).
/// </summary>
private readonly Queue<SurfaceId> meshWorkQueue = new Queue<SurfaceId>();
/// <summary>
/// To prevent too many meshes from being generated at the same time, we will
/// only request one mesh to be created at a time. This variable will track
/// if a mesh creation request is in flight.
/// </summary>
private SpatialAwarenessMeshObject outstandingMeshObject = null;
/// <summary>
/// When surfaces are replaced or removed, rather than destroying them, we'll keep
/// one as a spare for use in outstanding mesh requests. That way, we'll have fewer
/// game object create/destroy cycles, which should help performance.
/// </summary>
protected SpatialAwarenessMeshObject spareMeshObject = null;
/// <summary>
/// The time at which the surface observer was last asked for updated data.
/// </summary>
private float lastUpdated = 0;
#endif // UNITY_WSA
#if UNITY_WSA
private static readonly ProfilerMarker ResumePerfMarker = new ProfilerMarker("[MRTK] WindowsMixedRealitySpatialMeshObserver.Resume");
#endif // UNITY_WSA
/// <inheritdoc/>
public override void Resume()
{
#if UNITY_WSA
if (IsRunning)
{
Debug.LogWarning("The Windows Mixed Reality spatial observer is currently running.");
return;
}
using (ResumePerfMarker.Auto())
{
// We want the first update immediately.
lastUpdated = 0;
// UpdateObserver keys off of this value to start observing.
IsRunning = true;
}
#endif // UNITY_WSA
}
#if UNITY_WSA
private static readonly ProfilerMarker SuspendPerfMarker = new ProfilerMarker("[MRTK] WindowsMixedRealitySpatialMeshObserver.Suspend");
#endif // UNITY_WSA
/// <inheritdoc/>
public override void Suspend()
{
#if UNITY_WSA
if (!IsRunning)
{
Debug.LogWarning("The Windows Mixed Reality spatial observer is currently stopped.");
return;
}
using (SuspendPerfMarker.Auto())
{
// UpdateObserver keys off of this value to stop observing.
IsRunning = false;
// Halt any outstanding work.
if (outstandingMeshObject != null)
{
ReclaimMeshObject(outstandingMeshObject);
outstandingMeshObject = null;
}
// Clear any pending work.
meshWorkQueue.Clear();
}
#endif // UNITY_WSA
}
#if UNITY_WSA
private static readonly ProfilerMarker ClearObservationsPerfMarker = new ProfilerMarker("[MRTK] WindowsMixedRealitySpatialMeshObserver.ClearObservations");
#endif // UNITY_WSA
#if UNITY_WSA
/// <inheritdoc />
public override void ClearObservations()
{
using (ClearObservationsPerfMarker.Auto())
{
bool wasRunning = false;
if (IsRunning)
{
wasRunning = true;
Debug.Log("Cannot clear observations while the observer is running. Suspending this observer.");
Suspend();
}
IReadOnlyList<int> observations = new List<int>(Meshes.Keys);
foreach (int meshId in observations)
{
RemoveMeshObject(meshId);
}
// Cleanup the outstanding mesh object.
if (outstandingMeshObject != null)
{
// Destroy the game object, destroy the meshes.
SpatialAwarenessMeshObject.Cleanup(outstandingMeshObject);
outstandingMeshObject = null;
}
// Cleanup the spare mesh object
if (spareMeshObject != null)
{
// Destroy the game object, destroy the meshes.
SpatialAwarenessMeshObject.Cleanup(spareMeshObject);
spareMeshObject = null;
}
if (wasRunning)
{
Resume();
}
}
}
#endif // UNITY_WSA
#endregion IMixedRealitySpatialAwarenessObserver Implementation
#region Helpers
#if UNITY_WSA
private static readonly ProfilerMarker UpdateObserverPerfMarker = new ProfilerMarker("[MRTK] WindowsMixedRealitySpatialMeshObserver.UpdateObserver");
/// <summary>
/// Requests updates from the surface observer.
/// </summary>
private void UpdateObserver()
{
if (Service == null || HolographicSettings.IsDisplayOpaque || !XRDevice.isPresent) { return; }
using (UpdateObserverPerfMarker.Auto())
{
// Only update the observer if it is running.
if (IsRunning && (outstandingMeshObject == null))
{
// If we have a mesh to work on...
if (meshWorkQueue.Count > 0)
{
// We're using a simple first-in-first-out rule for requesting meshes, but a more sophisticated algorithm could prioritize
// the queue based on distance to the user or some other metric.
RequestMesh(meshWorkQueue.Dequeue());
}
// If enough time has passed since the previous observer update...
else if (Time.time - lastUpdated >= UpdateInterval)
{
// Update the observer orientation if user aligned
if (ObserverVolumeType == VolumeType.UserAlignedCube)
{
ObserverRotation = CameraCache.Main.transform.rotation;
}
// Update the observer location if it is not stationary
if (!IsStationaryObserver)
{
ObserverOrigin = CameraCache.Main.transform.position;
}
// The application can update the observer volume at any time, make sure we are using the latest.
ConfigureObserverVolume();
observer.Update(SurfaceObserver_OnSurfaceChanged);
lastUpdated = Time.time;
}
}
}
}
/// <summary>
/// Internal component to monitor the WorldAnchor's transform, apply the MixedRealityPlayspace transform,
/// and apply it to its parent.
/// </summary>
private class PlayspaceAdapter : MonoBehaviour
{
/// <summary>
/// Compute concatenation of lhs * rhs such that lhs * (rhs * v) = Concat(lhs, rhs) * v
/// </summary>
/// <param name="lhs">Second transform to apply</param>
/// <param name="rhs">First transform to apply</param>
private static Pose Concatenate(Pose lhs, Pose rhs)
{
return rhs.GetTransformedBy(lhs);
}
private static readonly ProfilerMarker UpdatePerfMarker = new ProfilerMarker("[MRTK] WindowsMixedRealitySpatialMeshObserver+PlayspaceAdapter.Update");
/// <summary>
/// Compute and set the parent's transform.
/// </summary>
private void Update()
{
using (UpdatePerfMarker.Auto())
{
Pose worldFromPlayspace = new Pose(MixedRealityPlayspace.Position, MixedRealityPlayspace.Rotation);
Pose anchorPose = new Pose(transform.position, transform.rotation);
/// Propagate any global scale on the playspace into the position.
Vector3 playspaceScale = MixedRealityPlayspace.Transform.lossyScale;
anchorPose.position *= playspaceScale.x;
Pose parentPose = Concatenate(worldFromPlayspace, anchorPose);
transform.parent.position = parentPose.position;
transform.parent.rotation = parentPose.rotation;
}
}
}
private static readonly ProfilerMarker RequestMeshPerfMarker = new ProfilerMarker("[MRTK] WindowsMixedRealitySpatialMeshObserver.RequestMesh");
/// <summary>
/// Issue a request to the Surface Observer to begin baking the mesh.
/// </summary>
/// <param name="surfaceId">ID of the mesh to bake.</param>
private void RequestMesh(SurfaceId surfaceId)
{
using (RequestMeshPerfMarker.Auto())
{
string meshName = ("SpatialMesh - " + surfaceId.handle);
SpatialAwarenessMeshObject newMesh;
WorldAnchor worldAnchor;
if (spareMeshObject == null)
{
newMesh = SpatialAwarenessMeshObject.Create(
null,
MeshPhysicsLayer,
meshName,
surfaceId.handle,
ObservedObjectParent);
// The WorldAnchor component places its object where the anchor is in the same space as the camera.
// But since the camera is repositioned by the MixedRealityPlayspace's transform, the meshes' transforms
// should also the WorldAnchor position repositioned by the MixedRealityPlayspace's transform.
// So rather than put the WorldAnchor on the mesh's GameObject, the WorldAnchor is placed out of the way in the scene,
// and its transform is concatenated with the Playspace transform to compute the transform on the mesh's object.
// That adapting the WorldAnchor's transform into playspace is done by the internal PlayspaceAdapter component.
// The GameObject the WorldAnchor is placed on is unimportant, but it is convenient for cleanup to make it a child
// of the GameObject whose transform will track it.
GameObject anchorHolder = new GameObject(meshName + "_anchor");
anchorHolder.AddComponent<PlayspaceAdapter>(); // replace with required component?
worldAnchor = anchorHolder.AddComponent<WorldAnchor>(); // replace with required component and GetComponent()?
anchorHolder.transform.SetParent(newMesh.GameObject.transform, false);
}
else
{
newMesh = spareMeshObject;
spareMeshObject = null;
newMesh.GameObject.name = meshName;
newMesh.Id = surfaceId.handle;
newMesh.GameObject.SetActive(true);
// There should be exactly one child on the newMesh.GameObject, and that is the GameObject added above
// to hold the WorldAnchor component and adapter.
Debug.Assert(newMesh.GameObject.transform.childCount == 1, "Expecting a single child holding the WorldAnchor");
worldAnchor = newMesh.GameObject.transform.GetChild(0).gameObject.GetComponent<WorldAnchor>();
}
Debug.Assert(worldAnchor != null);
SurfaceData surfaceData = new SurfaceData(
surfaceId,
newMesh.Filter,
worldAnchor,
newMesh.Collider,
TrianglesPerCubicMeter,
true);
if (observer.RequestMeshAsync(surfaceData, SurfaceObserver_OnDataReady))
{
outstandingMeshObject = newMesh;
}
else
{
Debug.LogError($"Mesh request failed for Id == surfaceId.handle");
outstandingMeshObject = null;
ReclaimMeshObject(newMesh);
}
}
}
private static readonly ProfilerMarker RemoveMeshObjectPerfMarker = new ProfilerMarker("[MRTK] WindowsMixedRealitySpatialMeshObserver.RemoveMeshObject");
/// <summary>
/// Removes the <see cref="SpatialAwarenessMeshObject"/> associated with the specified id.
/// </summary>
/// <param name="id">The id of the mesh to be removed.</param>
protected void RemoveMeshObject(int id)
{
using (RemoveMeshObjectPerfMarker.Auto())
{
SpatialAwarenessMeshObject mesh;
if (meshes.TryGetValue(id, out mesh))
{
// Remove the mesh object from the collection.
meshes.Remove(id);
// Reclaim the mesh object for future use.
ReclaimMeshObject(mesh);
// Send the mesh removed event
meshEventData.Initialize(this, id, null);
Service?.HandleEvent(meshEventData, OnMeshRemoved);
}
}
}
private static readonly ProfilerMarker ReclaimMeshObjectPerfMarker = new ProfilerMarker("[MRTK] WindowsMixedRealitySpatialMeshObserver.ReclaimMeshObject");
/// <summary>
/// Reclaims the <see cref="SpatialAwarenessMeshObject"/> to allow for later reuse.
/// </summary>
protected void ReclaimMeshObject(SpatialAwarenessMeshObject availableMeshObject)
{
using (ReclaimMeshObjectPerfMarker.Auto())
{
if (spareMeshObject == null)
{
// Cleanup the mesh object.
// Do not destroy the game object, destroy the meshes.
SpatialAwarenessMeshObject.Cleanup(availableMeshObject, false);
if (availableMeshObject.GameObject != null)
{
availableMeshObject.GameObject.name = "Unused Spatial Mesh";
availableMeshObject.GameObject.SetActive(false);
}
spareMeshObject = availableMeshObject;
}
else
{
// Cleanup the mesh object.
// Destroy the game object, destroy the meshes.
SpatialAwarenessMeshObject.Cleanup(availableMeshObject);
}
}
}
private static readonly ProfilerMarker ConfigureObserverVolumePerfMarker = new ProfilerMarker("[MRTK] WindowsMixedRealitySpatialMeshObserver.ConfigureObserverVolume");
/// <summary>
/// Applies the configured observation extents.
/// </summary>
private void ConfigureObserverVolume()
{
using (ConfigureObserverVolumePerfMarker.Auto())
{
if (MixedRealityPlayspace.Transform == null)
{
Debug.LogError("Unexpected failure acquiring MixedRealityPlayspace.");
return;
}
// If we aren't using a HoloLens or there isn't an XR device present, return.
if (observer == null || HolographicSettings.IsDisplayOpaque || !XRDevice.isPresent) { return; }
// The observer's origin is in world space, we need it in the camera's parent's space
// to set the volume. The MixedRealityPlayspace provides that space that the camera/head moves around in.
Vector3 observerOriginPlayspace = MixedRealityPlayspace.InverseTransformPoint(ObserverOrigin);
Quaternion observerRotationPlayspace = Quaternion.Inverse(MixedRealityPlayspace.Rotation) * ObserverRotation;
// Update the observer
switch (ObserverVolumeType)
{
case VolumeType.AxisAlignedCube:
observer.SetVolumeAsAxisAlignedBox(observerOriginPlayspace, ObservationExtents);
break;
case VolumeType.Sphere:
// We use the x value of the extents as the sphere radius
observer.SetVolumeAsSphere(observerOriginPlayspace, ObservationExtents.x);
break;
case VolumeType.UserAlignedCube:
observer.SetVolumeAsOrientedBox(observerOriginPlayspace, ObservationExtents, observerRotationPlayspace);
break;
default:
Debug.LogError($"Unsupported ObserverVolumeType value {ObserverVolumeType}");
break;
}
}
}
private static readonly ProfilerMarker OnSurfaceChangedPerfMarker = new ProfilerMarker("[MRTK] WindowsMixedRealitySpatialMeshObserver.SurfaceObserver_OnSurfaceChanged");
/// <summary>
/// Handles the SurfaceObserver's OnSurfaceChanged event.
/// </summary>
/// <param name="id">The identifier assigned to the surface which has changed.</param>
/// <param name="changeType">The type of change that occurred on the surface.</param>
/// <param name="bounds">The bounds of the surface.</param>
/// <param name="updateTime">The date and time at which the change occurred.</param>
private void SurfaceObserver_OnSurfaceChanged(SurfaceId id, SurfaceChange changeType, Bounds bounds, System.DateTime updateTime)
{
if (!IsRunning) { return; }
using (OnSurfaceChangedPerfMarker.Auto())
{
switch (changeType)
{
case SurfaceChange.Added:
case SurfaceChange.Updated:
meshWorkQueue.Enqueue(id);
break;
case SurfaceChange.Removed:
RemoveMeshObject(id.handle);
break;
}
}
}
private static readonly ProfilerMarker OnDataReadyPerfMarker = new ProfilerMarker("[MRTK] WindowsMixedRealitySpatialMeshObserver.SurfaceObserver_OnDataReady");
/// <summary>
/// Handles the SurfaceObserver's OnDataReady event.
/// </summary>
/// <param name="cookedData">Struct containing output data.</param>
/// <param name="outputWritten">Set to true if output has been written.</param>
/// <param name="elapsedCookTimeSeconds">Seconds between mesh cook request and propagation of this event.</param>
private void SurfaceObserver_OnDataReady(SurfaceData cookedData, bool outputWritten, float elapsedCookTimeSeconds)
{
if (!IsRunning) { return; }
using (OnDataReadyPerfMarker.Auto())
{
if (outstandingMeshObject == null)
{
return;
}
if (!outputWritten)
{
ReclaimMeshObject(outstandingMeshObject);
outstandingMeshObject = null;
return;
}
// Since there is only one outstanding mesh object, update the id to match
// the one received after baking.
SpatialAwarenessMeshObject meshObject = outstandingMeshObject;
meshObject.Id = cookedData.id.handle;
outstandingMeshObject = null;
// Check to see if this is a new or updated mesh.
bool isMeshUpdate = meshes.ContainsKey(meshObject.Id);
// We presume that if the display option is not occlusion, that we should
// default to the visible material.
// Note: We check explicitly for a display option of none later in this method.
Material material = (DisplayOption == SpatialAwarenessMeshDisplayOptions.Occlusion) ?
OcclusionMaterial : VisibleMaterial;
// If this is a mesh update, we want to preserve the mesh's previous material.
material = isMeshUpdate ? meshes[meshObject.Id].Renderer.sharedMaterial : material;
// Apply the appropriate material.
meshObject.Renderer.sharedMaterial = material;
// Recalculate the mesh normals if requested.
if (RecalculateNormals)
{
meshObject.Filter.sharedMesh.RecalculateNormals();
}
// Check to see if the display option is set to none. If so, we disable
// the renderer.
meshObject.Renderer.enabled = (DisplayOption != SpatialAwarenessMeshDisplayOptions.None);
// Set the physics material
if (meshObject.Renderer.enabled)
{
meshObject.Collider.material = PhysicsMaterial;
}
// Add / update the mesh to our collection
if (isMeshUpdate)
{
// Reclaim the old mesh object for future use.
ReclaimMeshObject(meshes[meshObject.Id]);
meshes.Remove(meshObject.Id);
}
meshes.Add(meshObject.Id, meshObject);
// Preserve local transform relative to parent.
meshObject.GameObject.transform.SetParent(ObservedObjectParent != null ?
ObservedObjectParent.transform : null, false);
meshEventData.Initialize(this, meshObject.Id, meshObject);
if (isMeshUpdate)
{
Service?.HandleEvent(meshEventData, OnMeshUpdated);
}
else
{
Service?.HandleEvent(meshEventData, OnMeshAdded);
}
}
}
#endif // UNITY_WSA
#endregion Helpers
}
}