WWDC Quick Look 💓 By SwiftGGTeam
Build spatial experiences with RealityKit

Build spatial experiences with RealityKit

观看原视频

Highlight

RealityKit 在 visionOS 上新增了 Model3D、RealityView、volumetric window 和 ImmersiveSpace 等 API,让开发者可以用 SwiftUI 的熟悉语法在 2D 窗口、体积窗口和沉浸空间中放置 3D 内容,并通过 Entity-Component-System 架构自定义行为。

核心内容

三种方式集成 3D 内容

在 2D 窗口中嵌入 3D:最简单的方式。用 Model3D 视图加载 USD 文件,配合 resizablescaledToFit 修饰符(03:21)。模型异步加载,可以自定义 placeholder。

Volumetric window(体积窗口):为 3D 模型提供独立的窗口,支持从任意角度查看,且保持真实尺寸。1 米宽的模型始终显示为 1 米(05:01)。

ImmersiveSpace(沉浸空间):App 可以在空间中任意位置放置 3D 元素,突破窗口边界。适合完全沉浸的体验(07:05)。

Entity-Component-System 架构

Entity 是容器对象,本身不渲染也不做物理模拟。给它添加 Component 才能赋予行为(09:36)。

常用 Component:

  • ModelComponent:渲染 3D 网格和材质
  • TransformComponent:控制位置、旋转、缩放
  • HoverEffectComponent:注视时高亮
  • InputTargetComponent + CollisionComponent:接收手势输入
  • SpatialAudioComponent:空间音频

System 包含定期执行的逻辑代码,作用于匹配特定条件的 Entity。ECS 架构让行为和数据分离,便于扩展。

RealityView

RealityView 是 SwiftUI 中的 3D 视图容器,提供三种 closure(12:27):

  • make:异步加载和初始化 Entity
  • update:响应 SwiftUI 状态变化更新 Entity
  • subscribe:订阅 RealityKit 事件(动画完成、物理碰撞等)

坐标系约定:原点在 RealityView 中心,Y 轴向上,Z 轴朝向用户,X 轴向右,1 单位 = 1 米。RealityView 提供 convert 函数在 SwiftUI 和 RealityKit 坐标空间之间转换。

详细内容

Model3D 在 2D 窗口中显示 3D 模型

03:40

import SwiftUI
import RealityKit

struct GlobeModule: View {
    var body: some View {
        Model3D(named: "Globe") { model in
            model
                .resizable()
                .scaledToFit()
        } placeholder: {
            ProgressView()
        }
    }
}

关键点

  • Model3D 加载 USD/USDZ 文件,支持异步加载
  • resizablescaledToFit 让模型适应可用空间
  • placeholder 在加载期间显示 ProgressView
  • 需要把 USD 文件作为资源添加到项目中

Volumetric window

05:52

import SwiftUI
import RealityKit

struct WorldApp: App {
    var body: some SwiftUI.Scene {
        WindowGroup(id: "planet-earth") {
            Model3D(named: "Globe")
        }
        .windowStyle(.volumetric)
        .defaultSize(width: 0.8, height: 0.8, depth: 0.8, in: .meters)
    }
}

关键点

  • .windowStyle(.volumetric) 把窗口变成体积窗口
  • .defaultSize 用真实世界单位(米)定义尺寸
  • 体积窗口中的模型保持真实比例,可以从任意角度查看
  • 通过 openWindow 环境值触发打开

ImmersiveSpace

07:31

import SwiftUI
import RealityKit

struct WorldApp: App {
    var body: some SwiftUI.Scene {
        ImmersiveSpace(id: "objects-in-orbit") {
            RealityView { content in
                // 添加 3D 内容
            }
        }
    }
}

关键点

  • ImmersiveSpace 是新的 Scene 类型
  • 通过 openImmersiveSpace 环境值异步打开
  • 内容可以放置在空间任意位置
  • 有多种 immersion style(mixed、progressive、full)

RealityView 异步加载和实体定位

12:54

import SwiftUI
import RealityKit

struct Orbit: View {
    var body: some View {
        RealityView { content in
            async let earth = ModelEntity(named: "Earth")
            async let moon = ModelEntity(named: "Moon")

            if let earth = try? await earth, let moon = try? await moon {
                content.add(earth)
                content.add(moon)
                moon.position = [0.5, 0, 0]
            }
        }
    }
}

关键点

  • async let 并发加载多个模型
  • content.add() 把 Entity 添加到场景中
  • position[x, y, z] 数组设置位置,单位是米
  • RealityKit 坐标系:Y 向上,Z 朝向用户,X 向右

拖拽交互

18:31

import SwiftUI
import RealityKit

struct DraggableModel: View {
    var earth: Entity

    var body: some View {
        RealityView { content in
            content.add(earth)
        }
        .gesture(DragGesture()
            .targetedToEntity(earth)
            .onChanged { value in
                earth.position = value.convert(value.location3D,
                                               from: .local, to: earth.parent!)
            })
    }
}

关键点

  • Entity 需要同时有 InputTargetComponentCollisionComponent 才能接收手势
  • .targetedToEntity 让手势只作用于特定 Entity
  • value.location3D 是手势位置,需要从 SwiftUI 坐标空间转换到 Entity 的父坐标空间
  • 在 Reality Composer Pro 中可以方便地添加 CollisionComponent

播放动画

14:56

import SwiftUI
import RealityKit

