
-
搭配使用更出色:SwiftUI 和 RealityKit
了解如何在 visionOS 26 中无缝整合 SwiftUI 和 RealityKit 的强大功能。我们将探索 Model3D 的增强功能 (包括动画和 ConfigurationCatalog 支持),并展示如何顺利过渡到 RealityView。你将了解如何借助 SwiftUI 动画实现 RealityKit 组件更改、实现交互式操控、使用新增 SwiftUI 组件打造更丰富的互动体验,以及从 SwiftUI 代码中观察 RealityKit 的变化。我们还将介绍如何利用统一坐标转换进行跨框架坐标变换。
章节
- 0:00 - 简介
- 1:24 - Model3D 增强功能
- 6:13 - RealityView 过渡
- 11:52 - 对象操控
- 15:35 - SwiftUI 组件
- 19:08 - 信息流
- 24:56 - 统一坐标系转换
- 27:01 - 动画
- 29:41 - 后续步骤
资源
- Canyon Crosser: Building a volumetric hike-planning app
- Rendering hover effects in Metal immersive apps
相关视频
WWDC24
WWDC23
WWDC21
WWDC20
-
搜索此视频…
大家好我叫 Amanda 是一名 RealityKit 工程师 我叫 Maks 是一名 SwiftUI 工程师 今天我们将为大家介绍 SwiftUI 和 RealityKit 的一些精彩增强功能 它们能让这两个框架协同工作时 表现地更加出色! 看看这个可爱的场景吧! 有一个迷人的 SwiftUI 机器人悬浮 在空中 还有一个在地面上的 RealityKit 机器人 它们都渴望 建立联系 当它们接近时 火花四溅! 但要如何让它们足够靠近 真正实现 互动呢? 接下来 我和 Maks 将介绍如何将 传统 UI 环境与交互式 3D 内容 融合在一起 首先 我会介绍 Model3D 的 一些新增强功能 接着 我会演示如何从使用 Model3D 过渡到使用 RealityView 并说明在不同场景下该如何选择 我还会介绍全新的 Object Manipulation API
RealityKit 获得了新的组件类型 进一步融合 SwiftUI 的各项特性 现在 SwiftUI 和 RealityKit 可 实现双向信息传递 我们稍后会解释
坐标空间的转换也变得前所未有的 简单 你还可以用 SwiftUI 的动画驱动 RealityKit 组件的变化 我们来动手实现吧!
只需一行代码 就能用 Model3D 在 你的 App 中展示 3D 模型 在 visionOS 26 中 两项增强功能 让你能借助 Model3D 做到更多: 支持播放动画 以及从 ConfigurationCatalog 载入资源 Model3D 是一个 SwiftUI 视图 因此它能融入 SwiftUI 的布局系统 我会利用这个布局系统 添加一个小牌子来显示机器人的名字
现在 牌子上写着这个机器人的名字 叫 Sparky
Sparky 还掌握了一套 超酷的舞蹈动作! 艺人已将这段动画打包进了机器人 模型资源中 visionOS 26 中新增了 Model3DAsset 类型 通过使用 Model3DAsset 创建 Model3D 你可以载入并控制 3D 内容中的动画 模型会从资源中载入动画 并让你选择要播放哪一个 “模型”这个词有多重含义 尤其是在这次讲座中 因为我们要将 UI 框架 与 3D 游戏框架融合在一起 在 UI 框架中 “模型”指的是表示 App 所用信息的数据结构 它承载数据和业务逻辑 供视图展示这些信息 在像 RealityKit 这样的 3D 框架 中 “模型”指的是 可以放置在场景中的 3D 对象 你可以通过 ModelComponent 来访问它 其中包含用于定义形状的 网格资源以及决定外观的材质 术语重叠有时在所难免 当两个体系同时出现时 它们各自 带来了自己的术语 而有时这些术语恰好会重叠 现在 回到 Sparky 和它的动画
我把这个 Model3D 放在了一个 选择器、播放按钮和时间滑块上方 在我的 RobotView 中 最上方展示 的是具备动画效果的机器人 下面是用于选择动画的选择器和 和动画播放控件 我首先用场景名称初始化一个 Model3DAsset 以便从捆绑包中载入资源 资源出现后 我将它传给 Model3D 构造器 在下方的 VStack 中 我放置了一个 自定选择器 用来列出这个模型资源中可用的动画 当用户从列表中选中某项时 选择器会将资源的 selectedAnimation 设置为新的值 接着 Model3DAsset 会为所选 动画创建一个 AnimationPlaybackController 用于控制播放 这个资源会提供一个 animationPlaybackController 你可以用这个对象来暂停、继续播放 或跳转到动画中的指定时间点
我把这个 animationController 传 入 RobotAnimationControls 视图中 待会儿我们也会看一下这个视图 的实现 在 visionOS 26 中 RealityKit 中 已有的 AnimationPlaybackController 类 现在是可观察的 在 SwiftUI 视图中 我可以观察它的 time 属性 用来显示动画的播放进度
我有一个名为 controller 的 @Bindable 属性 也就是说 我将 AnimationPlaybackController 作为这个视图的数据模型 当控制器的 isPlaying 值发生 变化时 SwiftUI 会重新评估 RobotAnimationControls 视图 我还添加了一个滑块 用于显示 当前动画播放的时间 相对于动画的总时长 你可以拖移这个滑块 在动画中 快进或倒退 来看 Sparky 的庆祝动作动画! 我可以通过滑块自由控制快进和后退 加油 Sparky 今天是你的生日! 跳完舞之后 Sparky 想在前往温室 与另一位机器人见面前换一身装扮 我可以借助 RealityKit 中增强的 ConfigurationCatalog 类型来帮它打扮一番 这个类型用于存储实体的不同 表示形式 比如不同的网格几何形状、组件数值 或材质属性 在 visionOS 26 中 你可以用 ConfigurationCatalog 初始化 Model3D 并在多个表示形式之间切换
为了让 Sparky 能尝试不同的装扮 艺人将多个身体样式打包进了 一个 reality 文件中 我从 App 的主捆绑包中加载这个 文件 作为 ConfigurationCatalog 再用这个配置创建 Model3D 这个弹出窗口展示了可选配置 选择其中一项 就能改变 Sparky 的外观 舞步? 准备好了 装扮? 也到位了 Sparky 已整装待发 准备去 RealityKit 温室结识新朋友 火花马上就要飞起来啦! 为了让火花四溅 我需要用到粒子 发射器 但这可不能直接在 Model3D 类型 中动态添加 粒子发射器是一个组件 需要加在 RealityKit 的实体上 稍后我会详细介绍 关键是 Model3D 不支持添加组件 所以 为了加入粒子发射器 我需要 改用 RealityView 接下来我会演示 如何在不改变布局 的前提下 顺畅地将 Model3D 替换为 RealityView 首先 我将视图从 Model3D 切换 为 RealityView 然后在 RealityView 的 make 闭包中 从 App 捆绑包中载入 botanist 生成一个实体 我将这个实体添加到 RealityView 的内容中 这样 Sparky 就会出现在屏幕上
不过……现在名字标牌 被挤到了一边 之前用 Model3D 的时候并不会 这样 会出现这个问题 是因为 RealityView 默认会占据 SwiftUI 布局系统分配给它的 所有可用空间 相比之下 Model3D 会根据 基础模型文件本身的固有尺寸 来自行调整大小 这个问题可以解决! 我为 RealityView 添加了新的 .realityViewLayoutBehavior 修饰符和 .fixedSize 让它紧贴模型的初始边界进行布局 看上去好多了 RealityView 会根据它的内容中 实体的可视边界 来决定自身的尺寸 这个尺寸只会在 make 闭包 执行完后评估一次 realityViewLayoutBehavior 还有两个选项:.flexible 和 .centered 在这三个 RealityView 中 我都让 Sparky 模型的底部 与场景的原点对齐 并用一个小工具标记了原点 也就是这个显示轴和原点的小十字 左边使用 .flexible 选项时 RealityView 的表现就像没有 应用任何修饰符一样 原点保持在视图中心 .centered 选项会调整 RealityView 的原点 使内容在视图中居中显示 而 .fixedSize 会让 RealityView 紧贴内容边界布局 使它的行为就像 Model3D 一样
这些选项都不会改变实体在 RealityViewContent 中的 位置或缩放 它们只会调整 RealityView 自身的 原点位置 我已经调整好 Sparky 在 RealityView 中的尺寸 接下来要让它重新动起来 我将从 Model3D 的新动画 API 转向直接在实体上使用 RealityKit 动画 API 如果要详细了解 RealityKit 中 众多的动画处理方式 请观看讲座 “在 Reality Composer Pro 中编写交互式 3D 内容” 我之所以从 Model3D 切换到 RealityView 是为了给 Sparky 添加 ParticleEmitterComponent 因为当两个机器人靠近时 必须得有火花飞溅 粒子发射器可以一次性 生成数百个小粒子动画 用来模拟烟火、雨滴、闪光等效果 RealityKit 提供了这些效果的 预设值 你可以根据需要调整这些预设值 打造理想的视觉效果 你既可以在 Reality Composer Pro 中设计它们 也可通过代码进行配置 你可以将粒子发射器作为组件添加 到实体上 组件是 RealityKit 的核心概念 之一 它基于“实体-组件-系统”范式 在场景中 每个对象都是一个实体 你通过为实体添加组件 来定义它的特性与行为 组件是用于存储实体相关数据的类型 系统则会处理具备特定组件的实体 并执行与它的数据相关的逻辑 RealityKit 提供了内建系统来 处理粒子动画、 物理模拟、渲染等功能 你可以在 RealityKit 中 编写自定系统 为游戏或 App 添加自定逻辑 如果要深入了解 RealityKit 中的 实体组件系统 请观看讲座 “深入了解 RealityKit 2“ 我会在 Sparky 头部的两侧 各添加一个粒子发射器 首先 我创建两个不可见的实体 作为火花效果的容器 我设计的粒子发射器默认朝右 因此我会将它直接添加到 Sparky 右侧的不可见实体
左侧的实体则绕 y 轴 旋转 180 度 让它朝左发射
将这一切组合在 RealityView 中 Sparky 拥有了动画、 位于正确位置的名字标牌 还有飞舞的火花!
RealityKit 非常适合做这种细致的 内容创作 如果你正在开发游戏或 打造以娱乐为导向的体验 或者需要对 3D 内容的行为 进行精细控制 那就选择 RealityView 而如果你只是想展示一个独立完整的 3D 资源 Model3D 会是更好的选择 你可以把它看作 SwiftUI 的 图像视图 但用于 3D 资源
借助 Model3D 的全新动画功能 和配置目录 你可以实现更多效果 而当设计需求变化并且你需要 直接访问实体、组件 和系统时 也可以通过 realityViewLayoutBehavior 顺畅 地从 Model3D 过渡到 RealityView 下面 我将详细介绍 visionOS 26 中 全新的 Object Manipulation API 它让用户可以直接拿起 App 中的虚拟对象! SwiftUI 和 RealityKit 均支持对象操控功能 借助对象操控功能 用户可以用单手移动对象 用单手或双手旋转对象 或者用双手捏合拖移来缩放对象 你甚至可以将对象从一只手 传递到另一只手
启用这一功能有两种方式 取决于具体对象是 RealityKit 实体 还是 SwiftUI 视图 在 SwiftUI 中 只需添加全新的 manipulable 修饰符 为了停用缩放 但仍允许用任意 一只手移动和旋转机器人 我指定了支持的操作类型
为了让机器人显得特别沉重 我指定它具有较高的惯性
当 Sparky 显示在 Model3D 视图 中时 .manipulable 修饰符即可生效 它可以作用于整个 Model3D 或它附着的任何视图 而当 Sparky 在 RealityView 中显示时 我只希望对机器人 实体本身而不是 整个 RealityView 启用操控功能 在 visionOS 26 中 ManipulationComponent 是一个 全新类型 你可以在实体上设置 这个类型以启用对象操控功能 静态函数 configureEntity 会将 ManipulationComponent 添加到实体 它还会添加 CollisionComponent 让交互系统知道 你何时轻点了这个实体 它会添加一个 InputTargetComponent 告诉系统 这个实体可以响应手势操作 最后 它会添加一个 HoverEffectComponent 用于在用户 注视或将鼠标悬停在实体上时 应用 视觉效果 只需这一行代码 就能在场景中启用 对实体的操控功能 你还可以传入多个参数进一步自定 体验 在这个示例中 我指定了紫色聚光灯 效果 我允许所有类型的输入:包括直接 触控、凝视和捏合操作 我还提供了碰撞形状 用于定义 机器人的外部边界 当用户在你的 App 中与对象 进行交互时 对了做出响应 对象操控系统会在 关键时刻触发事件 例如交互开始与结束时 当实体被移动、旋转和缩放 而更新状态时 被释放时 或从一只手传递到另一只手时 你可以订阅这些事件以更新状态 默认情况下 系统会在交互开始、 传递 或释放物体时播放标准音效 要使用自定音效 我先将 audioConfiguration 设置为 none 这将停用标准声音 接着 我订阅 ManipulationEvent.didHandOff 也就是当用户将机器人从一只手 传递到另一只手时触发的事件 在这个闭包中 我播放了自己的 音频资源 好了 Maks Sparky 的旅程真是精彩纷呈: 它在 Model3D 中灵动鲜活起来 在 RealityView 中找到了属于自己 的新家 通过火花展现出独特个性 还能让人们伸手触碰并与之互动 通往 RealityKit 温室的这一路 它已经走得很远了 确实如此! 但要让 Sparky 真正与等待它的 机器人建立联系 虚拟空间中的对象需要具备 新的能力 它们要能响应手势、 展示自身信息 并以 SwiftUI 原生的方式触发 交互行为
Sparky 通往 RealityKit 温室旅程的 核心是构建连接 而深层次的连接 离不开丰富的交互体验 这正是全新 SwiftUI RealityKit 组件 在 visionOS 26 中的设计初衷 这些组件将强大又熟悉的 SwiftUI 功能 直接引入 RealityKit 实体中 RealityKit 新增了三个关键组件: 首先 ViewAttachmentComponent 允许你 将 SwiftUI 视图直接附加到实体上 其次 GestureComponent 让实体 能够响应触控和手势操作 最后是 PresentationComponent 它可在 RealityKit 场景中呈现 SwiftUI 视图 例如弹出框
在 visionOS 1 中 你只能在 RealityView 构造器中 预先声明这些附件 在评估完你的附件视图构建器后 系统会将结果以实体形式传入 以调用更新闭包 你可以将这些实体添加到场景中 并将它们放置在 3D 空间中 在 visionOS 26 中 这一流程得到 简化: 你现在可以在 App 的任意位置 通过 RealityKit 组件 来创建附件 只需将任意 SwiftUI 视图传入 即可创建一个 ViewAttachmentComponent 然后把它加入实体的组件集合中
就这样 我把 NameSign 从 SwiftUI 移到了 RealityKit 接下来 我们来看看手势! 你已经可以通过 targetedToEntity 手势修饰符 将手势附加到 RealityView 上 而在 visionOS 26 中 全新推出了 GestureComponent 与 ViewAttachmentComponent 类似 你可以将 GestureComponent 直接添加到实体中 并传入标准的 SwiftUI 手势 手势的数值默认是在实体的 坐标空间中报告的 非常实用! 我用 GestureComponent 搭配轻点 手势来控制名字标牌的打开和关闭
来看一下效果 这台机器人的名字是……Bolts!
小贴士:对任何作为手势目标的 实体 还应添加 InputTargetComponent 和 CollisionComponent 这条建议适用于 GestureComponent 以及目标手势 API
通过 GestureComponent 和 ViewAttachmentComponent 我为 Bolts 创建了一个名字标牌 但 Bolts 正在准备迎接一位特别 来客:Sparky! 它希望在温室中见面时展现出 最佳状态 是时候换一身装扮了! 我会把 Bolts 的名字标牌替换为 一个 UI 用来选择它的穿搭 毕竟 这是个重要的决定
为了突出这一点 我会使用 PresentationComponent 直接在 RealityKit 中以弹出框的 形式展示这个 UI
首先 将 ViewAttachmentComponent 替换为 PresentationComponent 这个组件接收一个布尔类型的 绑定值 用于控制弹出框是否显示 并在用户关闭通知框时通知你 configuration 参数指定了展示的类型 这里我指定了“弹出框” 在弹出框中 我会展示一个配置视图 提供不同的 造型选项 帮 Bolts 换装打扮 这样 当 Sparky 来拜访时 我就能 帮 Bolts 选出最合适的颜色
嘿 Maks 你觉得 Bolts 更适合 夏天造型 还是秋天造型?
开个时尚玩笑而已
现在的 Bolts 打扮得光鲜亮丽 不过 它首先还有工作要做 Bolts 在温室里给植物浇水 我会制作一个小地图 就像游戏中的平视显示屏一样 用来追踪 Bolts 在温室中的位置 为此 我需要监听机器人的 Transform 组件 在 visionOS 26 实体现在是 可观察的 它们可以在属性发生变化时通知 其他代码 只需读取实体的 observable 属性 即可接收通知
通过这个属性 你可以追踪实体的 位置、 缩放及旋转方面的变化 监听它的子实体集合 以及组件变化 甚至包括你的自定 组件! 可以通过 withObservationTracking 块直接观察这些属性 或使用 SwiftUI 内建的观察机制 来完成 我将使用 SwiftUI 来实现小地图 要进一步了解观察功能 请观看 讲座“探索 SwiftUI 中的观察” 在这个视图中 我会在小地图上 显示实体的位置 我在实体上访问这个可观察值 这可以让 SwiftUI 知道视图 依赖于它
当 Bolts 在温室里移动、 为植物浇水时 它的位置会不断变化 每次发生变化 SwiftUI 都会重新 调用视图的 body 从而移动它在小地图中对应的符号! 要深入了解 SwiftUI 的数据流 机制 请观看讲座 “SwiftUI 中的数据要点” 我们的机器人伙伴们正在逐步成型! 这正是我们梦想中的效果! 我很喜欢你之前对模型之间 区别的描述 有时候 我们确实需要在数据模型 与 3D 对象模型之间 来回传递数据 在 visionOS 26 中 可观察实体 为我们提供了一种全新的工具 从一开始 你就可以在 RealityView 的 update 闭包中 将信息从 SwiftUI 传递到 RealityKit 现在 通过实体的 observable 属性 你也可以反向发送信息 RealityKit 的实体现在可以像 模型对象一样 驱动 SwiftUI 视图的更新! 这样一来 信息就能双向流动: 从 SwiftUI 到 RealityKit 也可以从 RealityKit 回到 SwiftUI 但是…… 这样是否有可能导致无限循环? 有可能! 我们来看看如何避免在 SwiftUI 与 RealityKit 之间 产生无限循环 当你在视图的 body 中读取某个 可观察属性时 就建立了依赖关系 视图依赖于这个属性 一旦属性值发生变化 SwiftUI 就会 更新视图 并重新执行 body RealityView 有一些特殊行为 可以把它的 update 闭包看作是它所在视图 body 的延伸
只要这个视图的任何状态发生变化 无论这些状态是否 在闭包中明确观察到 SwiftUI 都会调用这个闭包 在我的 RealityView 的 update 闭包中 我修改了这个位置 这会写入位置值 从而触发 SwiftUI 更新视图、 重新运行 body 最终造成无限循环
为了避免产生无限循环 不要在 update 闭包中 修改你观察到的状态
你可以自由修改那些未被观察的实体 它们不会引发无限循环 因为它们的变化 不会让 SwiftUI 重新评估视图的 body 如果确实需要修改某个被观察的 属性 请先检查它的当前值 避免写回相同 的值 这样可以打破循环 防止出现无限 更新 需要注意的是 RealityView 的 make 闭包很特殊 在 make 闭包中访问可观察属性 不会建立依赖关系 也不会被纳入它所在视图的观察范围 此外 make 闭包不会因状态变化 而重新执行 只会在它所在的视图首次出现时 运行一次 你也可以通过自定系统 在其中更新被观察实体的属性 系统的 update 方法不在 SwiftUI 视图 body 的 评估范围内 因此这里是修改被观察实体值 的理想位置
手势的闭包同样不在 SwiftUI 视图 body 的评估范围内 调用它们是为了响应用户输入 你也可以在这里修改被观察实体 的值 总结一下 在某些地方修改被观察的 实体是没问题的 而在另一些场景下则要避免
如果你在 App 中遇到了无限循环 的问题 这里有个实用建议: 将大型视图拆分成更小、 自包含的视图 每个视图只管理 它自身需要的状态 这样 当某个无关实体发生变化时 不会导致重新评估这些小视图 这样做还能显著提升性能! 你知道吗 Maks 也许你会发现 现在根本不需要再使用 update 闭包了 现在 实体本身就可以 作为视图的状态 因此你可以在习惯修改状态 的常规位置来修改它 完全不需要再使用 update 闭包了 没错! 我觉得 避免无限循环 是个需要反复学习的课题 但只要不使用 update 闭包 出现循环的可能性就会小很多 我想 是时候让 Bolts 和 Sparky 相聚了 Bolts 的工作终于完成 是时候和 Sparky 约会啦! 当我把 Sparky 拿过来 两位机器人 逐渐靠近时 我希望随着它们之间的距离变小 出现一些“火花四射”的效果 为此 我会用上全新的 Unified Coordinate Conversion API 现在 Sparky 仍在一个 Model3D SwiftUI 视图中 而 Bolts 则是 RealityKit 温室中的一个实体 我需要获取这两个机器人之间的 绝对距离 尽管它们位于不同的坐标空间中 为解决这个问题 Spatial 框架现在 引入了一个名为 CoordinateSpace3D 的协议 用于表示抽象的坐标空间 只要两个类型都遵循 CoordinateSpace3D 你就可以轻松在它们之间转换 坐标值 哪怕它们来自不同的框架 RealityKit 中的 Entity 和 Scene 类型都符合 CoordinateSpace3D 在 SwiftUI 中 GeometryProxy3D 新增了 .coordinateSpace3D() 函数 用于获取它对应的坐标空间 此外 多种手势类型也能提供相对于 任意 CoordinateSpace3D 的坐标值 CoordinateSpace3D 协议的 原理是 先将 Sparky 坐标空间 中的值转换为 RealityKit 和 SwiftUI 都共享的一个 中间坐标空间 接下来 系统会将坐标从共享空间 转换到 Bolts 的坐标空间 并自动处理诸如点到米的单位换算、 坐标轴方向等底层细节 在 Sparky 的 Model3D 视图中 每当视图几何信息发生变化时 系统就会调用我的 onGeometryChange3D 函数 并传入一个 GeometryProxy3D 我通过它获取坐标空间 然后 我可以将视图中的位置转换为 实体空间中的点 从而计算出两台机器人之间的距离 现在 当 Amanda 将 Bolts 和 Sparky 拉近时 火花会增加 拉远时 火花会减少
接下来 我要让两台机器人协同 移动、同步行动 我会使用 SwiftUI 驱动 RealityKit 组件的动画 SwiftUI 本身就提供了 优秀的动画 API 可以对视图属性的变化进行隐式 动画处理 在这里 我为 Sparky 所在的 Model3D 视图添加了动画 切换时它会向左移动 再次切换时则弹回原位 我为 isOffset 绑定添加了动画 并指定使用更富弹性的动画效果 在 visionOS 26 中 你现在可以 使用 SwiftUI 动画 隐式地为 RealityKit 组件添加 动画效果 你只需在 RealityKit 的动画块中 为实体 设置一个受支持的组件 剩下的交给框架处理即可 有两种方式可以将动画与状态变化 关联起来: 第一种是在 RealityView 中使用 content.animate() 以在动画块中设置组件的新值 RealityKit 会使用触发 update 闭包的 SwiftUI 交易 所关联的动画 这里就是一个富有弹性的动画
另一种方式是调用全新的 Entity.animate() 函数 传入一个 SwiftUI 动画 以及一个用于设置组件新值的闭包 在这个示例中 每当 isOffset 属性 变化时 我通过修改实体的位置 让 Sparky 向左或向右移动 在动画块中设置位置 会隐式地 为 Transform 组件 添加动画 从而让实体平滑地移动到新位置 当我将这个功能与 Amanda 提到 的 Object Manipulation API 结合使用时 隐式动画的优势 就更加明显了 我可以用 SwiftUI 动画为 Bolts 实现一个自定的释放行为 首先 我将对象操控的默认释放 行为设置为 .stay 以停用系统的默认释放动画 接着 我会订阅操控交互的 WillRelease 事件 当对象即将被释放时 我通过将 Sparky 的 transform 设置为 .identity 将它的缩放、位置和旋转全部重置 由于我在动画块中修改了 Sparky 的 transform Sparky 会以弹跳的方式返回 默认位置 现在 Sparky 回弹到原始位置的动画 效果就变得更加有趣了! 所有这些内建的 RealityKit 组件 都支持隐式动画 包括 Transform、音频组件 以及具有颜色属性的模型和光照组件 Sparky 和 Bolts 的旅程真是精彩! 看到 SwiftUI 与 RealityKit 的 强大功能携手发挥作用 真是太好了 有了这种连接 你也能打造出真正卓越的 空间 App 在虚拟与现实之间真正建立起连接 想象一下 将 SwiftUI 组件无缝 集成进 RealityKit 场景 实体又能动态驱动 SwiftUI 状态的 变化 会带来多少可能性! 就像 Sparky 和 Bolts 一样 我们也希望你受到启发 以前所未有的方式 将 SwiftUI 与 RealityKit 融合 共同构建精彩的未来体验!
-
-
1:42 - Sparky in Model3D
struct ContentView: View { var body: some View { Model3D(named: "sparky") } }
-
1:52 - Sparky in Model3D with a name sign
struct ContentView: View { var body: some View { HStack { NameSign() Model3D(named: "sparky") } } }
-
struct RobotView: View { @State private var asset: Model3DAsset? var body: some View { if asset == nil { ProgressView().task { asset = try? await Model3DAsset(named: "sparky") } } } }
-
struct RobotView: View { @State private var asset: Model3DAsset? var body: some View { if asset == nil { ProgressView().task { asset = try? await Model3DAsset(named: "sparky") } } else if let asset { VStack { Model3D(asset: asset) AnimationPicker(asset: asset) } } } }
-
struct RobotView: View { @State private var asset: Model3DAsset? var body: some View { if asset == nil { ProgressView().task { asset = try? await Model3DAsset(named: "sparky") } } else if let asset { VStack { Model3D(asset: asset) AnimationPicker(asset: asset) if let animationController = asset.animationPlaybackController { RobotAnimationControls(playbackController: animationController) } } } } }
-
struct RobotAnimationControls: View { @Bindable var controller: AnimationPlaybackController var body: some View { HStack { Button(controller.isPlaying ? "Pause" : "Play") { if controller.isPlaying { controller.pause() } else { controller.resume() } } Slider( value: $controller.time, in: 0...controller.duration ).id(controller) } } }
-
5:41 - Load a Model3D using a ConfigurationCatalog
struct ConfigCatalogExample: View { @State private var configCatalog: Entity.ConfigurationCatalog? @State private var configurations = [String: String]() @State private var showConfig = false var body: some View { if let configCatalog { Model3D(from: configCatalog, configurations: configurations) .popover(isPresented: $showConfig, arrowEdge: .leading) { ConfigPicker( name: "outfits", configCatalog: configCatalog, chosenConfig: $configurations["outfits"]) } } else { ProgressView() .task { await loadConfigurationCatalog() } } } }
-
6:51 - Switching from Model3D to RealityView
struct RobotView: View { let url: URL = Bundle.main.url(forResource: "sparky", withExtension: "reality")! var body: some View { HStack { NameSign() RealityView { content in if let sparky = try? await Entity(contentsOf: url) { content.add(sparky) } } } } }
-
7:25 - Switching from Model3D to RealityView with layout behavior
struct RobotView: View { let url: URL = Bundle.main.url(forResource: "sparky", withExtension: "reality")! var body: some View { HStack { NameSign() RealityView { content in if let sparky = try? await Entity(contentsOf: url) { content.add(sparky) } } .realityViewLayoutBehavior(.fixedSize) } } }
-
8:48 - Switching from Model3D to RealityView with layout behavior and RealityKit animation
struct RobotView: View { let url: URL = Bundle.main.url(forResource: "sparky", withExtension: "reality")! var body: some View { HStack { NameSign() RealityView { content in if let sparky = try? await Entity(contentsOf: url) { content.add(sparky) sparky.playAnimation(getAnimation()) } } .realityViewLayoutBehavior(.fixedSize) } } }
-
10:34 - Add 2 particle emitters; one to each side of the robot's head
func setupSparks(robotHead: Entity) { let leftSparks = Entity() let rightSparks = Entity() robotHead.addChild(leftSparks) robotHead.addChild(rightSparks) rightSparks.components.set(sparksComponent()) leftSparks.components.set(sparksComponent()) leftSparks.transform.rotation = simd_quatf(Rotation3D( angle: .degrees(180), axis: .y)) leftSparks.transform.translation = leftEarOffset() rightSparks.transform.translation = rightEarOffset() } // Create and configure the ParticleEmitterComponent func sparksComponent() -> ParticleEmitterComponent { ... }
-
12:30 - Apply the manipulable view modifier
struct RobotView: View { let url: URL var body: some View { HStack { NameSign() Model3D(url: url) .manipulable() } } }
-
12:33 - Allow translate, 1- and 2-handed rotation, but not scaling
struct RobotView: View { let url: URL var body: some View { HStack { NameSign() Model3D(url: url) .manipulable( operations: [.translation, .primaryRotation, .secondaryRotation] ) } } }
-
12:41 - The model feels heavy with high inertia
struct RobotView: View { let url: URL var body: some View { HStack { NameSign() Model3D(url: url) .manipulable(inertia: .high) } } }
-
13:18 - Add a ManipulationComponent to an entity
RealityView { content in let sparky = await loadSparky() content.add(sparky) ManipulationComponent.configureEntity(sparky) }
-
RealityView { content in let sparky = await loadSparky() content.add(sparky) ManipulationComponent.configureEntity( sparky, hoverEffect: .spotlight(.init(color: .purple)), allowedInputTypes: .all, collisionShapes: myCollisionShapes() ) }
-
14:08 - Manipulation interaction events
public enum ManipulationEvents { /// When an interaction is about to begin on a ManipulationComponent's entity public struct WillBegin: Event { } /// When an entity's transform was updated during a ManipulationComponent public struct DidUpdateTransform: Event { } /// When an entity was released public struct WillRelease: Event { } /// When the object has reached its destination and will no longer be updated public struct WillEnd: Event { } /// When the object is directly handed off from one hand to another public struct DidHandOff: Event { } }
-
14:32 - Replace the standard sounds with custom ones
RealityView { content in let sparky = await loadSparky() content.add(sparky) var manipulation = ManipulationComponent() manipulation.audioConfiguration = .none sparky.components.set(manipulation) didHandOff = content.subscribe(to: ManipulationEvents.DidHandOff.self) { event in sparky.playAudio(handoffSound) } }
-
16:19 - Builder based attachments
struct RealityViewAttachments: View { var body: some View { RealityView { content, attachments in let bolts = await loadAndSetupBolts() if let nameSign = attachments.entity( for: "name-sign" ) { content.add(nameSign) place(nameSign, above: bolts) } content.add(bolts) } attachments: { Attachment(id: "name-sign") { NameSign("Bolts") } } .realityViewLayoutBehavior(.centered) } }
-
16:37 - Attachments created with ViewAttachmentComponent
struct AttachmentComponentAttachments: View { var body: some View { RealityView { content in let bolts = await loadAndSetupBolts() let attachment = ViewAttachmentComponent( rootView: NameSign("Bolts")) let nameSign = Entity(components: attachment) place(nameSign, above: bolts) content.add(bolts) content.add(nameSign) } .realityViewLayoutBehavior(.centered) } }
-
17:04 - Targeted to entity gesture API
struct AttachmentComponentAttachments: View { @State private var bolts = Entity() @State private var nameSign = Entity() var body: some View { RealityView { ... } .realityViewLayoutBehavior(.centered) .gesture( TapGesture() .targetedToEntity(bolts) .onEnded { value in nameSign.isEnabled.toggle() } ) } }
-
17:10 - Gestures with GestureComponent
struct AttachmentComponentAttachments: View { var body: some View { RealityView { content in let bolts = await loadAndSetupBolts() let attachment = ViewAttachmentComponent( rootView: NameSign("Bolts")) let nameSign = Entity(components: attachment) place(nameSign, above: bolts) bolts.components.set(GestureComponent( TapGesture().onEnded { nameSign.isEnabled.toggle() } )) content.add(bolts) content.add(nameSign) } .realityViewLayoutBehavior(.centered) } }
-
-
- 0:00 - 简介
了解 RealityKit SwiftUI 增强功能,这些功能可助力实现传统 UI 与交互式 3D 内容的无缝集成。主要更新包括:Model3D 和 RealityView 的增强功能、Object Manipulation API 的引入、新增组件类型、SwiftUI 和 RealityKit 之间的双向数据流、更便捷的坐标空间转换以及面向 RealityKit 组件的 SwiftUI 驱动动画效果。
- 1:24 - Model3D 增强功能
在 visionOS 26 中,Model3D 支持显示带有动画效果的 3D 模型,并可通过“ConfigurationCatalog”载入。现在,只需几行代码,即可创建交互式 3D 体验。 你可使用新的“Model3DAsset”类型来载入和控制动画效果,并通过“ConfigurationCatalog”切换实体的不同表示形式,比如更换造型或调整体型。 以一个名为“Sparky”的机器人为例,你可以为它添加动画效果、更换不同造型,并使用 SwiftUI 视图和选择器进行控制,让这个机器人能够在虚拟温室中与其他机器人互动,从而生动展示了这些新功能的实际应用。
- 6:13 - RealityView 过渡
要在运行时为 3D 模型添加粒子发射器,需要在 RealityKit 中将原本使用的“Model3D”切换为“RealityView”,因为 Model3D 不支持添加组件。 RealityView 是与模型实体一并载入的,但这会导致布局问题,因为默认情况下,它会占用所有可用空间。要解决这一问题,可同时应用“realityViewLayoutBehavior”修饰符和“fixedSize”,让 RealityView 紧贴模型的初始边界。 有三种不同的“realityViewLayoutBehavior”选项:“flexible”、“centered”和“fixedSize”,每一项都会影响 RealityView 的原点位置,但不会更改其中实体的位置。 你可借助 Reality Composer Pro 设计可在代码中配置的粒子发射器。粒子发射器可作为组件添加到实体中。RealityKit 基于“实体-组件-系统”范式。示例中通过添加粒子发射器来实现火花特效,使用了不可见实体作为火花的容器。 RealityView 非常适合对 3D 内容行为进行细致创建和精细控制,而 Model3D 则更适合显示独立的 3D 素材。你可以根据实际需求,在两者之间灵活切换,实现无缝过渡。
- 11:52 - 对象操控
visionOS 26 中新增的 Object Manipulation API 让用户能够在使用 SwiftUI 和 RealityKit 的 App 中与虚拟物体进行交互。用户可以用手移动、旋转和缩放物体,甚至可以在双手之间传递物体。 对于 SwiftUI 视图,你可以应用“manipulable”修饰符,以实现对可执行操作和物体惯性的个性化配置。在 RealityKit 中,可通过静态“configureEntity”函数将“ManipulationComponent”添加到实体中,该函数还添加了碰撞、输入目标和悬停效果组件。这个函数提供多个参数,用于自定行为。 你可以订阅在交互过程中触发的“ManipulationEvents”,例如启动、停止、更新、发布和传递等事件,以更新 App 状态并自定用户体验,比如将默认的声音替换为自定音频资源。
- 15:35 - SwiftUI 组件
全新 SwiftUI RealityKit 组件可增强用户在 RealityKit 场景中的交互和关联体验。这些组件让你能够将 SwiftUI 视图无缝集成到 3D 环境中。 主要功能包括:“ViewAttachmentComponent”,支持直接将 SwiftUI 视图添加到实体;“GestureComponent”,使实体能够响应用户的触摸和手势操作;以及“PresentationComponent”,支持在场景中呈现弹出窗口等 SwiftUI 视图。“PresentationComponent”上的配置参数将定义要显示的呈现内容类型。 这些改进将简化开发流程,助你打造更生动且更富吸引力的用户体验。在本例中,名为“Bolts”的机器人可以拥有一个通过轻点手势开关的名字标牌,用户可从弹出菜单中选择 Bolts 的造型,所有这些操作都将在 RealityKit 沉浸式环境中完成。
- 19:08 - 信息流
现在,在 visionOS 26 中,实体都具备可观察性,也就是说,当某个实体 (比如机器人“Bolts”) 的属性发生变化时,它可以自动通知其他代码。当 Bolts 在温室中为植物浇水时,该功能可追踪机器人的移动轨迹,这一点非常实用。 本示例使用 SwiftUI 来实现小地图,其中会实时显示 Bolts 的位置。通过访问 Bolts 的可观察位置属性,每当 Bolts 移动时,SwiftUI 都会自动更新小地图。这种借助可观察实体实现的 SwiftUI 与 RealityKit 之间的双向数据流,是一项具有重要意义的新工具。 这项全新推出的可观察功能虽然强大,但如果管理不当,可能会引发无限循环的问题。为了避免无限循环,请注意你修改观察状态的位置。不要在“RealityView”的“update”闭包中更改观察状态,因为这可能会触发递归更新周期。建议在特定位置进行修改,例如自定系统、手势闭包或“make”闭包,这些都不在 SwiftUI 视图主体的评估范围内。 通过将大型视图分解为较小的独立视图,并谨慎选择状态的修改位置,你可以确保 SwiftUI 和 RealityKit 之间顺畅高效的数据流,同时避免无限循环并提高应用程序的整体性能。
- 24:56 - 统一坐标系转换
新的 Unified Coordinate Conversion API 弥合了 RealityKit 和 SwiftUI 之间的差距。通过实现“CoordinateSpace3D”协议,你可以在两个框架之间无缝转换值。这样就可以计算 Bolts (RealityKit 中的实体) 和 Sparky (SwiftUI 中的 Model3D) 之间的绝对距离,当它们彼此靠近时,会根据它们之间的距离动态生成火花效果。
- 27:01 - 动画
在 visionOS 26 中,你现在可以利用 SwiftUI 的动画 API 对 RealityKit 组件的变化进行隐式动画处理。你只需在动画块中设置支持的组件,即可实现实体的平滑且富有弹性的移动效果。 有两种方法可以实现这一点:在 RealityView 中使用“content.animate()”或直接调用“Entity.animate()”。这种集成支持在操纵实体时实现自定的释放行为,使用户与 3D 对象之间的交互更加生动有趣。Transform、音频、模型和光照等各种 RealityKit 组件都支持这些隐式动画。
- 29:41 - 后续步骤
利用 SwiftUI 和 RealityKit 之间的这种新关联,你可以将 SwiftUI 组件与 RealityKit 场景相集成,从而打造出创新的空间 App,实现虚拟世界与现实世界的动态交互,激发 App 开发的无限可能。