Permalink
Fetching contributors…
Cannot retrieve contributors at this time
308 lines (262 sloc) 12.4 KB
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if !UNITY_EDITOR && UNITY_WSA
using System.Threading;
using System.Threading.Tasks;
#endif
namespace HoloToolkit.Unity.SpatialMapping
{
/// <summary>
/// SurfaceMeshesToPlanes will find and create planes based on the meshes returned by the SpatialMappingManager's Observer.
/// </summary>
public class SurfaceMeshesToPlanes : Singleton<SurfaceMeshesToPlanes>
{
[Tooltip("Currently active planes found within the Spatial Mapping Mesh.")]
public List<GameObject> ActivePlanes;
[Tooltip("Object used for creating and rendering Surface Planes.")]
public GameObject SurfacePlanePrefab;
[Tooltip("Minimum area required for a plane to be created.")]
public float MinArea = 0.025f;
/// <summary>
/// Determines which plane types should be rendered.
/// </summary>
[HideInInspector]
public PlaneTypes drawPlanesMask =
(PlaneTypes.Wall | PlaneTypes.Floor | PlaneTypes.Ceiling | PlaneTypes.Table);
/// <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>
[HideInInspector]
public PlaneTypes destroyPlanesMask = PlaneTypes.Unknown;
/// <summary>
/// Floor y value, which corresponds to the maximum horizontal area found below the user's head position.
/// This value is reset by SurfaceMeshesToPlanes when the max floor plane has been found.
/// </summary>
public float FloorYPosition { get; private set; }
/// <summary>
/// Ceiling y value, which corresponds to the maximum horizontal area found above the user's head position.
/// This value is reset by SurfaceMeshesToPlanes when the max ceiling plane has been found.
/// </summary>
public float CeilingYPosition { get; private set; }
/// <summary>
/// Delegate which is called when the MakePlanesCompleted event is triggered.
/// </summary>
/// <param name="source"></param>
/// <param name="args"></param>
public delegate void EventHandler(object source, EventArgs args);
/// <summary>
/// EventHandler which is triggered when the MakePlanesRoutine is finished.
/// </summary>
public event EventHandler MakePlanesComplete;
/// <summary>
/// Empty game object used to contain all planes created by the SurfaceToPlanes class.
/// </summary>
private GameObject planesParent;
/// <summary>
/// Used to align planes with gravity so that they appear more level.
/// </summary>
private float snapToGravityThreshold = 5.0f;
/// <summary>
/// Indicates if SurfaceToPlanes is currently creating planes based on the Spatial Mapping Mesh.
/// </summary>
private bool makingPlanes = false;
#if UNITY_EDITOR || UNITY_STANDALONE
/// <summary>
/// How much time (in sec), while running in the Unity Editor, to allow RemoveSurfaceVertices to consume before returning control to the main program.
/// </summary>
private static readonly float FrameTime = .016f;
#else
/// <summary>
/// How much time (in sec) to allow RemoveSurfaceVertices to consume before returning control to the main program.
/// </summary>
private static readonly float FrameTime = .008f;
#endif
// GameObject initialization.
private void Start()
{
makingPlanes = false;
ActivePlanes = new List<GameObject>();
planesParent = new GameObject("SurfacePlanes");
planesParent.transform.position = Vector3.zero;
planesParent.transform.rotation = Quaternion.identity;
}
/// <summary>
/// Creates planes based on meshes gathered by the SpatialMappingManager's SurfaceObserver.
/// </summary>
public void MakePlanes()
{
if (!makingPlanes)
{
makingPlanes = true;
// Processing the mesh can be expensive...
// We use Coroutine to split the work across multiple frames and avoid impacting the frame rate too much.
StartCoroutine(MakePlanesRoutine());
}
}
/// <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(PlaneTypes planeTypes)
{
List<GameObject> typePlanes = new List<GameObject>();
foreach (GameObject plane in ActivePlanes)
{
SurfacePlane surfacePlane = plane.GetComponent<SurfacePlane>();
if (surfacePlane != null)
{
if ((planeTypes & surfacePlane.PlaneType) == surfacePlane.PlaneType)
{
typePlanes.Add(plane);
}
}
}
return typePlanes;
}
/// <summary>
/// Iterator block, analyzes surface meshes to find planes and create new 3D cubes to represent each plane.
/// </summary>
/// <returns>Yield result.</returns>
private IEnumerator MakePlanesRoutine()
{
// Remove any previously existing planes, as they may no longer be valid.
for (int index = 0; index < ActivePlanes.Count; index++)
{
Destroy(ActivePlanes[index]);
}
// Pause our work, and continue on the next frame.
yield return null;
float start = Time.realtimeSinceStartup;
ActivePlanes.Clear();
// Get the latest Mesh data from the Spatial Mapping Manager.
List<PlaneFinding.MeshData> meshData = new List<PlaneFinding.MeshData>();
List<MeshFilter> filters = SpatialMappingManager.Instance.GetMeshFilters();
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));
}
if ((Time.realtimeSinceStartup - start) > FrameTime)
{
// Pause our work, and continue to make more PlaneFinding objects on the next frame.
yield return null;
start = Time.realtimeSinceStartup;
}
}
// Pause our work, and continue on the next frame.
yield return null;
#if !UNITY_EDITOR && UNITY_WSA
// When not in the unity editor we can use a cool background task to help manage FindPlanes().
Task<BoundedPlane[]> planeTask = Task.Run(() => PlaneFinding.FindPlanes(meshData, snapToGravityThreshold, MinArea));
while (planeTask.IsCompleted == false)
{
yield return null;
}
BoundedPlane[] planes = planeTask.Result;
#else
// In the unity editor, the task class isn't available, but perf is usually good, so we'll just wait for FindPlanes to complete.
BoundedPlane[] planes = PlaneFinding.FindPlanes(meshData, snapToGravityThreshold, MinArea);
#endif
// Pause our work here, and continue on the next frame.
yield return null;
start = Time.realtimeSinceStartup;
float maxFloorArea = 0.0f;
float maxCeilingArea = 0.0f;
FloorYPosition = 0.0f;
CeilingYPosition = 0.0f;
float upNormalThreshold = 0.9f;
if (SurfacePlanePrefab != null && SurfacePlanePrefab.GetComponent<SurfacePlane>() != null)
{
upNormalThreshold = SurfacePlanePrefab.GetComponent<SurfacePlane>().UpNormalThreshold;
}
// 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;
}
}
}
// Create SurfacePlane objects to represent each plane found in the Spatial Mapping mesh.
for (int index = 0; index < planes.Length; index++)
{
GameObject destinationPlane;
BoundedPlane boundedPlane = planes[index];
// Instantiate a SurfacePlane object, which will have the same bounds as our BoundedPlane object.
if (SurfacePlanePrefab != null && SurfacePlanePrefab.GetComponent<SurfacePlane>() != null)
{
destinationPlane = Instantiate(SurfacePlanePrefab);
}
else
{
destinationPlane = GameObject.CreatePrimitive(PrimitiveType.Cube);
destinationPlane.AddComponent<SurfacePlane>();
destinationPlane.GetComponent<Renderer>().shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
}
destinationPlane.transform.parent = planesParent.transform;
var surfacePlane = destinationPlane.GetComponent<SurfacePlane>();
// Set the Plane property to adjust transform position/scale/rotation and determine plane type.
surfacePlane.Plane = boundedPlane;
SetPlaneVisibility(surfacePlane);
if ((destroyPlanesMask & surfacePlane.PlaneType) == surfacePlane.PlaneType)
{
DestroyImmediate(destinationPlane);
}
else
{
// Set the plane to use the same layer as the SpatialMapping mesh.
destinationPlane.layer = SpatialMappingManager.Instance.PhysicsLayer;
ActivePlanes.Add(destinationPlane);
}
// If too much time has passed, we need to return control to the main game loop.
if ((Time.realtimeSinceStartup - start) > FrameTime)
{
// Pause our work here, and continue making additional planes on the next frame.
yield return null;
start = Time.realtimeSinceStartup;
}
}
Debug.Log("Finished making planes.");
// We are done creating planes, trigger an event.
EventHandler handler = MakePlanesComplete;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
makingPlanes = false;
}
/// <summary>
/// Sets visibility of planes based on their type.
/// </summary>
/// <param name="surfacePlane"></param>
private void SetPlaneVisibility(SurfacePlane surfacePlane)
{
surfacePlane.IsVisible = ((drawPlanesMask & surfacePlane.PlaneType) == surfacePlane.PlaneType);
}
}
}