Erstellen einer AR-Anwendung mit RealityKit

von @ralfebert · aktualisiert am 20. November 2021
Xcode 13 & iOS 15
Fortgeschrittene iOS-Entwickler*innen
Deutsch

2020 hat Apple mit dem neuen RealityKit-Framework die Entwicklung von Augmented Reality Apps deutlich vereinfacht. RealityKit ersetzt den alten, auf SceneKit basierenden Ansatz zum Schreiben von AR-Apps. In diesem Tutorial lernst du die Grundlagen der Erstellung einer AR-App mit RealityKit am Beispiel einer AR-Würfel-App.

Dies ist ein fortgeschrittenes Tutorial für iOS-Entwickler. Du benötigst gute Kenntnisse in Swift und SwiftUI.

Grundlegende AR-Einrichtung

  1. Verwende die neueste Version von Xcode. Dieses Tutorial wurde zuletzt am 19. November 2021 mit Xcode 13 getestet.

  2. Erstelle ein neues iOS App-Projekt. Wähle SwiftUI als Interface-Technologie aus und nenne das Projekt ARDice:

    Für echte Projekte kann man die Vorlage Augmented Reality App verwenden, aber für dieses Tutorial ist es besser, alles von Grund auf einzurichten.

  3. Öffne ContentView.swift und erstelle eine UIViewRepresentable-struct, um ein ARView einzuhüllen, damit dieses in SwiftUI verwendet werden kann (derzeit gibt es im kein SwiftUI-View für RealityKit). Füge es zur ContentView hinzu:

    import ARKit
    import RealityKit
    import SwiftUI
    
    struct RealityKitView: UIViewRepresentable {
        func makeUIView(context: Context) -> ARView {
           let view = ARView()
           return view
        }
    
        func updateUIView(_ view: ARView, context: Context) {
        }
    }
    
    struct ContentView: View {
        var body: some View {
            RealityKitView()
        .ignoresSafeArea()
        }
    }
    
  4. Die App benötigt Zugriff auf die Kamera: Öffne die Target-Konfiguration und füge einen Key Privacy - Camera Usage Description in die Info.plist ein. Gib eine Beschreibung an, warum die Kamera benötigt wird:

  5. Starte die AR-Session in der makeUIView-Methode und konfiguriere sie mit einer ARWorldTrackingConfiguration so, dass sie automatisch horizontale Flächen erkennt.

    Füge außerdem ein ARCoachingOverlayView hinzu, das den Benutzer führt, bis die erste Fläche gefunden ist:

    Es empfiehlt sich auch, einige debugOptions zu setzen, damit du einen Einblick bekommst, wie das zugrunde liegende ARKit Framework die Umgebung interpretiert:

    import ARKit
    import SwiftUI
    import RealityKit
    
    struct RealityKitView: UIViewRepresentable {
        func makeUIView(context: Context) -> ARView {
            let view = ARView()
    
            // Start AR session
    let session = view.session
    let config = ARWorldTrackingConfiguration()
    config.planeDetection = [.horizontal]
    session.run(config)
    
            // Add coaching overlay
    let coachingOverlay = ARCoachingOverlayView()
    coachingOverlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    coachingOverlay.session = session
    coachingOverlay.goal = .horizontalPlane
    view.addSubview(coachingOverlay)
    
            // Set debug options
    #if DEBUG
    view.debugOptions = [.showFeaturePoints, .showAnchorOrigins, .showAnchorGeometry]
    #endif
    
            return view
        }
    
        func updateUIView(_ view: ARView, context: Context) {
        }
    
    }
    
  6. Starte die App auf einem iPhone oder iPad (RealityKit funktioniert nicht im Simulator).

    Du solltest das Coaching-Overlay sehen (außer auf Geräten mit einem LiDAR-Sensor, wo die Fläche so schnell erkannt wird, dass das Overlay möglicherweise gar nicht sichtbar wird).

    Du solltest eine größere Menge Feature-Punkte sehen - dies sind markante Punkte, die ARKit zusammen mit den Sensordaten für das World-Tracking verwendet, um die virtuelle AR-Welt über dem Kamerabild ausgerichtet zu halten.

    Nach ein paar Sekunden sollte eine horizontale Ebene erkannt werden, und du solltest eine großen grüne Fläche sehen. Um das Testen deiner AR-App zu beschleunigen, kann es hilfreich sein, eine Oberfläche mit viel Struktur zu verwenden - auf einer einfarbigen Oberfläche wird es für ARKit schwierig, markante Punkte zu finden.

    Die Achsen markieren den anchorOrigin - den Mittelpunkt der erkannten horizontalen Ebene. Mach dich mit den Farben vertraut. Du kannst dir hier ein Bild von dem Koordinatensystem machen, das ARKit/RealityKit verwendet. Die Y-Achse (grün) zeigt nach oben.

    Hier findest Du eine hilfreiche Übersicht zu dem Koordinatensystem - es könnte in den folgenden Schritten nützlich sein, zu wissen, welche Achse in welche Richtung zeigt:

