AR Design
UBC EML collab with UBC SALA - visualizing IoT data in AR
SurfaceMeshesToPlanes.cs
Go to the documentation of this file.
1 // Copyright (c) Microsoft Corporation. All rights reserved.
2 // Licensed under the MIT License. See LICENSE in the project root for license information.
3 
4 using System;
5 using System.Collections;
6 using System.Collections.Generic;
7 using UnityEngine;
8 
9 #if !UNITY_EDITOR && UNITY_WSA
10 using System.Threading;
11 using System.Threading.Tasks;
12 #endif
13 
14 namespace HoloToolkit.Unity.SpatialMapping
15 {
19  public class SurfaceMeshesToPlanes : Singleton<SurfaceMeshesToPlanes>
20  {
21  [Tooltip("Currently active planes found within the Spatial Mapping Mesh.")]
22  public List<GameObject> ActivePlanes;
23 
24  [Tooltip("Object used for creating and rendering Surface Planes.")]
25  public GameObject SurfacePlanePrefab;
26 
27  [Tooltip("Minimum area required for a plane to be created.")]
28  public float MinArea = 0.025f;
29 
33  [HideInInspector]
34  public PlaneTypes drawPlanesMask =
35  (PlaneTypes.Wall | PlaneTypes.Floor | PlaneTypes.Ceiling | PlaneTypes.Table);
36 
41  [HideInInspector]
42  public PlaneTypes destroyPlanesMask = PlaneTypes.Unknown;
43 
48  public float FloorYPosition { get; private set; }
49 
54  public float CeilingYPosition { get; private set; }
55 
61  public delegate void EventHandler(object source, EventArgs args);
62 
66  public event EventHandler MakePlanesComplete;
67 
71  private GameObject planesParent;
72 
76  private float snapToGravityThreshold = 5.0f;
77 
81  private bool makingPlanes = false;
82 
83 #if UNITY_EDITOR || UNITY_STANDALONE
84  private static readonly float FrameTime = .016f;
88 #else
89  private static readonly float FrameTime = .008f;
93 #endif
94 
95  // GameObject initialization.
96  private void Start()
97  {
98  makingPlanes = false;
99  ActivePlanes = new List<GameObject>();
100  planesParent = new GameObject("SurfacePlanes");
101  planesParent.transform.position = Vector3.zero;
102  planesParent.transform.rotation = Quaternion.identity;
103  }
104 
108  public void MakePlanes()
109  {
110  if (!makingPlanes)
111  {
112  makingPlanes = true;
113  // Processing the mesh can be expensive...
114  // We use Coroutine to split the work across multiple frames and avoid impacting the frame rate too much.
115  StartCoroutine(MakePlanesRoutine());
116  }
117  }
118 
124  public List<GameObject> GetActivePlanes(PlaneTypes planeTypes)
125  {
126  List<GameObject> typePlanes = new List<GameObject>();
127 
128  foreach (GameObject plane in ActivePlanes)
129  {
130  SurfacePlane surfacePlane = plane.GetComponent<SurfacePlane>();
131 
132  if (surfacePlane != null)
133  {
134  if ((planeTypes & surfacePlane.PlaneType) == surfacePlane.PlaneType)
135  {
136  typePlanes.Add(plane);
137  }
138  }
139  }
140 
141  return typePlanes;
142  }
143 
148  private IEnumerator MakePlanesRoutine()
149  {
150  // Remove any previously existing planes, as they may no longer be valid.
151  for (int index = 0; index < ActivePlanes.Count; index++)
152  {
153  Destroy(ActivePlanes[index]);
154  }
155 
156  // Pause our work, and continue on the next frame.
157  yield return null;
158  float start = Time.realtimeSinceStartup;
159 
160  ActivePlanes.Clear();
161 
162  // Get the latest Mesh data from the Spatial Mapping Manager.
163  List<PlaneFinding.MeshData> meshData = new List<PlaneFinding.MeshData>();
164  List<MeshFilter> filters = SpatialMappingManager.Instance.GetMeshFilters();
165 
166  for (int index = 0; index < filters.Count; index++)
167  {
168  MeshFilter filter = filters[index];
169  if (filter != null && filter.sharedMesh != null)
170  {
171  // fix surface mesh normals so we can get correct plane orientation.
172  filter.mesh.RecalculateNormals();
173  meshData.Add(new PlaneFinding.MeshData(filter));
174  }
175 
176  if ((Time.realtimeSinceStartup - start) > FrameTime)
177  {
178  // Pause our work, and continue to make more PlaneFinding objects on the next frame.
179  yield return null;
180  start = Time.realtimeSinceStartup;
181  }
182  }
183 
184  // Pause our work, and continue on the next frame.
185  yield return null;
186 
187 #if !UNITY_EDITOR && UNITY_WSA
188  // When not in the unity editor we can use a cool background task to help manage FindPlanes().
189  Task<BoundedPlane[]> planeTask = Task.Run(() => PlaneFinding.FindPlanes(meshData, snapToGravityThreshold, MinArea));
190 
191  while (planeTask.IsCompleted == false)
192  {
193  yield return null;
194  }
195 
196  BoundedPlane[] planes = planeTask.Result;
197 #else
198  // In the unity editor, the task class isn't available, but perf is usually good, so we'll just wait for FindPlanes to complete.
199  BoundedPlane[] planes = PlaneFinding.FindPlanes(meshData, snapToGravityThreshold, MinArea);
200 #endif
201 
202  // Pause our work here, and continue on the next frame.
203  yield return null;
204  start = Time.realtimeSinceStartup;
205 
206  float maxFloorArea = 0.0f;
207  float maxCeilingArea = 0.0f;
208  FloorYPosition = 0.0f;
209  CeilingYPosition = 0.0f;
210  float upNormalThreshold = 0.9f;
211 
212  if (SurfacePlanePrefab != null && SurfacePlanePrefab.GetComponent<SurfacePlane>() != null)
213  {
214  upNormalThreshold = SurfacePlanePrefab.GetComponent<SurfacePlane>().UpNormalThreshold;
215  }
216 
217  // Find the floor and ceiling.
218  // We classify the floor as the maximum horizontal surface below the user's head.
219  // We classify the ceiling as the maximum horizontal surface above the user's head.
220  for (int i = 0; i < planes.Length; i++)
221  {
222  BoundedPlane boundedPlane = planes[i];
223  if (boundedPlane.Bounds.Center.y < 0 && boundedPlane.Plane.normal.y >= upNormalThreshold)
224  {
225  maxFloorArea = Mathf.Max(maxFloorArea, boundedPlane.Area);
226  if (maxFloorArea == boundedPlane.Area)
227  {
228  FloorYPosition = boundedPlane.Bounds.Center.y;
229  }
230  }
231  else if (boundedPlane.Bounds.Center.y > 0 && boundedPlane.Plane.normal.y <= -(upNormalThreshold))
232  {
233  maxCeilingArea = Mathf.Max(maxCeilingArea, boundedPlane.Area);
234  if (maxCeilingArea == boundedPlane.Area)
235  {
236  CeilingYPosition = boundedPlane.Bounds.Center.y;
237  }
238  }
239  }
240 
241  // Create SurfacePlane objects to represent each plane found in the Spatial Mapping mesh.
242  for (int index = 0; index < planes.Length; index++)
243  {
244  GameObject destinationPlane;
245  BoundedPlane boundedPlane = planes[index];
246 
247  // Instantiate a SurfacePlane object, which will have the same bounds as our BoundedPlane object.
248  if (SurfacePlanePrefab != null && SurfacePlanePrefab.GetComponent<SurfacePlane>() != null)
249  {
250  destinationPlane = Instantiate(SurfacePlanePrefab);
251  }
252  else
253  {
254  destinationPlane = GameObject.CreatePrimitive(PrimitiveType.Cube);
255  destinationPlane.AddComponent<SurfacePlane>();
256  destinationPlane.GetComponent<Renderer>().shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
257  }
258 
259  destinationPlane.transform.parent = planesParent.transform;
260  var surfacePlane = destinationPlane.GetComponent<SurfacePlane>();
261 
262  // Set the Plane property to adjust transform position/scale/rotation and determine plane type.
263  surfacePlane.Plane = boundedPlane;
264 
265  SetPlaneVisibility(surfacePlane);
266 
267  if ((destroyPlanesMask & surfacePlane.PlaneType) == surfacePlane.PlaneType)
268  {
269  DestroyImmediate(destinationPlane);
270  }
271  else
272  {
273  // Set the plane to use the same layer as the SpatialMapping mesh.
274  destinationPlane.layer = SpatialMappingManager.Instance.PhysicsLayer;
275  ActivePlanes.Add(destinationPlane);
276  }
277 
278  // If too much time has passed, we need to return control to the main game loop.
279  if ((Time.realtimeSinceStartup - start) > FrameTime)
280  {
281  // Pause our work here, and continue making additional planes on the next frame.
282  yield return null;
283  start = Time.realtimeSinceStartup;
284  }
285  }
286 
287  Debug.Log("Finished making planes.");
288 
289  // We are done creating planes, trigger an event.
290  EventHandler handler = MakePlanesComplete;
291  if (handler != null)
292  {
293  handler(this, EventArgs.Empty);
294  }
295 
296  makingPlanes = false;
297  }
298 
303  private void SetPlaneVisibility(SurfacePlane surfacePlane)
304  {
305  surfacePlane.IsVisible = ((drawPlanesMask & surfacePlane.PlaneType) == surfacePlane.PlaneType);
306  }
307  }
308 }
EventHandler MakePlanesComplete
EventHandler which is triggered when the MakePlanesRoutine is finished.
The SurfacePlane class is used by SurfaceMeshesToPlanes to create different types of planes (walls...
Definition: SurfacePlane.cs:27
List< GameObject > GetActivePlanes(PlaneTypes planeTypes)
Gets all active planes of the specified type(s).
BoundedPlane Plane
Gets or Sets the BoundedPlane, which determines the orientation/size/position of the gameObject...
Definition: SurfacePlane.cs:72
static BoundedPlane [] FindPlanes(List< MeshData > meshes, float snapToGravityThreshold=0.0f, float minArea=0.0f)
Convenience wrapper that executes FindSubPlanes followed by MergeSubPlanes via a single call into nat...
static T Instance
Returns the Singleton instance of the classes type. If no instance is found, then we search for an in...
Definition: Singleton.cs:26
PlaneFinding is an expensive task that should not be run from Unity&#39;s main thread as it will stall th...
Definition: PlaneFinding.cs:56
bool IsVisible
Gets or sets the visibility of the current gameObject.
Definition: SurfacePlane.cs:93
PlaneTypes
All possible plane types that a SurfacePlane can be.
Definition: SurfacePlane.cs:13
The SpatialMappingManager class allows applications to use a SurfaceObserver or a stored Spatial Mapp...
void MakePlanes()
Creates planes based on meshes gathered by the SpatialMappingManager&#39;s SurfaceObserver.
SurfaceMeshesToPlanes will find and create planes based on the meshes returned by the SpatialMappingM...
Singleton behaviour class, used for components that should only have one instance.
Definition: Singleton.cs:14