Skip to content

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.44.0")     // 3D
implementation("io.github.sceneview:arsceneview:4.44.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
}

AutoPlacementScene is additive: one usable detected plane consumes one placement request. It uses normal camera tracking, upward-facing horizontal surfaces (SURFACE) or vertical planes (WALL), a center ray followed by visible polygon-validated plane centers, and a 0.25–3 m interaction range. It renders no plane grid or reticle.

import io.github.sceneview.ar.*

val engine = rememberEngine()
val modelLoader = rememberModelLoader(engine)
val model = rememberModelInstance(modelLoader, "models/khronos_toy_car.glb")
val placement = rememberAutoPlacementState()
AutoPlacementScene(
    assetReady = model != null,
    state = placement,
    engine = engine,
    modelLoader = modelLoader,
    surface = PlacementSurface.SURFACE, // WALL accepts vertical planes directly
    onPlaced = { result -> /* result.anchor, result.plane, result.pose */ },
) { result ->
    model?.let { AutoPlacementModel(result, placement, it, scaleToUnits = 0.3f) }
}
// Explicit reset retains the asset and the camera session:
// placement.resetPlacement(android.os.SystemClock.uptimeMillis())

AutoPlacementModel grounds the complete model bounds, preserves the contact pivot while rotating/scaling, and constrains dragging to supported plane geometry. Its 0.3 m longest-dimension default is Preview size; scaleToUnits = null retains trustworthy authored units (Actual size). Scale limits are 25–400% of that base.

For asynchronous selection, call placement.selectModel() before loading and attach only while placement.acceptsAsset(ticket) is true. Keep the previous rendered model until its replacement succeeds. Observe placement.phase; use requestPlacement(), resetPlacement(nowMillis) and keepScanning(nowMillis) for explicit actions. Reset removes the wrapper-owned anchor without restarting the camera. Interruption freezes manipulation and recovers the existing placement; it does not arm a new request. onARCoreAvailability, onTrackingFailureChanged, and onSessionFailed expose capability, tracking, and camera failures. Copy, permissions, asset selection and semantic haptics belong to the app.

States match Swift's ARPlacementPhase: INITIALIZING, SCANNING, NO_SURFACE, PLACED, ADJUSTING, TRACKING_LOST, RECOVERING, RECOVERY_FAILED, CAMERA_ERROR. The no-surface and recovery deadlines are both ten seconds. A controller manages one object; repeated requests while placed are ignored. Multi-object hosts explicitly own separate requests/controllers; tapping empty space never places.

Manual-placement compatibility: PlacementScene, WallPlacementScene (WallPlacement), onTapOnPlane, ReticleNode and the placement-reticle options remain manual-placement APIs. Their published defaults and tap behavior are unchanged. Use them for deliberate manual interactions or diagnostics; new placement flows should use AutoPlacementScene.

Direct wall placement

Use surface = PlacementSurface.WALL: detection is vertical-only, with no floor, seam alignment or placement tap. The first usable wall creates one real plane anchor. New plane detections never move a standing object; drag to a valid alternative wall or reset explicitly. Tracking loss retains that placement and enters recovery.

Author wall models with +Y up and +Z front (or supply assetRotation). AutoPlacementModel puts the bounding box's back and bottom at the contact pivot and faces its front toward the camera side of the wall, even if the detected normal points away. Drag projects the grab offset into the destination wall and validates its polygon; twist rotates in the wall plane; pinch preserves contact at 25–400% of the base size. The anchor frame retains the Android surface convention: +Y is the wall normal and −Z points up. directWallPose(point, normal, towardViewer) is an additive pure helper returning the authored +Y-up/+Z-front frame at the exact wall point; it requires a finite normal with a nonzero horizontal component. Legacy wallAnchorPose, wallFacingRotation, floorWallSeam and WallPlacementPhase keep their floor/seam semantics unchanged.

For procedural geometry, AutoPlacementNode(result, placement) { opacity -> … } uses that same gesture hierarchy. Supply content already sized (demo: 0.3 m longest dimension), with bottom at y=0 and back at z=0. Mark selectable children editable, but disable their individual position/rotation/scale editing so gestures reach the contact pivot. Apply opacity to transparent materials for the 300 ms placement/tracking fade.

Selected-object accessibility alternatives share the gesture constraints: placement.moveBy(x, y) moves in metres right/up on a wall, rotateBy(degrees) twists around its normal, and scaleTo(factor) changes its base-size multiplier. scaleFactor is observable. Keep these controls in a sheet, with Reset placement. playbackDataset is forwarded by the Android wall demo; a floor-only replay does not validate wall placement.

Wall-demo rendering Android iOS
TV geometry and size Two boxes, 0.3 m preview Same dimensions and material parameters
Contact Back/bottom pivot on the real wall Same
Wall contact shading Disabled in the demo; no procedural shadow blob No wall shadow; RealityKit grounding shadows project downward
Reveal / tracking loss 300 ms opacity fade 300 ms opacity fade

Native renderer lighting can differ. The demo does not claim physically identical wall shadows, and never substitutes a synthetic pool for renderer shading.

ARSceneView (low-level / manual placement)

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 */ }

Two different "recordings" — pick by what you capture:

  • SurfaceMirrorer (sceneview and arsceneview) — records the rendered scene to an MP4: exactly what Filament draws (in AR, camera feed + virtual content composited), no Compose UI. In-app, no MediaProjection consent dialog / foreground service. Share-ready video.
  • ARRecorder (above, arsceneview only) — records an ARCore session dataset (raw sensor streams) for deterministic replay through playbackDataset — a debugging tool, not a video.
val surfaceMirrorer = rememberSurfaceMirrorer()
SceneView(surfaceMirrorer = surfaceMirrorer) { /* DSL */ }   // or ARSceneView(surfaceMirrorer = …)

// Point a MediaRecorder (SURFACE video source) at the scene; frame is letterboxed to fit.
surfaceMirrorer.startMirroring(recorder.surface, width = 1280, height = 720)  // any thread, JNI-free
recorder.start()
// …record…
surfaceMirrorer.stopMirroring(recorder.surface)                              // main thread only
recorder.stop(); recorder.release()

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.