Hinzufügen eines Fokus-Cursors

Für dieses Beispiel werden wir einen 3D-Cursor hinzufügen, der es ermöglicht, interaktiv einen Ort auszuwählen, an dem der Würfel platziert werden soll. Dafür gibt es Beispielcode von Apple im Projekt ↗ Placing Objects and Handling 3D Interaction, den ↗ Max Cobb auf RealityKit portiert und das Swift-Package ↗ FocusEntity erstellt hat, das wir im Folgenden verwenden.

  1. In RealityKit gibt es Szenen, die aus Entities zusammengesetzt sind. Diese sind die "Objekte" der Szene und können mit Components konfiguriert werden, die Verhalten und Aussehen hinzufügen. Es könnte sinnvoll sein, die Übersichtsdokumentation zu diesen Konzepten zu lesen, da sie in den folgenden Schritten häufig verwendet werden:

  2. Füge das FocusEntity-Paket als Abhängigkeit zur Projektkonfiguration hinzu:

    https://github.com/maxxfrazer/FocusEntity
    
    Add FocusEntity package
  3. Wenn eine Fläche gefunden wird, wird ein Anker zur AR-Welt hinzugefügt. Auf dieses Ereignis können wir mit einem ↗ ARSessionDelegate reagieren. Um ein Delegate-Objekt in SwiftUI zu implementieren, müssen wir dem UIViewRepresentable ein ↗ Coordinator-Objekt hinzufügen:

    import ARKit
    import RealityKit
    import SwiftUI
    import FocusEntity
    
    struct RealityKitView: UIViewRepresentable {
        func makeUIView(context: Context) -> ARView {
            let view = ARView()
    
            // ...
    
            // Handle ARSession events via delegate
    context.coordinator.view = view
    session.delegate = context.coordinator
    
            return view
        }
    
        // ...
    
        func makeCoordinator() -> Coordinator {
        Coordinator()
    }
    
        class Coordinator: NSObject, ARSessionDelegate {
        weak var view: ARView?
        var focusEntity: FocusEntity?
    
        func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
            guard let view = self.view else { return }
            debugPrint("Anchors added to the scene: ", anchors)
            self.focusEntity = FocusEntity(on: view, style: .classic(color: .yellow))
        }
    }
    
    }
    
  4. Starte die App. Wenn du die Fehlermeldung dyld: Bibliothek für das RealityFoundation-Projekt nicht geladen, wende diesen Workaround an.

    Du solltest einen gelben, quadratischen Fokus-Cursor auf der Ebene sehen (es könnte eine gute Idee sein, die debugOptions zu deaktivieren, um den Cursor besser zu sehen).

