Building Apps for visionOS with Swift

10.1K Views

March 22, 24

スライド概要

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

try! Swift Tokyo 2024 Building Apps for visionOS with Swift Satoshi Hattori @shmdevelop 1

2.

Satoshi Hattori xR Engineer at Cyber AI Productions xR Guild Leader, AR Next Experts at Cyber Agent Owner of visionOS Engineer Meetup Interests ㅤvisionOS、Apple Vision Pro、Unreal Engine、NDI Youtube favorites Shiba Inu, Renovating old houses, SF6, M:tG 2

3.

visionOS 30 Days Challenge 3

8.

https://github.com/satoshi0212/visionOS̲30Days 8

9.

I think this session to be a great value! 9

10.

I've spent over a month understanding and organizing the content of this talk. 1 month * 300 people = 25 years' worth of time into just 20 minutes. The source code has been shared on GitHub. https://github.com/satoshi0212/MySpatialTimer If you have not yet started to develop visionOS apps, you may wonder what I'm talking about, but you'll come back to this content when you actually want to place objects in space! 10

11.

I've spent over a month understanding and organizing the content of this talk. 1 month * 300 people = 25 years' worth of time into just 20 minutes. The source code has been shared on GitHub. https://github.com/satoshi0212/MySpatialTimer If you have not yet started to develop visionOS apps, you may wonder what I'm talking about, but you'll come back to this content when you actually want to place objects in space! 11

12.

I've spent over a month understanding and organizing the content of this talk. 1 month * 300 people = 25 years' worth of time into just 20 minutes. The source code has been shared on GitHub. https://github.com/satoshi0212/MySpatialTimer If you have not yet started to develop visionOS apps, you may wonder what I'm talking about, but you'll come back to this content when you actually want to place objects in space! 12

13.

My Spatial Timer 13

14.

My Spatial Timer 14

15.

RealityKit / Entities / Components / Systems / Meshes / Materials / Image-based lighting / Shadows / Foveation / Dynamic content scaling / Spatial Audio / Video / Model3D / RealityView / Attachments / Animation / Particle emitters / Collision detection / Physics / Anchoring / Portals / Reality Composer Pro / USD / MaterialX 15

16.

RealityKit / Entities / Components / Systems / Meshes / Materials / Image-based lighting / Shadows / Foveation / Dynamic content scaling / Spatial Audio / Video / Model3D / RealityView / Attachments / Animation / Particle emitters / Collision detection / Physics / Anchoring / Portals / Reality Composer Pro / USD / MaterialX 16

17.

Immersive Space 17

18.

App 18

19.

App Scene Scene Scene 19

20.

App Window Scene Scene 20

21.

App Window Volume Scene 21

22.

App Window Volume ImmersiveSpace 22

23.

Placement Location Updates 23

24.

Placement Location Updates RealityKit attachments 24

25.

Placement Location Updates RealityKit attachments WorldAnchor 25

26.

Placement Location Updates RealityKit attachments WorldAnchor 26

27.

RealityView 27

30.

import SwiftUI import RealityKit struct ImmersiveView: View { var body: some View { RealityView { content in } } } 30

31.

