HomeGuidesAPI ReferenceChangelogDiscussions
Hire a ReactVision Expert 🛠️Launch Studio 🖥️Sponsor on GitHub ❤️Log In
Guides

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 endpoint or credentials, ViroVPS uses the Editor session, or the rvApiKey and rvProjectId you set in the Expo plugin. See Getting Started with ReactVision Spatial.


Platform support

PlatformSupportedNotes
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.

ValueWhat 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 as PAID_PLAN_REQUIRED or COMPUTE_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 with VPS_SERVER_NOT_CONFIGURED.


Props

PropTypeDefaultDescription
arSceneNavigator (required)ViroVPSNavigatorThe navigator your scene receives from ViroARSceneNavigator. Passed explicitly, as with ViroARCloudAnchor.
localization"device" | "server""device"Where localisation runs. See above.
endpointstringThe Editor session's address, else https://platform.reactvision.xyzReactVision Spatial's address, which serves location lookups and map downloads.
serverEndpointstringThe address ReactVision Spatial gives outThe VPS server's address, for server mode and image search.
credentialsViroVPSCredentialsSourceThe 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).
appUserIdstringYour 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.
autoRadiusMetersnumber200Search radius for startTracking("auto"), in metres.
pollIntervalMsnumber500How often the localisation is polled once a map is loaded.
loadTimeoutMsnumber30000How long startTracking waits for the map load before the state becomes error.
localizationTimeoutMsnumber5000How long one poll may take before it counts as failed. The state becomes error after three failed polls in a row.
visualSearchbooleantrueWhether "auto" may find the location from the camera image when GPS cannot. "visual" always does.
visualSearchTimeoutMsnumber30000How long the visual search keeps trying.
visualSearchIntervalMsnumber3000Pause between visual search attempts.
serverFixIntervalMsnumber1000Server mode: ms between frames until localisation converges.
serverRefreshIntervalMsnumber10000Server mode: ms between frames once converged.
worldMeshboolean | { occlusion?: boolean; physics?: boolean }offAlso 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) => voidFires once per state change.
onLocalized(event: ViroVPSLocalizedEvent) => voidFires once per new converged pose, at the poll rate.
onWorldMesh(event: ViroVPSWorldMeshEvent) => voidHow the worldMesh load went, once per tracked location. A failure here never fails tracking.
childrenReact.ReactNodeRendered 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 from arSceneNavigator.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" unless visualSearch is false.
  • "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.

StateMeaning
idleNo map requested, or stopTracking() has run.
searchingResolving a location and loading its map.
mapLoadedA map is loaded and matching runs, but the estimate has not converged yet.
trackingConverged. renderPose is usable.
lostWas tracking, then stopped matching. Lets you tell "never found it" from "found it, then lost it".
errorThe map failed to load, no location was found, or tracking ended with a code.
unsupportedThe 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