# Get started with VPS2 Source: https://www.nianticspatial.com/docs/nsdk/how-to/vps2/adding_vps2/ This guide shows how to add VPS2 to your project, configure its localization options, and manage its lifecycle. For an explanation of universal and VPS map localization, assets, and tracking states, see [Visual Positioning System 2](https://www.nianticspatial.com/docs/nsdk/features/vps2/). ## Prerequisites This guide assumes that you have already completed: ### Platform: unity - [Set up the Niantic SDK for Unity](https://www.nianticspatial.com/docs/nsdk/setup/#set-up-the-niantic-sdk-for-unity) ### Platform: swift - [Set up the NSDK in Swift](https://www.nianticspatial.com/docs/nsdk/setup/#set-up-the-nsdk-in-swift) ### Platform: kotlin - [Set up the NSDK in Kotlin](https://www.nianticspatial.com/docs/nsdk/setup/#set-up-the-nsdk-in-kotlin) * [Authorization](https://www.nianticspatial.com/docs/nsdk/auth_getting_started/) If you are upgrading an existing ARDK 3.x application, see [Migrate from VPS to VPS2](https://www.nianticspatial.com/docs/nsdk/features/vps2_migration/). ### Platform: unity ## Add and configure VPS2 The Niantic Spatial Unity SDK (NSDK) uses AR Foundation as the interface for exposing its features. Adding VPS2 is therefore similar to adding other AR Foundation components. ### ARVps2Manager [`ARVps2Manager`](https://www.nianticspatial.com/docs/api/unity/NianticSpatial.NSDK.AR.VPS2.ARVps2Manager) is the entry point to VPS2: configuration, localization, geolocation, anchors, and asset tracking. It owns the VPS2 feature's lifecycle. Tracked assets and anchors are served as [trackables](https://docs.unity3d.com/Packages/com.unity.xr.arfoundation@6.4/manual/architecture/managers.html#trackables-and-trackable-managers) by the [`ARVps2AssetManager`](https://www.nianticspatial.com/docs/api/unity/NianticSpatial.NSDK.AR.VPS2.ARVps2AssetManager) and [`ARVps2AnchorManager`](https://www.nianticspatial.com/docs/api/unity/NianticSpatial.NSDK.AR.VPS2.ARVps2AnchorManager) components it requires next to it; Unity adds both automatically when you add `ARVps2Manager` to a GameObject. The manager can also convert between local AR poses and global geopositions (and vice versa). (image: AR VPS2 Manager Component) #### Configuration Fields | Field | Default | What it controls | |---|---:|---| | **Universal Localization Enabled** | Disabled | When active, the VPS2 session sends camera imagery and the device's GPS position to the cloud for geopositioning, without needing a VPS map. This can improve geoposition accuracy beyond local sensor fusion but requires a network connection. If disabled, the device's geoposition away from VPS maps comes from local sensor fusion alone, which always runs. Either way, the VPS2 tracking state reaches at most `coarse` until it localizes to a VPS map. | | **Universal Localization Requests per Second** | `1` | Frequency of network requests sent for universal localization. | | **VPS Map Localization Enabled** | Enabled | When active, the VPS2 session sends AR sensor data and camera imagery to the server to localize against VPS maps. This only allows VPS map localization: no requests are sent until you call `localize` (Unity: `TryLocalize`) for a Site or a nearby coordinate. | | **Initial Requests per Second** | `1.0` | Server request frequency prior to the first successful VPS map localization. For example, `1.0` is one request per second. | | **Continuous Requests per Second** | `0.2` | Server request frequency after the initial VPS map localization. For example, `0.2` is one request every five seconds. | | **Temporal Fusion Enabled** | Enabled | Fuses successive VPS map localizations against the same map into one estimate, instead of using only the newest. Localizations that agree raise the asset's tracking confidence and steady its pose, at the cost of reacting more slowly to a genuine pose change. Unrelated to local sensor fusion of GPS and compass. | | **Geolocation Smoothing Enabled** | Enabled | Enables interpolation between localization updates. This minimizes "snapping" or visual jumps during abrupt GPS or compass recalibrations, for a more stable AR experience. | | **Blur Detection Enabled** | Disabled | Enables an advisory image blur check on the frames VPS2 sends for VPS map localization. A frame whose sharpness score falls below the threshold produces a `FrameFlagged` localization request record with error `ImageTooBlurry`, but is still sent for localization, never rejected. Use the signal to coach the user, for example "hold the device steady". See [Prompt users through blurry frames](https://www.nianticspatial.com/docs/nsdk/how-to/vps2/guide_users_during_localization/#prompt-users-through-blurry-frames). | | **Min Sharpness Score** | `0` | Sharpness threshold below which a frame is flagged as too blurry. The default of `0` flags nothing. Higher scores mean sharper images. There is no universal "sharp" value; `1000` is a good starting point and is the value the VPS2 samples use. | Universal and VPS map localization are configured independently. VPS map localization is enabled by default; universal localization is disabled by default, so turn it on explicitly if you want cloud geopositioning. Local sensor fusion is not configurable and runs in both cases. `ARVps2Manager` enables **VPS Map Localization** but not **Universal Localization**, so a scene that uses the component runs VPS map localization only, on top of local sensor fusion. Tick **Universal Localization Enabled** on the inspector component to run both together. ## Start and stop VPS2 VPS2 starts and stops according to the Unity component lifecycle. When started, VPS2 begins local sensor fusion, which provides coarse positioning estimates typically within a few seconds after the AR session begins running, and starts universal localization if it is enabled. VPS map localization does not start until you call [`TryLocalize`](https://www.nianticspatial.com/docs/api/unity/NianticSpatial.NSDK.AR.VPS2.ARVps2Manager.TryLocalize): - To localize to a specific Site, call [`TryLocalize(siteId)`](https://www.nianticspatial.com/docs/api/unity/NianticSpatial.NSDK.AR.VPS2.ARVps2Manager.TryLocalize) with its Site ID. Get a Site ID by [querying Sites](https://www.nianticspatial.com/docs/nsdk/how-to/sites/getting_started/#3-query-sites), or by navigating to the Site in the Scaniverse portal and copying its ID from the URL. - To localize to all nearby Sites, call [`TryLocalize(latitude, longitude, radiusMeters)`](https://www.nianticspatial.com/docs/api/unity/NianticSpatial.NSDK.AR.VPS2.ARVps2Manager.TryLocalize) with a coordinate and radius. A Site must have a processed VPS map tagged for production to be eligible for map-relative localization. Localize requests are additive, and the submitted Sites stay in the localization set until the manager stops. To check whether the Site has localized, call [`TryGetAssetTrackingData`](https://www.nianticspatial.com/docs/api/unity/NianticSpatial.NSDK.AR.VPS2.ARVps2Manager.TryGetAssetTrackingData) with the Site's asset ID, which you can get from the [Sites API](https://www.nianticspatial.com/docs/nsdk/how-to/sites/getting_started/) or from the localized asset in the latest localization; it returns data while that asset is tracked. Without an asset ID, it reports the asset the device most recently localized to, which may belong to a different Site when you localize to several. Do not use the session's [`Vps2TrackingState`](https://www.nianticspatial.com/docs/api/unity/NianticSpatial.NSDK.AR.XRSubsystems.Vps2TrackingState) for that check. For the difference between those two states, see [Tracking states](https://www.nianticspatial.com/docs/nsdk/features/vps2/#tracking-states). `TryLocalize(...)` requires a running manager: a request made while `ARVps2Manager` is disabled is rejected, returns `false`, and logs a warning. Enable the manager first, then request localization. When stopped, all localization and anchor state is reset. Set the configuration before enabling `ARVps2Manager`. While it runs, you can change request rates, blur detection, and area targeting margins through the `ARVps2Manager` properties; they apply live at the end of the frame. Editing fields in the Inspector during Play mode does not apply them. Avoid changing other fields on a running manager: depending on the field, the change briefly interrupts geopositioning (**Universal Localization Enabled**), takes effect only the next time the component is enabled (for example **Geolocation Smoothing Enabled**), or resets anchor tracking so assets must localize again. To change them, disable the component, change the fields, then enable it again. ### Platform: swift, kotlin ## Construct and Configure VPS2 ### Platform: swift [`NSDKVps2Session`](https://www.nianticspatial.com/docs/api/swift/NSDK.class-NSDKVps2Session) manages the VPS2 lifecycle: configuration, activation, and retrieval of geographic positioning data. It is created from an existing [`NSDKSession`](https://www.nianticspatial.com/docs/api/swift/NSDK.class-NSDKSession) and operates independently of other AR features. ### Platform: kotlin [`Vps2Session`](https://www.nianticspatial.com/docs/api/kotlin/com.nianticspatial.nsdk.vps2.Vps2Session) manages the VPS2 lifecycle: configuration, activation, and retrieval of geographic positioning data. It is created from an existing [`NSDKSession`](https://www.nianticspatial.com/docs/api/kotlin/com.nianticspatial.nsdk.NSDKSession) and operates independently of other AR features. ### Platform: swift ```swift let vps2Session = nsdkSession.acquireVps2Session() let config = NSDKVps2Session.Configuration( universalLocalizationEnabled: true, vpsMapLocalizationEnabled: true ) do { try vps2Session.configure(with: config) } catch { print("Configuration failed: \(error)") } ``` `NSDKVps2Session.Configuration` defaults `vpsMapLocalizationEnabled` to `true` and `universalLocalizationEnabled` to `false`, so a configuration that does not set them runs VPS map localization only, on top of local sensor fusion. Pass `true` for `universalLocalizationEnabled`, as the example above does, to also run cloud geopositioning; pass `false` for either one to turn that mode off. ### Platform: kotlin ```kotlin import com.nianticspatial.nsdk.Vps2Config val vps2Session = nsdkSession.vps2.acquire() // Example config, see documentation for all possible config options val config = Vps2Config( enableUniversalLocalization = true, enableVpsMapLocalization = true, ) try { vps2Session.configure(config) } catch (e: NsdkStatusException) { Log.e("VPS", "Configuration failed: ${e.message}") } ``` > **Note:** > > **`configure()` is optional** > > If you don't call `configure()`, VPS2 runs with the defaults in the following table, including VPS map localization on and universal localization off. Call it only to change those defaults. #### Configuration Fields | Field | Default | What it controls | |---|---:|---| | **Universal Localization Enabled** | Disabled | When active, the VPS2 session sends camera imagery and the device's GPS position to the cloud for geopositioning, without needing a VPS map. This can improve geoposition accuracy beyond local sensor fusion but requires a network connection. If disabled, the device's geoposition away from VPS maps comes from local sensor fusion alone, which always runs. Either way, the VPS2 tracking state reaches at most `coarse` until it localizes to a VPS map. | | **Universal Localization Requests per Second** | `1` | Frequency of network requests sent for universal localization. | | **VPS Map Localization Enabled** | Enabled | When active, the VPS2 session sends AR sensor data and camera imagery to the server to localize against VPS maps. This only allows VPS map localization: no requests are sent until you call `localize` (Unity: `TryLocalize`) for a Site or a nearby coordinate. | | **Initial Requests per Second** | `1.0` | Server request frequency prior to the first successful VPS map localization. For example, `1.0` is one request per second. | | **Continuous Requests per Second** | `0.2` | Server request frequency after the initial VPS map localization. For example, `0.2` is one request every five seconds. | | **Temporal Fusion Enabled** | Enabled | Fuses successive VPS map localizations against the same map into one estimate, instead of using only the newest. Localizations that agree raise the asset's tracking confidence and steady its pose, at the cost of reacting more slowly to a genuine pose change. Unrelated to local sensor fusion of GPS and compass. | | **Geolocation Smoothing Enabled** | Enabled | Enables interpolation between localization updates. This minimizes "snapping" or visual jumps during abrupt GPS or compass recalibrations, for a more stable AR experience. | | **Blur Detection Enabled** | Disabled | Enables an advisory image blur check on the frames VPS2 sends for VPS map localization. A frame whose sharpness score falls below the threshold produces a `FrameFlagged` localization request record with error `ImageTooBlurry`, but is still sent for localization, never rejected. Use the signal to coach the user, for example "hold the device steady". See [Prompt users through blurry frames](https://www.nianticspatial.com/docs/nsdk/how-to/vps2/guide_users_during_localization/#prompt-users-through-blurry-frames). | | **Min Sharpness Score** | `0` | Sharpness threshold below which a frame is flagged as too blurry. The default of `0` flags nothing. Higher scores mean sharper images. There is no universal "sharp" value; `1000` is a good starting point and is the value the VPS2 samples use. | Universal and VPS map localization are configured independently. VPS map localization is enabled by default; universal localization is disabled by default, so turn it on explicitly if you want cloud geopositioning. Local sensor fusion is not configurable and runs in both cases. ## Start VPS2 > **Caution:** > > **Configure before starting** > > If you call `configure()`, call it before VPS2 starts -- that is, before `start()` or before the first `localize(...)` call, whichever comes first, because `localize(...)` starts the session too. To change configuration after starting, `stop()` the session, reconfigure, then `start()` again. ### Platform: kotlin > **Note:** > > **`start()` is optional** > > `localize(siteId)` and `localize(latitude, longitude, radiusMeters)` start the session themselves, so a flow that localizes to a Site does not have to call `start()` first. Call `start()` explicitly to begin collecting sensor data -- and to run universal localization, if it is enabled -- before you know which Site to localize to, or to resume after `stop()`. Calling `start()` while the session is already running has no effect. > > Tracking an anchor payload does **not** start the session. If your app only calls `trackAnchor(payload)`, e.g. when using a device map, call `start()` first. ### Platform: swift > **Note:** > > **`start()` is optional** > > `localize(siteId:)` and `localize(latitude:longitude:radiusMeters:)` start the session themselves, so a flow that localizes to a Site does not have to call `start()` first. Call `start()` explicitly to begin collecting sensor data -- and to run universal localization, if it is enabled -- before you know which Site to localize to, or to resume after `stop()`. Calling `start()` while the session is already running has no effect. > > Tracking an anchor payload does **not** start the session. If your app only calls `trackAnchor(payload:)`, e.g. when using a device map, call `start()` first. Start the session to begin collecting sensor data: ### Platform: swift ```swift vps2Session.start() ``` ### Platform: kotlin ```kotlin vps2Session.start() ``` Calling ### Platform: swift[`start()`](https://www.nianticspatial.com/docs/api/swift/NSDK.NSDKVps2Session.method-start) ### Platform: kotlin[`start()`](https://www.nianticspatial.com/docs/api/kotlin/com.nianticspatial.nsdk.vps2.Vps2Session.start) begins local sensor fusion, which provides the device's approximate global position and heading typically within a few seconds of calling `NSDKSession.update()`, and starts universal localization if it is enabled. VPS map localization does not start automatically; request it with one of the `localize` calls below, each of which starts the session if it is not already running. - To localize to a specific Site, call ### Platform: swift[`localize(siteId:)`](https://www.nianticspatial.com/docs/api/swift/NSDK.NSDKVps2Session.method-localize) ### Platform: kotlin[`localize(siteId)`](https://www.nianticspatial.com/docs/api/kotlin/com.nianticspatial.nsdk.vps2.Vps2Session.localize) with its Site ID. Get a Site ID by [querying Sites](https://www.nianticspatial.com/docs/nsdk/how-to/sites/getting_started/#3-query-sites), or by navigating to the Site in the Scaniverse portal and copying its ID from the URL. - To localize to all nearby Sites, call ### Platform: swift[`localize(latitude:longitude:radiusMeters:)`](https://www.nianticspatial.com/docs/api/swift/NSDK.NSDKVps2Session.method-localize) ### Platform: kotlin[`localize(latitude, longitude, radiusMeters)`](https://www.nianticspatial.com/docs/api/kotlin/com.nianticspatial.nsdk.vps2.Vps2Session.localize) with a coordinate and radius. A Site must have a processed VPS map tagged for production to be eligible for map-relative localization. Localize requests are additive: calling `localize` again does not disturb a request already in flight, and the Sites accumulate. To check whether the Site has localized, call ### Platform: swift[`assetTrackingData()`](https://www.nianticspatial.com/docs/api/swift/NSDK.NSDKVps2Session.method-assetTrackingData) ### Platform: kotlin[`getAssetTrackingData()`](https://www.nianticspatial.com/docs/api/kotlin/com.nianticspatial.nsdk.vps2.Vps2Session.getAssetTrackingData) with the Site's asset ID, which you can get from the [Sites API](https://www.nianticspatial.com/docs/nsdk/how-to/sites/getting_started/) or from the localized asset in the latest localization; it returns data while that asset is tracked. Without an asset ID, it reports the asset the device most recently localized to, which may belong to a different Site when you localize to several. Do not use the session's ### Platform: swift[`Vps2TrackingState`](https://www.nianticspatial.com/docs/api/swift/NSDK.enum-Vps2TrackingState) ### Platform: kotlin[`Vps2TrackingState`](https://www.nianticspatial.com/docs/api/kotlin/com.nianticspatial.nsdk.vps2.Vps2TrackingState) for that check: it describes the device's geoposition, not whether the asset is tracked. For the difference between those two states, see [Tracking states](https://www.nianticspatial.com/docs/nsdk/features/vps2/#tracking-states). ### Platform: swift ### Restarting a session To restart VPS2 within a running app, reuse the session you already acquired: ```swift vps2Session.stop() try vps2Session.configure(with: newConfig) vps2Session.start() ``` `acquireVps2Session()` returns the *same* session every time it is called, so this is the supported restart path. Acquire it once and hold it for the lifetime of the app. Do not attempt to restart by discarding the session and rebuilding it. A session rebuilt that way can end up tracking anchors without ever attaching geolocations -- anchor tracking appears to work while the map frame never becomes available, and the app may need a relaunch to recover. ### Verifying that configuration succeeded `configure(with:)` throws only for invalid arguments. Configuration can still fail **after** it returns -- most commonly because the session was not stopped first. A call that returns without throwing therefore does not prove the feature is ready. Use `featureStatus()` as the authoritative readiness check shortly after starting: ```swift try vps2Session.configure(with: config) vps2Session.start() // Check shortly after start: a fault here means the feature is not running, // even though configure(with:) did not throw. let status = vps2Session.featureStatus() ``` Without this check, a failed configuration is easy to miss and can lead to unexpected behavior. ### Driving updates on the main thread VPS2 is polled by `NSDKSession.update()`. Call it **synchronously on the main thread**, from your ARKit frame callback: ```swift func session(_ session: ARSession, didUpdate frame: ARFrame) { nsdkSession.update() // synchronous, main thread } ``` `update()` publishes `$latestLocalization` and `localizationRequestRecords` **synchronously, inside the call**. Subscribers therefore run while the frame that produced the data is still current. > **Warning:** > > **Do not move `update()` or its publishers onto another queue** > > Wrapping `update()` in an async dispatch, or inserting `.receive(on:)` between these publishers and your `sink`, breaks that alignment: updates arrive a frame late, and can arrive out of order relative to the ARKit frames that produced them. Subscribe directly instead. > > This is easy to violate accidentally when integrating VPS2 into an existing reactive pipeline, because the code still compiles and mostly appears to work. ### Platform: kotlin ### Restarting a session To restart VPS2 within a running app, reuse the session you already acquired: ```kotlin vps2Session.stop() vps2Session.configure(newConfig) vps2Session.start() ``` Acquire the VPS2 session **once** and keep it for the lifetime of the app. Releasing it and acquiring a new one is not a supported restart path, and a session rebuilt that way can end up tracking anchors without ever attaching geolocations -- anchor tracking appears to work while the map frame never becomes available. ### Verifying that configuration succeeded `configure()` validates its arguments synchronously, but configuration can still fail **after** it returns -- most commonly because the session was not stopped first. A successful return therefore does not prove the feature is running. Use `featureStatus()` as the authoritative readiness check shortly after starting: ```kotlin vps2Session.configure(config) vps2Session.start() // Poll featureStatus() after start; a fault here means the feature is not running, // even though configure() returned without throwing. val status = vps2Session.featureStatus() ``` Without this check, a failed configuration is easy to miss and can lead to unexpected behavior. ### How updates are delivered VPS2's `Flow` properties are **not** driven by your frame loop. Each one polls in the background on its own schedule, and only while something is collecting it: | Flow | Poll interval | |---|---| | `localizationUpdates` | ~30 Hz | | `localizationRequestRecords` | ~30 Hz | | `debuggerEvents` | ~30 Hz | Design around these delivery behaviors: | Behavior | Application guidance | |---|---| | **Localization updates describe the current VPS2 result, not a particular camera frame.** | Read the localized asset ID from the latest localization, then query that asset's tracking data when rendering map-relative content. | | **Updates are not tied to a particular frame.** | An update does not necessarily match the AR frame currently on screen, so do not try to pair it with frame-specific state based on timing. Use the data inside the update instead. | | **Updates arrive on a background thread.** | Switch to the main dispatcher before updating UI, as shown in the following example. | ```kotlin vps2Session.localizationUpdates .flowOn(Dispatchers.Default) .onEach { update -> /* ... */ } .flowOn(Dispatchers.Main) .launchIn(lifecycleScope) ``` These intervals are fixed by the SDK and cannot be changed through `Vps2Config`. ## Get Updates ### Platform: swift VPS2 only advances when your app calls [`NSDKSession.update()`](https://www.nianticspatial.com/docs/api/swift/NSDK.NSDKSession.method-update), which collects the latest sensor inputs from the data source and submits a single frame for processing. Call it once per ARKit frame, on the main thread (see [Call update for each ARKit frame](https://www.nianticspatial.com/docs/nsdk/setup/#call-update-for-each-arkit-frame)). ### Platform: kotlin VPS2 only advances when your app calls [`NSDKSession.update()`](https://www.nianticspatial.com/docs/api/kotlin/com.nianticspatial.nsdk.NSDKSession.update), which pulls sensor samples from the data source and submits a frame for native processing. Call it once per ARCore session update (see [Update the session](https://www.nianticspatial.com/docs/nsdk/setup/#update-the-session)). ## Stop VPS2 To stop the VPS2 session, simply call: ### Platform: swift ```swift vps2Session.stop() ``` ### Platform: kotlin ```kotlin vps2Session.stop() ``` After stopping, you may reconfigure and restart the session. All localization and anchor state is reset when stopped. Calling `stop()` while the session is already stopped has no effect. ## Next Steps | Goal | Guide | | --- | --- | | Complete an end-to-end Site localization | [First localization with NSDK](https://www.nianticspatial.com/docs/nsdk/first_localization/) | | Retrieve global position, heading, and accuracy | [Geolocate with VPS2](https://www.nianticspatial.com/docs/nsdk/how-to/vps2/getting_vps2_geoposition/) | | Localize to a Site and attach AR content | [Place virtual content with VPS2](https://www.nianticspatial.com/docs/nsdk/how-to/vps2/placing_virtual_content/) | | Retrieve Sites and assets at runtime | [Getting Started with Sites](https://www.nianticspatial.com/docs/nsdk/how-to/sites/getting_started/) | | Guide users to a successful localization | [Guide users during localization](https://www.nianticspatial.com/docs/nsdk/how-to/vps2/guide_users_during_localization/) | | Explore complete implementations | [NSDK sample projects](https://www.nianticspatial.com/docs/nsdk/sample_projects/) |