WWDC Quick Look 💓 By SwiftGGTeam
Work with Reality Composer Pro content in Xcode

Work with Reality Composer Pro content in Xcode

观看原视频

Highlight

Reality Composer Pro 项目中的 3D 内容可以通过 RealityView 加载到 SwiftUI 中,结合自定义 Component 和 Attachments API,实现数据驱动的 3D 场景与 2D SwiftUI 视图的混合交互。

核心内容

从 Reality Composer Pro 到 Xcode

Reality Composer Pro 是准备 RealityKit 内容的开发者工具。项目中的每个顶部标签页代表一个可在运行时加载的根实体。

02:40

加载场景使用 RealityView 的异步初始化器,通过 Entity(named:in:) 从 Reality Composer Pro 包中加载指定名称的实体。realityKitContentBundle 是 Xcode 自动生成的常量,加载的实体是整个层级结构的根节点。

详细内容

ECS 架构

RealityKit 使用 Entity-Component-System(ECS)架构:

  • Entity:场景中的任何物体,不存储数据
  • Component:存储数据,可动态添加/移除
  • System:每帧执行的逻辑,查询特定 Component 并更新

04:49

这种设计与面向对象不同:Entity 的”性质”由它拥有的 Component 组合决定,而非继承层次。

自定义 Component

在 Reality Composer Pro 中创建自定义 Component,Xcode 会自动生成对应的 Swift 文件。

07:26

示例:为地形图上的兴趣点创建 Component

// Design-time component
public struct PointOfInterestComponent: Component, Codable {
    public var region: Region = .yosemite
    public var name: String = "Ribbon Beach"
    public var description: String?
}

22:31

关键点:

  • 必须遵循 Codable 协议才能在 Reality Composer Pro 中显示和编辑
  • 简单数据类型(Int、String、SIMD)可直接序列化
  • 复杂类型会导致 Xcode 编译错误

Attachments API

Attachments 允许将 SwiftUI 视图嵌入到 3D 场景中。

12:00

RealityView 的三参数初始化器:

RealityView { content, _ in
    // make: 加载初始场景,只执行一次
    if let entity = try? await Entity(named: "MyScene", in: realityKitContentBundle) {
        content.add(entity)
    }
} update: { content, attachments in
    // update: 状态变化时调用
    if let attachmentEntity = attachments.entity(for: "🐠") {
        content.add(attachmentEntity)
    }
} attachments: {
    // attachments: 声明要嵌入的 SwiftUI 视图
    Button { ... }
       .background(.green)
       .tag("🐠")
}

15:48

关键点:

  • make 闭包只执行一次,用于加载场景
  • update 闭包在 SwiftUI 状态变化时调用,非每帧
  • attachments ViewBuilder 中的视图会被转换为 Entity
  • tag 标识附件,在 update 中通过 attachments.entity(for:) 获取

数据驱动的动态附件

让 Reality Composer Pro 中的数据驱动附件生成:

@Observable final class AttachmentsProvider {
    var attachments: [ObjectIdentifier: AnyView] = [:]
    var sortedTagViewPairs: [(tag: ObjectIdentifier, view: AnyView)] { ... }
}

// 查询兴趣点并创建附件
static let markersQuery = EntityQuery(where: .has(PointOfInterestComponent.self))

rootEntity.scene?.performQuery(Self.markersQuery).forEach { entity in
    guard let pointOfInterest = entity.components[PointOfInterestComponent.self] else { return }
    
    let attachmentTag: ObjectIdentifier = entity.id
    let view = LearnMoreView(name: pointOfInterest.name, description: pointOfInterest.description)
                           .tag(attachmentTag)
    
    attachmentsProvider.attachments[attachmentTag] = AnyView(view)
}

20:43

关键点:

  • 使用 EntityQuery 查询拥有特定 Component 的所有实体
  • entity.id 作为唯一标识符,避免在 Reality Composer Pro 中手动管理 tag
  • AttachmentsProvider@Observable 触发 SwiftUI 更新

运行时 Component

将设计时数据和运行时数据分离:

// Run-time component
public struct PointOfInterestRuntimeComponent: Component {
    public let attachmentTag: ObjectIdentifier
}

