# SceneViewSwift 3D and AR scenes in SwiftUI, powered by RealityKit. The Apple companion to [SceneView for Android](https://github.com/sceneview/sceneview). ![iOS 18+](https://img.shields.io/badge/iOS-18%2B-blue) ![macOS 15+](https://img.shields.io/badge/macOS-15%2B-blue) ![visionOS 2+](https://img.shields.io/badge/visionOS-2%2B-blue) ## Installation Add SceneViewSwift via Swift Package Manager: 1. In Xcode, go to **File > Add Package Dependencies...** 2. Enter the repository URL: ``` https://github.com/sceneview/sceneview ``` 3. Select the version rule and add the package to your target. Or add it to your `Package.swift`: ```swift dependencies: [ .package(url: "https://github.com/sceneview/sceneview", from: "4.55.0") ] ``` ## Quick Start ### 3D Model Viewer ```swift import SwiftUI import SceneViewSwift struct ModelViewer: View { @State private var model: ModelNode? var body: some View { SceneView { content in if let model { content.addChild(model.entity) } } // The content closure runs once unless its identity changes: // re-run it when the async load lands, or the scene stays empty. .contentID(model != nil) .environment(.studio) .cameraControls(.orbit) .autoRotate() .task { model = try? await ModelNode.load("robot.usdz") } } } ``` ### AR Tap-to-Place ```swift import SwiftUI import SceneViewSwift struct ARPlacement: View { @State private var model: ModelNode? @State private var placedEntities: [Entity] = [] var body: some View { ARSceneView( planeDetection: .horizontal, onTapOnPlane: { position, arView in guard let model else { return } let anchor = AnchorNode.world(position: position) anchor.add(model.entity.clone(recursive: true)) arView.scene.addAnchor(anchor.entity) } ) .task { model = try? await ModelNode.load("chair.usdz") } } } ``` ### Procedural Shapes with PBR ```swift SceneView { root in let metal = GeometryNode.sphere( radius: 0.3, material: .pbr(color: .gray, metallic: 1.0, roughness: 0.1) ) .position(.init(x: 0, y: 0.3, z: 0)) .withGroundingShadow() root.addChild(metal.entity) } .environment(.sunset) ``` ### Lights ```swift let sun = LightNode.directional(color: .warm, intensity: 1000, castsShadow: true) .lookAt(.zero) let lamp = LightNode.point(color: .custom(r: 1, g: 0.5, b: 0), intensity: 500) .position(.init(x: 1, y: 2, z: 0)) ``` ### 3D Text & Billboards ```swift // Always faces camera let label = BillboardNode.text("Player 1", fontSize: 0.04) .position(.init(x: 0, y: 1.5, z: 0)) // 3D extruded text let title = TextNode(text: "SceneView", fontSize: 0.08, depth: 0.02) .centered() ``` ## API Reference ### Views | Type | Description | |---|---| | `SceneView` | 3D scene with orbit camera, lighting, and gestures | | `ARSceneView` | AR scene with plane detection and tap-to-place | ### Nodes | Type | Description | |---|---| | `ModelNode` | Model loading (USDZ, STL, OBJ, PLY) with animations and collision | | `GeometryNode` | Procedural shapes (cube, sphere, cylinder, cone, plane) | | `MeshNode` | Custom mesh geometry from raw vertex data | | `ShapeNode` | 2D polygon shapes (flat or extruded) with ear-clipping triangulation | | `TextNode` | 3D extruded text with centering | | `BillboardNode` | Always-faces-camera wrapper | | `LineNode` | Line segments and axis gizmos | | `PathNode` | Closed and open 3D paths with customizable geometry (circle, grid helpers) | | `LightNode` | Directional, point, and spot lights | | `CameraNode` | Programmatic camera control with orbit, fly-through, and custom modes | | `ImageNode` | Display images on 3D planes with automatic aspect ratio | | `VideoNode` | Play video content on 3D surfaces with playback controls | | `PhysicsNode` | Apply physics simulation (dynamic, static, kinematic) to entities | | `DynamicSkyNode` | Time-of-day sun positioning with atmospheric color model | | `FogNode` | Atmospheric fog (linear, exponential, height-based) | | `ReflectionProbeNode` | Local cubemap reflections for realistic surfaces | | `SpatialAudioNode` | Positional 3D audio source attached to the scene graph | | `ViewNode` | Deprecated placeholder plane for a SwiftUI view — the content is not rendered yet ([#1035](https://github.com/sceneview/sceneview/issues/1035)) | | `AnchorNode` | AR world/plane anchor | | `AugmentedImageNode` | Detect real-world images and place 3D content (iOS only) | | `CloudAnchorNode` | Host and resolve ARCore Cloud Anchors (iOS only) | | `SceneReconstructionNode` | LiDAR scene-reconstruction mesh as collidable geometry (iOS only) | ### Configuration | Type | Description | |---|---| | `SceneEnvironment` | 7 HDR presets: studio, outdoor, sunset, night, warm, autumn, nightSky | | `CameraControls` | Orbit camera with inertia and auto-rotation | | `GeometryMaterial` | PBR material: `.simple`, `.pbr`, `.unlit` | | `ModelFormat` | Supported formats + content-based sniffing | | `ModelUnit` | The unit a file's coordinates are in | | `MeshAsset` | Parsed geometry, before RealityKit — measurable off the main actor | ## Model Formats `ModelNode.load(contentsOf:unit:)` opens every supported format from one call. The format is decided by reading the file's bytes, not by trusting its extension — a model that arrived through a share sheet or a download often has the wrong one. | Format | Reader | Default unit | |---|---|---| | `.usdz` `.usda` `.usdc` `.usd` `.reality` | RealityKit | metres (already metric) | | `.stl` (ASCII + binary) | ModelIO | **millimetres** | | `.obj` (+ `.mtl` sidecar) | ModelIO | metres | | `.ply` (ASCII + binary, vertex colours) | ModelIO | metres | | `.3mf` | SceneViewSwift's own parser | declared by the file | 3MF is parsed here rather than by Apple: `MDLAsset` reads OBJ, STL, PLY and USD, Quick Look reads USDZ and Reality, and nothing on the platform opens the format 3D printing standardised on. `ThreeMFDocument` covers the core spec — units, meshes, components with row-vector transforms, build items, `` and `` resolved per triangle — plus the Production extension's `p:path` references, which is how Bambu Studio and Orca write project files. External XML entities are refused. glTF (`.glb` / `.gltf`) is not supported on Apple platforms — neither RealityKit nor ModelIO reads it. Convert to USDZ first. ### Units matter STL, OBJ and PLY store bare numbers with no unit. A slicer STL means millimetres; a photogrammetry OBJ means metres. RealityKit works in metres, so guessing wrong puts a 21 cm print in the room at 210 m. The conversion is baked into the vertex positions, so your own `.scale(_:)` stays yours. ```swift // Millimetres by default — a 210 mm print is 0.21 m in AR. let printed = try await ModelNode.load(contentsOf: stlURL) // Override when you know better. let scan = try await ModelNode.load(contentsOf: objURL, unit: .centimeters) ``` `ModelFormat.carriesUnit` is `false` for exactly those three formats — the signal to show a mm/cm/in picker rather than guess. ### Measure before rendering Parsing has no RealityKit dependency, so a file can be inspected off the main actor: ```swift let asset = try MeshAsset.load(contentsOf: url) print(asset.format, asset.unit, asset.triangleCount) print(asset.boundsInMeters?.extents ?? .zero) // real-world size let node = try await ModelNode(asset) // then display it ``` ## Platform Support | Feature | iOS 18+ | macOS 15+ | visionOS 2+ | |---|---|---|---| | `SceneView` — 3D scene | ✅ | ✅ | ✅ | | `ARSceneView` — AR/camera passthrough | ✅ | ❌ AR requires device camera | ❌ AR requires device camera | | All 3D nodes (`ModelNode`, `GeometryNode`, lights, etc.) | ✅ | ✅ | ✅ | | `BillboardNode` — always faces camera | ✅ | ✅ | ✅ | | `CameraNode` field-of-view / clip planes | ✅ | ⚠️ RealityKit `PerspectiveCameraComponent` not available on macOS | ✅ | | `SceneViewHaptic` | ✅ | ❌ iOS-only | ❌ | | `AnchorNode` / `AugmentedImageNode` | ✅ | ❌ ARKit is iOS/iPadOS-only | ❌ | **macOS:** `SceneView` (3D rendering) is fully supported on macOS 15+. `ARSceneView` is iOS/iPadOS-only and is not available on macOS — `ARSceneView.swift` is wrapped in `#if os(iOS)`. The same applies to all ARKit-dependent features. ## Platform Mapping (Android ↔ iOS) | Android (Compose) | iOS (SwiftUI) | |---|---| | `SceneView { }` | `SceneView { root in }` | | `ARSceneView { }` | `ARSceneView()` | | `rememberModelInstance` | `ModelNode.load()` | | `CubeNode` | `GeometryNode.cube()` | | `SphereNode` | `GeometryNode.sphere()` | | `ShapeNode` | `ShapeNode(points:)` | | `LightNode(apply = { })` | `LightNode.directional()` | | `rememberEnvironment` | `.environment(.studio)` | | `CameraManipulator` | `CameraControls` | ## Example App See [`Examples/SceneViewDemo/`](Examples/SceneViewDemo/) for a full 4-tab demo: - **Explore** -- 3D viewer with orbit camera and 7 HDR environments - **Shapes** -- All primitive shapes with live previews and code snippets - **AR** -- Tap-to-place objects on real surfaces - **About** -- SDK information and feature list ## License Apache 2.0. See [LICENSE](../LICENSE) for details.