ViroReact 3.1.0: Advanced VPS, a world mesh that persists and headset-native Quest scenes
Advanced VPS arrives: scan a place with a phone, then find it again and pin content to it, matched on the device or on the GPU server. Scans can carry the device's depth, so blank walls reconstruct. The world mesh now fuses depth across frames into a room that persists, and Meta Quest gets headset-native Editor scenes with the wearer's real hands showing through virtual content. Renderer binaries are rebuilt on virocore 3.1.0, released with ReactVision Spatial 1.4.0. The web build uses @reactvision/viro-web-renderer 1.1.0. Read Check when upgrading before you update.
Advanced VPS
<ViroVPS>finds a scanned place and keeps tracking it.startTracking(id)takes a known location,"auto"searches by GPS and"visual"by camera image. Results come throughonLocalizedandonTrackingState.- Two ways to localise, behind one prop.
localization="device"(the default) downloads the place's map and matches on the phone.localization="server"sends camera frames to the GPU server, which holds up much better across days and lighting and needs no map download (paid plans, billed as Advanced VPS compute).StudioSceneNavigatortakes the same choice asvpsLocalization, withonVpsStateChangeandretryVps(). <ViroVPSAnchor>puts content in the place's frame, and the scanned mesh gives occlusion and physics. Both follow the map every frame.- Scan from your app.
uploadScanRecording(target, outputDir, options?)zips a recording natively and uploads it, carrying on in the background on iOS and Android.meshQuality(low,medium,high) picks the mesh's detail,keepRecordingkeeps the recording for merging scans, andgetScan(scanId)follows processing. Each scan reportsquality.mesh_confidence.level(good,fairorpoor) with reasons for whoever recorded it. - Record depth while scanning (iOS with LiDAR, Android with the ARCore Depth API):
startRecording(outputDir, { depth: true }), withdepthCaptureEnabledset on the navigator when it mounts.stopRecording()now resolves to aViroRecordingResult, andisRecordingSupported({ depth })says what the device can do. - Help whoever is recording:
useViroTrackingReadyanduseViroCaptureGuidance(hints such as turning too fast or pointing at a blank wall), andonTrackingUpdatedonViroARSceneNavigator. - Locations and users:
listLocations,createLocation,renameLocation,deleteLocation,listScans,deleteScanandfindNearbyLocations.createUserTokenCredentials()lets an app act for its signed-in user, with no secret key on the device. - No set-up beyond your keys: VPS calls need nothing beyond the Editor session or the Expo plugin's
rvApiKeyandrvProjectId, and requests time out instead of hanging.
See Advanced VPS and Scanning, ViroVPS and ViroVPSAnchor.
World mesh
- The mesh fuses depth frames into a room that persists on the depth-image and monocular paths.
worldMeshConfigtakesaccumulate,voxelSizeandmaxMemoryMB,sceneNavigator.resetWorldMesh()starts a new room, andgetWorldMeshStats()reportssource(lidar,depth,monocular,plane) andaccumulated. - Android: the mesh is in the right place, and confidence weights samples instead of dropping them. iOS without LiDAR: estimated depth is placed where it was seen, so fusing it no longer duplicates surfaces.
- The world mesh types are exported from the package root, and the
ViroWorldMeshConfigreference now matches the type that ships. See ViroARSceneNavigator.
Meta Quest
- Editor scenes feel native in the headset. The headset view opens at once with a loading panel, the menu opens on the left controller's menu button (or a palm pinch) instead of Y, B closes it, and the panels are a quarter of the size.
- The room stays put. Content on room surfaces, tap-placed models and the co-location frame keep their place when the wearer recentres, and
onRoomMoved(listener)reports a recentre to your app. - Input behaves like the platform's. Y goes back as B does, only a trigger or pinch selects, and
ViroController.getControllerRayAsync(source)says where a ray points. - Hand occlusion in passthrough:
handOcclusiononViroXRSceneNavigatorandViroVRSceneNavigator(off by default;handOcclusionMode"mesh"or"capsules").questPermissionspicks the runtime permissions asked for at launch. - Expo plugin: Quest builds declare minSdkVersion 32, the Horizon OS SDK as
<metavr:uses-metavr-sdk>, a network security config and Meta's recommended activity settings, and no longer declare NFC.
Web
- Web AR content stays in the room on iPhone, and hides while tracking is lost. Set
hideContentWhenLost: falseinarOptionsto keep it visible. Skinned GLBs draw. Needs viro-web-renderer 1.1.0. See Web Setup Guide.
visionOS
- Editor scenes render in the ImmersiveSpace. They were blank before, because their root was a
ViroARScene, which renders nothing on visionOS.
iOS Simulator
- Apps using ViroReact build and run in the iOS Simulator. ViroKit and the bridge ship as
ViroKit.xcframeworkandViroReact.xcframework, each with a simulator slice (arm64 and x86_64). AR scenes still need a device.
Everywhere
- Every scene navigator sends one usage ping when it mounts, replacing the device registration ReactVision Spatial used to do. It carries no API key. Turn it off with
ViroTelemetry.setEnabled(false)or the Expo plugin'stelemetry: false. The iOS privacy manifest now ships with the pod. See Usage Data. - Co-location:
getColocationDeviceId(),credentialsfor user tokens, and joins refused for the team's compute billing say so (COMPUTE_SPEND_LIMIT_REACHED,COMPUTE_BILLING_REFUSED). Viro3DObjectreads more of glTF:KHR_texture_transform,KHR_node_visibility,KHR_animation_pointer, extra texture coordinate sets and emissive textures, with fixes for opaque, masked and blended materials.- A click goes to what is drawn on top, and a click on nothing reaches
ViroController'sonClickagain. Text sits where its vertical alignment puts it. - Types:
registerAnimationsaccepts names and nested chains,Viro360ImagetakesstereoMode(steroModeis a deprecated alias), andViroMaterialtypescolorWriteMaskas an array. - ViroKit targets iOS 15.1, the minimum the podspec already declared.
Check when upgrading
- Emission now shows on physically based materials. A model that relied on it being dropped may now glow.
- A
styleon a scene navigator now applies. Check the layout of any navigator you pass one. cloudAnchorProviderandgeospatialAnchorProviderare honoured whenprovideris not set.cloudAnchorProvider="arcore"now uses ARCore Cloud Anchors and needs a Google Cloud API key. Remove the prop, or passprovider, to keep ReactVision's.- The iOS binaries moved to
ios/dist/ViroRenderer/ViroKit.xcframeworkandios/dist/lib/ViroReact.xcframework. Update any script or Podfile that namesViroKit.frameworkorlibViroReact.aby path, and runexpo prebuild(orpod install) again. - Text moves to where its vertical alignment puts it. Drop any offset you added to make up for the old position.
- Quest: the menu button and the palm pinch no longer exit a scene (handle source 13 in a
ViroControllerif you want them to), and the manifest changes above apply after prebuild. - Recording audio, corrected: the 3.0.3 notes said session recordings carry an iOS audio track and need
RECORD_AUDIOon Android. Both apply tostartVideoRecording, notstartRecording.
Every change in 3.1.0
This is the whole of the 3.1.0 section of CHANGELOG.md, shortened.
Added
Advanced VPS
<ViroVPS localization="server">sends a camera frame everyserverFixIntervalMs(default 1000) to the VPS server and fuses its answers into tracking, then one everyserverRefreshIntervalMs(default 10000) once converged.<ViroVPSAnchor>, the world mesh andonLocalizedwork as in device mode.StudioSceneNavigatortakesvpsLocalization("device"or"server"); a change restarts tracking of the scene on screen.- VPS calls accept a user token (
ViroVPS,ViroVPSClient,ViroVPSScanUpload): an app with its own sign-in acts for its signed-in user, with no secret key on the device. createUserTokenCredentials({ projectId, fetchToken })keeps a user token between requests, refreshes it before it expires, shares one fetch between concurrent requests and replaces a refused token once.uploadScanRecordingtakesmeshQuality("low","medium"or"high") andonProgress(sentBytes, totalBytes).ViroVPSMeshQualityis exported.- The rest of the VPS device routes:
listLocations,getLocation,createLocation,renameLocation,deleteLocation,listScans,deleteScan,getVPSServerAddress,getLocationFileDownload(locationId, kind)andgetScanFileDownload(scanId, kind). ViroVPSErrorCodes, the codes ViroReact itself gives aViroVPSError.- Capture guidance for recording a place:
createViroCaptureGuidanceanduseViroCaptureGuidance(hintsfast,pitch,closeandblank, and a count of tracking jumps), anduseViroTrackingReady(state, { normalMs, limitedMs }). startRecording(outputDir, { depth: true })records the device's depth, withdepthCaptureEnabledonViroARSceneNavigator(iOS with LiDAR, Android with the ARCore Depth API).stopRecording()resolves to aViroRecordingResult, andisRecordingSupported({ depth })is on the AR scene navigator.onTrackingUpdatedonViroARSceneNavigator, andViroARTrackingReasonConstants.TRACKING_REASON_INSUFFICIENT_LIGHT(4; Android only).
World mesh
sceneNavigator.resetWorldMesh(), andworldMeshConfigtakesaccumulate,voxelSize(default 0.04 m) andmaxMemoryMB(default 64).getWorldMeshStats()reportssource("lidar","depth","monocular","plane"or"unknown") andaccumulated.- The world mesh types are exported from the package root.
Co-location
getColocationDeviceId(); the replication socket sends it asx-rv-device, so a device is billed once across its two sockets.- Co-location takes
credentials, as<ViroVPS credentials>does, so a device can join with a user token minted with thecolocationscope (needs the rebuilt native binaries). - A join refused for the team's compute billing says so:
COMPUTE_SPEND_LIMIT_REACHEDorCOMPUTE_BILLING_REFUSED(StudioColocationErrorCode).
Meta Quest
- Hand occlusion in passthrough:
handOcclusion,handOcclusionMode("mesh"or"capsules") andhandOcclusionInflateMmonViroVRSceneNavigatorandViroXRSceneNavigator. onRoomMoved(listener)reports a recentre, or a headset waking, as a 4x4 move.ViroController.getControllerRayAsync(source)says where a ray last pointed.- The Editor scene menu highlights the item a controller or hand points at.
Everywhere
- Every scene navigator sends one usage ping when it mounts. Turn it off with
ViroTelemetry.setEnabled(false)or the Expo plugin'stelemetry: false. See Usage Data. - The iOS privacy manifest ships with the pod (
ViroReact_privacy). - iOS Simulator builds (
ViroKit.xcframeworkandViroReact.xcframework, arm64 and x86_64 simulator slices). hideContentWhenLostin web AR options (viro-web-renderer 1.1.0).Viro3DObjectreads glTFKHR_texture_transform,KHR_node_visibilityandKHR_animation_pointer, texture coordinate sets beyond the first, and emissive textures.
Changed
Advanced VPS
- VPS calls go to ReactVision Spatial and need no address or credentials beyond the Editor session or
rvApiKeyandrvProjectId. Only image search and server-mode frames go to the VPS server, whose address ReactVision Spatial gives out (GET /vps/server). uploadScanRecordingsends the recording straight to storage and carries on in the background (iOS backgroundURLSession, Android WorkManager). ReactVision Spatial keeps the recording only while it processes the scan, unlesskeepRecordingis true.ViroVPSScan.qualityis typed and carriesmesh_confidence(good,fairorpoor, with reasons).ViroVPSScanalso typesoutputs,keep_recording,app_user_id,queued_at,started_at,finished_atandmesh_quality, andViroVPSMapDownloadtypesgeo. Code that buildsViroVPSScanobjects by hand must add the first six.uploadScanRecordingreads credentials again just before its final call, so a credentials function is called twice per upload.
Meta Quest
- The Editor scene menu opens on the left controller's menu button or a palm pinch, not Y. B closes it while it is open, and B dismisses an ALERT before it exits the scene.
- The menu button and the palm pinch no longer exit a scene. They reach a
ViroController'sonClickStateandonClickas source 13. Y goes back as B does. - Editor scene panels are drawn at a quarter of their size, look like Meta Horizon OS panels, and open centred where the wearer looks.
- Editor scenes with plane detection use the room's planes (Space Setup) when the wearer has granted spatial data.
ViroXRSceneNavigatortakesquestPermissions.StudioSceneNavigatorasks for no permissions at launch, and opens its headset view at once with a loading panel.- A co-location failure is explained by its error code in the indicator and the Quest HUD.
- Expo plugin, Quest builds: minSdkVersion 32 and
installLocation="auto", the Horizon OS SDK as<metavr:uses-metavr-sdk>, a network security config,excludeFromRecentsand the2Dcategory on the launching activity, Meta's recommendedconfigChanges, and no NFC.
Everywhere
minConfidenceis documented per platform: a cutoff on iOS, a fusion weight on Android.- Calls that do nothing on a platform say so once (
resetARSessionandsetWorldOriginon Android,ViroARObjectMarkeron Android and Quest, scene navigation in the web navigators).depthDebugEnabledis iOS only. - The scene navigators apply the
styleprop. Check the layout of any navigator you pass one. ViroARSceneNavigatorhonourscloudAnchorProviderandgeospatialAnchorProviderwhenprovideris not set.- Android:
startVideoRecordingfails with error 1 whenRECORD_AUDIOis denied,takeScreenshotkeeps only the last path segment of its file name, andprojectPoint/unprojectPointon a stale view reject. - The iOS binaries are xcframeworks (
ios/dist/ViroRenderer/ViroKit.xcframework,ios/dist/lib/ViroReact.xcframework), and the Expo plugin no longer excludes arm64 from simulator builds. - Scenes rendered by
ViroARSceneNavigatorremount once after upgrading.
Fixed
glTF and materials (virocore 3.1.0)
- A GLB with animation pointers loads, and so does one whose samplers omit
interpolation. - Emission shows on physically based materials, visionOS included.
- Opaque glTF materials are opaque, masked ones are opaque where they pass the cutoff, and blended ones no longer hide what is behind them.
- GLB characters whose rig animates a node above each joint move, skinned parts no longer vanish up close, a GLB with 8-bit indices draws, and skinned GLBs draw on the web.
Editor scenes
- Editor scenes render on visionOS, in the ImmersiveSpace.
- A scene stops downloading a map that keeps failing (four retries, then
erroruntilretryVps()). - An animation can follow one that started before its model loaded.
- On Meta Quest: tap-placed models and the co-location frame stay in the room when the wearer recentres; only a trigger or pinch selects; a placement aimed at nothing lands along the controller's ray, and one aimed through a panel's text lands where it points; panels draw over scene objects in front of them, and a click between menu lines no longer reaches what is behind; long ALERTs fit their text and the view; wrapped lines no longer overlap; "No assets to display" appears in front of the wearer; content on a plane faces the wearer; the spatial data and microphone prompts open over the headset view; a failed photo, recording or scene change is reported in the headset; image-triggered assets that Quest cannot show are announced; text assets are sharp; an asset that fails to load no longer freezes the app.
Meta Quest renderer
- Videos no longer abort the app or leak, pause when the headset sleeps, and pause while Horizon OS has input focus.
- Content on a room surface stays on it after a recentre, and
recenterTracking()no longer races the renderer. - A plane scene opened right after spatial data is granted gets its planes, and uses the plane nearest the wearer. A desk is classified as a table.
- Hand input releases and presses once, a scene no longer crashes when its root changes as the headset view starts, and closing a scene logs no OpenXR errors.
- The right eye clears its depth buffer, each eye's framebuffer is made once, and depth buffers are no longer written back to memory.
- A top-level node in a
ViroARSceneno longer asks for a world anchor, switching AR scenes no longer logs a passthrough error, and AR hit tests no longer keep references to their results.
Rendering and input
- A click goes to what is drawn on top, and a controller click on nothing reaches
ViroController'sonClickagain. - Text no longer disappears after a front-culled material, and sits where its vertical alignment puts it.
iOS
- iOS:
maxMemoryMB: 0andvoxelSize: 0keep the current value. ViroKit.frameworktargets iOS 15.1, so apps targeting iOS 15 launch on iOS 15 devices.- A paused video no longer starts again on its own, and a closing scene no longer risks a crash from its videos' late callbacks.
ViroOrbitCameraorbits itsfocalPointwhen it also has aposition.
Android
- Going back to a scene keeps its
ViroController, and dropping a Viro view no longer logs an error. - A paused-then-played video keeps playing, and play and pause reach the player in the order they were made.
- Stopping a scan recording no longer logs
signalEndOfInputStream failed. - Hit tests always settle, and
getRecordingStatus()returns"Unsupported"when the AR view cannot be found.
VPS and co-location
- A VPS error the gateway answers itself keeps its code, and a VPS request with no answer fails with
NETWORK_ERRORafter 30 s (longer for downloads). - The app's own credentials go only to https or the local network (
ENDPOINT_NOT_ALLOWEDotherwise), on iOS and Android too. uploadScanRecording()reports a native failure as aViroVPSErrorand discards the zip, and a lost connection givesNETWORK_ERROR.- One failed poll no longer ends
<ViroVPS>tracking. - The co-location hooks no longer stay
workingor throw on a native failure, room requests time out after 30 s, and a device id that failed once is asked for again. <ViroSharedFrame>and<ViroARCloudAnchor>survive a source change.<ViroARCloudAnchor>takesonLocalizeProgressandmaxAttempts.ViroReplicationClientoperations return whether they were sent.rvCreateSharedFrameandrvJoinSharedFrameresolve{ success: false }outside Quest.
API and types
Viro360ImagetakesstereoMode(steroModeis a deprecated alias), and sounds seek to fractional seconds.ViroMaterialtypescolorWriteMaskas an array, andViroMaterials.createMaterials()tolerates a missing texture orsemanticMasklabels.ViroAnimationDicttypes animation chains, and acolorinside a chain is processed.ViroARScenesets its view's ref, asViroScenedoes.- Navigation:
replace("key")on the top scene no longer throws, scene state no longer leaks between scenes on pop or replace, the navigators no longer modifyviroAppProps, a scene'spassPropscannot replacearSceneNavigator, and twopop()calls in one tick no longer pop the root scene. - Deprecation warnings are logged once.
- The Expo plugin can run again over existing
android/andios/folders, and adds ARCore to the app's<queries>instead of replacing them. reactis a peer dependency only.- iOS and Android bridge hardening: downloads fail on a non-2xx status, credentials go only to https or a local host, promises settle on every path, and R8 keep rules ship with the library.
Full list: Upgrading to 3.1.0, the GitHub release and CHANGELOG.md.