// 在创建附件后添加运行时组件
let runtimeComponent = PointOfInterestRuntimeComponent(attachmentTag: attachmentTag)
entity.components.set(runtimeComponent)

25:38

运行时组件不需要 Codable,不会在 Reality Composer Pro 中显示。

音频播放

播放 Reality Composer Pro 中设置的音频:

func playOceanSound() {
    guard let entity = entity.findEntity(named: "OceanEmitter"),
        let resource = try? AudioFileResource(named: "/Root/Resources/Ocean_Sounds_wav",
                                   from: "DioramaAssembled.usda",
                                   in: RealityContent.realityContentBundle) else { return }
    
    let audioPlaybackController = entity.prepareAudio(resource)
    audioPlaybackController.play()
}

28:55

关键点:

  • findEntity(named:) 定位音频实体
  • AudioFileResource 需要完整路径、来源 usda 文件和 bundle
  • prepareAudio 返回 AudioPlaybackController,支持 play/pause/stop

Shader Graph 材质控制

在代码中驱动 Shader Graph 材质的参数:

@State private var sliderValue: Float = 0.0

Slider(value: $sliderValue, in: (0.0)...(1.0))
    .onChange(of: sliderValue) { _, _ in
        guard let terrain = rootEntity.findEntity(named: "DioramaTerrain"),
                var modelComponent = terrain.components[ModelComponent.self],
                var shaderGraphMaterial = modelComponent.materials.first 
                    as? ShaderGraphMaterial else { return }
        do {
            try shaderGraphMaterial.setParameter(name: "Progress", value: .float(sliderValue))
            modelComponent.materials = [shaderGraphMaterial]
            terrain.components.set(modelComponent)
        } catch { }
    }

31:02

关键点:

  • 在 Reality Composer Pro 中 Command-click 节点选择 “Promote” 暴露参数
  • ShaderGraphMaterial.setParameter(name:value:) 设置运行时值
  • 修改后需将材质存回 ModelComponent,再将 Component 存回 Entity

音频与滑块联动

static let audioQuery = EntityQuery(where: .has(RegionSpecificComponent.self) 
                                    && .has(AmbientAudioComponent.self))

rootEntity?.scene?.performQuery(Self.audioQuery).forEach({ audioEmitter in
    guard var audioComponent = audioEmitter.components[AmbientAudioComponent.self],
          let regionComponent = audioEmitter.components[RegionSpecificComponent.self]
    else { return }

    let gain = regionComponent.region.gain(forSliderValue: sliderValue)
    audioComponent.gain = gain
    audioEmitter.components.set(audioComponent)
})

31:57

核心启发

  1. 用 Reality Composer Pro 作为 3D 内容的数据源

    • 做什么:在 Reality Composer Pro 中放置不可见实体作为标记,用自定义 Component 存储数据
    • 为什么值得做:设计师可以独立调整位置和属性,代码自动适应变化
    • 怎么开始:创建 Codable 的自定义 Component,在 Reality Composer Pro 中添加到实体并填写属性
  2. 在 3D 场景中悬浮 SwiftUI 信息卡片

    • 做什么:用 Attachments API 在兴趣点上方显示 2D 按钮或信息面板
    • 为什么值得做:结合 SwiftUI 的丰富 UI 能力和 RealityKit 的 3D 定位
    • 怎么开始:创建 AttachmentsProvider,用 EntityQuery 动态生成附件,在 RealityView 的 update 中定位
  3. 用滑块控制 3D 场景的材质和音频过渡

    • 做什么:创建一个滑块同时控制地形变形和音频淡入淡出
    • 为什么值得做:Shader Graph 参数和音频增益都可以用代码驱动,创造沉浸式过渡效果
    • 怎么开始:在 Reality Composer Pro 中 Promote Shader Graph 参数,用 onChange 同时更新材质和 AmbientAudioComponent
  4. 将 USD 资产放入 .rkassets 目录

    • 做什么:把 USD 文件放入 Swift Package 的 .rkassets 目录
    • 为什么值得做:Xcode 编译为运行时更快加载的格式
    • 怎么开始:在 Swift Package 中创建 .rkassets 目录,将 USD 文件移入

关联 Session

评论

GitHub Issues · utterances