HoloLens Tutorial – Finalize Spatial Understanding

Last Updated: Apr 24, 2017

Tutorial Accomplishments

  1. Test for Spatial Understanding Completeness
  2. Add Input Manager
  3. Finalizing Spatial Understanding

 

This section of the tutorial will wrap up spatial mapping and spatial understanding setting us up to place some holograms in the next section. First we will test to make sure we have a certain level of quality in our spatial understanding. Second when the spatial understanding is high enough quality the user can air tap to Finalize Spatial Understanding.

 

Test for Spatial Understanding Completeness

Open SpatialUnderstandingState.cs and make these changes:


using System;
using UnityEngine;
using HoloToolkit.Unity;
using HoloToolkit.Unity.SpatialMapping;
public class SpatialUnderstandingState : Singleton<SpatialUnderstandingState>
{
public float MinAreaForStats = 5.0f;
public float MinAreaForComplete = 50.0f;
public float MinHorizAreaForComplete = 25.0f;
public float MinWallAreaForComplete = 10.0f;
public TextMesh DebugDisplay;
public TextMesh DebugSubDisplay;
private bool _triggered;
public bool HideText = false;
private string _spaceQueryDescription;
public string SpaceQueryDescription
{
get
{
return _spaceQueryDescription;
}
set
{
_spaceQueryDescription = value;
}
}
public bool DoesScanMeetMinBarForCompletion
{
get
{
// Only allow this when we are actually scanning
if ((SpatialUnderstanding.Instance.ScanState != SpatialUnderstanding.ScanStates.Scanning) ||
(!SpatialUnderstanding.Instance.AllowSpatialUnderstanding))
{
return false;
}
// Query the current playspace stats
IntPtr statsPtr = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceStatsPtr();
if (SpatialUnderstandingDll.Imports.QueryPlayspaceStats(statsPtr) == 0)
{
return false;
}
SpatialUnderstandingDll.Imports.PlayspaceStats stats = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceStats();
// Check our preset requirements
if ((stats.TotalSurfaceArea > MinAreaForComplete) ||
(stats.HorizSurfaceArea > MinHorizAreaForComplete) ||
(stats.WallSurfaceArea > MinWallAreaForComplete))
{
return true;
}
return false;
}
}
public string PrimaryText
{
get
{
if (HideText)
return string.Empty;
// Display the space and object query results (has priority)
if (!string.IsNullOrEmpty(SpaceQueryDescription))
{
return SpaceQueryDescription;
}
// Scan state
if (SpatialUnderstanding.Instance.AllowSpatialUnderstanding)
{
switch (SpatialUnderstanding.Instance.ScanState)
{
case SpatialUnderstanding.ScanStates.Scanning:
// Get the scan stats
IntPtr statsPtr = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceStatsPtr();
if (SpatialUnderstandingDll.Imports.QueryPlayspaceStats(statsPtr) == 0)
{
return "playspace stats query failed";
}
return "Walk around and scan in your playspace";
case SpatialUnderstanding.ScanStates.Finishing:
return "Finalizing scan (please wait)";
case SpatialUnderstanding.ScanStates.Done:
return "Scan complete";
default:
return "ScanState = " + SpatialUnderstanding.Instance.ScanState;
}
}
return string.Empty;
}
}
public Color PrimaryColor
{
get
{
if (SpatialUnderstanding.Instance.ScanState == SpatialUnderstanding.ScanStates.Scanning)
{
return DoesScanMeetMinBarForCompletion ? Color.yellow : Color.white;
}
float alpha = 1.0f;
// Special case processing &
return (!string.IsNullOrEmpty(SpaceQueryDescription)) ?
(PrimaryText.Contains("processing") ? new Color(1.0f, 0.0f, 0.0f, 1.0f) : new Color(1.0f, 0.7f, 0.1f, alpha)) :
new Color(1.0f, 1.0f, 1.0f, alpha);
}
}
public string DetailsText
{
get
{
if (SpatialUnderstanding.Instance.ScanState == SpatialUnderstanding.ScanStates.None)
{
return "";
}
// Scanning stats get second priority
if ((SpatialUnderstanding.Instance.ScanState == SpatialUnderstanding.ScanStates.Scanning) &&
(SpatialUnderstanding.Instance.AllowSpatialUnderstanding))
{
IntPtr statsPtr = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceStatsPtr();
if (SpatialUnderstandingDll.Imports.QueryPlayspaceStats(statsPtr) == 0)
{
return "Playspace stats query failed";
}
SpatialUnderstandingDll.Imports.PlayspaceStats stats = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceStats();
// Start showing the stats when they are no longer zero
if (stats.TotalSurfaceArea > MinAreaForStats)
{
SpatialMappingManager.Instance.DrawVisualMeshes = false;
string subDisplayText = string.Format("totalArea={0:0.0}, horiz={1:0.0}, wall={2:0.0}", stats.TotalSurfaceArea, stats.HorizSurfaceArea, stats.WallSurfaceArea);
subDisplayText += string.Format("\nnumFloorCells={0}, numCeilingCells={1}, numPlatformCells={2}", stats.NumFloor, stats.NumCeiling, stats.NumPlatform);
subDisplayText += string.Format("\npaintMode={0}, seenCells={1}, notSeen={2}", stats.CellCount_IsPaintMode, stats.CellCount_IsSeenQualtiy_Seen + stats.CellCount_IsSeenQualtiy_Good, stats.CellCount_IsSeenQualtiy_None);
return subDisplayText;
}
return "";
}
return "";
}
}
private void Update_DebugDisplay()
{
// Basic checks
if (DebugDisplay == null)
{
return;
}
// Update display text
DebugDisplay.text = PrimaryText;
DebugDisplay.color = PrimaryColor;
DebugSubDisplay.text = DetailsText;
}
// Update is called once per frame
private void Update()
{
// Updates
Update_DebugDisplay();
}
}

