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

  1. Go to Google Cloud Console
  2. Create a new project or select an existing one
  3. Enable the ARCore API:
    • Navigate to "APIs & Services" > "Library"
    • Search for "ARCore API"
    • Click "Enable"
  4. 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

PlatformRequirements
iOSiOS 12.0+, ARKit-capable device, ARCore iOS SDK
AndroidAndroid 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:

PlatformWhat the plugin adds
iOSGARAPIKey in Info.plist, use_frameworks! :linkage => :dynamic in Podfile, ARCore/CloudAnchors and ARCore/Semantics pods
Androidcom.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

PropTypeDescription
cloudAnchorProvider"none" | "arcore"Enable cloud anchors. Set to "arcore" to use ARCore Cloud Anchors.
onCloudAnchorStateChange(event) => voidCallback fired when cloud anchor state changes.

sceneNavigator Methods

hostCloudAnchor(anchorId: string, ttlDays?: number): Promise<ViroHostCloudAnchorResult>

Hosts a local anchor to the cloud.

ParameterTypeDescription
anchorIdstringThe local anchor ID (from ViroAnchor.anchorId)
ttlDaysnumberTime-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>

Resolves a cloud anchor by its ID.

ParameterTypeDescription
cloudAnchorIdstringThe 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

Cancels 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 CaseRecommended TTL
Real-time multiplayer1 day
Event-based experience1-7 days
Persistent installation365 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

ResourceLimit
Host requests30 per minute per project
Resolve requests300 per minute per project
Anchor TTL1-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 GARAPIKey is 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>
  );
}