| name | sceneview |
| description | Build 3D and AR apps with the SceneView SDK in Jetpack Compose, SwiftUI (iOS/macOS/visionOS via SceneViewSwift), Web (Filament.js), Flutter and React Native. Use whenever the user asks for "3D in Compose", "AR with ARCore in Compose", a model viewer, or any cross-platform 3D/AR app where the dependency is `io.github.sceneview:sceneview` / `io.github.sceneview:arsceneview` / `SceneViewSwift` / `sceneview-web` / `sceneview_flutter` / `@sceneview-sdk/react-native`. Skip for plain ARCore-SDK / Sceneform / Unity / Unreal / RealityKit-without-SceneViewSwift work. |
| license | Apache-2.0 |
| metadata | {"author":"SceneView","source":"https://github.com/sceneview/sceneview","last-updated":"2026-05-21","keywords":["sceneview","3d","ar","arcore","filament","realitykit","jetpack compose","swiftui","model viewer","augmented reality","kotlin multiplatform","kmp","webxr","visionos"]} |
What SceneView is
SceneView is a declarative 3D and AR SDK. One mental model across every platform:
- Android โ
SceneView { โฆ } (3D) and ARSceneView { โฆ } (AR) composables.
Filament renderer. Artifacts: io.github.sceneview:sceneview:4.16.10 and
io.github.sceneview:arsceneview:4.16.10.
- Apple (iOS / macOS / visionOS) โ
SceneView { } and ARSceneView { } SwiftUI
views from the sceneview monorepo
via Swift Package Manager (tag 4.16.10). RealityKit renderer.
- Web โ
sceneview-web@4.16.10 on npm (Filament.js + WebXR).
- Flutter โ
sceneview_flutter plugin (PlatformView bridge).
- React Native โ
@sceneview-sdk/react-native@4.16.10 (Fabric bridge).
- MCP โ
sceneview-mcp on npm โ gives AI agents direct API access from chat.
Nodes are declared as composables / SwiftUI views inside the parent SceneView's
trailing content. No imperative scene.addChild(node).
Authoritative API reference
Always treat llms.txt in the repo root as the source of truth. It carries the
full SceneView / ARSceneView signatures, every node type, every helper. URL:
https://github.com/sceneview/sceneview/blob/main/llms.txt
Repo-side samples/android-demo/src/main/java/io/github/sceneview/demo/demos/
contains a working demo for every node type โ when in doubt, read the demo, do
NOT improvise an API.
When to use this skill
Trigger on any of:
- "Build me a 3D viewer / AR app in Compose / SwiftUI."
- "Load a
.glb / .gltf / .usdz model in Compose."
- "Place a model on a detected AR plane / image / face."
- "Render 3D on the web with Filament.js or WebXR."
- "Bridge a 3D scene to Flutter or React Native."
- "Convert a 2.x / 3.x SceneView snippet to 4.x."
Skip for plain ARCore-SDK, Sceneform (deprecated), Unity, Unreal, or RealityKit
projects that do NOT use the SceneViewSwift wrapper.
The minimal correct Android example
Verified against samples/android-demo/.../ModelViewerDemo.kt:
@Composable
fun ModelViewerDemo() {
val engine = rememberEngine()
val modelLoader = rememberModelLoader(engine)
val environmentLoader = rememberEnvironmentLoader(engine)
val modelInstance = rememberModelInstance(modelLoader, "models/helmet.glb")
SceneView(
modifier = Modifier.fillMaxSize(),
engine = engine,
modelLoader = modelLoader,
environmentLoader = environmentLoader,
) {
modelInstance?.let { instance ->
ModelNode(
modelInstance = instance,
scaleToUnits = 0.3f,
centerOrigin = Position(0f, 0f, 0f),
)
}
}
}
AR tap-to-place is the same shape with ARSceneView. Verified against
samples/android-demo/.../ARPlacementDemo.kt:
@Composable
fun ARPlacementDemo() {
val engine = rememberEngine()
val modelLoader = rememberModelLoader(engine)
val placedAnchors = remember { mutableStateListOf<Anchor>() }
var latestFrame by remember { mutableStateOf<Frame?>(null) }
ARSceneView(
modifier = Modifier.fillMaxSize(),
engine = engine,
modelLoader = modelLoader,
planeRenderer = true,
sessionConfiguration = { _, config ->
config.planeFindingMode = Config.PlaneFindingMode.HORIZONTAL_AND_VERTICAL
config.lightEstimationMode = Config.LightEstimationMode.ENVIRONMENTAL_HDR
},
onSessionUpdated = { _, frame -> latestFrame = frame },
onGestureListener = rememberOnGestureListener(
onSingleTapConfirmed = { event, node ->
if (node != null) return@rememberOnGestureListener
val frame = latestFrame ?: return@rememberOnGestureListener
val hit = frame.hitTest(event).firstOrNull {
it.trackable is Plane && (it.trackable as Plane).isPoseInPolygon(it.hitPose)
}
hit?.createAnchor()?.let { placedAnchors.add(it) }
}
),
) {
placedAnchors.forEach { anchor ->
key(anchor) {
AnchorNode(anchor = anchor) {
rememberModelInstance(modelLoader, "models/helmet.glb")?.let { instance ->
ModelNode(modelInstance = instance, scaleToUnits = 0.3f, isEditable = true)
}
}
}
}
}
}
Note: ARSceneView takes a sessionConfiguration: (Session, Config) -> Unit
lambda โ there is NO rememberARSession() helper, do NOT invent one.
AnchorNode takes a com.google.ar.core.Anchor instance; create one via
hit.createAnchor() after a hit-test on the latest Frame.
Critical rules (verified โ do not break)
-
rememberModelInstance returns nullable. First recomposition returns
null while loading. Always guard with ?.let { โฆ } or ?:. Never !!.
-
Filament JNI is main-thread-only. The remember* helpers handle this.
For imperative code use modelLoader.loadModelInstanceAsync (see
llms.txt ยง Threading rules).
-
LightNode accepts both top-level params and apply = { โฆ } for builder
extras. The canonical form for intensity/color/direction is top-level
(verified against LightingDemo.kt):
LightNode(
type = LightManager.Type.POINT,
intensity = 30_000f,
direction = Direction(-x, -y, -z),
position = Position(x, y, z),
color = colorOf(r = 1.0f, g = 0.95f, b = 0.8f),
apply = { falloff(6f) },
)
type is com.google.android.filament.LightManager.Type (DIRECTIONAL,
POINT, FOCUSED_SPOT, SPOT, SUN).
-
AR anchors come from ARCore. Build an AnchorNode with a real
com.google.ar.core.Anchor from hit.createAnchor(). There are NO
AnchorNode.image() / .face() / .plane() factory functions on Android
in v4.2 โ use AugmentedImageNode for tracked images and
AugmentedFaceNode for face meshes (both in arsceneview package).
-
SceneView vs ARSceneView ship in different artifacts. Don't mix.
3D-only โ io.github.sceneview:sceneview. AR โ io.github.sceneview:arsceneview
(it transitively includes sceneview).
-
Don't recompose-thrash the loaders. rememberEngine / rememberModelLoader
/ rememberMaterialLoader / rememberEnvironmentLoader belong at the top of
the screen-level composable, NOT inside scroll lists or item composables.
Performance / hot paths
Never call a decomposing or allocating getter inside onFrame (or any 30โ60 Hz
loop). Set the whole node.transform = โฆ once instead of writing position /
quaternion / scale one at a time (one-at-a-time writes recompose the matrix and
drift โ issue #2187); use
Mat4.copyColumnsInto(scratch) not Mat4.toColumnsFloatArray() for per-frame uniform
uploads; prefer the TRS-tuple slerp(startPosition, startQuaternion, startScale, โฆ)
when you already hold the components. Reading node.worldPosition /
worldQuaternion per frame is fine now โ those are cached. Always load via
rememberModelInstance / rememberNode (cached + main-thread). Full table:
docs/docs/performance.md ยง Hot Paths & Allocation-Free APIs
(audit umbrella #2263).
Toolchain pairing
This skill is most useful paired with the android-cli skill:
android run --apks=APK --activity=PKG/.MainActivity โ install + launch in
one call.
android screen capture --annotate -o ui.png + android screen resolve --screenshot=ui.png --string="tap #N" โ visual UI testing of a 3D scene.
android layout --pretty -o ui.json โ Compose UI tree dump (the 3D viewport
reports as a single AndroidView, so for in-3D tap targets you still need
pointerInput / hit testing).
android docs search "compose canvas" โ underlying Compose APIs.
Haptic feedback
io.github.sceneview.haptic.SceneViewHaptic wraps Android's Vibrator
behind seven semantic presets plus low-level escape hatches. Get an
instance with rememberHapticFeedback() inside a @Composable; it is a
silent no-op (one Log.d, never throws) when the device has no vibrator or
the consumer app omits the permission.
import io.github.sceneview.haptic.rememberHapticFeedback
@Composable
fun PlaceAnchorButton(onPlace: () -> Unit) {
val haptic = rememberHapticFeedback()
Button(onClick = { haptic.medium(); onPlace() }) { Text("Place") }
}
- Presets:
light() medium() heavy() success() warning() error()
selection(). Escape hatches: continuous(intensity, durationMs) and
pattern(events). cancel() stops an in-progress vibration โ
rememberHapticFeedback() calls it automatically on dispose.
- The consumer app MUST opt in by adding
<uses-permission android:name="android.permission.VIBRATE" /> to its
manifest โ the sceneview library does not auto-merge it.
- The same API ships on iOS (
SceneViewSwift.SceneViewHaptic) and Web
(sceneview.haptic.*). See llms.txt ยง Haptic Feedback.
Resources
- Cheat sheet โ every public composable, node, and
helper, with their actual signatures pulled from
llms.txt.
- Recipes โ pointers to the working demo in
samples/android-demo/ for each of the 13 canonical patterns. Read the demo
file, copy from it. Do not improvise.
- Migration to 4.x โ see also
docs/docs/migration.md
for the full rename map.
Workflow guidance
When the user asks for a SceneView feature:
- Confirm the platform. Android / iOS / Web / Flutter / RN โ don't assume.
- Pick the right entrypoint.
SceneView { } for 3D-only,
ARSceneView { } for AR. Mention the matching Gradle artifact.
- Read the matching demo in
samples/android-demo/.../demos/ before
writing code. If you can't find one, fall back to llms.txt. Never
invent an API.
- Use the
remember* helpers at the top of the composable. Never call
raw constructors for Engine / ModelLoader.
- Handle the
null from rememberModelInstance with ?.let { โฆ }.
- For AR, remind the user about
Manifest.permission.CAMERA and the
com.google.ar.core <meta-data> entry.
- If the user pastes 2.x / 3.x code, point them at
docs/docs/migration.md
and the local references/migration.md.