Place virtual content with VPS2
This guide explains how to place virtual content in an AR scene relative to a localized asset or at known geo coordinates.
Virtual content is any digital object rendered in AR, such as a 3D model, label, or visual effect. To appear in the correct location and remain stable as the user moves, it needs a reliable spatial reference. Depending on the placement approach, the app either attaches the content to a localized asset's origin or repeatedly calculates its AR pose from geo coordinates.
When VPS2 localizes to a Site, it matches the camera view against the Site's VPS map asset. The asset's origin defines a tracked position and orientation in the real world. Placing virtual content relative to that origin allows the AR system to keep the content aligned with the environment over time.
VPS2 handles the conversions between AR space, map-relative space, and global space, so content placed against an asset can be expressed in whichever frame your app needs.
Because the asset is a mapped real-world location, content placed relative to it appears consistently across sessions and devices.
Follow this workflow:
- Choose a placement approach based on where the content's position comes from and how your app needs to restore it.
- Follow the readiness guidance for that approach. For asset-relative placement, localize to the Site and evaluate the localized asset's tracking data and confidence. For geo-coordinate placement, evaluate the device's VPS2 tracking state and reported geolocation accuracy.
- Use the instructions for the selected approach to place the content and save the data needed to restore it.
- Test the placement and verify that the content remains stable as the AR session updates.
You can optionally use a Site mesh when positioning content, either as an asset-aligned placement guide or as a georeferenced authoring reference.
Choose the approach once based on your application's placement and persistence requirements. If its readiness conditions are not met, wait for localization to improve or show a localizing state rather than switching approaches.
Prerequisites
Before you start, make sure you already have:
- a project with NSDK installed and a basic AR scene set up
- a Site in Scaniverse for workflows that localize to a Site or use Site-derived content
For setup details, see Set up the Niantic SDK, Set up a basic AR scene, and First Localization with NSDK.
Choose how to place virtual content
There are multiple ways to place virtual content in an AR scene. Each approach defines how content is positioned, how it behaves over time, and what data is stored so it can be reused. Choose an approach based on where the content's position comes from and how it must be restored. The current device or asset tracking state does not determine which approach to use.
Key considerations
- Using multiple approaches - Different features in your app can use different placement approaches. You do not need to choose only one for the entire application.
- Persistence - Each approach defines what data, if any, is stored for reuse, and whether that reuse is limited to the current session or can work across sessions and devices:
- VPS-based placement stores data relative to a real-world location, which can allow content to appear in the same place across sessions and devices.
- Local placement can keep content stable during the current session, including user-driven placement such as hit tests, but it does not necessarily restore to the same real-world position later unless your app persists additional data.
Use the following table to compare the available approaches based on when to use them, what data they save, and how each one restores content later.
| Approach | Best for | Saved data | Restoration |
|---|---|---|---|
| Place content relative to a localized asset | You localize to a Site (by Site ID, or by coordinates for nearby Sites), then want content to stay attached to that asset's origin across sessions. | The Site ID, the asset ID, and each content item's asset-local transform. | Re-localizes to the same Site and reapplies the saved asset-local transform. |
| Place content using geo coordinates | Content is authored at a known latitude, longitude, and altitude. | Geolocation data such as latitude, longitude, altitude, and optional heading. | Reconstructs placement from geospatial coordinates and reapplies the resulting pose. |
Know which readiness signal your approach uses
The two approaches read different readiness signals, and using the wrong one is a common source of misplaced content:
| Approach | Check this before placing | Why |
|---|---|---|
| Place content relative to a localized asset | The asset's tracking data | The content is attached to the asset's origin, so the asset's pose is what has to be reliable. |
| Place content using geo coordinates | The device's Vps2TrackingState | The content's position comes from the device's localization, not from a tracked asset. |
These two states change independently — a device can report precise while an asset is not yet returning tracking data. See Tracking states.
Place content relative to a localized asset
Use this approach when the user decides where content should appear during a localized session. Save the content's local transform relative to the asset's origin, then apply that same local transform again the next time the asset is tracked.
When you check an asset's tracking data, follow these rules:
| Rule | Why it matters | What to do |
|---|---|---|
| Evaluate each asset separately. | If you localize to more than one Site, each asset is tracked on its own. Do not combine them into a single session-wide flag, or a distant asset that is not tracked will misreport a nearby asset that is. | Store and evaluate readiness by asset ID. |
| Tracking data means the pose is usable. | When VPS2 returns asset tracking data, the origin pose is ready to place content against. The data also carries a confidence value (0.0–1.0) you can use as an extra signal. | Place content once tracking data is available. |
| Check again after AR tracking is disturbed. | If the camera is covered, the device is moved abruptly, or the session is interrupted, the asset can stop returning tracking data while content stays on screen at an out-of-date position. | Hide the placed content while tracking data is unavailable, and show it again once it returns. |
Localize to a Site with TryLocalize on ARVps2Manager, track the localized asset with TryTrackAsset, and place content under the returned ARVps2Asset trackable, whose transform follows the asset's origin in Unity world space.
Follow this workflow to place, save, and later restore content relative to a localized asset:
-
Localize to a Site with one of the
TryLocalizeoverloads. Localization is asynchronous; VPS2 reports the localized asset throughTryGetLatestLocalizationonce the device matches a Site's VPS map.- To localize to a specific Site, create a Site in Scaniverse and get its Site ID (from Scaniverse or the Sites API), then call
TryLocalize(siteId). - To localize to nearby Sites, call
TryLocalize(latitude, longitude, radiusMeters)with the device's location. You do not need a Site ID. While the device localizes, guide the user toward a successful localization, for example by prompting them to hold steady, improve the camera angle, or check their connection.
- To localize to a specific Site, create a Site in Scaniverse and get its Site ID (from Scaniverse or the Sites API), then call
-
Track the localized asset with
TryTrackAsset, using the asset ID thatTryGetLatestLocalizationreports. Keep the returnedARVps2Asset, because your content will be attached relative to it. This code is introduced in step 1 of the following runnable example. -
Wait until the asset reaches
Tracking. This prevents you from placing content against an unsettled asset pose: the trackable starts at the scene origin and snaps to the asset's origin when tracking data first arrives. An asset whose tracking lapses degrades toLimitedand keeps its last known pose, so also hide placed content while the asset is notTracking. This check is introduced in step 1 of the following runnable example. -
When the user chooses a placement pose, instantiate the content and parent it under
asset.transform. Parenting the content under the tracked asset keeps it aligned as VPS2 refines the asset's origin. This code is introduced in step 1 of the following runnable example. -
Save the Site ID and asset ID together with the content's
localPositionandlocalRotation. Those values are what let you restore the same content relative to the same asset later. This code is introduced in step 1 of the following runnable example. Expand the following example if you want a runnable Unity component that implements this workflow end to end, including localizing to the Site, tracking its asset, placing content under it, and saving the asset-relative pose for later restoration.Runnable example: Place content relative to a localized asset
The code comments in this example refer to the workflow steps in the previous list. To wire this example into a runnable scene, do the following:
This example already includes a small set of
Debug.LogandDebug.LogErrorstatements that you can use during testing. Open the Unity Console while the scene is running to confirm that localization starts, the placement succeeds, and the pose is saved.To keep this example runnable without extra UI, paste a known Site ID into the
Site Idfield on thePlaceContentAtLocalizedAssetcomponent in the Unity Inspector. Get the ID by navigating to the Site in the Scaniverse portal and copying it from the URL, or from the Sites API.-
Create a new Unity script file named
PlaceContentAtLocalizedAsset.cs, then replace its contents with the following code: The following example defines the component that localizes to a Site, tracks its localized asset, places content under that asset, and saves the asset-relative pose:using System;
using NianticSpatial.NSDK.AR;
using NianticSpatial.NSDK.AR.VPS2;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR.ARSubsystems;
[Serializable]
public struct SavedAssetRelativeContent
{
public string siteId;
public string assetId;
public Vector3 localPosition;
public Quaternion localRotation;
}
public class PlaceContentAtLocalizedAsset : MonoBehaviour
{
[SerializeField] private ARVps2Manager _arVps2Manager;
[SerializeField] private string _siteId;
[SerializeField] private GameObject _contentPrefab;
[SerializeField] private Button _placeButton;
[SerializeField] private Camera _arCamera;
[SerializeField, Range(0f, 1f)] private float _minimumTrackingConfidence = 0f;
private ARVps2Asset _asset;
private void OnEnable()
{
_placeButton.onClick.AddListener(OnPlaceButtonClicked);
// Workflow step 1: request localization to the Site by its Site ID.
if (!string.IsNullOrWhiteSpace(_siteId) && !_arVps2Manager.TryLocalize(_siteId))
{
Debug.LogError("PlaceContentAtLocalizedAsset: TryLocalize failed.");
}
}
private void OnDisable()
{
_placeButton.onClick.RemoveListener(OnPlaceButtonClicked);
}
private void Update()
{
if (_asset != null)
{
return;
}
// Workflow step 2: once VPS2 reports the localized asset, take its trackable. The
// asset manager already tracks the localized asset, so TryTrackAsset returns the
// existing trackable, whose transform follows the asset's origin in Unity world space.
if (_arVps2Manager.TryGetLatestLocalization(out var localization) &&
localization.LocalizedAsset.HasValue)
{
_arVps2Manager.TryTrackAsset(localization.LocalizedAsset.Value.AssetId, out _asset);
}
}
private void OnPlaceButtonClicked()
{
// Workflow step 3: wait until the asset's origin is tracked before placing content.
if (_asset == null ||
_asset.trackingState != TrackingState.Tracking ||
_asset.Confidence < _minimumTrackingConfidence)
{
return;
}
// Workflow step 4: place the content in world space, then parent it under the asset.
var worldPosition = _arCamera.transform.position + _arCamera.transform.forward;
var worldRotation = Quaternion.LookRotation(_arCamera.transform.forward, Vector3.up);
var content = Instantiate(_contentPrefab, worldPosition, worldRotation);
content.transform.SetParent(_asset.transform, true);
// Workflow step 5: save the Site ID and asset ID plus the content's asset-local pose.
var savedPose = new SavedAssetRelativeContent
{
siteId = _asset.SiteId,
assetId = _asset.AssetId,
localPosition = content.transform.localPosition,
localRotation = content.transform.localRotation
};
SavePose(savedPose);
}
private void SavePose(SavedAssetRelativeContent savedPose)
{
Debug.Log("PlaceContentAtLocalizedAsset: content placed and pose saved.");
// Store the Site ID, asset ID, and local pose in PlayerPrefs, a file, or your backend.
}
} -
Create a clean test scene for this example. In a fresh Unity scene, add the following objects from the
Hierarchymenu:- in the
Hierarchycontext menu, chooseXR > AR Session - in the
Hierarchycontext menu, chooseXR > XR Origin (Mobile AR) - create an empty
GameObject, name itAR Input Manager, then useAdd Componentto addAR Input Manager - in the
Hierarchycontext menu, chooseUI > Canvas - under
Canvas, chooseUI > Button - an empty
GameObjectnamedPlacedContentManagerThen: - add
ARVps2ManagertoXR Origin. Unity also addsARVps2AnchorManagerandARVps2AssetManagerautomatically; the asset manager serves theARVps2Assettrackables this example places content under. - add
PlaceContentAtLocalizedAssettoPlacedContentManager - add your new scene to
Build Profiles > Scenesand move it ahead ofHomeso the app opens directly into it on device
- in the
-
Select the
GameObjectthat has thePlaceContentAtLocalizedAssetcomponent, then assign the serialized fields in the Inspector:- Drag the object that has
ARVps2Managerinto theARVps2Managerfield. In thensdk-samples-csharpproject, dragXR Origininto theARVps2Managerfield. - Paste the Site ID into the
Site Idfield. - Drag your AR camera into the camera field. In the
nsdk-samples-csharpproject, dragMain CamerafromXR Origin > Camera Offset > Main Camerainto the camera field. - Create or choose the UI button that should trigger placement, then drag it into the button field.
- Drag the prefab asset you want to place into the content field. For a fast visible test object, create
Hierarchy > 3D Object > Cube, then drag thatCubeinto your project to create a prefab asset. - Set
Minimum Tracking Confidenceto the threshold required by your experience. The default0demonstrates the minimum tracked-state check; increase it when content must align precisely with a real-world surface.
- Drag the object that has
-
Run the scene and wait for the asset to reach
Tracking.OnEnable()callsTryLocalize(_siteId)automatically when theSite Idfield is not empty, andUpdate()tracks the localized asset as soon as VPS2 reports it. View the asset status in your device logs by logging_asset.trackingStatewhile testing. Wait until the asset status reachesTrackingbefore pressing the button you created in step 3. The on-screen validation order for this runnable example is:- launch the scene on device
- wait for the localized asset to reach
Tracking - press the placement button once Build to iPhone or Android to validate asset-relative placement at a real Site.
-
-
Restore the content in a later session. Localize to the same Site, track the same asset by its saved ID, then reapply the saved local transform so the content returns to the same real-world location. This code is introduced in steps 1 through 6 of the following runnable example. Expand the following example if you want a runnable Unity restore component for the same workflow. Test it in a second run after you already placed content once and captured the saved Site ID, asset ID, and local transform values from step 5.
Runnable example: Restore content relative to a localized asset
The code in this example implements the restore phase from step 6 of the previous list. To wire it into the same scene and test it, do the following:
-
Create a new Unity script file named
ContentRestore.cs, then replace its contents with the following code: The following example defines the component that localizes to the saved Site again, tracks the saved asset, and reapplies the saved local pose when tracking is stable. This script reuses theSavedAssetRelativeContentstruct that you already created inPlaceContentAtLocalizedAsset.csin step 5:using NianticSpatial.NSDK.AR;
using NianticSpatial.NSDK.AR.VPS2;
using UnityEngine;
using UnityEngine.XR.ARSubsystems;
public class ContentRestore : MonoBehaviour
{
[SerializeField] private ARVps2Manager _arVps2Manager;
[SerializeField] private GameObject _contentPrefab;
[SerializeField] private SavedAssetRelativeContent _savedContent;
[SerializeField, Range(0f, 1f)] private float _minimumTrackingConfidence = 0f;
private ARVps2Asset _asset;
private bool _contentRestored;
public bool BeginRestore()
{
var started = _arVps2Manager.TryLocalize(_savedContent.siteId);
Debug.Log($"ContentRestore: BeginRestore started = {started}");
return started;
}
private void Update()
{
if (_contentRestored)
{
return;
}
// The saved asset can be tracked as soon as its Site's localize request has
// been submitted; the trackable snaps to the asset's origin once tracking
// data arrives.
if (_asset == null && !_arVps2Manager.TryTrackAsset(_savedContent.assetId, out _asset))
{
return;
}
if (_asset.trackingState != TrackingState.Tracking ||
_asset.Confidence < _minimumTrackingConfidence)
{
return;
}
var content = Instantiate(_contentPrefab, _asset.transform);
content.transform.localPosition = _savedContent.localPosition;
content.transform.localRotation = _savedContent.localRotation;
_contentRestored = true;
Debug.Log("ContentRestore: content restored under the tracked asset.");
}
} -
Add
ContentRestoreto the same scene object that already stays active while VPS2 localization is running. In a fresh Unity scene, add it to the samePlacedContentManagerGameObjectthat already holdsPlaceContentAtLocalizedAsset. In thensdk-samples-csharpproject, the closest reference point is theVPS2LocalizeDemoobject inNsdkSamples/Assets/Samples/VPS2/Scenes/VPS2Localization.unity. -
Select the
GameObjectthat has theContentRestorecomponent, then assign the serialized fields in the Inspector:- Drag the object that has
ARVps2Managerinto theARVps2Managerfield. In thensdk-samples-csharpproject, dragXR Origininto theARVps2Managerfield. - Drag the same prefab asset you used during placement into the content field. In the
nsdk-samples-csharpproject, use the same prefab you assigned toPlaceContentAtLocalizedAsset. - Enter the saved
siteId,assetId,localPosition, andlocalRotationinto theSaved Contentfield. For a real restore test, use the values you captured from the successful placement run in step 5. - Use the same
Minimum Tracking Confidencethreshold as the placement flow.
- Drag the object that has
-
Add a small helper script that starts the restore flow when the scene loads. Create a new Unity script file named
RestoreStarter.cs, then place it on the samePlacedContentManagerobject asContentRestore.The following script example starts the restore flow automatically when the scene loads:
using UnityEngine;
public class RestoreStarter : MonoBehaviour
{
[SerializeField] private ContentRestore _contentRestore;
private void Start()
{
_contentRestore.BeginRestore();
}
} -
Select the
GameObjectthat has theRestoreStartercomponent, then drag theContentRestorecomponent into theContent Restorefield. For the restore run, disablePlaceContentAtLocalizedAssetso the scene does not place more content when it starts. -
Build to iPhone or Android and run the scene again at the same Site. Do not press the placement button in this run. Wait for the asset to reach
Tracking. The on-screen validation order for this runnable example is:- launch the restore scene on device
- wait for the saved asset to reach
Tracking - do not press any placement button in this run, because
RestoreStarterbegins restore automatically The expected result is that the content is recreated automatically under the tracked asset and returns to the same real-world location.
-
Anchors remain supported: track a Site anchor from its payload with TryTrackAnchor and place content relative to the returned ARVps2Anchor, served by ARVps2AnchorManager. The VPS2Localization sample scene keeps this flow in the disabled-by-default VPS2AnchorLocalizeDemo component. TryCreateAnchor additionally creates an anchor at an arbitrary pose, for example far away from the Site. New apps should prefer the localized-asset flow above.
Place content using geo coordinates
When you place content from geo coordinates, follow this rule:
| Rule | Why it matters | What to do |
|---|---|---|
| Wait for the precise state before placing. | TryGetPose returns a pose even in the coarse tracking state, but it is only accurate enough to anchor virtual content once the state is precise. | Gate content placement on the precise state, and treat coarse poses as approximate. |
Follow this workflow to place content from geo coordinates:
- Get the known latitude, longitude, and altitude for the real-world location where the content should appear. For example, these values might come from authored content data, a backend, or a fixed test location. For quick testing, use a known nearby coordinate from a maps app.
- On each update, call
TryGetPosewith the target latitude, longitude, and altitude, and an EDN orientation quaternion (oftenQuaternion.identitywhen the target has no specific heading).TryGetPoseuses the latest localization and returnsfalseuntil VPS2 has localized. The returnedXRVps2Posecarries itsTrackingState; when your content needs accurate placement, wait for thePrecisestate. This code is introduced in step 1 of the following runnable example. - Apply the returned pose to your
GameObjectand keep updating it as VPS2 improves. Repeating this conversion lets the content stay aligned to the latest VPS2 estimate. This code is introduced in step 1 of the following runnable example.
The following example converts a fixed geolocation into an AR pose and applies that pose to a Unity GameObject as VPS2 improves:
Runnable example: Place content from geo coordinates
The code comments in this example refer to the workflow steps in the previous list. To wire this example into a runnable scene and test it, do the following:
-
Create a new Unity script file named
GeoPositionedObjectHelper.cs. In thensdk-samples-csharpproject, place the script inNsdkSamples/Assets/Samples/VPS2/Scripts. The following example defines the component that converts a fixed geolocation into aGameObjectpose. It includes a smallDebug.Logyou can watch on device to confirm that the helper is applying updated poses:using NianticSpatial.NSDK.AR;
using NianticSpatial.NSDK.AR.VPS2;
using NianticSpatial.NSDK.AR.XRSubsystems;
using UnityEngine;
public class GeoPositionedObjectHelper : MonoBehaviour
{
[SerializeField] private ARVps2Manager _vps2Manager;
[SerializeField] private double _latitude;
[SerializeField] private double _longitude;
[SerializeField] private double _altitude;
void Update()
{
// Workflow step 2: convert the target geolocation into an AR pose. TryGetPose uses
// the latest localization and returns false until VPS2 has localized.
if (!_vps2Manager.TryGetPose(
_latitude,
_longitude,
_altitude,
Quaternion.identity,
out var pose))
{
return;
}
// This content needs accurate placement, so wait for the precise tracking state.
if (pose.TrackingState != Vps2TrackingState.Precise)
{
return;
}
// Workflow step 3: apply the latest pose to the GameObject.
gameObject.transform.SetPositionAndRotation(
pose.Pose.position,
pose.Pose.rotation);
Debug.Log("GeoPositionedObjectHelper: applied latest pose to GameObject.");
}
} -
Create a clean test scene for this example. In a fresh Unity scene, add the following objects from the
Hierarchymenu:- in the
Hierarchycontext menu, chooseXR > AR Session - in the
Hierarchycontext menu, chooseXR > XR Origin (Mobile AR) - create an empty
GameObject, name itAR Input Manager, then useAdd Componentto addAR Input Manager - in the
Hierarchycontext menu, choose3D Object > Cube, then rename itGeoContentThen: - add
ARVps2ManagertoXR Origin - add
GeoPositionedObjectHelpertoGeoContentAdd your new scene toBuild Profiles > Scenesand move it ahead ofHomeso the app opens directly into it on device.
- in the
-
Select the
GameObjectthat has theGeoPositionedObjectHelpercomponent, then assign the serialized fields in the Inspector:- Drag the object that has
ARVps2Managerinto theARVps2Managerfield. In thensdk-samples-csharpproject, dragXR Origininto theARVps2Managerfield. - Enter the target latitude in the
Latitudefield. In thensdk-samples-csharpproject, enter the latitude for the real-world location you want to test. - Enter the target longitude in the
Longitudefield. In thensdk-samples-csharpproject, enter the longitude for the same real-world location. - Enter the target altitude in the
Altitudefield. In thensdk-samples-csharpproject, enter the altitude for that same location. If the latitude and longitude are correct butGeoContentis not visible, the altitude may be placing it too high or too low. Start with a large visible cube and adjust altitude until it enters view.
- Drag the object that has
-
Build to iPhone or Android and run the scene while VPS2 is localizing. This helper does not start VPS2 by itself. It only reads the latest localization after
ARVps2Managerhas started. The on-screen validation order for this runnable example is:-
launch the scene on device
-
let VPS2 localize normally
-
do not press a placement button, because this helper updates automatically during
Update()Watch the device logs for:The following text example shows the log message that confirms the helper applied an updated pose:
GeoPositionedObjectHelper: applied latest pose to GameObject.
The expected result is that the visible
GeoContentcube moves automatically into the AR pose for that real-world location and stays aligned as localization improves. Unlike the asset-relative example, this flow does not use a placement button. -
Use a Site mesh when positioning content
A Site mesh is optional for both placement approaches. It is a 3D model of the physical environment reconstructed from a scan. An app can render it, use it for collision or occlusion, or use it as a visual guide for placing content. The mesh itself does not track the Site.
A Site mesh can support either placement approach:
- For precise runtime placement, stream the mesh under the localized asset's trackable and save the content's asset-local transform.
- For geo-coordinate placement, use a georeferenced mesh as an authoring reference for choosing coordinates. Runtime accuracy still depends on device localization and the map's geo-alignment.
The following workflow demonstrates precise asset-relative placement. Tracking and mesh geometry are requested separately using the same VPS map asset ID:
- Localize to the Site and track its localized asset, as shown in Place content relative to a localized asset.
- Stream the Site mesh with
LocationMeshManager.GetLocationMeshChunksByVpsMapAssetIdAsync, passing the same asset ID and the Site's approximate coordinates. Chunks arrive one at a time, closest-first to the given position. - Add each downloaded chunk as a child of the asset's trackable without changing its local transform, then activate it. Chunks arrive inactive so they never flash at the scene origin.
- Add virtual content as a child of the same trackable. Use the mesh as a guide for its local position, rotation, and scale.
The following excerpt shows how the four steps connect. It is intentionally not a complete component: assign the manager and prefab references, and call the asynchronous portion from your app's localization flow.
// 1. Track the localized asset reported by TryGetLatestLocalization.
if (!_arVps2Manager.TryTrackAsset(localizedAssetId, out ARVps2Asset siteAsset))
return;
// 2. Stream the Site mesh chunks using the same asset ID.
await foreach (var chunk in _locationMeshManager.GetLocationMeshChunksByVpsMapAssetIdAsync(
localizedAssetId, siteLatitude, siteLongitude, getTexture: true))
{
// 3. Align each chunk with the asset's trackable, then activate it.
chunk.transform.SetParent(siteAsset.transform, false);
chunk.SetActive(true);
}
// 4. Place content relative to the same asset, using the mesh as a visual guide.
GameObject content = Instantiate(_contentPrefab, siteAsset.transform);
content.transform.localPosition = contentPositionOnMesh;
content.transform.localRotation = contentRotationOnMesh;
See Place content relative to a localized asset for the full
placement workflow, Place content using geo coordinates for runtime geo placement, and Mesh Download API for mesh download options. The VPS2AssetLocalizeDemo.cs script in the sample project implements this flow end to end.
Test Your Placement
In addition to testing your app on-location, you can also use Playback in order to iterate quickly without traveling to a VPS-enabled Site.
Playback runs NSDK against a prerecorded AR session dataset instead of live camera input. It can exercise the full asset-relative and geo-coordinate workflows, as long as the dataset was recorded at the Site. Still, Playback is only a simulation of a live session, so it is best for iteration and debugging rather than final validation. On-location device testing is the most accurate way to confirm that content appears in the correct real-world place for the actual Site.