6.5K Views
December 13, 24
スライド概要
XRKaigi 2024
AR Developer
XRKaigi 2024 visionOSアプリ開発 : Swiftで挑む最前線 服部 智 @shmdevelop
服部 智 xR Experts (visionOS) Cyber AI Productions visionOS Engineer Meetup 主催 GitHub: satoshi 2 1 2 0 X: @shmdevelop
visionOS 30 Days Challenge visionOS2 30 Days Challenge
https://www.amazon.co.jp/dp/4297143119/
https://www.amazon.co.jp/dp/4297143119/
皆さんへ質問
実際に を動かした ? 方 方 方 を動かしたけれど、今は停滞している ? 手 手 visionOSの開発に少しでも興味を持っている ?
今回のセッションの主な対象
visionOSで開発始めてみたい visionOSで開発始めたがちょっと 人 人 止 手 いアイデアやサービス考えたい 人 白 面 Apple Vision Proで が まった
今回話す内容
開発の情報源について Appleのサンプル群を解説! 自 用 サンプルを応 して独 機能を作ってみる
開発の情報源について Appleのサンプル群を解説! して独 自 用 サンプルを応 機能を作ってみる
Apple公式ページ: visionOS https://developer.apple.com/documentation/visionos
Apple公式ページ: RealityKit https://developer.apple.com/documentation/realitykit
Youtube: Apple Developer チャンネル https://www.youtube.com/@AppleDeveloper
書籍 https://www.amazon.co.jp/dp/4297143119 https://www.amazon.co.jp/dp/B0DDK5V9QQ
ソースコード GitHub、X、ブログなど 共有されている情報増えています
私はXとGitHubで共有中 visionOS 30 Days Challenge visionOS2 30 Days Challenge
Image Board 5x5 3 lines static pictures tap to dynamic motion
Street View Images from Google StreetView API Combine images to a panoramic image covering 360°
Metal Shader Quote from ShaderToy Use CAMetalLayer, CADisplayLink visionOS doesn't support MTKView visionOS 2 has LowLevelTexture
開発の情報源について Appleのサンプル群を解説! 自 用 サンプルを応 して独 機能を作ってみる
開発の情報源について Appleのサンプル群を解説! して独 自 用 サンプルを応 機能を作ってみる
Appleのサンプル群を解説! ( 3 1 2 1 4 2 0 48件の公式サンプルが提供されている 2 visionOS関連で / / )
Appleのサンプル群を解説! 用 とても有 !
色 々なページにあるためすべて集めたページ作りました https://zenn.dev/satoshi0212/articles/da9b47a42586f4
Appleのサンプル群を解説! Introductory visionOS samples: visionOS: RealityKit: Group Activities: TabletopKit: Compositor Services: AVFoundation: 8 1 1 1 2 3 1 1 1 1 1 Image I/O:
Introductory visionOS samples から抜粋
Creating D shapes with SwiftUI 2 https://developer.apple.com/documentation/visionos/creating-2d-shapes-in-visionos-with-swiftui
キモとなる処理
Triangle.swift
struct Triangle: Shape {
let vertex1: CGPoint
let vertex2: CGPoint
let vertex3: CGPoint
init(_ point1: CGPoint, _ point2: CGPoint, _ point3: CGPoint) {
vertex1 = point1
vertex2 = point2
vertex3 = point3
}
func path(in bounds: CGRect) -> Path {
/// The drawing path for the triangle shape.
var path = Path()
// Start at the first vertex.
path.move(to: vertex1)
// Draw the triangle's first two sides.
path.addLine(to: vertex2)
path.addLine(to: vertex3)
// Draw the triangle's third side by returning to the first vertex.
path.closeSubpath()
return path
}
}
Creating D entities with RealityKit 3 https://developer.apple.com/documentation/visionos/creating-3d-shapes-in-visionos-with-realitykit
ShapesView.swift struct ShapesView: View { var body: some View { RealityView { content in addGeometryShapes(to: content) } } func addGeometryShapes(to content: RealityViewContent) { let allGeometryEntities = [ ShapesView.boxEntity, ShapesView.roundedBoxEntity, ShapesView.sphereEntity, ShapesView.coneEntity, ShapesView.cylinderEntity ] var xOffset: Float = -0.25 for entity in allGeometryEntities { entity.position.x = xOffset content.add(entity) xOffset += 0.125 } } }
ShapesView.swift struct ShapesView: View { var body: some View { RealityView { content in addGeometryShapes(to: content) } } func addGeometryShapes(to content: RealityViewContent) { let allGeometryEntities = [ ShapesView.boxEntity, ShapesView.roundedBoxEntity, ShapesView.sphereEntity, ShapesView.coneEntity, ShapesView.cylinderEntity ] var xOffset: Float = -0.25 for entity in allGeometryEntities { entity.position.x = xOffset content.add(entity) xOffset += 0.125 } } }
ShapesView+Entities.swift extension ShapesView { /// The white material that responds to lighting. static let whiteMaterial = SimpleMaterial(color: .white, isMetallic: false) /// The entity with a box geometry. static let boxEntity: Entity = { // Create a new entity instance. let entity = Entity() // Create a new mesh resource. let boxSize: Float = 0.1 let boxMesh = MeshResource.generateBox(size: boxSize) // Add the mesh resource to a model component, and add it to the entity. entity.components.set(ModelComponent(mesh: boxMesh, materials: [whiteMaterial])) return entity }() ... }
Creating SwiftUI windows in visionOS https://developer.apple.com/documentation/visionos/creating-3d-shapes-in-visionos-with-realitykit
OpenWindowView.swift struct OpenWindowView: View { /// The `id` value that the main view uses to identify the SwiftUI window. @State var nextWindowID = NewWindowID(id: 1) /// The environment value for getting an `OpenWindowAction` instance. @Environment(\.openWindow) private var openWindow var body: some View { // Create a button in the center of the window that // launches a new SwiftUI window. Button("Open a new window") { // Open a new SwiftUI window with the assigned ID. openWindow(value: nextWindowID.id) // Increment the `id` value of the `nextWindowID` by 1. nextWindowID.id += 1 } } } fi /// A structure that gives each window a unique ID. struct NewWindowID: Identifiable { /// The unique identi er for the window. var id: Int }
OpenWindowView.swift struct OpenWindowView: View { /// The `id` value that the main view uses to identify the SwiftUI window. @State var nextWindowID = NewWindowID(id: 1) /// The environment value for getting an `OpenWindowAction` instance. @Environment(\.openWindow) private var openWindow var body: some View { // Create a button in the center of the window that // launches a new SwiftUI window. Button("Open a new window") { // Open a new SwiftUI window with the assigned ID. openWindow(value: nextWindowID.id) // Increment the `id` value of the `nextWindowID` by 1. nextWindowID.id += 1 } } } fi /// A structure that gives each window a unique ID. struct NewWindowID: Identifiable { /// The unique identi er for the window. var id: Int }
NewWindowView.swift struct NewWindowView: View { let id: Int var body: some View { // Create a text view that displays // the window's `id` value. Text("New window number \(id)") } }
Creating an immersive space in visionOS https://developer.apple.com/documentation/visionos/creating-immersive-spaces-in-visionos-with-swiftui
ImmersionView.swift struct ImmersiveView: View { let avgHeight: Float = 1.70 let speed: TimeInterval = 0.03 var body: some View { // Initiate a `RealityView` to create a ring // of rocks to orbit around a person. RealityView { content in /// The entity to contain the models. let rootEntity = Entity() // Set the y-axis position to the average human height. rootEntity.position.y += avgHeight // Create the halo effect with the `addHalo` method. rootEntity.addHalo() // Set the rotation speed for the rocks. rootEntity.components.set(TurnTableComponent(speed: speed)) // Register the `TurnTableSystem` to handle the rotation logic. TurnTableSystem.registerSystem() // Add the entity to the view. content.add(rootEntity) } } }
ImmersionView.swift struct ImmersiveView: View { let avgHeight: Float = 1.70 let speed: TimeInterval = 0.03 var body: some View { // Initiate a `RealityView` to create a ring // of rocks to orbit around a person. RealityView { content in /// The entity to contain the models. let rootEntity = Entity() // Set the y-axis position to the average human height. rootEntity.position.y += avgHeight // Create the halo effect with the `addHalo` method. rootEntity.addHalo() // Set the rotation speed for the rocks. rootEntity.components.set(TurnTableComponent(speed: speed)) // Register the `TurnTableSystem` to handle the rotation logic. TurnTableSystem.registerSystem() // Add the entity to the view. content.add(rootEntity) } } }
ImmersionView.swift rock.usdz
ImmersionView.swift struct ImmersiveView: View { let avgHeight: Float = 1.70 let speed: TimeInterval = 0.03 var body: some View { // Initiate a `RealityView` to create a ring // of rocks to orbit around a person. RealityView { content in /// The entity to contain the models. let rootEntity = Entity() // Set the y-axis position to the average human height. rootEntity.position.y += avgHeight // Create the halo effect with the `addHalo` method. rootEntity.addHalo() // Set the rotation speed for the rocks. rootEntity.components.set(TurnTableComponent(speed: speed)) // Register the `TurnTableSystem` to handle the rotation logic. TurnTableSystem.registerSystem() // Add the entity to the view. content.add(rootEntity) } } }
TurnTableSystem.swift
struct TurnTableComponent: Component {
var time: TimeInterval = 0
var speed: TimeInterval
var axis: SIMD3<Float>
init(speed: TimeInterval = 1.0, axis: SIMD3<Float> = [0, 1, 0]) {
self.speed = speed
self.axis = axis
}
}
struct TurnTableSystem: System {
static let query = EntityQuery(where: .has(TurnTableComponent.self))
init(scene: RealityKit.Scene) { }
func update(context: SceneUpdateContext) {
for entity in context.entities(matching: Self.query, updatingSystemWhen: .rendering) {
var comp = entity.components[TurnTableComponent.self]!
comp.time += context.deltaTime
entity.components[TurnTableComponent.self] = comp
// Adjust the orientation to update the angle, speed, and axis of rotation.
entity.setOrientation(simd_quatf(angle: Float(0.1 * comp.speed), axis: comp.axis),
relativeTo: entity)
}
}
}
TurnTableSystem.swift
struct TurnTableComponent: Component {
var time: TimeInterval = 0
var speed: TimeInterval
var axis: SIMD3<Float>
init(speed: TimeInterval = 1.0, axis: SIMD3<Float> = [0, 1, 0]) {
self.speed = speed
self.axis = axis
}
}
struct TurnTableSystem: System {
static let query = EntityQuery(where: .has(TurnTableComponent.self))
init(scene: RealityKit.Scene) { }
func update(context: SceneUpdateContext) {
for entity in context.entities(matching: Self.query, updatingSystemWhen: .rendering) {
var comp = entity.components[TurnTableComponent.self]!
comp.time += context.deltaTime
entity.components[TurnTableComponent.self] = comp
// Adjust the orientation to update the angle, speed, and axis of rotation.
entity.setOrientation(simd_quatf(angle: Float(0.1 * comp.speed), axis: comp.axis),
relativeTo: entity)
}
}
}
https://developer.apple.com/documentation/RealityKit/implementing-systems-for-entities-in-a-scene
Displaying a stereoscopic image https://developer.apple.com/documentation/visionos/creating-stereoscopic-image-in-visionos
LeftTexture RightTexture
https://developer.apple.com/documentation/ShaderGraph/realitykit/Camera-Index-Switch-(RealityKit)
StereoImage.swift struct StereoImage: View { var body: some View { let spacing: CGFloat = 10.0 let padding: CGFloat = 40.0 VStack(spacing: spacing) { Text("Stereoscopic Image Example") .font(.largeTitle) RealityView { content in /// The creator instance of `StereoImageCreator`. let creator = StereoImageCreator() /// The image entity that `StereoImageCreator` generates. guard let entity = await creator.createImageEntity() else { print("Failed to create the stereoscopic image entity.") return } content.add(entity) } } .padding(padding) } }
StereoImage.swift struct StereoImage: View { var body: some View { let spacing: CGFloat = 10.0 let padding: CGFloat = 40.0 VStack(spacing: spacing) { Text("Stereoscopic Image Example") .font(.largeTitle) RealityView { content in /// The creator instance of `StereoImageCreator`. let creator = StereoImageCreator() /// The image entity that `StereoImageCreator` generates. guard let entity = await creator.createImageEntity() else { print("Failed to create the stereoscopic image entity.") return } content.add(entity) } } .padding(padding) } }
StereoImageCreator.swift
@MainActor
public func createImageEntity() async -> ModelEntity? {
let materialRoot: String = "/Root/Material"
let materialName: String = "StereoscopicMaterial"
guard var material = try? await ShaderGraphMaterial(named: materialRoot, from: materialName) else {
print("Failed to load shader graph material.")
return nil
}
/// The size of the box in three dimensions.
let size: Float = 0.3
/// The z-axis scale to compress the box into an image placeholder.
let zScale: Float = 1E-3
// Generates the model entity in the shape of a box and applies the shader graph material.
let box = ModelEntity(
mesh: .generateBox(size: size),
materials: [material]
)
// Apply the z-axis scale to compress the box into a flat plane.
box.scale.z = zScale
// Load textures and apply to the box.
await applyTextureToEntity(box: box, material: &material)
return box
}
StereoImageCreator.swift
@MainActor
public func createImageEntity() async -> ModelEntity? {
let materialRoot: String = "/Root/Material"
let materialName: String = "StereoscopicMaterial"
guard var material = try? await ShaderGraphMaterial(named: materialRoot, from: materialName) else {
print("Failed to load shader graph material.")
return nil
}
/// The size of the box in three dimensions.
let size: Float = 0.3
/// The z-axis scale to compress the box into an image placeholder.
let zScale: Float = 1E-3
// Generates the model entity in the shape of a box and applies the shader graph material.
let box = ModelEntity(
mesh: .generateBox(size: size),
materials: [material]
)
// Apply the z-axis scale to compress the box into a flat plane.
box.scale.z = zScale
// Load textures and apply to the box.
await applyTextureToEntity(box: box, material: &material)
return box
}
StereoImageCreator.swift
@MainActor
private func applyTextureToEntity(box: ModelEntity, material: inout ShaderGraphMaterial) async {
do {
let leftFileName = "Shop_L"
let rightFileName = "Shop_R"
let leftTexture = try await TextureResource(named: leftFileName)
let rightTexture = try await TextureResource(named: rightFileName)
let leftParameter = "LeftTexture"
let rightParameter = "RightTexture"
// Set the textures into the shader graph material.
try material.setParameter(name: leftParameter, value: .textureResource(leftTexture))
try material.setParameter(name: rightParameter, value: .textureResource(rightTexture))
// Apply the results to the material and assign to the box's material.
box.model?.materials = [material]
} catch {
// Handle any errors that occur.
assertionFailure("\(error)")
}
}
StereoImageCreator.swift
@MainActor
private func applyTextureToEntity(box: ModelEntity, material: inout ShaderGraphMaterial) async {
do {
let leftFileName = "Shop_L"
let rightFileName = "Shop_R"
let leftTexture = try await TextureResource(named: leftFileName)
let rightTexture = try await TextureResource(named: rightFileName)
let leftParameter = "LeftTexture"
let rightParameter = "RightTexture"
// Set the textures into the shader graph material.
try material.setParameter(name: leftParameter, value: .textureResource(leftTexture))
try material.setParameter(name: rightParameter, value: .textureResource(rightTexture))
// Apply the results to the material and assign to the box's material.
box.model?.materials = [material]
} catch {
// Handle any errors that occur.
assertionFailure("\(error)")
}
}
Displaying a D environment through a portal 3 https://developer.apple.com/documentation/visionos/displaying-a-3d-environment-through-a-portal
UIPortalView.swift struct UIPortalView: View { @Environment(AppModel.self) private var appModel private let root = Entity() private let portalPlane = ModelEntity( mesh: .generatePlane(width: 1.0, height: 1.0), materials: [PortalMaterial()] ) var body: some View { ZStack(alignment: .bottom) { if appModel.immersiveSpaceState == .closed { portalView } ToggleImmersiveSpaceButton() .padding(50) } } var portalView: some View { ... } func createPortal() async throws { ... } }
UIPortalView.swift struct UIPortalView: View { @Environment(AppModel.self) private var appModel private let root = Entity() private let portalPlane = ModelEntity( mesh: .generatePlane(width: 1.0, height: 1.0), materials: [PortalMaterial()] ) var body: some View { ZStack(alignment: .bottom) { if appModel.immersiveSpaceState == .closed { portalView } ToggleImmersiveSpaceButton() .padding(50) } } var portalView: some View { ... } func createPortal() async throws { ... } }
UIPortalView.swift var portalView: some View { GeometryReader3D { geometry in RealityView { content in try? await createPortal() content.add(root) } update: { content in // Resize the scene based on the size of the reality view content. let size = content.convert(geometry.size, from: .local, to: .scene) updatePortalSize(width: size.x, height: size.y) } .frame(depth: 0.4) } .frame(depth: 0.4) .frame(width: 1200, height: 800) }
UIPortalView.swift var portalView: some View { GeometryReader3D { geometry in RealityView { content in try? await createPortal() content.add(root) } update: { content in // Resize the scene based on the size of the reality view content. let size = content.convert(geometry.size, from: .local, to: .scene) updatePortalSize(width: size.x, height: size.y) } .frame(depth: 0.4) } .frame(depth: 0.4) .frame(width: 1200, height: 800) }
UIPortalView.swift
/// Sets up the portal and adds it to the `root.`
func createPortal() async throws {
// Create the entity that stores the content within the portal.
let world = Entity()
// Shrink the portal world and update the position
// to make it fit into the portal view.
world.scale *= 0.5
world.position.y -= 0.5
world.position.z -= 0.5
// Allow the entity to be visible only through a portal.
world.components.set(WorldComponent())
// Create the box environment and add it to the root.
try await createEnvironment(on: world)
root.addChild(world)
// Set up the portal to show the content in the `world`.
portalPlane.components.set(PortalComponent(target: world))
root.addChild(portalPlane)
}
fi
/// Con gures the portal mesh's width and height.
func updatePortalSize(width: Float, height: Float) {
portalPlane.model?.mesh = .generatePlane(width: width, height: height, cornerRadius: 0.03)
}
UIPortalView.swift
/// Sets up the portal and adds it to the `root.`
func createPortal() async throws {
// Create the entity that stores the content within the portal.
let world = Entity()
// Shrink the portal world and update the position
// to make it fit into the portal view.
world.scale *= 0.5
world.position.y -= 0.5
world.position.z -= 0.5
// Allow the entity to be visible only through a portal.
world.components.set(WorldComponent())
// Create the box environment and add it to the root.
try await createEnvironment(on: world)
root.addChild(world)
// Set up the portal to show the content in the `world`.
portalPlane.components.set(PortalComponent(target: world))
root.addChild(portalPlane)
}
fi
/// Con gures the portal mesh's width and height.
func updatePortalSize(width: Float, height: Float) {
portalPlane.model?.mesh = .generatePlane(width: width, height: height, cornerRadius: 0.03)
}
ImmersiveView.swift
func createEnvironment(on root: Entity) async throws {
/// The root entity for the box environment.
let assetRoot = try await Entity(named: "CornellBox.usda")
// Convert the image-based lighting file into a URL, and load it as an environment resource.
guard let iblURL = Bundle.main.url(forResource: "TeapotIBL", withExtension: "exr") else {
fatalError("Failed to load the Image-Based Lighting file.")
}
let iblEnv = try await EnvironmentResource(fromImage: iblURL)
/// The entity to perform image-based lighting on the environment.
let iblEntity = await Entity()
/// The image-based lighting component that contains background and lighting information.
var iblComp = ImageBasedLightComponent(source: .single(iblEnv))
iblComp.inheritsRotation = true
// Add the image-based lighting component to the entity.
await iblEntity.components.set(iblComp)
// Set up image-based lighting for the box environment.
await assetRoot.components.set(ImageBasedLightReceiverComponent(imageBasedLight: iblEntity))
// Add the image-based lighting entity to the box environment.
await assetRoot.addChild(iblEntity)
// Add the box environment to `root`.
await root.addChild(assetRoot)
}
ImmersiveView.swift
func createEnvironment(on root: Entity) async throws {
/// The root entity for the box environment.
let assetRoot = try await Entity(named: "CornellBox.usda")
// Convert the image-based lighting file into a URL, and load it as an environment resource.
guard let iblURL = Bundle.main.url(forResource: "TeapotIBL", withExtension: "exr") else {
fatalError("Failed to load the Image-Based Lighting file.")
}
let iblEnv = try await EnvironmentResource(fromImage: iblURL)
/// The entity to perform image-based lighting on the environment.
let iblEntity = await Entity()
/// The image-based lighting component that contains background and lighting information.
var iblComp = ImageBasedLightComponent(source: .single(iblEnv))
iblComp.inheritsRotation = true
// Add the image-based lighting component to the entity.
await iblEntity.components.set(iblComp)
// Set up image-based lighting for the box environment.
await assetRoot.components.set(ImageBasedLightReceiverComponent(imageBasedLight: iblEntity))
// Add the image-based lighting entity to the box environment.
await assetRoot.addChild(iblEntity)
// Add the box environment to `root`.
await root.addChild(assetRoot)
}
用 他サンプルも有 な情報が豊富!
開発の情報源について Appleのサンプル群を解説! 自 用 サンプルを応 して独 機能を作ってみる
開発の情報源について Appleのサンプル群を解説! 自 用 サンプルを応 して独 機能を作ってみる
して独 自 用 サンプルを応 機能を作ってみる
示 ポータル複数表 機能!
元ネタ
Displaying a D environment through a portal 3 https://developer.apple.com/documentation/visionos/displaying-a-3d-environment-through-a-portal
示 複数シーンを作り同時にポータル表
ContentView.swift struct ContentView: View { @Environment(\.openImmersiveSpace) var openImmersiveSpace @Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace @Environment(\.dismissWindow) var dismissWindow var loader: EnvironmentLoader private let portalPlanes = [ ModelEntity(mesh: .generatePlane(width: 1.0, height: 1.0), materials: [PortalMaterial()]), ModelEntity(mesh: .generatePlane(width: 1.0, height: 1.0), materials: [PortalMaterial()]), ModelEntity(mesh: .generatePlane(width: 1.0, height: 1.0), materials: [PortalMaterial()]) ] var body: some View { HStack(spacing: 80) { portalView_Garden portalView_MyEnvironment portalView_CornellBox } .padding(80) } ... }
ContentView.swift struct ContentView: View { @Environment(\.openImmersiveSpace) var openImmersiveSpace @Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace @Environment(\.dismissWindow) var dismissWindow var loader: EnvironmentLoader private let portalPlanes = [ ModelEntity(mesh: .generatePlane(width: 1.0, height: 1.0), materials: [PortalMaterial()]), ModelEntity(mesh: .generatePlane(width: 1.0, height: 1.0), materials: [PortalMaterial()]), ModelEntity(mesh: .generatePlane(width: 1.0, height: 1.0), materials: [PortalMaterial()]) ] var body: some View { HStack(spacing: 80) { portalView_Garden portalView_MyEnvironment portalView_CornellBox } .padding(80) } ... }
ContentView.swift private var portalView_Garden: some View { ZStack { GeometryReader3D { geometry in RealityView { content in let world = await makeWorldEntity(spaceType: .garden) content.add(world) let plane = getTargetPortalPlane(spaceType: .garden) plane.components.set(PortalComponent(target: world)) content.add(plane) } update: { content in let plane = getTargetPortalPlane(spaceType: .garden) let size = content.convert(geometry.size, from: .local, to: .scene) plane.model?.mesh = .generatePlane(width: size.x, height: size.y, cornerRadius: 0.03) }.frame(depth: 0.4) }.frame(depth: 0.4) VStack { Button("Enter") { Task { await openImmersiveSpace(id: Day14App.ImmersiveSpaceSelection.garden.rawValue) dismissWindow() } }.glassBackgroundEffect() } } .frame(width: Day14App.previewSize.width, height: Day14App.previewSize.height) }
ContentView.swift private var portalView_Garden: some View { ZStack { GeometryReader3D { geometry in RealityView { content in let world = await makeWorldEntity(spaceType: .garden) content.add(world) let plane = getTargetPortalPlane(spaceType: .garden) plane.components.set(PortalComponent(target: world)) content.add(plane) } update: { content in let plane = getTargetPortalPlane(spaceType: .garden) let size = content.convert(geometry.size, from: .local, to: .scene) plane.model?.mesh = .generatePlane(width: size.x, height: size.y, cornerRadius: 0.03) }.frame(depth: 0.4) }.frame(depth: 0.4) VStack { Button("Enter") { Task { await openImmersiveSpace(id: Day14App.ImmersiveSpaceSelection.garden.rawValue) dismissWindow() } }.glassBackgroundEffect() } } .frame(width: Day14App.previewSize.width, height: Day14App.previewSize.height) }
ContentView.swift
private func makeWorldEntity(spaceType: Day14App.ImmersiveSpaceSelection) async -> Entity {
switch (spaceType) {
case .garden:
let world = Entity()
world.components.set(WorldComponent())
world.addChild(try! await loader.getEntity_Garden())
let scale: Float = 0.2
world.scale *= scale
world.position.y -= scale * 1.5
return world
case .myEnvironment:
let world = Entity()
world.components.set(WorldComponent())
world.addChild(try! await loader.getEntity_MyEnvironment())
let scale: Float = 0.2
world.scale *= scale
world.position.y -= scale * 1.5
return world
case .cornellBox:
let world = Entity()
world.components.set(WorldComponent())
world.scale *= 0.5
world.position.y -= 0.5
world.position.z -= 0.5
world.addChild(try! await loader.getEntity_CornellBox())
return world
}
}
ContentView.swift
private func makeWorldEntity(spaceType: Day14App.ImmersiveSpaceSelection) async -> Entity {
switch (spaceType) {
case .garden:
let world = Entity()
world.components.set(WorldComponent())
world.addChild(try! await loader.getEntity_Garden())
let scale: Float = 0.2
world.scale *= scale
world.position.y -= scale * 1.5
return world
case .myEnvironment:
let world = Entity()
world.components.set(WorldComponent())
world.addChild(try! await loader.getEntity_MyEnvironment())
let scale: Float = 0.2
world.scale *= scale
world.position.y -= scale * 1.5
return world
case .cornellBox:
let world = Entity()
world.components.set(WorldComponent())
world.scale *= 0.5
world.position.y -= 0.5
world.position.z -= 0.5
world.addChild(try! await loader.getEntity_CornellBox())
return world
}
}
EnvironmentLoader.swift
actor EnvironmentLoader {
private weak var entity_Garden: Entity?
private weak var entity_MyImmersive: Entity?
private weak var entity_CornellBox: Entity?
func getEntity_Garden() async throws -> Entity {
if let entity = entity_Garden { return entity }
let entity = try await Entity(named: "Garden", in: realityKitContentBundle)
entity_Garden = entity
return entity
}
func getEntity_MyImmersive() async throws -> Entity {
if let entity = entity_MyEnvironment { return entity }
let entity = try await Entity(named: "Immersive", in: realityKitContentBundle)
entity_MyEnvironment = entity
return entity
}
func getEntity_CornellBox() async throws -> Entity {
...
}
}
EnvironmentLoader.swift
actor EnvironmentLoader {
private weak var entity_Garden: Entity?
private weak var entity_MyImmersive: Entity?
private weak var entity_CornellBox: Entity?
func getEntity_Garden() async throws -> Entity {
if let entity = entity_Garden { return entity }
let entity = try await Entity(named: "Garden", in: realityKitContentBundle)
entity_Garden = entity
return entity
}
func getEntity_MyImmersive() async throws -> Entity {
if let entity = entity_MyEnvironment { return entity }
let entity = try await Entity(named: "Immersive", in: realityKitContentBundle)
entity_MyEnvironment = entity
return entity
}
func getEntity_CornellBox() async throws -> Entity {
...
}
}
🎉
して独 自 用 サンプルを応 機能を作ってみる
Shader Graphで動画からクロマキー処理!
元ネタ
Displaying a stereoscopic image https://developer.apple.com/documentation/visionos/creating-stereoscopic-image-in-visionos
動画からリアルタイムで画像データを切り出す + クロマキー処理
元素材
動画からリアルタイムで画像データを切り出す
ViewModel.swift
@MainActor
func setup() async {
...
displayLink = CADisplayLink(target: self, selector: #selector(displayLinkUpdated(link:)))
displayLink!.preferredFramesPerSecond = 60
displayLink!.add(to: .main, forMode: .default)
let urlPath = Bundle.main.path(forResource: "video001", ofType: "mov")!
let url = URL(fileURLWithPath: urlPath)
let videoAsset = AVURLAsset(url: url, options: [:])
videoPlayerItem = AVPlayerItem(asset: videoAsset)
let settings: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
kCVPixelBufferMetalCompatibilityKey as String: true
]
videoPlayerOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: settings)
videoPlayerItem?.add(videoPlayerOutput!)
player = AVPlayer(playerItem: videoPlayerItem)
player!.play()
}
func exit() {
displayLink?.invalidate()
player?.pause()
}
ViewModel.swift
@MainActor
func setup() async {
...
displayLink = CADisplayLink(target: self, selector: #selector(displayLinkUpdated(link:)))
displayLink!.preferredFramesPerSecond = 60
displayLink!.add(to: .main, forMode: .default)
let urlPath = Bundle.main.path(forResource: "video001", ofType: "mov")!
let url = URL(fileURLWithPath: urlPath)
let videoAsset = AVURLAsset(url: url, options: [:])
videoPlayerItem = AVPlayerItem(asset: videoAsset)
let settings: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
kCVPixelBufferMetalCompatibilityKey as String: true
]
videoPlayerOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: settings)
videoPlayerItem?.add(videoPlayerOutput!)
player = AVPlayer(playerItem: videoPlayerItem)
player!.play()
}
生
func exit() {
displayLink?.invalidate()
player?.pause()
}
動画再
ViewModel.swift
@MainActor
func setup() async {
...
displayLink = CADisplayLink(target: self, selector: #selector(displayLinkUpdated(link:)))
displayLink!.preferredFramesPerSecond = 60
displayLink!.add(to: .main, forMode: .default)
let urlPath = Bundle.main.path(forResource: "video001", ofType: "mov")!
let url = URL(fileURLWithPath: urlPath)
let videoAsset = AVURLAsset(url: url, options: [:])
videoPlayerItem = AVPlayerItem(asset: videoAsset)
let settings: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
kCVPixelBufferMetalCompatibilityKey as String: true
]
videoPlayerOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: settings)
videoPlayerItem?.add(videoPlayerOutput!)
player = AVPlayer(playerItem: videoPlayerItem)
player!.play()
}
func exit() {
displayLink?.invalidate()
player?.pause()
}
リアルタイム
切り出し
ViewModel.swift
@objc private func displayLinkUpdated(link: CADisplayLink) {
guard let textureResource else { return }
guard let cgImage = getCurrentFrameTexture()?.cgImage else { return }
try? textureResource.replace(withImage: cgImage,
options: TextureResource.CreateOptions(semantic: nil))
}
extension MTLTexture {
var cgImage: CGImage? {
guard let image = CIImage(mtlTexture: self, options: nil) else { return nil }
let flipped = image.transformed(by: CGAffineTransform(scaleX: 1, y: -1))
return context.createCGImage(flipped,
from: flipped.extent,
format: CIFormat.BGRA8,
colorSpace: CGColorSpace(name: CGColorSpace.linearDisplayP3)!)
}
}
ViewModel.swift
@objc private func displayLinkUpdated(link: CADisplayLink) {
guard let textureResource else { return }
guard let cgImage = getCurrentFrameTexture()?.cgImage else { return }
try? textureResource.replace(withImage: cgImage,
options: TextureResource.CreateOptions(semantic: nil))
}
extension MTLTexture {
var cgImage: CGImage? {
guard let image = CIImage(mtlTexture: self, options: nil) else { return nil }
let flipped = image.transformed(by: CGAffineTransform(scaleX: 1, y: -1))
return context.createCGImage(flipped,
from: flipped.extent,
format: CIFormat.BGRA8,
colorSpace: CGColorSpace(name: CGColorSpace.linearDisplayP3)!)
}
}
ViewModel.swift
private func getCurrentFrameTexture() -> MTLTexture? {
guard let videoPlayerOutput = videoPlayerOutput,
let videoPlayerItem = videoPlayerItem else { return nil }
guard let textureCache else { return nil }
let currentTime = videoPlayerItem.currentTime()
var itemTimeForDisplay = CMTime.zero
guard let buffer = videoPlayerOutput.copyPixelBuffer(forItemTime: currentTime,
itemTimeForDisplay: &itemTimeForDisplay) else { return nil }
let width = CVPixelBufferGetWidth(buffer)
let height = CVPixelBufferGetHeight(buffer)
let planeCount = CVPixelBufferGetPlaneCount(buffer)
var textureRef: CVMetalTexture?
let result = CVMetalTextureCacheCreateTextureFromImage(
kCFAllocatorDefault, textureCache, buffer, nil,
.bgra8Unorm_srgb, width, height, planeCount, &textureRef
)
guard result == kCVReturnSuccess,
let unwrappedTextureRef = textureRef
else { return nil }
return CVMetalTextureGetTexture(unwrappedTextureRef)
}
ViewModel.swift
private func getCurrentFrameTexture() -> MTLTexture? {
guard let videoPlayerOutput = videoPlayerOutput,
let videoPlayerItem = videoPlayerItem else { return nil }
guard let textureCache else { return nil }
let currentTime = videoPlayerItem.currentTime()
var itemTimeForDisplay = CMTime.zero
guard let buffer = videoPlayerOutput.copyPixelBuffer(forItemTime: currentTime,
itemTimeForDisplay: &itemTimeForDisplay) else { return nil }
let width = CVPixelBufferGetWidth(buffer)
let height = CVPixelBufferGetHeight(buffer)
let planeCount = CVPixelBufferGetPlaneCount(buffer)
var textureRef: CVMetalTexture?
let result = CVMetalTextureCacheCreateTextureFromImage(
kCFAllocatorDefault, textureCache, buffer, nil,
.bgra8Unorm_srgb, width, height, planeCount, &textureRef
)
guard result == kCVReturnSuccess,
let unwrappedTextureRef = textureRef
else { return nil }
return CVMetalTextureGetTexture(unwrappedTextureRef)
}
クロマキー処理
クロマキー処理 ImageInput HueRange SaturateRange KeyColor ValueRange
Shader Graphへ値を渡す箇所
ViewModel.swift
@MainActor
func setup() async {
...
dynamicMaterial = try! await ShaderGraphMaterial(named: "/Root/DynamicMaterial",
from: "Immersive.usda", in: realityKitContentBundle)
try! dynamicMaterial!.setParameter(name: "ImageInput",
value: .textureResource(self.textureResource!))
try! dynamicMaterial!.setParameter(name: "KeyColor",
value: .color(CGColor(red: 67/255, green: 133/255, blue: 74/255, alpha: 1.0)))
try! dynamicMaterial!.setParameter(name: "HueRange", value: .float(0.03))
try! dynamicMaterial!.setParameter(name: "SaturateRange", value: .float(0.39))
try! dynamicMaterial!.setParameter(name: "ValueRange", value: .float(0.45))
entity.components.set(ModelComponent(mesh: .generatePlane(width: 0.8, height: 0.8),
materials: [dynamicMaterial!]))
...
}
🎉
まとめ 開発の情報源について Appleのサンプル群を解説! サンプルを応 して独 機能を作ってみる 自 用 X: @shmdevelop で資料、参考URL、最新情報共有しています