These changes cause the Primary Text color to change from White to Yellow when spatial understanding criteria specified in constants is met. The first change is made to the Get property of PrimaryColor. In the previous version it always returned White. This makes a call to DoesScanMeetMinBarForCompletion to determine if  the criteria have been met. In this method we call SpatialUnderstandingDll.Imports.QueryPlayspaceStats which returns an object of PlayspaceStats. This method compares the constants that have been added to determine if our spatial understanding is good enough for the application to continue.

 

Add Input Manager

To finalize spatial understanding the user will air tap. In order to detect user input from the HoloLens the HoloToolkit provides the Input Manager. Open the project in Unity and Add a new gameobject named “Control” in the Hierarchy. Drag the InputManager.prefab from the HoloToolkit (in the Input, Prefabs folder) into the new Control object. This results in a few gameobjects and components getting added to the Control Gameobject. This is everything that is needed to detect input from the HoloLens. The project should now look like this:

Finalizing Spatial Understanding

When the spatial understanding is good enough for our app, the message should both change color and instructions.  The instructions will also change to tell the user to air tap in order to finish mapping.

Update SpatialUnderstandingState.CS to this:


using System;
using UnityEngine;
using HoloToolkit.Unity;
using HoloToolkit.Unity.InputModule;
using HoloToolkit.Unity.SpatialMapping;
public class SpatialUnderstandingState : Singleton<SpatialUnderstandingState>, IInputClickHandler, ISourceStateHandler
{
public float MinAreaForStats = 5.0f;
public float MinAreaForComplete = 50.0f;
public float MinHorizAreaForComplete = 25.0f;
public float MinWallAreaForComplete = 10.0f;
private uint trackedHandsCount = 0;
public TextMesh DebugDisplay;
public TextMesh DebugSubDisplay;
private bool _triggered;
public bool HideText = false;
private bool ready = false;
private string _spaceQueryDescription;
public string SpaceQueryDescription
{
get
{
return _spaceQueryDescription;
}
set
{
_spaceQueryDescription = value;
}
}
public bool DoesScanMeetMinBarForCompletion
{
get
{
// Only allow this when we are actually scanning
if ((SpatialUnderstanding.Instance.ScanState != SpatialUnderstanding.ScanStates.Scanning) ||
(!SpatialUnderstanding.Instance.AllowSpatialUnderstanding))
{
return false;
}
// Query the current playspace stats
IntPtr statsPtr = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceStatsPtr();
if (SpatialUnderstandingDll.Imports.QueryPlayspaceStats(statsPtr) == 0)
{
return false;
}
SpatialUnderstandingDll.Imports.PlayspaceStats stats = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceStats();
// Check our preset requirements
if ((stats.TotalSurfaceArea > MinAreaForComplete) ||
(stats.HorizSurfaceArea > MinHorizAreaForComplete) ||
(stats.WallSurfaceArea > MinWallAreaForComplete))
{
return true;
}
return false;
}
}
public string PrimaryText
{
get
{
if (HideText)
return string.Empty;
// Display the space and object query results (has priority)
if (!string.IsNullOrEmpty(SpaceQueryDescription))
{
return SpaceQueryDescription;
}
// Scan state
if (SpatialUnderstanding.Instance.AllowSpatialUnderstanding)
{
switch (SpatialUnderstanding.Instance.ScanState)
{
case SpatialUnderstanding.ScanStates.Scanning:
// Get the scan stats
IntPtr statsPtr = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceStatsPtr();
if (SpatialUnderstandingDll.Imports.QueryPlayspaceStats(statsPtr) == 0)
{
return "playspace stats query failed";
}
// The stats tell us if we could potentially finish
if (DoesScanMeetMinBarForCompletion)
{
return "When ready, air tap to finalize your playspace";
}
return "Walk around and scan in your playspace";
case SpatialUnderstanding.ScanStates.Finishing:
return "Finalizing scan (please wait)";
case SpatialUnderstanding.ScanStates.Done:
return "Scan complete";
default:
return "ScanState = " + SpatialUnderstanding.Instance.ScanState;
}
}
return string.Empty;
}
}
public Color PrimaryColor
{
get
{
ready = DoesScanMeetMinBarForCompletion;
if (SpatialUnderstanding.Instance.ScanState == SpatialUnderstanding.ScanStates.Scanning)
{
if (trackedHandsCount > 0)
{
return ready ? Color.green : Color.red;
}
return ready ? Color.yellow : Color.white;
}
// If we're looking at the menu, fade it out
float alpha = 1.0f;
// Special case processing &
return (!string.IsNullOrEmpty(SpaceQueryDescription)) ?
(PrimaryText.Contains("processing") ? new Color(1.0f, 0.0f, 0.0f, 1.0f) : new Color(1.0f, 0.7f, 0.1f, alpha)) :
new Color(1.0f, 1.0f, 1.0f, alpha);
}
}
public string DetailsText
{
get
{
if (SpatialUnderstanding.Instance.ScanState == SpatialUnderstanding.ScanStates.None)
{
return "";
}
// Scanning stats get second priority
if ((SpatialUnderstanding.Instance.ScanState == SpatialUnderstanding.ScanStates.Scanning) &&
(SpatialUnderstanding.Instance.AllowSpatialUnderstanding))
{
IntPtr statsPtr = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceStatsPtr();
if (SpatialUnderstandingDll.Imports.QueryPlayspaceStats(statsPtr) == 0)
{
return "Playspace stats query failed";
}
SpatialUnderstandingDll.Imports.PlayspaceStats stats = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceStats();
// Start showing the stats when they are no longer zero
if (stats.TotalSurfaceArea > MinAreaForStats)
{
SpatialMappingManager.Instance.DrawVisualMeshes = false;
string subDisplayText = string.Format("totalArea={0:0.0}, horiz={1:0.0}, wall={2:0.0}", stats.TotalSurfaceArea, stats.HorizSurfaceArea, stats.WallSurfaceArea);
subDisplayText += string.Format("\nnumFloorCells={0}, numCeilingCells={1}, numPlatformCells={2}", stats.NumFloor, stats.NumCeiling, stats.NumPlatform);
subDisplayText += string.Format("\npaintMode={0}, seenCells={1}, notSeen={2}", stats.CellCount_IsPaintMode, stats.CellCount_IsSeenQualtiy_Seen + stats.CellCount_IsSeenQualtiy_Good, stats.CellCount_IsSeenQualtiy_None);
return subDisplayText;
}
return "";
}
return "";
}
}
private void Update_DebugDisplay()
{
// Basic checks
if (DebugDisplay == null)
{
return;
}
// Update display text
DebugDisplay.text = PrimaryText;
DebugDisplay.color = PrimaryColor;
DebugSubDisplay.text = DetailsText;
}
private void Start()
{
InputManager.Instance.PushFallbackInputHandler(gameObject);
}
// Update is called once per frame
private void Update()
{
// Updates
Update_DebugDisplay();
if (!_triggered && SpatialUnderstanding.Instance.ScanState == SpatialUnderstanding.ScanStates.Done)
{
_triggered = true;
}
}
public void OnInputClicked(InputClickedEventData eventData)
{
if (ready &&
(SpatialUnderstanding.Instance.ScanState == SpatialUnderstanding.ScanStates.Scanning) &&
!SpatialUnderstanding.Instance.ScanStatsReportStillWorking)
{
SpatialUnderstanding.Instance.RequestFinishScan();
}
}
void ISourceStateHandler.OnSourceDetected(SourceStateEventData eventData)
{
trackedHandsCount++;
}
void ISourceStateHandler.OnSourceLost(SourceStateEventData eventData)
{
trackedHandsCount–;
}
}

