API Cheatsheet¶
A quick reference for SceneView's most-used APIs. Print it, pin it, keep it next to your keyboard.
Building for Apple platforms?
See the Apple API Cheatsheet for SwiftUI + RealityKit equivalents.
Setup¶
// build.gradle
implementation("io.github.sceneview:sceneview:4.18.0") // 3D
implementation("io.github.sceneview:arsceneview:4.18.0") // AR + 3D
Core Remember Hooks¶
val engine = rememberEngine()
val modelLoader = rememberModelLoader(engine)
val materialLoader = rememberMaterialLoader(engine)
val environmentLoader = rememberEnvironmentLoader(engine)
val model = rememberModelInstance(modelLoader, "models/file.glb") // null while loading
val env = rememberEnvironment(environmentLoader) {
createHDREnvironment("environments/sky.hdr")
?: createEnvironment(environmentLoader)
}
val cameraManipulator = rememberCameraManipulator()
val mainLight = rememberMainLightNode(engine) { intensity = 100_000f }
val cameraNode = rememberCameraNode(engine) { position = Position(0f, 2f, 5f) }
val viewNodeManager = rememberViewNodeManager()
SceneView¶
SceneView(
modifier = Modifier.fillMaxSize(),
engine = engine,
modelLoader = modelLoader,
cameraManipulator = cameraManipulator, // orbit/pan/zoom
cameraNode = cameraNode, // OR fixed camera
environment = env,
mainLightNode = mainLight,
surfaceType = SurfaceType.Surface, // or TextureSurface
isOpaque = true,
viewNodeWindowManager = viewNodeManager, // for ViewNode
onGestureListener = rememberOnGestureListener(
onSingleTapConfirmed = { event, node -> },
onDoubleTap = { event, node -> },
onLongPress = { event, node -> }
),
onTouchEvent = { event, hitResult -> false },
onFrame = { frameTimeNanos -> }
) {
// SceneScope — declare nodes here
}
ARSceneView¶
ARSceneView(
modifier = Modifier.fillMaxSize(),
engine = engine,
modelLoader = modelLoader,
planeRenderer = true,
sessionConfiguration = { session, config ->
config.depthMode = Config.DepthMode.AUTOMATIC
// ENVIRONMENTAL_HDR is the v4.3.0+ library default — pre-set BEFORE this callback.
// Override only to opt back into AMBIENT_INTENSITY for the cost profile.
},
sessionFeatures = setOf(), // e.g., Session.Feature.FRONT_CAMERA
// fillLightNode = null, // v4.3.0+: pass null to disable the dual-light AR baseline
cameraExposure = null, // null = default (recommended); absolute exposure scale, NOT EV stops (#1179)
flashMode = Config.FlashMode.OFF, // v4.11+: Config.FlashMode.TORCH for low-light tracking
// playbackDataset = file, // v4.5+: deterministic replay from a recorded MP4 (File)
// playbackDatasetUri = uri, // v4.11+ scoped-storage equivalent (mutually exclusive)
onSessionUpdated = { session, frame -> },
onSessionFailure = { failure -> // v4.11+: typed exhaustive when (#1759)
when (failure) {
is ARSessionFailure.ArCoreNotInstalled -> { /* install ARCore */ }
is ARSessionFailure.CameraNotAvailable -> { /* camera busy — close other camera apps */ }
// ... see ARSessionFailure for the full sealed hierarchy. Prefer an exhaustive
// `when` with NO `else` branch so the compiler flags new failure modes;
// an `else ->` silently swallows future subtypes (llms.txt § Error Handling).
else -> { /* fallback while prototyping only */ }
}
},
onTouchEvent = { event, hitResult -> true }
) {
// ARSceneScope — declare AR nodes here
}
Recording / playback¶
val recorder = rememberARRecorder()
val status by rememberARPlaybackStatus(arSession) // v4.11+: PlaybackStatus as State
ARSceneView(
playbackDatasetUri = pickedUri, // scoped-storage Uri
onSessionUpdated = { s, _ -> recorder.recordFrame(s) },
onPlaybackFailed = { e -> /* MP4 unreadable */ },
) { /* DSL */ }
Camera exposure override¶
cameraExposure is Filament's absolute exposure scale (the single-Float setExposure
overload — 1.0 ≈ ISO 100 ≈ EV 0). It is NOT a signed EV-stop bias: negative values clamp
to zero and render a fully black framebuffer (#1179). Realistic range is roughly 0.05–16.
// Brighten a too-dark camera preview
ARSceneView(
cameraExposure = 2.0f // > 1.0 = brighter, < 1.0 = darker, null = default (recommended)
) { }
// Darken a washed-out preview
ARSceneView(
cameraExposure = 0.5f // NEVER pass a negative value — it clamps to a black frame
) { }
Prefer leaving it null — the default AR camera tuning is correct for both back- and
front-camera sessions. Note: the iOS ARSceneView(cameraExposure:) is a different
mechanism (EV-stop post-process via CIColorControls) — do not copy values across platforms.
Node Types — 3D¶
| Node | Key Parameters |
|---|---|
ModelNode |
modelInstance, scaleToUnits, centerOrigin, position, rotation, isEditable, autoAnimate, animationName, animationLoop |
CubeNode |
size: Size, materialInstance |
SphereNode |
radius: Float, materialInstance |
CylinderNode |
radius, height, materialInstance |
PlaneNode |
size: Size, materialInstance |
LightNode |
type: LightManager.Type, apply = { intensity(); color(); castShadows() } |
ImageNode |
imageFileLocation / imageResId / bitmap, size |
VideoNode |
videoPath (simple) / player: MediaPlayer (advanced), chromaKeyColor, size |
ViewNode |
windowManager, content = @Composable |
TextNode |
text, fontSize, textColor, backgroundColor, widthMeters |
BillboardNode |
bitmap, widthMeters, heightMeters |
LineNode |
start, end, materialInstance |
PathNode |
points: List<Position>, closed, materialInstance |
DynamicSkyNode |
timeOfDay (0-24), turbidity, sunIntensity |
FogNode |
view, density, height, color, enabled |
ReflectionProbeNode |
filamentScene, environment, position, radius, cameraPosition |
PhysicsNode |
node, restitution, linearVelocity, floorY, radius, floorProvider (mass overload is deprecated — no-op) |
MeshNode |
primitiveType, vertexBuffer, indexBuffer, materialInstance |
Node |
position, rotation, scale + child content |
SecondaryCamera |
apply — non-active camera (formerly CameraNode) |
Node Types — AR¶
| Node | Key Parameters |
|---|---|
AnchorNode |
anchor: Anchor + child content |
HitResultNode |
xPx, yPx + child content (reticle) |
AugmentedImageNode |
augmentedImage + child content |
AugmentedFaceNode |
augmentedFace, meshMaterialInstance |
CloudAnchorNode |
anchor, cloudAnchorId, onHosted + child content |
Common Node Properties¶
node.position = Position(x, y, z) // meters
node.rotation = Rotation(x, y, z) // degrees
node.scale = Scale(x, y, z) // multiplier
node.isVisible = true
node.isEditable = true // pinch-scale, drag-move, rotate
node.isTouchable = true
node.onSingleTapConfirmed = { event -> true }
node.onFrame = { frameTimeNanos -> }
// Smooth movement
node.transform(position = Position(2f, 0f, 0f), smooth = true, smoothSpeed = 5f)
node.lookAt(targetNode)
// Animation
node.animateRotations(Rotation(0f), Rotation(y = 360f)).also {
it.duration = 2000
it.repeatCount = ValueAnimator.INFINITE
}.start()
Math Types¶
import io.github.sceneview.math.*
Position(x = 0f, y = 1f, z = -2f) // Float3, meters
Rotation(x = 0f, y = 90f, z = 0f) // Float3, degrees
Scale(1.5f) // uniform
Scale(x = 2f, y = 1f, z = 2f) // non-uniform
Direction(x = 0f, y = 1f, z = 0f) // unit vector
Size(x = 1f, y = 0.5f, z = 0f) // Float3 — dimensions in meters
Resource Loading¶
// Composable (preferred)
val model = rememberModelInstance(modelLoader, "models/file.glb")
// Imperative
val model = modelLoader.loadModelInstance("models/file.glb") // suspend — call from a coroutine
modelLoader.loadModelInstanceAsync("models/file.glb") { instance -> }
// Environment
environmentLoader.createHDREnvironment("environments/sky.hdr")
environmentLoader.createKTX1Environment(iblAssetFile = "environments/studio_ibl.ktx")
// Material
materialLoader.createColorInstance(Color.Red)
Custom 3D content¶
Authoring your own model? See the Blender pipeline recipe:
export .glb from Blender for Android (native), or convert .glb → .usdz via Reality
Converter + Reality Composer Pro for Apple platforms.
Threading Rules¶
| Safe | Unsafe |
|---|---|
rememberModelInstance(...) |
modelLoader.createModelInstance(...) on IO |
loadModelInstanceAsync(...) |
materialLoader.createMaterial(...) on IO |
Any composable in SceneView { } |
Direct Filament API on background thread |
Rule: Filament JNI = main thread only. remember* hooks handle this for you.
AR debug — Rerun.io¶
Stream ARCore frames into the Rerun viewer for scrub-and-replay debugging.
import io.github.sceneview.ar.rerun.rememberRerunBridge
@Composable
fun ARDebugScreen() {
val bridge = rememberRerunBridge(rateHz = 10, enabled = BuildConfig.DEBUG)
ARSceneView(onSessionUpdated = { s, f -> bridge.logFrame(s, f) })
}
| Mode | Sidecar command | Shareable? |
|---|---|---|
| Live | python rerun-bridge.py |
No — viewer is local-only |
| Save | python rerun-bridge.py --save |
Yes — writes a .rrd file |
Save & Share trigger from the app:
bridge.requestSaveAndShare { result ->
// result.path -> /Users/dev/.sceneview/recordings/<ts>.rrd
// result.viewerUrl -> https://sceneview.github.io/rerun/?url=<…>
// result.events -> 1234
}
Drop the saved .rrd onto https://sceneview.github.io/rerun/ to scrub
the AR session frame-by-frame in any browser — no install required, no
re-hosting needed for local inspection. To share with a remote teammate,
re-host the file (R2, GitHub release, gist) and send them
https://sceneview.github.io/rerun/?url=<encoded-public-url>.
Spatial Audio & Haptic — cross-platform availability¶
v4.12.0 shipped Spatial Audio (#1900) and Haptic Feedback (#1901). Both have real implementations on all three platforms — they are not Android-only — each using the platform-native audio / vibration backend:
| Feature | Android | Web (sceneview-web) |
iOS |
|---|---|---|---|
| Spatial Audio | SpatialAudioNode { } composable |
io.github.sceneview.web.audio.SpatialAudioNode — a real Kotlin/JS class backed by the Web Audio PannerNode (HRTF panning + distance falloff). Load assets with loadAudioSource(url) / loadAudioSourcePromise(url); drive the listener with setSpatialAudioListenerPose(...). Exposed to Kotlin/JS consumers — it is not @JsExport-ed to a plain-JavaScript sceneview.js global (the module uses suspend, external Web Audio declarations and a sealed interface, none @JsExport-compatible). The web Spatial Audio demo (#1944) uses this API. |
SpatialAudioNode.spatial(...) |
| Haptic Feedback | rememberHapticFeedback() → SceneViewHaptic |
io.github.sceneview.web.haptic.SceneViewHaptic — semantic presets via the Web Vibration API (navigator.vibrate). @JsExport-ed — callable from plain JavaScript as sceneview.haptic.light() / .success() / .continuous(intensity, durationMs) etc. Durations only — intensity / sharpness are accepted for cross-platform parity but ignored at runtime (the Vibration API has no amplitude control). Silent no-op on browsers without navigator.vibrate (most desktop, Safari iOS). |
SceneViewHaptic() (Core Haptics) |
See the Apple API Cheatsheet for the iOS maturity detail.
Apple platforms¶
Building for iOS, macOS, or visionOS? See the Apple API Cheatsheet.