mixedreality/com.microsoft.mixedreality..../SDK/Experimental/SpatialAwareness/SurfaceMeshesToPlanes.cs

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
}
}