Last Updated: Apr 24, 2017
Tutorial Accomplishments
- Test for Spatial Understanding Completeness
- Add Input Manager
- 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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 OnInputClicked, OnSourceDetected, 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.0 | Visual Studio 2017 15.3.2Unity 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 |
- Download the completed app Western Town in the Windows Store!
- Completed Source code from the entire tutorial available on GitHub.
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!
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…
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)
You have a newer version of the HoloToolkit, that change appeared on Jan 14th, it moves fast…
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.