# RealityKit Patterns

Longer examples for the `realitykit-ar` skill. SKILL.md covers setup, anchoring,
raycasting and gestures; this file goes deeper.

## Contents

- [Physics](#physics)
- [Animation](#animation)
- [Materials and lighting](#materials-and-lighting)
- [Custom components and systems](#custom-components-and-systems)
- [Occlusion and environment](#occlusion-and-environment)
- [A SwiftUI model viewer](#a-swiftui-model-viewer)
- [Entity accessibility](#entity-accessibility)
- [Performance](#performance)

## Physics

A static floor and a bouncing ball:

```swift
@MainActor
func makeCourt() -> (floor: ModelEntity, ball: ModelEntity) {
    let floor = ModelEntity(mesh: .generatePlane(width: 3, depth: 3))
    floor.components.set(PhysicsBodyComponent(massProperties: .default, material: .default, mode: .static))
    floor.components.set(CollisionComponent(shapes: [.generateBox(size: [3, 0.005, 3])]))

    let bounce = PhysicsMaterialResource.generate(staticFriction: 0.4, dynamicFriction: 0.3, restitution: 0.8)
    let ball = ModelEntity(mesh: .generateSphere(radius: 0.05))
    ball.components.set(PhysicsBodyComponent(massProperties: .init(mass: 0.6), material: bounce, mode: .dynamic))
    ball.components.set(CollisionComponent(shapes: [.generateSphere(radius: 0.05)]))
    ball.position = [0, 1, 0]
    return (floor, ball)
}
```

Entities with a physics body conform to `HasPhysicsBody`, which adds:

- `addForce(_:relativeTo:)`: a continuous force in newtons, applied each frame
  you call it.
- `applyLinearImpulse(_:relativeTo:)`: an instant kick.

```swift
@MainActor
func kick(_ ball: ModelEntity) {
    ball.applyLinearImpulse([0.3, 1.2, 0], relativeTo: nil)
}
```

Collision events:

```swift
RealityView { content in
    let court = makeCourt()
    content.add(court.floor)
    content.add(court.ball)
    _ = content.subscribe(to: CollisionEvents.Began.self) { contact in
        Log.scene.debug("\(contact.entityA.name) touched \(contact.entityB.name)")
    }
    _ = content.subscribe(to: CollisionEvents.Ended.self) { contact in
        Log.scene.debug("\(contact.entityA.name) separated")
    }
}
```

## Animation

Move an entity by animating to a changed copy of its transform:

```swift
@MainActor
func slideUp(_ panel: Entity) {
    var target = panel.transform
    target.translation.y += 0.2
    panel.move(to: target, relativeTo: panel.parent, duration: 0.8, timingFunction: .easeInOut)
}
```

Circle an entity around a point with a per-frame subscription:

```swift
RealityView { content in
    let moon = ModelEntity(mesh: .generateSphere(radius: 0.02))
    content.add(moon)
    let center: SIMD3<Float> = [0, 0, -0.7]
    var angle: Float = 0
    _ = content.subscribe(to: SceneEvents.Update.self) { frame in
        angle += Float(frame.deltaTime) * 0.9
        moon.position = center + [0.25 * cos(angle), 0, 0.25 * sin(angle)]
    }
}
```

Play an animation baked into a USDZ file:

```swift
@MainActor
func startIdleLoop(on robot: Entity) {
    guard let clip = robot.availableAnimations.first else { return }
    robot.playAnimation(clip.repeat(duration: .infinity), transitionDuration: 0.4, startsPaused: false)
}
```

## Materials and lighting

```swift
@MainActor
func chrome() -> SimpleMaterial {
    SimpleMaterial(color: .lightGray, roughness: .float(0.05), isMetallic: true)
}

@MainActor
func frostedGlass() -> PhysicallyBasedMaterial {
    var glass = PhysicallyBasedMaterial()
    glass.baseColor = .init(tint: .white)
    glass.roughness = 0.3
    glass.blending = .transparent(opacity: 0.5)
    return glass
}

@MainActor
func woodGrain() async -> SimpleMaterial {
    var wood = SimpleMaterial()
    if let grain = try? await TextureResource(named: "oak_diffuse") {
        wood.color = .init(tint: .white, texture: .init(grain))
    }
    return wood
}
```

`SimpleMaterial` has no `blending` property; transparency needs
`PhysicallyBasedMaterial` or `UnlitMaterial`.

Image-based lighting from an environment resource: put an
`ImageBasedLightComponent` on one entity and point each lit entity at it with
`ImageBasedLightReceiverComponent`. `RealityViewCameraContent.environment` has
no lighting properties on iOS.

```swift
RealityView { content in
    guard let studio = try? await EnvironmentResource(named: "softbox") else { return }
    let light = Entity()
    light.components.set(ImageBasedLightComponent(source: .single(studio), intensityExponent: 1.2))
    content.add(light)

    let vase = ModelEntity(mesh: .generateSphere(radius: 0.1), materials: [chrome()])
    vase.components.set(ImageBasedLightReceiverComponent(imageBasedLight: light))
    content.add(vase)
}
```

## Custom components and systems

```swift
struct Hover: Component {
    var height: Float = 0.03
    var speed: Float = 2
    var phase: Float = 0
    var offset: Float { sin(phase) * height }
}

struct HoverSystem: System {
    static let query = EntityQuery(where: .has(Hover.self))

    init(scene: RealityKit.Scene) {}

    func update(context: SceneUpdateContext) {
        for entity in context.entities(matching: Self.query, updatingSystemWhen: .rendering) {
            guard var hover = entity.components[Hover.self] else { continue }
            hover.phase += hover.speed * Float(context.deltaTime)
            entity.position.y = hover.offset
            entity.components[Hover.self] = hover

            if var model = entity.components[ModelComponent.self], hover.offset > 0 {
                model.materials = [SimpleMaterial(color: .systemYellow, isMetallic: false)]
                entity.components[ModelComponent.self] = model
            }
        }
    }
}
```

Register both once at launch, then attach the component to entities:

```swift
Hover.registerComponent()
HoverSystem.registerSystem()
lantern.components.set(Hover())
```

Components are value types, so read, change and write back; the same goes for
`ModelComponent`.

## Occlusion and environment

An invisible occluder hides virtual objects behind a real table or wall:

```swift
let tableTop = ModelEntity(mesh: .generatePlane(width: 1.2, depth: 0.7), materials: [OcclusionMaterial()])
```

In AR mode the camera feed is the background. With the `.virtual` camera, the
iOS `RealityViewEnvironment` offers `.default` and `.skybox(_:)`:

```swift
content.camera = .virtual
if let sky = try? await EnvironmentResource(named: "softbox") {
    content.environment = .skybox(sky)
}
```

## A SwiftUI model viewer

```swift
import AVFoundation
import Observation
import RealityKit
import SwiftUI

@Observable @MainActor
final class ShowroomModel {
    var selection = "chair"
    var scale: Float = 0.01
    private var current: ModelEntity?

    func show(in content: RealityViewCameraContent) async {
        current?.removeFromParent()
        guard let piece = try? await ModelEntity(named: selection) else { return }
        piece.name = "showpiece"
        piece.scale = SIMD3(repeating: scale)
        piece.position = [0, -0.1, -0.6]
        piece.generateCollisionShapes(recursive: true)
        piece.components.set(InputTargetComponent())
        content.add(piece)
        current = piece
    }
}

struct ShowroomView: View {
    @State private var model = ShowroomModel()
    @State private var cameraAllowed = false

    var body: some View {
        Group {
            if cameraAllowed {
                RealityView { content in
                    await model.show(in: content)
                } update: { content in
                    content.entities.first { $0.name == "showpiece" }?.scale = SIMD3(repeating: model.scale)
                }
                .gesture(
                    DragGesture()
                        .targetedToAnyEntity()
                        .onChanged { drag in
                            guard let parent = drag.entity.parent,
                                  let point = drag.unproject(\.location, to: parent) else { return }
                            drag.entity.position = point
                        }
                )
                .overlay(alignment: .bottom) { controls }
            } else {
                ContentUnavailableView("Camera needed", systemImage: "camera",
                                       description: Text("Allow camera access in Settings to place furniture."))
            }
        }
        .task { cameraAllowed = await cameraReady() }
    }

    private var controls: some View {
        VStack {
            Picker("Item", selection: $model.selection) {
                Text("Armchair").tag("chair")
                Text("Desk lamp").tag("desk_lamp")
            }
            .pickerStyle(.segmented)
            Slider(value: $model.scale, in: 0.001...0.05, step: 0.001)
        }
        .padding()
        .background(.ultraThinMaterial, in: .rect(cornerRadius: 20))
        .padding()
    }
}
```

`cameraReady()` is the permission helper from SKILL.md. The drag handler
guards for a parent entity instead of assuming one exists.
`generateCollisionShapes(recursive: true)` together with
`InputTargetComponent()` makes a loaded model interactive.

## Entity accessibility

```swift
@MainActor
func describe(_ door: Entity) {
    var info = AccessibilityComponent()
    info.isAccessibilityElement = true
    info.label = "Garage door"
    info.value = "Closed"
    info.traits = [.button]
    info.systemActions = [.activate]
    door.components.set(info)
}
```

## Performance

Keep a pool of entities and recycle them, rather than allocating fresh ones
and throwing them away:

```swift
@MainActor
final class DebrisPool {
    private var idle: [ModelEntity] = []
    private let mesh = MeshResource.generateBox(size: 0.02)
    private let paint = [SimpleMaterial(color: .gray, isMetallic: false)]

    init(size: Int) {
        for _ in 0..<size {
            let chip = ModelEntity(mesh: mesh, materials: paint)
            chip.isEnabled = false
            idle.append(chip)
        }
    }

    func take() -> ModelEntity {
        let chip = idle.popLast() ?? ModelEntity(mesh: mesh, materials: paint)
        chip.isEnabled = true
        return chip
    }

    func giveBack(_ chip: ModelEntity) {
        chip.isEnabled = false
        chip.removeFromParent()
        idle.append(chip)
    }
}
```

Fewer draw calls:

- Merge static entities where you can.
- Use instancing for geometry that repeats.
- Keep the number of distinct materials low.
- Swap in lower-detail models as distance grows.

Memory:

- Unload entities nobody can see.
- Load with `ModelEntity(named:in:)` and an explicit bundle to control where
  resources come from.
- Profile with the RealityKit Trace template in Instruments.
- For entities you will show again, set `isEnabled = false` rather than removing
  them.
