AR Design
UBC EML collab with UBC SALA - visualizing IoT data in AR
SpatialMappingObserver.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.Generic;
6 using UnityEngine;
7 
8 #if UNITY_WSA
9 #if UNITY_2017_2_OR_NEWER
10 using UnityEngine.XR.WSA;
11 #else
12 using UnityEngine.VR.WSA;
13 #endif
14 #endif
15 
16 namespace HoloToolkit.Unity.SpatialMapping
17 {
21  public enum ObserverStates
22  {
26  Running = 0,
27 
31  Stopped = 1
32  }
33 
37  public enum ObserverVolumeTypes
38  {
42  AxisAlignedBox = 0,
43 
47  OrientedBox = 1,
48 
52  Sphere = 2
53  }
54 
60  {
61  [Tooltip("The number of triangles to calculate per cubic meter.")]
62  public float TrianglesPerCubicMeter = 500f;
63 
64  [Tooltip("How long to wait (in sec) between Spatial Mapping updates.")]
65  public float TimeBetweenUpdates = 3.5f;
66 
70  public ObserverStates ObserverState { get; private set; }
71 
75  [SerializeField]
76  [Tooltip("The shape of the observation volume.")]
77  private ObserverVolumeTypes observerVolumeType = ObserverVolumeTypes.AxisAlignedBox;
78  public ObserverVolumeTypes ObserverVolumeType
79  {
80  get
81  {
82  return observerVolumeType;
83  }
84  set
85  {
86  if (observerVolumeType != value)
87  {
88  observerVolumeType = value;
89  SwitchObservedVolume();
90  }
91  }
92  }
93 
94 #if UNITY_WSA
95  private SurfaceObserver observer;
99 
103  private readonly Queue<SurfaceId> surfaceWorkQueue = new Queue<SurfaceId>();
104 
110  private SurfaceObject? outstandingMeshRequest = null;
111 #endif
112 
118  private SurfaceObject? spareSurfaceObject = null;
119 
123  private float updateTime;
124 
125  [SerializeField]
126  [Tooltip("The extents of the observation volume.")]
127  private Vector3 extents = Vector3.one * 10.0f;
128  public Vector3 Extents
129  {
130  get
131  {
132  return extents;
133  }
134  set
135  {
136  if (extents != value)
137  {
138  extents = value;
139  SwitchObservedVolume();
140  }
141  }
142  }
143 
147  [SerializeField]
148  [Tooltip("The origin of the observation volume.")]
149  private Vector3 origin = Vector3.zero;
150  public Vector3 Origin
151  {
152  get
153  {
154  return origin;
155  }
156  set
157  {
158  if (origin != value)
159  {
160  origin = value;
161  SwitchObservedVolume();
162  }
163  }
164  }
165 
169  [SerializeField]
170  [Tooltip("The direction of the observation volume.")]
171  private Quaternion orientation = Quaternion.identity;
172  public Quaternion Orientation
173  {
174  get
175  {
176  return orientation;
177  }
178  set
179  {
180  if (orientation != value)
181  {
182  orientation = value;
183  // Only needs to be changed if the corresponding mode is active.
184  if (ObserverVolumeType == ObserverVolumeTypes.OrientedBox)
185  {
186  SwitchObservedVolume();
187  }
188  }
189  }
190  }
191 
192  protected override void Awake()
193  {
194  base.Awake();
195 
196  ObserverState = ObserverStates.Stopped;
197  }
198 
199 #if UNITY_WSA
200  private void Update()
204  {
205  if ((ObserverState == ObserverStates.Running) && (outstandingMeshRequest == null))
206  {
207  if (surfaceWorkQueue.Count > 0)
208  {
209  // We're using a simple first-in-first-out rule for requesting meshes, but a more sophisticated algorithm could prioritize
210  // the queue based on distance to the user or some other metric.
211  SurfaceId surfaceID = surfaceWorkQueue.Dequeue();
212 
213  string surfaceName = ("Surface-" + surfaceID.handle);
214 
215  SurfaceObject newSurface;
216  WorldAnchor worldAnchor;
217 
218  if (spareSurfaceObject == null)
219  {
220  newSurface = CreateSurfaceObject(
221  mesh: null,
222  objectName: surfaceName,
223  parentObject: transform,
224  meshID: surfaceID.handle,
225  drawVisualMeshesOverride: false
226  );
227 
228  worldAnchor = newSurface.Object.AddComponent<WorldAnchor>();
229  }
230  else
231  {
232  newSurface = spareSurfaceObject.Value;
233  spareSurfaceObject = null;
234 
235  Debug.Assert(!newSurface.Object.activeSelf);
236  newSurface.Object.SetActive(true);
237 
238  Debug.Assert(newSurface.Filter.sharedMesh == null);
239  Debug.Assert(newSurface.Collider.sharedMesh == null);
240  newSurface.Object.name = surfaceName;
241  Debug.Assert(newSurface.Object.transform.parent == transform);
242  newSurface.ID = surfaceID.handle;
243  newSurface.Renderer.enabled = false;
244 
245  worldAnchor = newSurface.Object.GetComponent<WorldAnchor>();
246  Debug.Assert(worldAnchor != null);
247  }
248 
249  var surfaceData = new SurfaceData(
250  surfaceID,
251  newSurface.Filter,
252  worldAnchor,
253  newSurface.Collider,
254  TrianglesPerCubicMeter,
255  _bakeCollider: true
256  );
257 
258  if (observer.RequestMeshAsync(surfaceData, SurfaceObserver_OnDataReady))
259  {
260  outstandingMeshRequest = newSurface;
261  }
262  else
263  {
264  Debug.LogErrorFormat("Mesh request for failed. Is {0} a valid Surface ID?", surfaceID.handle);
265 
266  Debug.Assert(outstandingMeshRequest == null);
267  ReclaimSurface(newSurface);
268  }
269  }
270  else if ((Time.unscaledTime - updateTime) >= TimeBetweenUpdates)
271  {
272  observer.Update(SurfaceObserver_OnSurfaceChanged);
273  updateTime = Time.unscaledTime;
274  }
275  }
276  }
277 #endif
278 
282  public void StartObserving()
283  {
284 #if UNITY_WSA
285  if (observer == null)
286  {
287  observer = new SurfaceObserver();
288  SwitchObservedVolume();
289  }
290 
291  if (ObserverState != ObserverStates.Running)
292  {
293  Debug.Log("Starting the observer.");
294  ObserverState = ObserverStates.Running;
295 
296  // We want the first update immediately.
297  updateTime = 0;
298  }
299 #endif
300  }
301 
306  public void StopObserving()
307  {
308 #if UNITY_WSA
309  if (ObserverState == ObserverStates.Running)
310  {
311  Debug.Log("Stopping the observer.");
312  ObserverState = ObserverStates.Stopped;
313 
314  surfaceWorkQueue.Clear();
315  updateTime = 0;
316  }
317 #endif
318  }
319 
323  public void CleanupObserver()
324  {
325 #if UNITY_WSA
326  StopObserving();
327 
328  if (observer != null)
329  {
330  observer.Dispose();
331  observer = null;
332  }
333 
334  if (outstandingMeshRequest != null)
335  {
336  CleanUpSurface(outstandingMeshRequest.Value);
337  outstandingMeshRequest = null;
338  }
339 
340  if (spareSurfaceObject != null)
341  {
342  CleanUpSurface(spareSurfaceObject.Value);
343  spareSurfaceObject = null;
344  }
345 
346  Cleanup();
347 #endif
348  }
349 
354  public bool SetObserverOrigin(Vector3 origin)
355  {
356  bool originUpdated = false;
357 
358 #if UNITY_WSA
359  if (observer != null)
360  {
361  Origin = origin;
362  originUpdated = true;
363  }
364 #endif
365 
366  return originUpdated;
367  }
368 
372  private void SwitchObservedVolume()
373  {
374 #if UNITY_WSA
375  if (observer == null)
376  {
377  return;
378  }
379 
380  switch (observerVolumeType)
381  {
382  case ObserverVolumeTypes.AxisAlignedBox:
383  observer.SetVolumeAsAxisAlignedBox(origin, extents);
384  break;
385  case ObserverVolumeTypes.OrientedBox:
386  observer.SetVolumeAsOrientedBox(origin, extents, orientation);
387  break;
388  case ObserverVolumeTypes.Sphere:
389  observer.SetVolumeAsSphere(origin, extents.magnitude); //workaround
390  break;
391  default:
392  observer.SetVolumeAsAxisAlignedBox(origin, extents);
393  break;
394  }
395 #endif
396  }
397 
398 #if UNITY_WSA
399  private void SurfaceObserver_OnDataReady(SurfaceData cookedData, bool outputWritten, float elapsedCookTimeSeconds)
406  {
407  if (outstandingMeshRequest == null)
408  {
409  Debug.LogErrorFormat("Got OnDataReady for surface {0} while no request was outstanding.",
410  cookedData.id.handle
411  );
412 
413  return;
414  }
415 
416  if (!IsMatchingSurface(outstandingMeshRequest.Value, cookedData))
417  {
418  Debug.LogErrorFormat("Got mismatched OnDataReady for surface {0} while request for surface {1} was outstanding.",
419  cookedData.id.handle,
420  outstandingMeshRequest.Value.ID
421  );
422 
423  ReclaimSurface(outstandingMeshRequest.Value);
424  outstandingMeshRequest = null;
425 
426  return;
427  }
428 
429  if (ObserverState != ObserverStates.Running)
430  {
431  Debug.LogFormat("Got OnDataReady for surface {0}, but observer was no longer running.",
432  cookedData.id.handle
433  );
434 
435  ReclaimSurface(outstandingMeshRequest.Value);
436  outstandingMeshRequest = null;
437 
438  return;
439  }
440 
441  if (!outputWritten)
442  {
443  ReclaimSurface(outstandingMeshRequest.Value);
444  outstandingMeshRequest = null;
445 
446  return;
447  }
448 
449  Debug.Assert(outstandingMeshRequest.Value.Object.activeSelf);
450  outstandingMeshRequest.Value.Renderer.enabled = SpatialMappingManager.Instance.DrawVisualMeshes;
451 
452  SurfaceObject? replacedSurface = UpdateOrAddSurfaceObject(outstandingMeshRequest.Value, destroyGameObjectIfReplaced: false);
453  outstandingMeshRequest = null;
454 
455  if (replacedSurface != null)
456  {
457  ReclaimSurface(replacedSurface.Value);
458  }
459  }
460 
468  private void SurfaceObserver_OnSurfaceChanged(SurfaceId id, SurfaceChange changeType, Bounds bounds, DateTime updateTime)
469  {
470  // Verify that the client of the Surface Observer is expecting updates.
471  if (ObserverState != ObserverStates.Running)
472  {
473  return;
474  }
475 
476  switch (changeType)
477  {
478  case SurfaceChange.Added:
479  case SurfaceChange.Updated:
480  surfaceWorkQueue.Enqueue(id);
481  break;
482 
483  case SurfaceChange.Removed:
484  SurfaceObject? removedSurface = RemoveSurfaceIfFound(id.handle, destroyGameObject: false);
485  if (removedSurface != null)
486  {
487  ReclaimSurface(removedSurface.Value);
488  }
489  break;
490 
491  default:
492  Debug.LogErrorFormat("Unexpected {0} value: {1}.", changeType.GetType(), changeType);
493  break;
494  }
495  }
496  private bool IsMatchingSurface(SurfaceObject surfaceObject, SurfaceData surfaceData)
497  {
498  return (surfaceObject.ID == surfaceData.id.handle)
499  && (surfaceObject.Filter == surfaceData.outputMesh)
500  && (surfaceObject.Collider == surfaceData.outputCollider)
501  ;
502  }
503 #endif
504 
508  private void OnDestroy()
509  {
510  CleanupObserver();
511  }
512 
513  private void ReclaimSurface(SurfaceObject availableSurface)
514  {
515  if (spareSurfaceObject == null)
516  {
517  CleanUpSurface(availableSurface, destroyGameObject: false);
518 
519  availableSurface.Object.name = "Unused Surface";
520  availableSurface.Object.SetActive(false);
521 
522  spareSurfaceObject = availableSurface;
523  }
524  else
525  {
526  CleanUpSurface(availableSurface);
527  }
528  }
529  }
530 }
The SurfaceObserver is currently running.
The SpatialMappingObserver class encapsulates the SurfaceObserver into an easy to use object that han...
Definition: Origin.cs:6
bool SetObserverOrigin(Vector3 origin)
Can be called to override the default origin for the observed volume. Can only be called while observ...
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
ObserverStates
Spatial Mapping Observer states.
The SpatialMappingManager class allows applications to use a SurfaceObserver or a stored Spatial Mapp...
ObserverVolumeTypes
Spatial Mapping Volume Type
The SurfaceObserver is currently idle.
void CleanupObserver()
Cleans up all memory and objects associated with the observer.