struct AnimatedModel: View {
    @State var subscription: EventSubscription?

    var body: some View {
        RealityView { content in
            if let moon = try? await Entity(named: "Moon"),
               let animation = moon.availableAnimations.first {
                moon.playAnimation(animation)
                content.add(moon)
            }
            subscription = content.subscribe(to: AnimationEvents.PlaybackCompleted.self) {
                // 动画完成后的处理
            }
       }
   }
}

关键点

  • availableAnimations 获取模型中预置的动画
  • playAnimation() 播放动画
  • content.subscribe() 订阅事件,这里是动画播放完成事件
  • @State 保存订阅对象,防止被提前释放

轨道动画

20:20

let orbit = OrbitAnimation(name: "Orbit",
                           duration: 30,
                           axis: [0, 1, 0],
                           startTransform: moon.transform,
                           bindTarget: .transform,
                           repeatMode: .repeat)

if let animation = try? AnimationResource.generate(with: orbit) {
    moon.playAnimation(animation)
}

关键点

  • OrbitAnimation 让 Entity 围绕父级旋转
  • duration: 30 表示 30 秒完成一圈
  • axis: [0, 1, 0] 表示围绕 Y 轴旋转
  • startTransform 从当前位置开始,避免跳变
  • repeatMode: .repeat 让动画循环播放

空间音频

22:12

// 创建空 Entity 作为音频源
let audioSource = Entity()

// 配置音频为定向发射,聚焦度 0.75
audioSource.spatialAudio = SpatialAudioComponent(directivity: .beam(focus: 0.75))

// 旋转音频源,让声音朝特定方向发射
audioSource.orientation = .init(angle: .pi, axis: [0, 1, 0])

// 加载循环音频并播放
if let audio = try? await AudioFileResource(named: "SatelliteLoop",
                                            configuration: .init(shouldLoop: true)) {
    satellite.addChild(audioSource)
    audioSource.playAudio(audio)
}

关键点

  • 音频默认是空间化的,声音像真实存在于空间中
  • directivity: .beam(focus: 0.75) 创建聚焦声束
  • 通过 orientation 控制声音发射方向
  • shouldLoop: true 让音频循环播放
  • 三种音频类型:Spatial(定向)、Ambient(环境)、Channel(直达扬声器)

自定义 Component 和 System

23:47

import RealityKit

// Component 是附加在 Entity 上的数据
struct TraceComponent: Component {
    var mesh = TraceMesh()
}

// 从 Entity 上读取或写入 Component
func updateTrace(for entity: Entity) {
    var component = entity.components[TraceComponent.self] ?? TraceComponent()
    component.update()
    entity.components[TraceComponent.self] = component
}

// Codable Component 可以在 Reality Composer Pro 中使用
struct PointOfInterestComponent: Component, Codable {
    var name = ""
}

关键点

  • Component 遵循 Component 协议
  • 通过 entity.components[ComponentType.self] 读写
  • 遵循 Codable 后可在 Reality Composer Pro 中编辑

24:51

import SwiftUI
import RealityKit

// System 包含逻辑代码
struct TraceSystem: System {
    static let query = EntityQuery(where: .has(TraceComponent.self))
    
    init(scene: Scene) {
        // 初始化
    }

    func update(context: SceneUpdateContext) {
        for entity in context.entities(Self.query, updatingSystemWhen: .rendering) {
            addCurrentPositionToTrace(entity)
        }
    }
}

// 注册 System
struct MyApp: App {
    init() {
        TraceSystem.registerSystem()
    }
}

关键点

  • System 遵循 System 协议
  • EntityQuery 定义 System 关心的 Entity
  • updatingSystemWhen: .rendering 按渲染帧率更新
  • 在 App 的 init() 中调用 registerSystem() 注册
  • System 自动作用于 App 中所有 RealityKit 内容

核心启发

  • 做什么:做一个 3D 太阳系教育应用,让学生可以拖拽行星、观察轨道运动。

  • 为什么值得做:RealityKit 的 OrbitAnimation 和拖拽手势让抽象的天文概念变得可触摸。

  • 怎么开始:用 RealityView 加载行星模型,添加 DragGesture 实现拖拽,OrbitAnimation 实现公转动画。

  • 做什么:开发一个产品展示应用,让客户可以从各个角度查看商品 3D 模型。

  • 为什么值得做:Volumetric window 保持真实尺寸,客户可以绕着商品走一圈查看细节。

  • 怎么开始:用 WindowGroup + .windowStyle(.volumetric) 创建体积窗口,内部用 Model3D 加载 USDZ 模型。

  • 做什么:做一个沉浸式音乐可视化应用,音符在空间中漂浮并发出空间音频。

  • 为什么值得做SpatialAudioComponent 让声音有方向感,配合粒子效果营造沉浸氛围。

  • 怎么开始:创建 ImmersiveSpace,用 Entity + SpatialAudioComponent 放置音频源,配合自定义 System 更新粒子位置。

  • 做什么:开发一个室内设计预览工具,让用户在真实房间里摆放虚拟家具。

  • 为什么值得做:RealityKit 的锚点功能可以把 3D 模型固定在墙面或地板上,空间音频可以模拟房间声学效果。

  • 怎么开始:用 AnchorEntity 把家具模型锚定到平面,用 CollisionComponent 实现拖拽调整位置。

关联 Session

评论

GitHub Issues · utterances