This feature is experimental and may not work as expected. For more information about it, see the Device Mapping Feature page.
How to Place Virtual Content Using a Device Map
Now that we have created a device map and saved it to a file, we can read it to place virtual objects in the AR scene. In this how-to, we will cover reading a device map file into your Unity project and how to use it to place content.
Prerequisites
Before you start, complete How to Create a Device Map. You will need the Unity project from that tutorial for this one. You will also need one prefab asset to place as virtual content, such as a basic cube.
Placing Content on the Map
-
Select
DeviceMappingDemofrom the Hierarchy, then, in the Inspector, click Add Component. Search for and add a New Script to it, then name itTracker.cs. -
Open
Tracker.csand replace its contents with the following snippet:using System.Collections;
using System.IO;
using NianticSpatial.NSDK.AR.Mapping;
using NianticSpatial.NSDK.AR.PersistentAnchors;
using NianticSpatial.NSDK.AR.VPS2;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
public class Tracker : MonoBehaviour
{
} -
In the Hierarchy, select the XROrigin, then, in the Inspector, click Add Component and add an
ARVps2Managerto it. -
Add a reference to the manager and an
Awake()method so VPS2 uses device map localization (and turns off VPS map and universal localization for this tutorial flow):[SerializeField]
private ARVps2Manager _vps2Manager;
private void Awake()
{
_vps2Manager.DeviceMapLocalizationEnabled = true;
_vps2Manager.VpsMapLocalizationEnabled = false;
_vps2Manager.UniversalLocalizationEnabled = false;
}VPS2 reads these settings when the subsystem starts. Set them in
Awake()or match them on the ARVps2Manager component in the Inspector before play. -
Add UI elements to the
Trackerscript for choosing a device map:- Add a button to start and stop tracking:
- Right-click in the Hierarchy, then open the Create menu and select Button from the UI sub-menu. This will create a Canvas element and add the button to it.
- In the script, add a serialized field for the button.
- Create a private method,
OnStartTrackingClicked(), and have it listen for button clicks inStart().
[SerializeField]
private Button _startTrackingButton;
private void Start()
{
_startTrackingButton.onClick.AddListener(OnStartTrackingClicked);
}
// When tracking button is clicked
private void OnStartTrackingClicked()
{
} - Add a button to start and stop tracking:
-
Add logic to
Tracker.csthat toggles the tracking state when the button is clicked:private ARVps2Anchor _anchor;
// variable for reference to the object on the map
private GameObject _anchorVisualObject;
private bool _isTrackingRunning;
// When tracking button is clicked
private void OnStartTrackingClicked()
{
var buttonText = _startTrackingButton.GetComponentInChildren<Text>();
if (_isTrackingRunning)
{
// Stop tracking if running and clean up anchor
if (_anchor)
{
_vps2Manager.RemoveAnchor(_anchor);
_anchor = null;
_anchorVisualObject = null;
}
buttonText.text = "Start Tracking";
_isTrackingRunning = false;
}
// otherwise, change the button to "stop" and start tracking
else
{
buttonText.text = "Stop Tracking";
_isTrackingRunning = true;
StartCoroutine(RestartTracking());
}
} -
Add the next code snippet to restart tracking using the map from the file:
- We need to clean up some elements before restarting tracking:
- If there is already an anchor, remove it. Wait a moment before continuing because the anchor can take an extra frame to be removed in some situations.
- Disable and re-enable
ARVps2Managerto restart tracking. We wait a moment between stopping and starting so that subsystem state is cleaned up asynchronously.
- Once cleanup is done, we can set the saved serialized map data from the file we saved it into earlier.
- Read the serialized map data into memory and pass it to
DeviceMapAccessController.AddMap()so that it will be tracked.
- Read the serialized map data into memory and pass it to
- Call
ARVps2Manager.TryTrackAnchorwith a base64 payload string (see snippet) to start tracking again using a new anchor.
- We need to clean up some elements before restarting tracking:
-
After adding this snippet to
Tracker.cs, open the Hierarchy and select DeviceMappingDemo. Then, in the Inspector, findTracker.csand assign ARVps2Manager from the XROrigin to the fields in the script.private DeviceMapAccessController _deviceMapAccessController;
private void Awake()
{
_vps2Manager.DeviceMapLocalizationEnabled = true;
_vps2Manager.VpsMapLocalizationEnabled = false;
_vps2Manager.UniversalLocalizationEnabled = false;
_deviceMapAccessController = DeviceMapAccessController.Acquire();
}
private IEnumerator RestartTracking()
{
if (_anchor)
{
_vps2Manager.RemoveAnchor(_anchor);
_anchor = null;
_anchorVisualObject = null;
}
// wait a moment after removing anchor
yield return null;
_vps2Manager.enabled = false;
// wait a moment before toggling tracking
yield return null;
_vps2Manager.enabled = true;
// Read a new device map from file
var path = Path.Combine(Application.persistentDataPath, Mapper.MapFileName);
var serializedDeviceMap = File.ReadAllBytes(path);
// Assign the device map to the DeviceMapAccessController
_deviceMapAccessController.AddMap(serializedDeviceMap);
// Create a root anchor payload and track it
if (_deviceMapAccessController.CreateRootAnchor(out var anchorPayload))
{
_vps2Manager.TryTrackAnchor(anchorPayload, out _anchor);
}
} -
When the anchor enters the Tracking state, place virtual objects according to the device map:
- Create variables to hold the virtual content:
- Add a serialized field variable for the prefab, then assign it to yours.
- Replace your earlier
Start()method so you subscribe once to the button and toARVps2Manager.trackablesChanged(VPS2 consolidates anchor add/update/remove into this event). - Add
OnDestroy()to unsubscribe, and implement the handler:- For the
ARVps2Anchorreference you received fromTryTrackAnchor, when itstrackingStateisTracking, instantiate an object from the prefab with the tracked anchor as its parent.
- For the
[SerializeField]
private GameObject _locationAnchorPrefab;
private void Start()
{
_startTrackingButton.onClick.AddListener(OnStartTrackingClicked);
_vps2Manager.trackablesChanged += OnVps2TrackablesChanged;
}
private void OnDestroy()
{
if (_vps2Manager != null)
{
_vps2Manager.trackablesChanged -= OnVps2TrackablesChanged;
}
_deviceMapAccessController?.Release();
_deviceMapAccessController = null;
}
// Event listener for anchor changes
private void OnVps2TrackablesChanged(ARTrackablesChangedEventArgs<ARVps2Anchor> args)
{
foreach (var u in args.updated)
{
// Handle anchor updates
}
// `args` contains added anchors and deleted anchors. You can process them as needed
} - Create variables to hold the virtual content:
Your virtual object should now be visible!
Completed Tracker Script
If you are having trouble with your tracking script, compare it to the finished product here!
Click to reveal the final Tracker.cs script
using System.Collections;
using System.IO;
using NianticSpatial.NSDK.AR.Mapping;
using NianticSpatial.NSDK.AR.PersistentAnchors;
using NianticSpatial.NSDK.AR.VPS2;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
public class Tracker : MonoBehaviour
{
[SerializeField]
private ARVps2Manager _vps2Manager;
[SerializeField]
private GameObject _locationAnchorPrefab;
[SerializeField]
private Button _startTrackingButton;
private GameObject _anchorVisualObject;
private ARVps2Anchor _anchor;
private bool _isTrackingRunning;
private DeviceMapAccessController _deviceMapAccessController;
private void Awake()
{
_vps2Manager.DeviceMapLocalizationEnabled = true;
_vps2Manager.VpsMapLocalizationEnabled = false;
_vps2Manager.UniversalLocalizationEnabled = false;
_deviceMapAccessController = DeviceMapAccessController.Acquire();
}
private void Start()
{
_vps2Manager.trackablesChanged += OnVps2TrackablesChanged;
_startTrackingButton.onClick.AddListener(OnStartTrackingClicked);
}
private void OnDestroy()
{
if (_vps2Manager != null)
{
_vps2Manager.trackablesChanged -= OnVps2TrackablesChanged;
}
}
private void OnStartTrackingClicked()
{
var buttonText = _startTrackingButton.GetComponentInChildren<Text>();
if (_isTrackingRunning)
{
if (_anchor)
{
_vps2Manager.RemoveAnchor(_anchor);
_anchor = null;
_anchorVisualObject = null;
}
buttonText.text = "Start Tracking";
_isTrackingRunning = false;
}
else
{
buttonText.text = "Stop Tracking";
_isTrackingRunning = true;
StartCoroutine(RestartTracking());
}
}
private IEnumerator RestartTracking()
{
if (_anchor)
{
_vps2Manager.RemoveAnchor(_anchor);
_anchor = null;
_anchorVisualObject = null;
}
// wait a moment after removing anchor
yield return null;
_vps2Manager.enabled = false;
// wait a moment before toggling tracking
yield return null;
_vps2Manager.enabled = true;
// Read a new device map from file
var path = Path.Combine(Application.persistentDataPath, Mapper.MapFileName);
var serializedDeviceMap = File.ReadAllBytes(path);
// Assign the device map to the DeviceMapAccessController
_deviceMapAccessController.AddMap(serializedDeviceMap);
// Create a root anchor payload and track it
if (_deviceMapAccessController.CreateRootAnchor(out var anchorPayload))
{
_vps2Manager.TryTrackAnchor(anchorPayload, out _anchor);
}
}
private void OnVps2TrackablesChanged(ARTrackablesChangedEventArgs<ARVps2Anchor> args)
{
if (_anchor == null || _locationAnchorPrefab == null)
return;
void ConsiderAnchor(ARVps2Anchor anchor)
{
if (anchor != _anchor)
return;
if (anchor.trackingState == TrackingState.Tracking && !_anchorVisualObject)
{
_anchorVisualObject = Instantiate(_locationAnchorPrefab, _anchor.transform);
Debug.Log("Tracking");
}
}
foreach (var a in args.added)
ConsiderAnchor(a);
foreach (var u in args.updated)
ConsiderAnchor(u);
}
}