464 lines
17 KiB
C#
464 lines
17 KiB
C#
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
using Microsoft.MixedReality.Toolkit.SpatialAwareness;
|
|
using Microsoft.MixedReality.Toolkit.Utilities;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using UnityEngine;
|
|
|
|
#if PLANE_FINDING_PRESENT && (UNITY_WSA || UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN)
|
|
using Microsoft.MixedReality.Toolkit.SpatialAwareness.Processing;
|
|
using System;
|
|
#endif
|
|
|
|
namespace Microsoft.MixedReality.Toolkit.Experimental.SpatialAwareness
|
|
{
|
|
/// <summary>
|
|
/// SurfaceMeshesToPlanes will find and create planes based on the meshes by a spatial awareness mesh observer.
|
|
/// </summary>
|
|
public class SurfaceMeshesToPlanes : MonoBehaviour
|
|
{
|
|
#region Public properties
|
|
|
|
[SerializeField]
|
|
[Tooltip("Empty game object used to contain all planes created by the SurfaceToPlanes class")]
|
|
private GameObject planesParent;
|
|
|
|
/// <summary>
|
|
/// Empty game object used to contain all planes created by the SurfaceToPlanes class
|
|
/// </summary>
|
|
public GameObject PlanesParent
|
|
{
|
|
get => planesParent;
|
|
set => planesParent = value;
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("The physics layer for planes to be set to")]
|
|
private int physicsLayer;
|
|
|
|
/// <summary>
|
|
/// The physics layer for planes to be set to
|
|
/// </summary>
|
|
public int PhysicsLayer
|
|
{
|
|
get => physicsLayer;
|
|
set => physicsLayer = value;
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("Material used to render planes")]
|
|
private Material defaultMaterial;
|
|
|
|
/// <summary>
|
|
/// Material used to render planes
|
|
/// </summary>
|
|
public Material DefaultMaterial
|
|
{
|
|
get => defaultMaterial;
|
|
set => defaultMaterial = value;
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("Minimum area required for a plane to be created.")]
|
|
private float minArea = 0.025f;
|
|
|
|
/// <summary>
|
|
/// Minimum area required for a plane to be created.
|
|
/// </summary>
|
|
public float MinArea
|
|
{
|
|
get => minArea;
|
|
set => minArea = value;
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("Threshold for acceptable normals (the closer to 1, the stricter the standard). Used when determining plane type.")]
|
|
[Range(0.0f, 1.0f)]
|
|
private float upNormalThreshold = 0.9f;
|
|
|
|
/// <summary>
|
|
/// Threshold for acceptable normals (the closer to 1, the stricter the standard). Used when determining plane type.
|
|
/// </summary>
|
|
public float UpNormalThreshold
|
|
{
|
|
get => upNormalThreshold;
|
|
set => upNormalThreshold = value;
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("Buffer to use when determining if a horizontal plane near the floor should be considered part of the floor.")]
|
|
[Range(0.0f, 1.0f)]
|
|
private float floorBuffer = 0.1f;
|
|
|
|
/// <summary>
|
|
/// Buffer to use when determining if a horizontal plane near the floor should be considered part of the floor.
|
|
/// </summary>
|
|
public float FloorBuffer
|
|
{
|
|
get => floorBuffer;
|
|
set => floorBuffer = value;
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("Buffer to use when determining if a horizontal plane near the ceiling should be considered part of the ceiling.")]
|
|
[Range(0.0f, 1.0f)]
|
|
private float ceilingBuffer = 0.1f;
|
|
|
|
/// <summary>
|
|
/// Buffer to use when determining if a horizontal plane near the ceiling should be considered part of the ceiling.
|
|
/// </summary>
|
|
public float CeilingBuffer
|
|
{
|
|
get => ceilingBuffer;
|
|
set => ceilingBuffer = value;
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("Thickness of rendered plane objects")]
|
|
[Range(0.0f, 1.0f)]
|
|
private float planeThickness = 0.01f;
|
|
|
|
/// <summary>
|
|
/// Thickness of rendered plane objects
|
|
/// </summary>
|
|
public float PlaneThickness
|
|
{
|
|
get => planeThickness;
|
|
set => planeThickness = value;
|
|
}
|
|
|
|
[HideInInspector]
|
|
private SpatialAwarenessSurfaceTypes drawPlanesMask =
|
|
(SpatialAwarenessSurfaceTypes.Wall | SpatialAwarenessSurfaceTypes.Floor | SpatialAwarenessSurfaceTypes.Ceiling | SpatialAwarenessSurfaceTypes.Platform);
|
|
|
|
/// <summary>
|
|
/// Determines which plane types should be rendered
|
|
/// </summary>
|
|
public SpatialAwarenessSurfaceTypes DrawPlanesMask
|
|
{
|
|
get => drawPlanesMask;
|
|
set => drawPlanesMask = value;
|
|
}
|
|
|
|
[HideInInspector]
|
|
private SpatialAwarenessSurfaceTypes destroyPlanesMask = SpatialAwarenessSurfaceTypes.Unknown;
|
|
|
|
/// <summary>
|
|
/// Determines which plane types should be discarded.
|
|
/// Use this when the spatial mapping mesh is a better fit for the surface (ex: round tables).
|
|
/// </summary>
|
|
public SpatialAwarenessSurfaceTypes DestroyPlanesMask
|
|
{
|
|
get => destroyPlanesMask;
|
|
set => destroyPlanesMask = value;
|
|
}
|
|
|
|
#if PLANE_FINDING_PRESENT && (UNITY_WSA || UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN)
|
|
/// <summary>
|
|
/// Delegate which is called when the MakePlanesCompleted event is triggered.
|
|
/// </summary>
|
|
public delegate void EventHandler(object source, EventArgs args);
|
|
|
|
/// <summary>
|
|
/// EventHandler which is triggered when the MakePlanesRoutine is finished.
|
|
/// </summary>
|
|
public event EventHandler MakePlanesComplete;
|
|
#endif // PLANE_FINDING_PRESENT && (UNITY_WSA || UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN)
|
|
|
|
/// <summary>
|
|
/// Indicates whether or not the project contains the required components for SurfaceMeshesToPlanes
|
|
/// to enable plane creation.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// For SurfaceMeshesToPlanes to create planes, the Mixed Reality Toolkit Plane Finding package
|
|
/// must be imported.
|
|
/// </remarks>
|
|
public static bool CanCreatePlanes =>
|
|
#if PLANE_FINDING_PRESENT && (UNITY_WSA || UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN)
|
|
true;
|
|
#else
|
|
false;
|
|
#endif // PLANE_FINDING_PRESENT && (UNITY_WSA || UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN)
|
|
|
|
#endregion
|
|
|
|
#region Private Properties
|
|
|
|
private List<SpatialAwarenessPlanarObject> activePlanes;
|
|
private bool makingPlanes = false;
|
|
private CancellationTokenSource tokenSource;
|
|
|
|
#if PLANE_FINDING_PRESENT && (UNITY_WSA || UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN)
|
|
private float floorYPosition;
|
|
private float ceilingYPosition;
|
|
private const float SnapToGravityThreshold = 5.0f;
|
|
#endif // PLANE_FINDING_PRESENT && (UNITY_WSA || UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN)
|
|
|
|
#endregion
|
|
|
|
#region Public Methods
|
|
|
|
/// <summary>
|
|
/// Gets all active planes of the specified type(s).
|
|
/// </summary>
|
|
/// <param name="planeTypes">A flag which includes all plane type(s) that should be returned.</param>
|
|
/// <returns>A collection of planes that match the expected type(s).</returns>
|
|
public List<GameObject> GetActivePlanes(SpatialAwarenessSurfaceTypes planeTypes)
|
|
{
|
|
List<GameObject> typePlanes = new List<GameObject>();
|
|
|
|
foreach (SpatialAwarenessPlanarObject planes in activePlanes)
|
|
{
|
|
if ((planeTypes & planes.SurfaceType) == planes.SurfaceType)
|
|
{
|
|
typePlanes.Add(planes.GameObject);
|
|
}
|
|
}
|
|
|
|
return typePlanes;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs background task to create new planes based on data from mesh observers
|
|
/// </summary>
|
|
public void MakePlanes()
|
|
{
|
|
if (!makingPlanes)
|
|
{
|
|
makingPlanes = true;
|
|
tokenSource = new CancellationTokenSource();
|
|
_ = MakePlanes(tokenSource.Token).ConfigureAwait(true);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Private Methods
|
|
|
|
private void Start()
|
|
{
|
|
activePlanes = new List<SpatialAwarenessPlanarObject>();
|
|
|
|
if (planesParent == null)
|
|
{
|
|
planesParent = new GameObject("SurfaceMeshesToPlanes");
|
|
}
|
|
planesParent.transform.position = Vector3.zero;
|
|
planesParent.transform.rotation = Quaternion.identity;
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
Destroy(PlanesParent);
|
|
tokenSource?.Cancel();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Task to analyze surface meshes to find planes and create new 3D cubes to represent each plane.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Token that allows cancellation of an asynchronous process.</param>
|
|
/// <returns>Yield result.</returns>
|
|
private async Task MakePlanes(CancellationToken cancellationToken)
|
|
{
|
|
await new WaitForUpdate();
|
|
|
|
#if PLANE_FINDING_PRESENT && (UNITY_WSA || UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN)
|
|
List<PlaneFinding.MeshData> meshData = new List<PlaneFinding.MeshData>();
|
|
List<MeshFilter> filters = new List<MeshFilter>();
|
|
|
|
var spatialAwarenessSystem = CoreServices.SpatialAwarenessSystem;
|
|
if (spatialAwarenessSystem != null)
|
|
{
|
|
GameObject parentObject = spatialAwarenessSystem.SpatialAwarenessObjectParent;
|
|
|
|
// Get mesh filters from SpatialAwareness Mesh Observer
|
|
foreach (MeshFilter filter in parentObject.GetComponentsInChildren<MeshFilter>())
|
|
{
|
|
filters.Add(filter);
|
|
}
|
|
}
|
|
|
|
for (int index = 0; index < filters.Count; index++)
|
|
{
|
|
MeshFilter filter = filters[index];
|
|
if (filter != null && filter.sharedMesh != null)
|
|
{
|
|
// fix surface mesh normals so we can get correct plane orientation.
|
|
filter.mesh.RecalculateNormals();
|
|
meshData.Add(new PlaneFinding.MeshData(filter));
|
|
}
|
|
}
|
|
|
|
BoundedPlane[] planes;
|
|
|
|
await new WaitForBackgroundThread();
|
|
{
|
|
planes = PlaneFinding.FindPlanes(meshData, SnapToGravityThreshold, MinArea);
|
|
}
|
|
|
|
await new WaitForUpdate();
|
|
|
|
DestroyPreviousPlanes();
|
|
activePlanes.Clear();
|
|
ClassifyAndCreatePlanes(planes);
|
|
|
|
// We are done creating planes, trigger an event.
|
|
MakePlanesComplete?.Invoke(this, EventArgs.Empty);
|
|
#endif // PLANE_FINDING_PRESENT && (UNITY_WSA || UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN)
|
|
makingPlanes = false;
|
|
}
|
|
|
|
#if PLANE_FINDING_PRESENT && (UNITY_WSA || UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN)
|
|
/// <summary>
|
|
/// Create game objects to represent bounded planes in scene
|
|
/// </summary>
|
|
private void ClassifyAndCreatePlanes(BoundedPlane[] planes)
|
|
{
|
|
SetFloorAndCeilingPositions(planes);
|
|
|
|
// Create SurfacePlane objects to represent each plane found in the Spatial Mapping mesh.
|
|
for (int index = 0; index < planes.Length; index++)
|
|
{
|
|
BoundedPlane boundedPlane = planes[index];
|
|
|
|
Vector3 size = boundedPlane.Bounds.Extents * 2;
|
|
size.z = PlaneThickness;
|
|
|
|
var planeObject = SpatialAwarenessPlanarObject.CreateSpatialObject(
|
|
boundedPlane.Bounds.Center,
|
|
size,
|
|
boundedPlane.Bounds.Rotation,
|
|
PhysicsLayer,
|
|
$"SurfacePlane {index}",
|
|
index,
|
|
GetPlaneType(boundedPlane));
|
|
|
|
planeObject.GameObject.transform.parent = PlanesParent.transform;
|
|
if (DefaultMaterial != null)
|
|
{
|
|
planeObject.Renderer.material = DefaultMaterial;
|
|
}
|
|
SetPlaneVisibility(planeObject);
|
|
|
|
if ((destroyPlanesMask & planeObject.SurfaceType) == planeObject.SurfaceType)
|
|
{
|
|
DestroyImmediate(planeObject.GameObject);
|
|
}
|
|
else
|
|
{
|
|
activePlanes.Add(planeObject);
|
|
}
|
|
}
|
|
|
|
Debug.Log("Finished creating planes.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create game objects to represent bounded planes in scene
|
|
/// </summary>
|
|
private void SetFloorAndCeilingPositions(BoundedPlane[] planes)
|
|
{
|
|
floorYPosition = 0.0f;
|
|
ceilingYPosition = 0.0f;
|
|
float maxFloorArea = 0.0f;
|
|
float maxCeilingArea = 0.0f;
|
|
|
|
// Find the floor and ceiling.
|
|
// We classify the floor as the maximum horizontal surface below the user's head.
|
|
// We classify the ceiling as the maximum horizontal surface above the user's head.
|
|
for (int i = 0; i < planes.Length; i++)
|
|
{
|
|
BoundedPlane boundedPlane = planes[i];
|
|
if (boundedPlane.Bounds.Center.y < 0 && boundedPlane.Plane.normal.y >= UpNormalThreshold)
|
|
{
|
|
maxFloorArea = Mathf.Max(maxFloorArea, boundedPlane.Area);
|
|
if (maxFloorArea == boundedPlane.Area)
|
|
{
|
|
floorYPosition = boundedPlane.Bounds.Center.y;
|
|
}
|
|
}
|
|
else if (boundedPlane.Bounds.Center.y > 0 && boundedPlane.Plane.normal.y <= -(UpNormalThreshold))
|
|
{
|
|
maxCeilingArea = Mathf.Max(maxCeilingArea, boundedPlane.Area);
|
|
if (maxCeilingArea == boundedPlane.Area)
|
|
{
|
|
ceilingYPosition = boundedPlane.Bounds.Center.y;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Classifies the surface as a floor, wall, ceiling, table, etc.
|
|
/// </summary>
|
|
private SpatialAwarenessSurfaceTypes GetPlaneType(BoundedPlane plane)
|
|
{
|
|
SpatialAwarenessSurfaceTypes PlaneType;
|
|
var surfaceNormal = plane.Plane.normal;
|
|
|
|
// Determine what type of plane this is.
|
|
// Use the upNormalThreshold to help determine if we have a horizontal or vertical surface.
|
|
if (surfaceNormal.y >= UpNormalThreshold)
|
|
{
|
|
// If we have a horizontal surface with a normal pointing up, classify it as a floor.
|
|
PlaneType = SpatialAwarenessSurfaceTypes.Floor;
|
|
|
|
if (plane.Bounds.Center.y > (floorYPosition + FloorBuffer))
|
|
{
|
|
// If the plane is too high to be considered part of the floor, classify it as a table.
|
|
PlaneType = SpatialAwarenessSurfaceTypes.Platform;
|
|
}
|
|
}
|
|
else if (surfaceNormal.y <= -(UpNormalThreshold))
|
|
{
|
|
// If we have a horizontal surface with a normal pointing down, classify it as a ceiling.
|
|
PlaneType = SpatialAwarenessSurfaceTypes.Ceiling;
|
|
|
|
if (plane.Bounds.Center.y < (ceilingYPosition - CeilingBuffer))
|
|
{
|
|
// If the plane is not high enough to be considered part of the ceiling, classify it as a table.
|
|
PlaneType = SpatialAwarenessSurfaceTypes.Platform;
|
|
}
|
|
}
|
|
else if (Mathf.Abs(surfaceNormal.y) <= (1 - UpNormalThreshold))
|
|
{
|
|
// If the plane is vertical, then classify it as a wall.
|
|
PlaneType = SpatialAwarenessSurfaceTypes.Wall;
|
|
}
|
|
else
|
|
{
|
|
// The plane has a strange angle, classify it as 'unknown'.
|
|
PlaneType = SpatialAwarenessSurfaceTypes.Unknown;
|
|
}
|
|
|
|
return PlaneType;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Destroys all game objects under parent
|
|
/// </summary>
|
|
private void DestroyPreviousPlanes()
|
|
{
|
|
// Remove any previously existing planes, as they may no longer be valid.
|
|
for (int index = 0; index < activePlanes.Count; index++)
|
|
{
|
|
Destroy(activePlanes[index].GameObject);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets visibility of planes based on their type.
|
|
/// </summary>
|
|
private void SetPlaneVisibility(SpatialAwarenessPlanarObject plane)
|
|
{
|
|
plane.GameObject.SetActive((drawPlanesMask & plane.SurfaceType) == plane.SurfaceType);
|
|
}
|
|
#endif // PLANE_FINDING_PRESENT && (UNITY_WSA || UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN)
|
|
|
|
#endregion
|
|
}
|
|
}
|