The class now implements the interfaces IInputClickHandler and ISourceStateHandler.  These interfaces contain OnInputClickedOnSourceDetected, and OnSourceLost.  The Start method is added making a call to the input manager telling it that this gameobject will subscribe to the input events.  When these events occur the methods implemented by the interfaces are called notifying this class of user input.  Visible hands detected are tracked so that the application can respond to the users hand coming into view. Air taps are detected allowing the user to finalize spatial understanding by calling RequestFinishScan which finalizes spatial understanding. Build the app and run it on the HoloLens.  Map your room and air tap to finalize spatial understanding.  Notice that after finalization is complete all of the gaps in the spatial map are approximated based on the rest of the spatial map.  This is done to make the spatial understanding “water tight” so there is not ambiguity on how objects should be placed.  Be careful not to allow the user to finalize with too many gaps in the spatial understanding or the results of object placement may be disappointing.

 

Tutorial Index

  Versions: Unity 2017.1.0p5 | MixedRealityToolkit-Unity v1.2017.1.0Visual Studio 2017 15.3.2
Unity 3D Project Creation How to create a HoloLens project in Unity 3D
Source Control Configure git for HoloLens / Unity work
Spatial Mapping How to spatial map a Room
Object Surface Observer Set up fake spatial map data for the Unity editor
TagAlongs and Billboarding   Tag along instructions to the user to force text on screen
Spatial Understanding Add spatial understanding to get play space detail
Finalizing Spatial Understanding Complete Spatial Understanding and Input Manager
Object Placement and Scaling Find valid locations for holograms in the play space
Hologram Management Manage the holograms you want to place in the world
Cursor and Voice Add a cursor and voice commands
Occlusion Add occlusion with the real world to your scene
Colliders and Rigidbodys Add Colliders and RigidBodys to your holograms

