Migrating to MRTK2: Interacting With the Spatial Map
Migrating to HoloLens MRTK2? Learn more about interacting with the Spatial Map in MRTK2.
Join the DZone community and get the full member experience.
Join For FreeOne of HoloLens' greatest features is the ability to interact with real physical objects. This allows apps to place holograms on or adjacent to real objects, enables occlusion (the ability to let holograms appear to be hidden because they disappear behind physical objects), etc. This is all done using the Spatial Map, a graphical representation of whatever the HoloLens has observed to be present in the physical reality. Interacting with the Spatial Map used to be easy — and it actually still isn't that hard, it's just that — as with most of the things in the MRTK2 — quite some cheese has been moved
This blog post handles a common, and a not-so-common, scenario for interacting with the Spatial Map:
- Placing objects on the Spatial Map
- Programmatically enabling and disabling/clearing the Spatial Map
I have included a demo project that allows you to place cylinders on the Spatial Map by air tapping — and you can turn the Spatial Map on and off using a floating button.
Placing Objects on the Spatial Map, MRKT2 Style
I wrote about this already in November 2017 in my article about finding the floor using a HoloLens. In MRTK2, that process is a bit much different. Create a raycast from the Camera along the camera viewing angle and try to hit the Spatial Map. For this, you need the Spatial Map Layer mask. In the HoloToolkit, you could simply access:
SpatialMappingManager.Instance.LayerMask
This would get to that layer mask. Finding that now is a wee bit more complicated. You see, first, you need to extract the configuration from the Spatial Awareness System service like this:
var spatialMappingConfig =
CoreServices.SpatialAwarenessSystem.ConfigurationProfile as
MixedRealitySpatialAwarenessMeshObserverProfile;
The spatial mapping config contains a property called ObserverConfigurations
containing a list of configurations (apparently taking provisions there might actually be more than one configuration). For each configuration, you can take the profile from it's ObserverProfile
property — that you have to cast to MixedRealitySpatialAwarenessMeshObserverProfile
. Then, you find the layer used by this config in it's MeshPhysicsLayer
property.
I repeat — you can find the layer.
That is not the layer mask. It took me quite some time debugging to find out what was going on here — because if you feed that layer number into the raycast, it won't 'see' the Spatial Map. I have no idea why this was changed. Anyway, to get the layer mask, as required by raycast methods, you have to bit shift the actual layer number, like this:
1 << observerProfile.MeshPhysicsLayer
So what used to be a single property, now requires this method:
private static int GetSpatialMeshMask()
{
if (_meshPhysicsLayer == 0)
{
var spatialMappingConfig =
CoreServices.SpatialAwarenessSystem.ConfigurationProfile as
MixedRealitySpatialAwarenessSystemProfile;
if (spatialMappingConfig != null)
{
foreach (var config in spatialMappingConfig.ObserverConfigurations)
{
var observerProfile = config.ObserverProfile
as MixedRealitySpatialAwarenessMeshObserverProfile;
if (observerProfile != null)
{
_meshPhysicsLayer |= (1 << observerProfile.MeshPhysicsLayer);
}
}
}
}
return _meshPhysicsLayer;
}
private static int _meshPhysicsLayer = 0;
And I added a static backing variable to speed up this process; otherwise, this whole loop will be run 60 times a second in my TapToPlaceController, as well as every time you air tap to place a cylinder.
The method to find a point on the Spatial Map simply is then simply this:
public static Vector3? GetPositionOnSpatialMap(float maxDistance = 2)
{
RaycastHit hitInfo;
var transform = CameraCache.Main.transform;
var headRay = new Ray(transform.position, transform.forward);
if (Physics.Raycast(headRay, out hitInfo, maxDistance, GetSpatialMeshMask()))
{
return hitInfo.point;
}
return null;
}
This sits in the updated LookingDirectionHelpers class. In the demo project, you can see how it is actually used.
In the TapToPlaceController, the Update
method will flip the text from "Please look at the spatial map max 2m ahead of you" to "Tap to select a location" when the gaze strikes the Spatial Map (and the Spatial Map ONLY, not another hologram).
protected override void Update()
{
_instructionTextMesh.text =
LookingDirectionHelpers.GetPositionOnSpatialMap(_maxDistance) != null ?
"Tap to select a location" : _lookAtSurfaceText;
}
If you then air tap, it will place a squatted cylinder on the spatial map at the place you are looking to. This is done in the OnPointerDown
method — using the same call to LookingDirectionHelpers.GetPositionOnSpatialMap
to get a point to place the cylinder.
You will notice a floating cube as well. You can't place a cylinder on the cube - it only finds the Spatial Map. Demonstrating that you can't place a cylinder on it, is the cube's sole purpose ;). What might happen is that you place a cylinder behind the cube on the Spatial Map, if your opposite wall is closer than two meters. It requires additional logic to handle that situation, but that is beyond the scope of this blog post.
Starting, Stopping, and Clearing the Spatial Map
For some apps, most notably my AMS HoloATC app, the Spatial Map is used to help to get an initial place to put an object but then it needs to go away, as to not get the view blocked by occlusion. Making the Spatial Map transparent sometimes helps, but then still the walls get in the way of selecting objects as they block the gaze and other cursors. Long story short - it is sometimes desirable to be able to turn the Spatial map on and off. And this is actually pretty simple:
public void ToggleSpatialMap()
{
if( CoreServices.SpatialAwarenessSystem != null)
{
if( IsObserverRunning )
{
CoreServices.SpatialAwarenessSystem.SuspendObservers();
CoreServices.SpatialAwarenessSystem.ClearObservations();
}
else
{
CoreServices.SpatialAwarenessSystem.ResumeObservers();
}
}
}
Note that "ClearObservations
" is necessary, as merely calling Suspend only stops the updating of the Spatial Map — the graphic representation still stays active. This was actually added after feedback from yours truly ;)
As to checking whether or not the observer(s) are actually running, I have devised this little trick
private bool IsObserverRunning
{
get
{
var providers =
((IMixedRealityDataProviderAccess)CoreServices.SpatialAwarenessSystem)
.GetDataProviders<IMixedRealitySpatialAwarenessObserver>();
return providers.FirstOrDefault()?.IsRunning == true;
}
}
I check if there's an observer and assume that if the first one is running, so is probably the rest. Although in practice, on a HoloLens, there will be only one observer running anyway.
You can activate and de-activate the Spatial Map by pressing the floating button, where the SpatialMapToggler behavior is attached to.
Conclusion
If you run and deploy the demo project, you will find a button floating before you (in the direction that you looked when the app started) that you can use to toggle the Spatial Map, and to the right a little cube. In addition, a text floating in your vision instructs you either to look at the spatial map or air tap when you actually do — and then a cylinder will appear. Like this in this little video:
Published at DZone with permission of Joost van Schaik, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments