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 状态变化时调用,非每帧attachmentsViewBuilder 中的视图会被转换为 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 中手动管理 tagAttachmentsProvider用@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 文件和 bundleprepareAudio返回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)
核心启发
-
用 Reality Composer Pro 作为 3D 内容的数据源
- 做什么:在 Reality Composer Pro 中放置不可见实体作为标记,用自定义 Component 存储数据
- 为什么值得做:设计师可以独立调整位置和属性,代码自动适应变化
- 怎么开始:创建
Codable的自定义 Component,在 Reality Composer Pro 中添加到实体并填写属性
-
在 3D 场景中悬浮 SwiftUI 信息卡片
- 做什么:用 Attachments API 在兴趣点上方显示 2D 按钮或信息面板
- 为什么值得做:结合 SwiftUI 的丰富 UI 能力和 RealityKit 的 3D 定位
- 怎么开始:创建
AttachmentsProvider,用EntityQuery动态生成附件,在RealityView的 update 中定位
-
用滑块控制 3D 场景的材质和音频过渡
- 做什么:创建一个滑块同时控制地形变形和音频淡入淡出
- 为什么值得做:Shader Graph 参数和音频增益都可以用代码驱动,创造沉浸式过渡效果
- 怎么开始:在 Reality Composer Pro 中 Promote Shader Graph 参数,用
onChange同时更新材质和AmbientAudioComponent
-
将 USD 资产放入 .rkassets 目录
- 做什么:把 USD 文件放入 Swift Package 的 .rkassets 目录
- 为什么值得做:Xcode 编译为运行时更快加载的格式
- 怎么开始:在 Swift Package 中创建 .rkassets 目录,将 USD 文件移入
关联 Session
- Meet Reality Composer Pro — Reality Composer Pro 工具介绍
- Explore materials in Reality Composer Pro — 在 Reality Composer Pro 中探索材质
- Build spatial experiences with RealityKit — 使用 RealityKit 构建空间体验
- Work with Metal on visionOS — 在 visionOS 上使用 Metal
评论
GitHub Issues · utterances