5 thoughts on “HoloLens Tutorial – Finalize Spatial Understanding

  • Posted on January 18, 2017 at 2:50 pm

    Following your tutorial. It’s been great.
    When will the next ones be up? Really interested in how you place your holograms and objects.
    Thanks!

    Reply
    • Posted on January 19, 2017 at 11:27 am

      I hope to add the next section of the tutorial next week, changes in the HoloToolkit have caused me to have to go back and redo some code…

      Reply
  • Posted on February 11, 2017 at 11:57 pm

    I’m not sure if I have an old or new version of Holotoolkit, but for this to work I had to update line 204:

    void IInputClickHandler.OnInputClicked(InputEventData eventData)

    to

    void IInputClickHandler.OnInputClicked(InputClickedEventData eventData)

    Reply
    • Posted on February 12, 2017 at 10:37 am

      You have a newer version of the HoloToolkit, that change appeared on Jan 14th, it moves fast…

      Reply
  • Posted on March 8, 2018 at 8:49 am

    Hi Mr Vetter,

    First of all, thank you for your tutorial, it really helps me to understand how to develop on HoloLens. I would like to point out that the most recent version of toolkit manager attaches to input manager a new script called “simple single pointer selector”. In order to continue the tutorial, it is necessary to deactivate it, or to add a cursor (present in the toolkit) in the project and then add it in the script’s cursor property. Otherwise, the project cannot be deployed on emulator or hololens. Thank you again for your tutorial, and I hope to have been clear despite my bad English.

    Reply

Leave a Reply