import SwiftUI import RealityKit struct ImmersiveView: View { var body: some View { RealityView { content, attachments in // make } update: { content, attachments in // update } attachments: { Attachment(id: "") { Text("") } } } } 31

32.

import SwiftUI import RealityKit struct ImmersiveView: View { var body: some View { RealityView { content in } } } 32

33.
[beta]
import SwiftUI
import RealityKit
struct ImmersiveView: View {
var body: some View {
RealityView { content in
let cubeEntity = ModelEntity(
mesh: .generateBox(size: 0.5),
materials: [SimpleMaterial(color: .blue, isMetallic: false)]
)
cubeEntity.position = SIMD3<Float>(x: -0.5, y: 1.0, z: -1.0)
content.add(cubeEntity)
}
}
}

33

35.
[beta]
import SwiftUI
import RealityKit
struct ImmersiveView: View {
var body: some View {
RealityView { content in
let cylinderEntity = ModelEntity(
mesh: .generateCylinder(height: 0.01, radius: 0.3),
materials: [SimpleMaterial(color: .blue, isMetallic: false)]
)
cylinderEntity.position = SIMD3<Float>(x: -0.5, y: 1.0, z: -1.0)
content.add(cylinderEntity)
}
}
}

35

37.
[beta]
import SwiftUI
import RealityKit
struct ImmersiveView: View {
var body: some View {
RealityView { content in
let cylinderEntity = ModelEntity(
mesh: .generateCylinder(height: 0.01, radius: 0.3),
materials: [SimpleMaterial(color: .init(red: 0, green: 0, blue: 1, alpha: 0.5), isMetallic: false)]
)
cylinderEntity.position = SIMD3<Float>(x: -0.5, y: 1.0, z: -1.0)
let rotationQuaternionX = simd_quatf(angle: .pi / 2, axis: SIMD3<Float>(1, 0, 0))
let rotationQuaternionY = simd_quatf(angle: .pi, axis: SIMD3<Float>(0, 1, 0))
let rotationQuaternionZ = simd_quatf(angle: .pi, axis: SIMD3<Float>(0, 0, 1))
cylinderEntity.orientation = rotationQuaternionX * rotationQuaternionY * rotationQuaternionZ
content.add(cylinderEntity)
}
}
}

37

39.

ViewModel 39

40.

import Observation import RealityKit @Observable class ViewModel { } 40

41.
[beta]
import Observation
import RealityKit
@Observable
class ViewModel {
private let rootEntity = Entity()
func setupRootEntity() -> Entity {
return rootEntity
}
func addCylinder() {
let cylinderEntity = ModelEntity(
mesh: .generateCylinder(height: 0.01, radius: 0.3),
materials: [SimpleMaterial(color: .init(red: 0, green: 0, blue: 1, alpha: 0.5), isMetallic: false)]
)
cylinderEntity.position = SIMD3<Float>(x: -0.5, y: 1.0, z: -1.0)
let rotationQuaternionX = simd_quatf(angle: .pi / 2, axis: SIMD3<Float>(1, 0, 0))
let rotationQuaternionY = simd_quatf(angle: .pi, axis: SIMD3<Float>(0, 1, 0))
let rotationQuaternionZ = simd_quatf(angle: .pi, axis: SIMD3<Float>(0, 0, 1))
cylinderEntity.orientation = rotationQuaternionX * rotationQuaternionY * rotationQuaternionZ
rootEntity.addChild(cylinderEntity)
}
}
41

42.

import SwiftUI import RealityKit struct ImmersiveView: View { @State private var viewModel = ViewModel() var body: some View { RealityView { content in content.add(viewModel.setupRootEntity()) viewModel.addCylinder() } } } 42

43.
[beta]
import Observation
import RealityKit
@Observable
class ViewModel {
private let rootEntity = Entity()
func setupRootEntity() -> Entity {
return rootEntity
}
func addCylinder() {
let cylinderEntity = ModelEntity(
mesh: .generateCylinder(height: 0.01, radius: 0.3),
materials: [SimpleMaterial(color: .init(red: 0, green: 0, blue: 1, alpha: 0.5), isMetallic: false)]
)
cylinderEntity.position = SIMD3<Float>(x: -0.5, y: 1.0, z: -1.0)
let rotationQuaternionX = simd_quatf(angle: .pi / 2, axis: SIMD3<Float>(1, 0, 0))
let rotationQuaternionY = simd_quatf(angle: .pi, axis: SIMD3<Float>(0, 1, 0))
let rotationQuaternionZ = simd_quatf(angle: .pi, axis: SIMD3<Float>(0, 0, 1))
cylinderEntity.orientation = rotationQuaternionX * rotationQuaternionY * rotationQuaternionZ
rootEntity.addChild(cylinderEntity)
}
}
43

44.

import SwiftUI import RealityKit struct ImmersiveView: View { @State private var viewModel = ViewModel() var body: some View { RealityView { content in content.add(viewModel.setupRootEntity()) viewModel.addCylinder() } } } 44

46.

ARKitSession 46

47.

import import import import ARKit Observation QuartzCore RealityKit @Observable class ViewModel { private let rootEntity = Entity() private let arkitSession = ARKitSession() private let worldTracking = WorldTrackingProvider() private let placementLocation = Entity() func setupRootEntity() -> Entity { rootEntity.addChild(placementLocation) return rootEntity } @MainActor func runARKitSession() async { do { try await arkitSession.run([worldTracking]) } catch { return } } ... } 47

48.

import import import import ARKit Observation QuartzCore RealityKit @Observable class ViewModel { private let rootEntity = Entity() private let arkitSession = ARKitSession() private let worldTracking = WorldTrackingProvider() private let placementLocation = Entity() func setupRootEntity() -> Entity { rootEntity.addChild(placementLocation) return rootEntity } @MainActor func runARKitSession() async { do { try await arkitSession.run([worldTracking]) } catch { return } } ... } 48

49.

import import import import ARKit Observation QuartzCore RealityKit @Observable class ViewModel { private let rootEntity = Entity() private let arkitSession = ARKitSession() private let worldTracking = WorldTrackingProvider() private let placementLocation = Entity() func setupRootEntity() -> Entity { rootEntity.addChild(placementLocation) return rootEntity } @MainActor func runARKitSession() async { do { try await arkitSession.run([worldTracking]) } catch { return } } ... } 49

50.
[beta]
@MainActor
func processDeviceAnchorUpdates() async {
await run(function: self.queryAndProcessLatestDeviceAnchor, withFrequency: 90)
}
@MainActor
private func queryAndProcessLatestDeviceAnchor() async {
guard worldTracking.state == .running else { return }
let deviceAnchor = worldTracking.queryDeviceAnchor(atTimestamp: CACurrentMediaTime())
guard let deviceAnchor, deviceAnchor.isTracked else { return }
let matrix = deviceAnchor.originFromAnchorTransform
let forward = simd_float3(0, 0, -1)
let cameraForward = simd_act(matrix.rotation, forward)
let front = SIMD3<Float>(x: cameraForward.x, y: cameraForward.y, z: cameraForward.z)
let length: Float = 0.5
let offset = length * simd_normalize(front)
placementLocation.position = matrix.position + offset
placementLocation.orientation = matrix.rotation
}

50

51.
[beta]
@MainActor
func processDeviceAnchorUpdates() async {
await run(function: self.queryAndProcessLatestDeviceAnchor, withFrequency: 90)
}
@MainActor
private func queryAndProcessLatestDeviceAnchor() async {
guard worldTracking.state == .running else { return }
let deviceAnchor = worldTracking.queryDeviceAnchor(atTimestamp: CACurrentMediaTime())
guard let deviceAnchor, deviceAnchor.isTracked else { return }
let matrix = deviceAnchor.originFromAnchorTransform
let forward = simd_float3(0, 0, -1)
let cameraForward = simd_act(matrix.rotation, forward)
let front = SIMD3<Float>(x: cameraForward.x, y: cameraForward.y, z: cameraForward.z)
let length: Float = 0.5
let offset = length * simd_normalize(front)
placementLocation.position = matrix.position + offset
placementLocation.orientation = matrix.rotation
}

51

52.
[beta]
extension ViewModel {
@MainActor
func run(function: () async -> Void, withFrequency hz: UInt64) async {
while true {
if Task.isCancelled {
return
}
// Sleep for 1 s / hz before calling the function.
let nanoSecondsToSleep: UInt64 = NSEC_PER_SEC / hz
do {
try await Task.sleep(nanoseconds: nanoSecondsToSleep)
} catch {
// Sleep fails when the Task is cancelled. Exit the loop.
return
}
await function()
}
}
}

52

53.
[beta]
@MainActor
func processDeviceAnchorUpdates() async {
await run(function: self.queryAndProcessLatestDeviceAnchor, withFrequency: 90)
}
@MainActor
private func queryAndProcessLatestDeviceAnchor() async {
guard worldTracking.state == .running else { return }
let deviceAnchor = worldTracking.queryDeviceAnchor(atTimestamp: CACurrentMediaTime())
guard let deviceAnchor, deviceAnchor.isTracked else { return }
let matrix = deviceAnchor.originFromAnchorTransform
let forward = simd_float3(0, 0, -1)
let cameraForward = simd_act(matrix.rotation, forward)
let front = SIMD3<Float>(x: cameraForward.x, y: cameraForward.y, z: cameraForward.z)
let length: Float = 0.5
let offset = length * simd_normalize(front)
placementLocation.position = matrix.position + offset
placementLocation.orientation = matrix.rotation
}

53

54.
[beta]
@MainActor
func processDeviceAnchorUpdates() async {
await run(function: self.queryAndProcessLatestDeviceAnchor, withFrequency: 90)
}
@MainActor
private func queryAndProcessLatestDeviceAnchor() async {
guard worldTracking.state == .running else { return }
let deviceAnchor = worldTracking.queryDeviceAnchor(atTimestamp: CACurrentMediaTime())
guard let deviceAnchor, deviceAnchor.isTracked else { return }
let matrix = deviceAnchor.originFromAnchorTransform
let forward = simd_float3(0, 0, -1)
let cameraForward = simd_act(matrix.rotation, forward)
let front = SIMD3<Float>(x: cameraForward.x, y: cameraForward.y, z: cameraForward.z)
let length: Float = 0.5
let offset = length * simd_normalize(front)
placementLocation.position = matrix.position + offset
placementLocation.orientation = matrix.rotation
}

54

55.
[beta]
func addMarker() {
let entity = ModelEntity(
mesh: .generateCylinder(height: 0.01, radius: 0.1),
materials: [SimpleMaterial(color: .init(red: 0, green: 0, blue: 1, alpha: 0.5), isMetallic: false)]
)
entity.components.set(InputTargetComponent())
entity.generateCollisionShapes(recursive: true)
let rotationQuaternionX = simd_quatf(angle: .pi / 2, axis: SIMD3<Float>(1, 0, 0))
let rotationQuaternionY = simd_quatf(angle: .pi, axis: SIMD3<Float>(0, 1, 0))
let rotationQuaternionZ = simd_quatf(angle: .pi, axis: SIMD3<Float>(0, 0, 1))
entity.orientation = rotationQuaternionX * rotationQuaternionY * rotationQuaternionZ
placementLocation.addChild(entity)
}

55

56.

struct ImmersiveView: View { var body: some View { RealityView { content in content.add(viewModel.setupRootEntity()) viewModel.addMarker() Task { await viewModel.runARKitSession() } } .task { await viewModel.processDeviceAnchorUpdates() } } } 56

58.
[beta]
func addPlaceHolder() {
let entity = ModelEntity(
mesh: .generateCylinder(height: 0.01, radius: 0.1),
materials: [SimpleMaterial(color: .init(red: 1, green: 0, blue: 0, alpha: 1), isMetallic: false)]
)
entity.transform = placementLocation.transform
let rotationQuaternionX = simd_quatf(angle: .pi / 2, axis: SIMD3<Float>(1, 0, 0))
entity.orientation *= rotationQuaternionX
rootEntity.addChild(entity)
}

58

59.

struct ImmersiveView: View { var body: some View { RealityView { content in ... } .gesture(SpatialTapGesture() .targetedToAnyEntity() .onEnded { _ in viewModel.addPlaceHolder() }) } } 59

61.

🎉 61

62.

Placement Location Updates RealityKit attachments WorldAnchor 62

63.

Placement Location Updates RealityKit attachments WorldAnchor 63

64.

Timer UI and functions 64

65.

TimerView 65

66.

TimerManager TimerModel TimerModel TimerModel 66

67.

Attachments 67

68.

RealityView { content, attachments in ... } update: { update, attachments in for timerModel in timerManager.timerModels { if let attachment = attachments.entity(for: timerModel.id), let placeHolder = viewModel.getTargetEntity(name: timerModel.id.uuidString) { if !placeHolder.children.contains(attachment) { placeHolder.addChild(attachment) } } } } attachments: { ForEach(timerManager.timerModels) { timerModel in Attachment(id: timerModel.id) { TimerView(viewModel: viewModel, timerManager: timerManager, timerModel: timerModel) } } } 68

69.

RealityView { content, attachments in ... } update: { update, attachments in for timerModel in timerManager.timerModels { if let attachment = attachments.entity(for: timerModel.id), let placeHolder = viewModel.getTargetEntity(name: timerModel.id.uuidString) { if !placeHolder.children.contains(attachment) { placeHolder.addChild(attachment) } } } } attachments: { ForEach(timerManager.timerModels) { timerModel in Attachment(id: timerModel.id) { TimerView(viewModel: viewModel, timerManager: timerManager, timerModel: timerModel) } } } 69

70.

RealityView { content, attachments in ... } update: { update, attachments in for timerModel in timerManager.timerModels { if let attachment = attachments.entity(for: timerModel.id), let placeHolder = viewModel.getTargetEntity(name: timerModel.id.uuidString) { if !placeHolder.children.contains(attachment) { placeHolder.addChild(attachment) } } } } attachments: { ForEach(timerManager.timerModels) { timerModel in Attachment(id: timerModel.id) { TimerView(viewModel: viewModel, timerManager: timerManager, timerModel: timerModel) } } } 70

71.

func addPlaceHolder() { guard let timerManager = self.timerManager else { return } let timerModel = timerManager.makeTimerModel() timerManager.addTimerModel(timerModel: timerModel) let entity = ModelEntity( mesh: .generateSphere(radius: 0), materials: [SimpleMaterial(color: .init(red: 1, green: 1, blue: 1, alpha: 0), isMetallic: false)] ) entity.name = timerModel.id.uuidString entity.transform = placementLocation.transform rootEntity.addChild(entity) } 71

73.

🎉 73

74.

Placement Location Updates RealityKit attachments WorldAnchor 74

75.

Placement Location Updates RealityKit attachments WorldAnchor 75

77.

The manner of linking with WorldAnchor 77

78.

private var anchoredObjects: [UUID: Entity] = [:] private var objectsBeingAnchored: [UUID: Entity] = [:] private func attachObjectToWorldAnchor(_ object: Entity) async { let anchor = await WorldAnchor(originFromAnchorTransform: object.transformMatrix(relativeTo: nil)) objectsBeingAnchored[anchor.id] = object do { try await worldTracking.addAnchor(anchor) } catch { print("Failed to add world anchor \(anchor.id) with error: \(error).") objectsBeingAnchored.removeValue(forKey: anchor.id) await object.removeFromParent() return } } 78

79.

private var anchoredObjects: [UUID: Entity] = [:] private var objectsBeingAnchored: [UUID: Entity] = [:] private func attachObjectToWorldAnchor(_ object: Entity) async { let anchor = await WorldAnchor(originFromAnchorTransform: object.transformMatrix(relativeTo: nil)) objectsBeingAnchored[anchor.id] = object do { try await worldTracking.addAnchor(anchor) } catch { print("Failed to add world anchor \(anchor.id) with error: \(error).") objectsBeingAnchored.removeValue(forKey: anchor.id) await object.removeFromParent() return } } 79

80.

func addPlaceHolder() { guard let timerManager = self.timerManager else { return } let timerModel = timerManager.makeTimerModel() timerManager.addTimerModel(timerModel: timerModel) let entity = ModelEntity( mesh: .generateSphere(radius: 0), materials: [SimpleMaterial(color: .init(red: 1, green: 1, blue: 1, alpha: 0), isMetallic: false)] ) entity.name = timerModel.id.uuidString entity.transform = placementLocation.transform rootEntity.addChild(entity) Task { await attachObjectToWorldAnchor(entity) } } 80

81.

RealityView { content, attachments in ... } update: { update, attachments in ... } attachments: { ... } .task { await viewModel.processWorldAnchorUpdates() } .task { await viewModel.processDeviceAnchorUpdates() } .gesture(SpatialTapGesture() .targetedToAnyEntity() .onEnded { _ in viewModel.addPlaceHolder() }) 81

82.

@MainActor func processWorldAnchorUpdates() async { for await anchorUpdate in worldTracking.anchorUpdates { process(anchorUpdate) } } 82

83.

@MainActor private func process(_ anchorUpdate: AnchorUpdate<WorldAnchor>) { let anchor = anchorUpdate.anchor switch anchorUpdate.event { case .added: if let objectBeingAnchored = objectsBeingAnchored[anchor.id] { objectsBeingAnchored.removeValue(forKey: anchor.id) anchoredObjects[anchor.id] = objectBeingAnchored } else { if anchoredObjects[anchor.id] == nil { Task { await removeAnchorWithID(anchor.id) } } } fallthrough case .updated: if let object = anchoredObjects[anchor.id] { object.position = anchor.originFromAnchorTransform.translation object.orientation = anchor.originFromAnchorTransform.rotation object.isEnabled = anchor.isTracked } case .removed: if let object = anchoredObjects[anchor.id] { object.removeFromParent() } anchoredObjects.removeValue(forKey: anchor.id) } } 83

84.

@MainActor private func process(_ anchorUpdate: AnchorUpdate<WorldAnchor>) { let anchor = anchorUpdate.anchor switch anchorUpdate.event { case .added: if let objectBeingAnchored = objectsBeingAnchored[anchor.id] { objectsBeingAnchored.removeValue(forKey: anchor.id) anchoredObjects[anchor.id] = objectBeingAnchored } else { if anchoredObjects[anchor.id] == nil { Task { await removeAnchorWithID(anchor.id) } } } fallthrough case .updated: if let object = anchoredObjects[anchor.id] { object.position = anchor.originFromAnchorTransform.translation object.orientation = anchor.originFromAnchorTransform.rotation object.isEnabled = anchor.isTracked } case .removed: if let object = anchoredObjects[anchor.id] { object.removeFromParent() } anchoredObjects.removeValue(forKey: anchor.id) } } 84

85.

@MainActor private func process(_ anchorUpdate: AnchorUpdate<WorldAnchor>) { let anchor = anchorUpdate.anchor switch anchorUpdate.event { case .added: if let objectBeingAnchored = objectsBeingAnchored[anchor.id] { objectsBeingAnchored.removeValue(forKey: anchor.id) anchoredObjects[anchor.id] = objectBeingAnchored } else { if anchoredObjects[anchor.id] == nil { Task { await removeAnchorWithID(anchor.id) } } } fallthrough case .updated: if let object = anchoredObjects[anchor.id] { object.position = anchor.originFromAnchorTransform.translation object.orientation = anchor.originFromAnchorTransform.rotation object.isEnabled = anchor.isTracked } case .removed: if let object = anchoredObjects[anchor.id] { object.removeFromParent() } anchoredObjects.removeValue(forKey: anchor.id) } } 85

86.

@MainActor private func process(_ anchorUpdate: AnchorUpdate<WorldAnchor>) { let anchor = anchorUpdate.anchor switch anchorUpdate.event { case .added: if let objectBeingAnchored = objectsBeingAnchored[anchor.id] { objectsBeingAnchored.removeValue(forKey: anchor.id) anchoredObjects[anchor.id] = objectBeingAnchored } else { if anchoredObjects[anchor.id] == nil { Task { await removeAnchorWithID(anchor.id) } } } fallthrough case .updated: if let object = anchoredObjects[anchor.id] { object.position = anchor.originFromAnchorTransform.translation object.orientation = anchor.originFromAnchorTransform.rotation object.isEnabled = anchor.isTracked } case .removed: if let object = anchoredObjects[anchor.id] { object.removeFromParent() } anchoredObjects.removeValue(forKey: anchor.id) } } 86

88.

🎉 88

89.

Demo 89

90.

Demo Video 90

91.

Wrap up Placement Location Updates RealityKit attachments https://github.com/satoshi0212/MySpatialTimer WorldAnchor and more... Collision Group ScenePhase Data Save / Load https://github.com/satoshi0212/visionOS_30Days 91

92.

Wrap up Placement Location Updates RealityKit attachments https://github.com/satoshi0212/MySpatialTimer WorldAnchor and more... Collision Group ScenePhase Data Save / Load https://github.com/satoshi0212/visionOS_30Days 92