ViroVPS
Track a scanned place with ReactVision Spatial's visual positioning service and place content in its frame.
Introduced in ViroReact 3.1.0
ViroVPS finds a place that was scanned with uploadScanRecording, then keeps tracking it. It loads the place's map, localises the device against it, and reports the result through onTrackingState and onLocalized. Put content you want pinned to the place inside a ViroVPSAnchor.
ViroVPS draws nothing itself. It is a data and event source that sits inside your ViroARScene and renders its children unchanged.
import * as React from "react";
import {
ViroARScene,
ViroTrackingStateConstants,
ViroVPS,
ViroVPSAnchor,
Viro3DObject,
} from "@reactvision/react-viro";
export function PlaceScene(props: any) {
const vpsRef = React.useRef<ViroVPS>(null);
const started = React.useRef(false);
const onTrackingUpdated = (state: number) => {
// Start once, when AR tracking is normal. Calling startTracking again replaces the load.
if (state === ViroTrackingStateConstants.TRACKING_NORMAL && !started.current) {
started.current = true;
vpsRef.current?.startTracking("auto");
}
};
return (
<ViroARScene onTrackingUpdated={onTrackingUpdated}>
<ViroVPS
ref={vpsRef}
arSceneNavigator={props.arSceneNavigator}
onTrackingState={(e) => console.log(e.state, e.locationId)}
onLocalized={(e) => console.log("pose in map frame", e.renderPose)}
/>
<ViroVPSAnchor>
<Viro3DObject source={require("./statue.glb")} type="GLB" position={[1, 0, -2]} />
</ViroVPSAnchor>
</ViroARScene>
);
}
No address or key to pass. VPS calls go to ReactVision Spatial. With no
endpointorcredentials,ViroVPSuses the Editor session, or thervApiKeyandrvProjectIdyou set in the Expo plugin. See Getting Started with ReactVision Spatial.
Platform support
| Platform | Supported | Notes |
|---|---|---|
| iOS | ✅ | ARKit session. |
| Android | ✅ | ARCore session. |
| Meta Quest | ❌ | No VPS. |
| Apple Vision Pro, web | ❌ | No VPS. onTrackingState reports unsupported. |
ViroVPS needs a navigator that exposes VPS (loadVPSMap, or for server mode startVPSServerSession, reportVPSServerFix and captureVPSQueryFrame). Otherwise startTracking fails with the code UNSUPPORTED and the state becomes unsupported.
Localising on the device or on the server
The localization prop picks where matching runs.
| Value | What happens |
|---|---|
"device" (default) | The location's map is downloaded and the device matches the camera against it. |
"server" | No map is downloaded. The device sends a camera frame every serverFixIntervalMs (default 1000 ms) to the VPS server, which answers with the pose. Once converged, a frame goes every serverRefreshIntervalMs (default 10000 ms) to keep correcting drift. It holds up much better across days, lighting and moved objects, at the cost of a network round trip per fix. |
Server mode needs a paid plan and a map built for it. It is billed as Advanced VPS compute time, at the same rate as scan processing: $0.60 a minute by default. The location's map must have been built with server mode on, otherwise tracking ends with the code
NO_SERVER_MAP. A 401 or 403 (such asPAID_PLAN_REQUIREDorCOMPUTE_SPEND_LIMIT_REACHED) ends tracking with its code. No answer, a 429 or a 5xx is retried with the next frame. Without a VPS server address from ReactVision Spatial, server mode ends withVPS_SERVER_NOT_CONFIGURED.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
arSceneNavigator (required) | ViroVPSNavigator | The navigator your scene receives from ViroARSceneNavigator. Passed explicitly, as with ViroARCloudAnchor. | |
localization | "device" | "server" | "device" | Where localisation runs. See above. |
endpoint | string | The Editor session's address, else https://platform.reactvision.xyz | ReactVision Spatial's address, which serves location lookups and map downloads. |
serverEndpoint | string | The address ReactVision Spatial gives out | The VPS server's address, for server mode and image search. |
credentials | ViroVPSCredentialsSource | The Editor session, or rvApiKey and rvProjectId from the Expo plugin | { projectId, apiKey } or { projectId, accessToken }, or a function returning one. A function is called before every request. For a user token minted by your backend, pass createUserTokenCredentials() (see Acting for a signed-in user). |
appUserId | string | Your own id for the signed-in user, sent as x-app-user-id. 1 to 255 printable ASCII characters, not . or ... Replaces one given in credentials. | |
autoRadiusMeters | number | 200 | Search radius for startTracking("auto"), in metres. |
pollIntervalMs | number | 500 | How often the localisation is polled once a map is loaded. |
loadTimeoutMs | number | 30000 | How long startTracking waits for the map load before the state becomes error. |
localizationTimeoutMs | number | 5000 | How long one poll may take before it counts as failed. The state becomes error after three failed polls in a row. |
visualSearch | boolean | true | Whether "auto" may find the location from the camera image when GPS cannot. "visual" always does. |
visualSearchTimeoutMs | number | 30000 | How long the visual search keeps trying. |
visualSearchIntervalMs | number | 3000 | Pause between visual search attempts. |
serverFixIntervalMs | number | 1000 | Server mode: ms between frames until localisation converges. |
serverRefreshIntervalMs | number | 10000 | Server mode: ms between frames once converged. |
worldMesh | boolean | { occlusion?: boolean; physics?: boolean } | off | Also load the location's scanned mesh, for occlusion and physics against the real place. true turns on both. It needs no worldMeshEnabled and no depth sensor, appears when localisation converges and follows the map's corrections. A few MB more to download. |
onTrackingState | (event: ViroVPSTrackingStateEvent) => void | Fires once per state change. | |
onLocalized | (event: ViroVPSLocalizedEvent) => void | Fires once per new converged pose, at the poll rate. | |
onWorldMesh | (event: ViroVPSWorldMeshEvent) => void | How the worldMesh load went, once per tracked location. A failure here never fails tracking. | |
children | React.ReactNode | Rendered as they are. |
Methods
Call these through a ref.
startTracking(locationId)
startTracking(locationId: string | "auto" | "visual"): Promise<ViroVPSStartTrackingResult>
- A location id loads that location's map.
"auto"reads the device's geospatial pose fromarSceneNavigator.getCameraGeospatialPose()and tracks the nearest location with a map. Geospatial tracking must be on and producing a pose. Without a pose, or with no location nearby (indoors, for example), it falls back to"visual"unlessvisualSearchisfalse."visual"sends camera images to the VPS server to find the project's location from what the camera sees. No GPS needed. Point the camera at the scanned place, not a blank wall.
Calling it again replaces whatever was loaded before. It resolves to { success, locationId?, error?, errorDetail? }, where locationId is the location actually tracked.
stopTracking()
Unloads the map and stops polling. Safe to call when nothing is loaded.
getSemanticObjects()
Promise<ViroVPSSemanticObject[]>: the objects found in the tracked location's scan, in the map's frame. Place content at their positions inside a ViroVPSAnchor. [] when the scan has none or nothing is tracked.
Tracking states
onTrackingState receives { state, locationId?, error?, errorDetail? }. error and errorDetail are present only for error and unsupported.
| State | Meaning |
|---|---|
idle | No map requested, or stopTracking() has run. |
searching | Resolving a location and loading its map. |
mapLoaded | A map is loaded and matching runs, but the estimate has not converged yet. |
tracking | Converged. renderPose is usable. |
lost | Was tracking, then stopped matching. Lets you tell "never found it" from "found it, then lost it". |
error | The map failed to load, no location was found, or tracking ended with a code. |
unsupported | The navigator cannot do VPS, or not the mode asked for. |
errorDetail is { code, status, message }. code is the server's own code when the failure is its answer (such as NO_ASSET for a location with no map yet, or PAID_PLAN_REQUIRED), HTTP_ERROR for an error answer without one, or one set on the device: NETWORK_ERROR, CREDENTIALS_UNAVAILABLE, ENDPOINT_NOT_ALLOWED (the app's own credentials would go to an address that is not https or the local network), VPS_SERVER_NOT_CONFIGURED, INVALID_APP_USER_ID, UNSUPPORTED, NO_LOCATION, MAP_LOAD_FAILED, LOCALIZATION_UNAVAILABLE, SUPERSEDED or UNKNOWN. ViroVPSErrorCodes exports the device-side codes as constants.
onLocalized
onLocalized receives { locationId, renderPose, lastHitInliers?, lastHitReprojRms? }. renderPose is 16 comma-separated floats, a column-major 4x4 matrix: the camera's current pose in the map's frame. It is reported only once the estimate has converged. For content, prefer ViroVPSAnchor, which applies the same correction every frame for you.
One tracking ViroVPS per navigator
A navigator holds one map. Several ViroVPS components may sit on one navigator, but only one may track at a time. Stop or unmount the first before you start another. Unmounting stops polling and unloads the map the component loaded, and no callback fires after unmount.
See also
- ViroVPSAnchor
- Advanced VPS: setup, using it in your app and retrieving scans with the API
- StudioSceneNavigator (
vpsLocalization) - VPS (room scans with
startScan/finishScan)
Updated about 3 hours ago