Warning
This library is under development and may not work correctly in all cases yet.
Experimental Cesium rendering for React Native using Nitro Modules, with the native Metal/Vulkan renderer powered by Cesium Native.
- Platform maturity: under development
- Supported renderers: Metal (iOS) and Vulkan (Android)
- Native engine: Cesium Native
- React Native integration: Nitro host component + imperative hybrid ref API
- React Native new architecture
- React Native Nitro Modules is a dependency
- Follow the native build instructions below: additional setup is required by you.
- A valid Cesium Ion access token and asset IDs for your content (get these from https://ion.cesium.com)
- Disk and time: building Cesium Native pulls vcpkg dependencies, can use several GB of disk, and often takes a long time on first build.
First install the Nitro Modules runtime dependency:
yarn add react-native-nitro-modulesor:
npm install react-native-nitro-modulesThen add react-native-cesium:
yarn add react-native-cesiumor:
npm install react-native-cesiumThe package does not include the required iOS or Android Cesium Native libraries. Generate them locally by compiling Cesium Native with the required toolchains.
These are not installed by yarn add / pod install. Install them on your local machine first.
| Tool | Typical install (macOS) | Why |
|---|---|---|
| CMake | brew install cmake |
Configures and drives the native build |
| Ninja | brew install ninja |
Required generator on macOS for the bundled build script |
| Git | Xcode includes /usr/bin/git |
Clones Cesium Native and vcpkg |
| Python 3 | Usually present on macOS; brew install python@3 if needed |
Some vcpkg/port steps expect a working python3 |
Optional but recommended for faster or more reliable builds (see Cesium Native developer setup):
nasm—brew install nasm(speeds some JPEG-related builds in dependency trees)
Ensure Homebrew’s binary directory is on your PATH when you run the build (typically /opt/homebrew/bin on Apple Silicon or /usr/local/bin on Intel Macs). The build script also tries to prepend common Homebrew paths for nested CMake/vcpkg processes.
Additional iOS specific requirements
- Xcode or Command Line Tools:
xcode-select --install(or install Xcode from the App Store) - Needed for Apple
clang, SDKs, andxcodebuild(XCFramework assembly)
Additional Android specific requirements
- Install Android Studio
- In SDK Manager, install:
- Android SDK
- NDK (the project currently uses
27.1.12297006)
- Ensure Java 17 (JDK) is available (Android Studio bundled JDK is fine)
For Android builds, make sure one of these is set so the NDK can be discovered:
ANDROID_NDK_HOME(explicit NDK path), orANDROID_SDK_ROOT/ANDROID_HOMEwith an installed NDK underndk/
If ANDROID_NDK_HOME is unset, the build script will try to auto-detect the latest installed NDK from your SDK directory.
Your app ios/Podfile must require two helpers from this package and call them in pre_install and post_install.
Top of the Podfile (paths assume the default node_modules layout):
require_relative "../node_modules/react-native-cesium/ios/react_native_cesium_ensure_native"
require_relative "../node_modules/react-native-cesium/ios/react_native_cesium_post_install"pre_install (runs before pods resolve; triggers automatic Cesium Native clone/build when the XCFramework is missing):
pre_install do |installer|
react_native_cesium_ensure_native
endpost_install (header search paths and simulator arch exclusions):
post_install do |installer|
react_native_post_install(
installer,
config[:reactNativePath],
:mac_catalyst_enabled => false
)
react_native_cesium_post_install(installer)
endThe post-install helper is currently needed to:
- prepend this package's
cpp/headers before the locally built XCFramework headers - exclude
x86_64iOS simulator builds, because the simulator slice is Apple Siliconarm64only
Unlike iOS, you do not need to copy anything into your application’s android/build.gradle (or settings.gradle) so Cesium Native can be cloned and built automatically. React Native already includes this package as an Android library dependency; this repo’s android/build.gradle registers ensureCesiumNativeAndroid and makes preBuild depend on it.
What happens on ./gradlew / Android Studio build:
preBuildrunsensureCesiumNativeAndroidbefore CMake compiles the JNI/native code.- That task checks for a marker file under the installed package:
vendor/android/share/cesium-native/cmake/cesium-nativeConfig.cmake. - If the marker exists, the task does nothing.
- If the marker is missing, it runs (from the package root, e.g.
node_modules/react-native-cesium):
node scripts/cesium/ensure-native.mjs --android
which may runupdatethen aCESIUM_BUILD_ONLY=androidbuild, same as a manual run (can take a long time). - If
REACT_NATIVE_CESIUM_SKIP_NATIVE_BUILD=1is set and artifacts are still missing, the build fails fast with an error instead of running the script.
You still need a discoverable Android NDK (expand Additional Android specific requirements under System dependencies above) so that build can succeed.
Automatic (default): assuming you have done the above and have all the required dependencies in place, when the native output is missing, the package tries to build it for you:
- iOS: add
pre_installin your appPodfile(one-time; see Required Podfile hooks) sopod installrunsscripts/cesium/ensure-native.mjs --ios, which runsnpm run update/yarn run update(if needed) and thenCESIUM_BUILD_ONLY=iosbuild. The first run can take a long time and needs the same system dependencies as a manual build (CMake, Ninja, Xcode, disk space, etc.). - Android: no extra hooks in your app; see Android (Gradle automatic ensure-native). Needs a discoverable NDK.
Manual (optional): run the update and build scripts defined in react-native-cesium’s package.json. They are not available as top-level commands in your app; you must run them in the context of the installed package.
From your application project root (where your app’s package.json lives), after yarn add / npm install:
Yarn:
yarn --cwd node_modules/react-native-cesium run update
yarn --cwd node_modules/react-native-cesium run buildnpm:
npm run update --prefix node_modules/react-native-cesium
npm run build --prefix node_modules/react-native-cesiumAlternative: cd node_modules/react-native-cesium and run yarn run update / yarn run build (or the equivalent npm run …).
Yarn/npm workspaces: if react-native-cesium is a workspace package in your monorepo, use your tool’s workspace runner, e.g. yarn workspace react-native-cesium run update (exact syntax depends on your workspace layout).
updatechecks out Cesium Nativev0.59.0intovendor/cesium-nativeunder the package (next to its other files; typically ignored in app repos).buildrunsscripts/cesium/build.mjs(CMake, vcpkg undervendor/vcpkgunlessVCPKG_ROOTis set, Ninja on macOS) and writesvendor/ios/CesiumNative.xcframeworkand/orvendor/androiddepending onCESIUM_BUILD_ONLY.
Then install pods (iOS):
cd ios && pod install && cd ..The public JS surface is exported from react-native-cesium:
CesiumViewCameraStateQuaternionCesiumMetricsCesiumViewPropsCesiumViewMethods
Minimal usage:
import React, { useRef } from 'react'
import { StyleSheet, View } from 'react-native'
import { callback } from 'react-native-nitro-modules'
import {
CesiumView,
type CameraState,
type CesiumMetrics,
type CesiumViewMethods,
} from 'react-native-cesium'
const initialCamera: CameraState = {
latitude: 46.02,
longitude: 7.6,
altitude: 5800,
heading: 220,
pitch: -20,
roll: 0,
verticalFovDeg: 60,
}
export function GlobeScreen() {
const cesiumRef = useRef<CesiumViewMethods | null>(null)
return (
<View style={styles.container}>
<CesiumView
hybridRef={callback((ref: CesiumViewMethods | null) => {
cesiumRef.current = ref
})}
style={styles.fill}
ionAccessToken="YOUR_CESIUM_ION_ACCESS_TOKEN"
ionAssetId={1}
initialCamera={initialCamera}
pauseRendering={false}
maximumScreenSpaceError={16}
maximumSimultaneousTileLoads={8}
loadingDescendantLimit={20}
msaaSampleCount={1}
ionImageryAssetId={2}
onMetrics={callback((metrics: CesiumMetrics) => {
console.log('Cesium FPS:', metrics.fps)
})}
/>
</View>
)
}
const styles = StyleSheet.create({
container: { flex: 1 },
fill: { flex: 1 },
})When you pass a callback ref to CesiumView, wrap it with callback(...) from react-native-nitro-modules. Plain callback functions do not make it across the React Native bridge correctly for this component.
CesiumView is a Nitro host component. In addition to standard view props like style, it currently expects these Cesium-specific props:
Consumer-required props (TypeScript)
| Prop | Type | Default | Description |
|---|---|---|---|
ionAccessToken |
string |
none | Cesium Ion token for authenticated asset/imagery requests. An invalid token usually leaves the globe empty or partially loaded due to 401/403 responses. |
ionAssetId |
number |
none | Main Ion asset to render (tileset/terrain). |
initialCamera |
CameraState |
none | Initial camera used when the view is created. This is only used for initial camera; use setCamera() for runtime moves. |
pauseRendering |
boolean |
false |
Pauses the native render loop. Set true to stop rendering and reduce GPU/CPU usage. |
maximumScreenSpaceError |
number |
32 |
Quality/performance trade-off for tile refinement. Lower values are sharper (more work); higher values are faster/blurrier. |
maximumSimultaneousTileLoads |
number |
12 |
Max concurrent tile fetch/decode operations. Raising 8 -> 16 can improve fast camera moves on good networks but may increase memory/bandwidth spikes. |
loadingDescendantLimit |
number |
20 |
Caps descendant tile fan-out during traversal. Lower values like 10 smooth bursts on low-end devices; higher values like 40 can fill detail faster. |
msaaSampleCount |
number |
1 |
Anti-aliasing sample count. iOS uses 1, 2, or 4 (>=4 -> 4, >=2 -> 2, otherwise 1); Android currently renders at 1 (MSAA setting is currently ignored in Vulkan backend). |
ionImageryAssetId |
number |
1 |
Imagery layer to drape over terrain/tiles. Use a satellite imagery asset for a photoreal look, or switch to a streets/map layer for legibility. See your Cesium Ion Asset IDs. |
Consumer-optional props (TypeScript)
| Prop | Type | Default | Description |
|---|---|---|---|
onMetrics |
(metrics: CesiumMetrics) => void |
undefined |
Receives throttled runtime stats and credits text. |
type CameraState = {
latitude: number
longitude: number
altitude: number
heading: number
pitch: number
roll: number
verticalFovDeg: number
}| Field | Type | Valid range / format | Description |
|---|---|---|---|
latitude |
number |
-90..90 |
Camera latitude in degrees. |
longitude |
number |
-180..180 |
Camera longitude in degrees. |
altitude |
number |
Meters above ellipsoid | Camera height in meters. |
heading |
number |
Degrees | Compass direction the camera faces. Example: 0 faces north, 90 faces east. |
pitch |
number |
Degrees | Tilt angle. Negative values look downward toward terrain; positive values tilt up toward horizon/sky. |
roll |
number |
Degrees | Bank/rotation around forward axis. Positive right bank, negative left bank. |
verticalFovDeg |
number |
20..100 (clamped) |
Vertical field of view in degrees; see Field of view (next section). |
Use this when matching Skia, custom HUDs, or CesiumJS math to the same frustum as this view.
| Concept | What this library uses |
|---|---|
| API field | verticalFovDeg is the full vertical aperture in degrees (top to bottom through the view axis), not a half-angle and not the horizontal FOV. |
| Horizontal FOV | Not stored. With viewport width w and height h in pixels and aspect = w / h, the native tile ViewState uses full vertical FOV vfov_rad = radians(verticalFovDeg) and full horizontal FOV hfov_rad = 2 * atan(tan(vfov_rad / 2) * aspect). |
| Half-angles | The projection uses the usual symmetric perspective form: tan(vfov_rad / 2) (and the same pattern horizontally via aspect). Those are half of the full vertical/horizontal FOV, in radians—standard for tan of frustum slopes, not an alternate “FOV definition.” |
Cesium Native ViewState |
GlobeCamera passes that vfov_rad and hfov_rad into Cesium3DTilesSelection::ViewState together with the viewport size—consistent with full-angle vertical/horizontal FOV in radians for culling. |
CesiumJS PerspectiveFrustum / Camera.frustum |
Comparable vertical angle: In CesiumJS, PerspectiveFrustum#fovy is the full vertical FOV in radians (derived from fov and aspectRatio). This package’s verticalFovDeg is always vertical in degrees; use vfov_rad = Cesium.Math.toRadians(verticalFovDeg) (or equivalent) to compare to fovy when the same aspect ratio and symmetric frustum assumptions apply. This repo uses Cesium Native, not CesiumJS—tile culling uses ViewState with vfov_rad / hfov_rad as above, not the JS frustum object. |
Used for camera-space view correction with setCameraQuaternion (see below). Components are w, x, y, z (Hamilton convention). Non-unit values are normalized on the native side.
type Quaternion = {
w: number
x: number
y: number
z: number
}| Method | Signature | Description |
|---|---|---|
setCamera |
(camera: CameraState) => void |
Applies a new camera state at runtime (position, heading, pitch, roll, vertical FOV). Does not change the stored view-correction quaternion; use setCameraQuaternion when you need to update that. |
setCameraQuaternion |
(camera: CameraState, viewCorrection: Quaternion) => void |
Same camera fields as setCamera, plus a rotation applied in camera space after heading/pitch/roll. Use this for screen-fixed adjustments (e.g. boresight calibration, aligning a synthetic horizon overlay) without expressing the offset as extra Euler angles. |
getCameraState |
() => Promise<CameraState> |
Returns the current native camera snapshot (lat/lon/alt, HPR, VFOV). This is the underlying globe attitude; it does not encode the view correction into HPR. |
getViewCorrection |
() => Promise<Quaternion> |
Returns the smoothed view-correction quaternion currently applied (identity w=1,x=y=z=0 if you have never called setCameraQuaternion). |
| Call style | Supported / best practice |
|---|---|
setCamera / setCameraQuaternion |
Synchronous native updates. Supported from the UI thread, including Reanimated worklets and useAnimatedReaction when you call through a Nitro hybridRef (same pattern as driving camera demand from shared values). This is the intended path for high-frequency camera updates. |
getCameraState / getViewCorrection |
Async (Promise). Call from the JavaScript thread (e.g. useEffect, handlers, throttled HUD state)—not from worklets—unless you have a clear, tested pattern for async in your runtime. |
Avoid assuming main-thread-only vs JS-thread-only labels beyond the above: Nitro invokes the hybrid object on the thread that called the method; use sync setters on the UI/worklet path you already use for gestures, and reserve Promise-based getters for JS.
- Use
setCameraalone when you only need the classic globe camera. - Use
setCameraQuaternionwhen you need both the usualCameraStateand a small rotation relative to the uncorrected camera (HUD alignment, horizon line offset in screen space, lens/display calibration, etc.). - You may mix the two: calling
setCameraupdates position and HPR but leaves the last view-correction target unchanged, so a calibration quaternion set earlier continues to apply until you change it withsetCameraQuaternion. - Prefer
setCameraQuaternionon frames where you need to drive both the globe camera and the correction together so the native target stays consistent.
| Field | Type | Description |
|---|---|---|
fps |
number |
Smoothed frames-per-second estimate from the native render loop. |
tilesRendered |
number |
Number of tiles currently rendered in the frame. |
tilesLoading |
number |
Number of tiles still loading. If this stays high for long periods, reduce load pressure (maximumSimultaneousTileLoads) or check network. |
tilesVisited |
number |
Number of tiles visited during traversal/culling. |
ionTokenConfigured |
boolean |
Whether a non-empty Ion token is configured natively. false is a quick signal to check ionAccessToken. |
tilesetReady |
boolean |
Whether the primary tileset is initialized and ready. |
creditsPlainText |
string |
Plain-text attribution/credits from Cesium data sources. Display this in your app footer to satisfy attribution requirements. |
The example app in example/App.tsx is the best current integration reference. It shows:
- creating and storing a Nitro
hybridRef - driving camera updates with
setCamera(...)from a ReanimateduseAnimatedReaction(UI-thread / worklet path; see Threading underCesiumViewMethodsabove) - the
setCameraQuaternion/getViewCorrectionAPIs for camera-space HUD alignment (the example does not demonstrate them yet; seeCesiumViewMethodsabove) - listening to
onMetrics - switching imagery layers
- presenting Cesium attribution from
creditsPlainText
Before running the example, copy example/.env_example to example/.env and set CESIUM_ION_ACCESS_TOKEN to your own token. Restart Metro after editing .env so the env transform is reapplied.
The example project currently links this library locally via link:...
- Native rendering is built on Cesium Native
- Cesium Native is licensed under Apache 2.0; see
NOTICE - React Native integration is built with Nitro Modules