Platzieren des Würfels

  1. Füge einen UITapGestureRecognizer hinzu, um einen Tap auf die ARView zu erkennen und mit einem ModelEntity einen blauen Quader an der Position des Fokus-Cursors zu erstellen:

    struct RealityKitView: UIViewRepresentable {
        func makeUIView(context: Context) -> ARView {
            let view = ARView()
    
            // ...
    
            // Handle taps
    view.addGestureRecognizer(
        UITapGestureRecognizer(
            target: context.coordinator,
            action: #selector(Coordinator.handleTap)
        )
    )
    
            return view
        }
    
        // ...
    
        class Coordinator: NSObject, ARSessionDelegate {
            // ...
    
            @objc func handleTap() {
        guard let view = self.view, let focusEntity = self.focusEntity else { return }
    
        // Create a new anchor to add content to
        let anchor = AnchorEntity()
        view.scene.anchors.append(anchor)
    
        // Add a Box entity with a blue material
        let box = MeshResource.generateBox(size: 0.5, cornerRadius: 0.05)
        let material = SimpleMaterial(color: .blue, isMetallic: true)
        let diceEntity = ModelEntity(mesh: box, materials: [material])
        diceEntity.position = focusEntity.position
    
        anchor.addChild(diceEntity)
    }
    
        }
    
    }
    
  2. Starte die App und überprüfe, ob das Platzieren der Quader wie erwartet funktioniert. Du solltest Quader erhalten, die 0,5 Meter groß sind:

  3. Fügen wir dem Projekt ein 3D-Modell eines echten Würfels hinzu. RealityKit kann nur .usdz-Dateien laden. Hier kannst Du eine konvertierte Modelldatei herunterladen: Dice.zip.

    Ich habe das ↗ Original-Würfelmodell ausgewählt, da es unter der Creative Commons-Lizenz verfügbar ist. Die Konvertierung von 3D-Modellen kann ein wenig knifflig sein: Die Reality Converter App kann das USDZ-Format schreiben, aber hier war es notwendig, die ursprüngliche .stl-Datei in Blender in das .glb-Format zu konvertieren, um sie in die Reality Converter App zu importieren.

  4. Füge das Dice.usdz-Modell zu deinem Xcode-Projekt hinzu, entferne den Code zum Erzeugen des Box-Meshes und lade stattdessen das Modell und verkleinere es ein wenig:

    // Add a dice entity
    let diceEntity = try! ModelEntity.loadModel(named: "Dice")
    diceEntity.scale = [0.1, 0.1, 0.1]
    diceEntity.position = focusEntity.position
    
  5. Starte die App und überprüfe, ob der Würfel beim Antippen erscheint.

Den Würfel rollen

  1. Aktivieren wir die Physik-Simulation für den Würfel, damit wir tatsächlich würfeln können:

    Ermittle nach Laden des Würfelmodells die Größe des Würfels (dies muss die unskalierte Größe sein, deshalb wird relativeTo: diceEntity verwendet):

    let size = diceEntity.visualBounds(relativeTo: diceEntity).extents
    

    Erstelle eine Box und setze sie als CollisionComponent, damit RealityKit schnell auf Kollisionen prüfen kann:

    let boxShape = ShapeResource.generateBox(size: size)
    diceEntity.collision = CollisionComponent(shapes: [boxShape])
    

    Aktiviere die Physiksimulation durch Setzen einer PhysicsBodyComponent:

    diceEntity.physicsBody = PhysicsBodyComponent(
        massProperties: .init(shape: boxShape, mass: 50),
        material: nil,
        mode: .dynamic
    )
    
  2. Tipp: Du kannst die Option .showPhysics zu den DebugOptions hinzufügen, um den Umriss des Kollisionskörpers in der Szene zu sehen:

    view.debugOptions = [.showAnchorOrigins, .showPhysics]
    
  3. Starte die App. Du solltest sehen, dass der Würfel herunterfällt, weil es keine Bodenfläche gibt.

  4. Als Workaround fügen wir eine Ebene unter den Würfel hinzu, damit dieser nicht herunterfallen kann:

    // Create a plane below the dice
    let planeMesh = MeshResource.generatePlane(width: 2, depth: 2)
    let material = SimpleMaterial(color: .init(white: 1.0, alpha: 0.1), isMetallic: false)
    let planeEntity = ModelEntity(mesh: planeMesh, materials: [material])
    planeEntity.position = focusEntity.position
    planeEntity.physicsBody = PhysicsBodyComponent(massProperties: .default, material: nil, mode: .static)
    planeEntity.collision = CollisionComponent(shapes: [.generateBox(width: 2, height: 0.001, depth: 2)])
    planeEntity.position = focusEntity.position
    anchor.addChild(planeEntity)
    
  5. Um tatsächlich zu würfeln, halte die diceEntity-Instanz und füge ihr bei einem zweiten Tap eine zufällige Kraft und einen Drehmoment hinzu:

    diceEntity.addForce([0, 2, 0], relativeTo: nil)
    diceEntity.addTorque([Float.random(in: 0 ... 0.4), Float.random(in: 0 ... 0.4), Float.random(in: 0 ... 0.4)], relativeTo: nil)
    
  6. Starte die App und lass den Würfel rollen, indem du ihn antippst:

    Dice roll

Weitere Informationen