Cloud Anchors
Cloud Anchors - Cross-Platform AR Anchor Sharing
Cloud Anchors enable you to share AR experiences across multiple devices and platforms (iOS and Android). When you host an anchor, it's uploaded to Google's servers and can be resolved by any device with the cloud anchor ID.
Features
- Cross-Platform: Share anchors between iOS and Android devices
- Persistent: Anchors can be stored for 1-365 days
- Accurate: Sub-centimeter accuracy in good conditions
- Scalable: Supports multiple concurrent users
Prerequisites
1. Google Cloud Setup
- Go to Google Cloud Console
- Create a new project or select an existing one
- Enable the ARCore API:
- Navigate to "APIs & Services" > "Library"
- Search for "ARCore API"
- Click "Enable"
- Create an API key:
- Navigate to "APIs & Services" > "Credentials"
- Click "Create Credentials" > "API Key"
- (Optional) Restrict the key to ARCore API for security
2. Platform Requirements
| Platform | Requirements |
|---|---|
| iOS | iOS 12.0+, ARKit-capable device, ARCore iOS SDK |
| Android | Android 7.0+ (API 24), ARCore-supported device |
Installation
Expo Projects
For Expo projects, the plugin handles all native configuration automatically.
1. Configure app.json or app.config.js:
{
"expo": {
"plugins": [
[
"@reactvision/react-viro",
{
"googleCloudApiKey": "YOUR_GOOGLE_CLOUD_API_KEY",
"cloudAnchorProvider": "arcore",
"android": {
"xRMode": ["AR"]
}
}
]
]
}
}
2. Rebuild your app:
npx expo prebuild --clean
npx expo run:ios
# or
npx expo run:android
That's it! The Expo plugin automatically configures:
| Platform | What the plugin adds |
|---|---|
| iOS | GARAPIKey in Info.plist, use_frameworks! :linkage => :dynamic in Podfile, ARCore/CloudAnchors and ARCore/Semantics pods |
| Android | com.google.android.ar.API_KEY meta-data in AndroidManifest.xml |
Bare React Native Projects
For bare React Native projects, you must configure native files manually.
iOS Setup
1. Modify your Podfile:
Add the following to your ios/Podfile:
# Enable dynamic frameworks (required for ARCore)
use_frameworks! :linkage => :dynamic
# Add ARCore pods
pod 'ARCore/CloudAnchors', '~> 1.51.0'
pod 'ARCore/Semantics', '~> 1.51.0' # Required for Scene Semantics support
Important: ARCore SDK requires
use_frameworks!with dynamic linkage. This may affect other dependencies in your project.
2. Add API key to Info.plist:
Add the following to your ios/[YourApp]/Info.plist:
<key>GARAPIKey</key>
<string>YOUR_GOOGLE_CLOUD_API_KEY</string>
3. Install pods:
cd ios && pod install && cd ..
Android Setup
1. Add API key to AndroidManifest.xml:
Add the following inside the <application> tag in android/app/src/main/AndroidManifest.xml:
<meta-data
android:name="com.google.android.ar.API_KEY"
android:value="YOUR_GOOGLE_CLOUD_API_KEY" />
2. Rebuild your app:
npx react-native run-android
Usage
Basic Example - Hosting
Use ViroARPlaneSelector to let users tap on a detected plane to place and host an anchor:
import React, { useRef, useState } from "react";
import {
ViroARSceneNavigator,
ViroARScene,
ViroARPlaneSelector,
ViroNode,
ViroBox,
} from "@reactvision/react-viro";
function HostScene({ sceneNavigator }) {
const [cloudAnchorId, setCloudAnchorId] = useState<string | null>(null);
const [isHosting, setIsHosting] = useState(false);
const handlePlaneSelected = async (anchor) => {
if (isHosting) return;
setIsHosting(true);
try {
// Host the anchor with TTL of 1 day (API key auth limit)
const result = await sceneNavigator.hostCloudAnchor(anchor.anchorId, 1);
if (result.success && result.cloudAnchorId) {
console.log("Cloud Anchor ID:", result.cloudAnchorId);
setCloudAnchorId(result.cloudAnchorId);
// Share this ID with other users via your backend
} else {
console.error("Failed to host:", result.error);
}
} catch (error) {
console.error("Failed to host anchor:", error);
} finally {
setIsHosting(false);
}
};
return (
<ViroARScene>
<ViroARPlaneSelector onPlaneSelected={handlePlaneSelected}>
{/* This content appears at the anchor location */}
<ViroBox
position={[0, 0.05, 0]}
scale={[0.1, 0.1, 0.1]}
materials={["boxMaterial"]}
/>
</ViroARPlaneSelector>
</ViroARScene>
);
}
export default function App() {
return (
<ViroARSceneNavigator
cloudAnchorProvider="arcore"
initialScene={{ scene: HostScene }}
/>
);
}
Basic Example - Resolving
Resolve a cloud anchor and place content at its position:
function ResolveScene({ sceneNavigator, cloudAnchorIdToResolve }) {
const [resolvedPosition, setResolvedPosition] = useState(null);
useEffect(() => {
const resolve = async () => {
try {
const result = await sceneNavigator.resolveCloudAnchor(
cloudAnchorIdToResolve
);
if (result.success && result.anchor) {
console.log("Resolved at:", result.anchor.position);
setResolvedPosition(result.anchor.position);
} else {
console.error("Failed to resolve:", result.error);
}
} catch (error) {
console.error("Failed to resolve anchor:", error);
}
};
if (cloudAnchorIdToResolve) {
resolve();
}
}, [cloudAnchorIdToResolve]);
return (
<ViroARScene>
{resolvedPosition && (
<ViroNode position={resolvedPosition}>
<ViroBox
position={[0, 0.05, 0]}
scale={[0.1, 0.1, 0.1]}
materials={["boxMaterial"]}
/>
</ViroNode>
)}
</ViroARScene>
);
}
API Reference
ViroARSceneNavigator Props
| Prop | Type | Description |
|---|---|---|
cloudAnchorProvider | "none" | "arcore" | Enable cloud anchors. Set to "arcore" to use ARCore Cloud Anchors. |
onCloudAnchorStateChange | (event) => void | Callback fired when cloud anchor state changes. |
sceneNavigator Methods
hostCloudAnchor(anchorId: string, ttlDays?: number): Promise<ViroHostCloudAnchorResult>
hostCloudAnchor(anchorId: string, ttlDays?: number): Promise<ViroHostCloudAnchorResult>Hosts a local anchor to the cloud.
| Parameter | Type | Description |
|---|---|---|
anchorId | string | The local anchor ID (from ViroAnchor.anchorId) |
ttlDays | number | Time-to-live in days (1-365). Default: 1 |
Returns:
{
success: boolean;
cloudAnchorId?: string; // The cloud anchor ID to share
error?: string;
state: ViroCloudAnchorState;
}
resolveCloudAnchor(cloudAnchorId: string): Promise<ViroResolveCloudAnchorResult>
resolveCloudAnchor(cloudAnchorId: string): Promise<ViroResolveCloudAnchorResult>Resolves a cloud anchor by its ID.
| Parameter | Type | Description |
|---|---|---|
cloudAnchorId | string | The cloud anchor ID to resolve |
Returns:
{
success: boolean;
anchor?: {
anchorId: string;
cloudAnchorId: string;
position: [number, number, number];
rotation: [number, number, number];
scale: [number, number, number];
state: ViroCloudAnchorState;
};
node?: ViroARNode; // May be null for cloud anchors
error?: string;
state: ViroCloudAnchorState;
}
Note: The node property may be null for resolved cloud anchors. Use the anchor.position to place content at the resolved location.
cancelCloudAnchorOperations(): void
cancelCloudAnchorOperations(): voidCancels all pending cloud anchor operations.
Cloud Anchor States
type ViroCloudAnchorState =
| "None"
| "Success"
| "ErrorInternal"
| "TaskInProgress"
| "ErrorNotAuthorized" // Invalid or missing API key
| "ErrorResourceExhausted" // Quota exceeded
| "ErrorHostingDatasetProcessingFailed" // Need more visual data
| "ErrorCloudIdNotFound" // Cloud anchor expired or doesn't exist
| "ErrorResolvingSdkVersionTooOld"
| "ErrorResolvingSdkVersionTooNew"
| "ErrorHostingServiceUnavailable";
Best Practices
1. Ensure Good Tracking Quality
Cloud anchors work best when:
- The environment has distinct visual features
- Lighting is consistent
- The device has moved around the anchor location
- At least 30 seconds of tracking data is available
// Wait for good tracking before hosting
const handleAnchorFound = async (anchor: ViroAnchor) => {
// Give ARKit/ARCore time to build a good map
await new Promise((resolve) => setTimeout(resolve, 3000));
const result = await sceneNavigator.hostCloudAnchor(anchor.anchorId, 7);
// ...
};
2. Handle Errors Gracefully
const result = await sceneNavigator.hostCloudAnchor(anchorId, 7);
switch (result.state) {
case "Success":
// Success!
break;
case "ErrorNotAuthorized":
console.error("Check your API key configuration");
break;
case "ErrorResourceExhausted":
console.error("API quota exceeded. Try again later.");
break;
case "ErrorHostingDatasetProcessingFailed":
console.error("Move the device around to capture more environment data");
break;
default:
console.error("Error:", result.error);
}
3. Use Appropriate TTL
| Use Case | Recommended TTL |
|---|---|
| Real-time multiplayer | 1 day |
| Event-based experience | 1-7 days |
| Persistent installation | 365 days |
Important: API key authentication only supports TTL of 1 day. For TTL > 1 day, you need to configure keyless authorization using OAuth or Service Accounts. See ARCore Cloud Anchors Authorization for details.
4. Share Cloud Anchor IDs
Cloud anchor IDs need to be shared between devices. Common approaches:
- Firebase Realtime Database: Real-time sync for multiplayer
- QR Codes: Simple sharing for co-located experiences
- REST API: Your own backend for custom logic
- Nearby Connections: Peer-to-peer sharing
Quotas and Limits
| Resource | Limit |
|---|---|
| Host requests | 30 per minute per project |
| Resolve requests | 300 per minute per project |
| Anchor TTL | 1-365 days |
| Cloud anchor size | ~50KB per anchor |
Troubleshooting
"ErrorNotAuthorized"
- Verify your API key is correct
- Ensure ARCore API is enabled in Google Cloud Console
- Check that the API key is properly configured for your bundle ID / package name
"ErrorHostingDatasetProcessingFailed"
- Move the device around more to capture environment data
- Ensure good lighting conditions
- Try hosting in an area with more visual features
"ErrorCloudIdNotFound"
- The cloud anchor may have expired (exceeded TTL)
- The cloud anchor ID may be incorrect
- Network connectivity issues
iOS: "GARSession initialization failed"
- Verify
GARAPIKeyis in Info.plist - Ensure ARCore/CloudAnchors pod is installed
- Check Xcode build settings (Bitcode = NO)
Android: Cloud anchors not working
- Verify API key in AndroidManifest.xml
- Ensure device supports ARCore
- Check that ARCore app is installed and updated
Example: Multiplayer AR Game
import { useEffect, useState } from "react";
import { database } from "./firebase"; // Your Firebase setup
function MultiplayerARScene({ sceneNavigator, roomId }) {
const [sharedObjects, setSharedObjects] = useState([]);
// Listen for cloud anchors shared by other players
useEffect(() => {
const unsubscribe = database
.ref(`rooms/${roomId}/anchors`)
.on("child_added", async (snapshot) => {
const { cloudAnchorId, objectType } = snapshot.val();
const result = await sceneNavigator.resolveCloudAnchor(cloudAnchorId);
if (result.success && result.anchor) {
setSharedObjects((prev) => [
...prev,
{
id: snapshot.key,
position: result.anchor.position,
objectType,
},
]);
}
});
return () => unsubscribe();
}, [roomId]);
// Host a new object and share with other players
const placeObject = async (anchor, objectType) => {
const result = await sceneNavigator.hostCloudAnchor(anchor.anchorId, 1);
if (result.success) {
// Share with other players via Firebase
await database.ref(`rooms/${roomId}/anchors`).push({
cloudAnchorId: result.cloudAnchorId,
objectType,
timestamp: Date.now(),
});
}
};
return (
<ViroARScene onAnchorFound={(anchor) => placeObject(anchor, "cube")}>
{sharedObjects.map((obj) => (
<ViroNode key={obj.id} position={obj.position}>
<ViroBox scale={[0.1, 0.1, 0.1]} />
</ViroNode>
))}
</ViroARScene>
);
}
Updated 1 day ago