大多数浏览器和
Developer App 均支持流媒体播放。
-
深入探究空间容器和沉浸式空间
探索可通过哪些强大的新方式在 visionOS 中自定空间容器和沉浸式空间。了解如何微调空间容器自行调整大小并根据周围用户移动做出响应的方式。利用强大的坐标转换功能,让空间容器和沉浸式空间能够进行交互。了解如何让你的 App 在用户通过数码旋钮调整沉浸度时做出响应,以及使用环绕效果动态自定沉浸式空间体验中的透视色调。
章节
- 0:00 - Introduction
- 2:04 - Volumes
- 2:06 - Volumes: Baseplate
- 4:08 - Volumes: Size
- 6:59 - Volumes: Toolbars
- 8:48 - Volumes: Ornaments
- 11:36 - Volumes: Viewpoints
- 15:34 - Volumes: World alignment
- 16:52 - Volumes: Dynamic scale
- 18:26 - Intermezzo
- 18:42 - Immersive spaces
- 19:38 - Immersive spaces: Coordinate conversions
- 22:40 - Immersive spaces: Immersion styles
- 26:08 - Immersive spaces: Anchored UI interactions
- 29:03 - Immersive spaces: Surroundings effects
- 31:21 - Next steps
资源
相关视频
WWDC24
WWDC23
-
下载
大家好!我叫 Owen 是一名 SwiftUI 工程师 我叫 Troy 也是一名 SwiftUI 工程师 在这个视频中 我们将深入探讨 如何使用 visionOS 空间容器 和沉浸式空间中的 3D 内容
visionOS 上的场景分为三种类型 窗口、空间容器和沉浸式空间 你可以搭配使用所有三类场景 打造独一无二的精彩体验 今天 我们将重点介绍 空间容器和沉浸式空间 这两类场景是 visionOS 独有的 可用于制作丰富的沉浸式 3D 内容 使用这些场景的视体 App 是 Apple Vision Pro 最令人兴奋的 独家特色之一 它们为 App 和游戏开辟了全新维度 让用户能够体验到你的 App 与现实世界的交融
visionOS 上已经有了许多 令人愉悦的空间体验 这些 App 充分利用三维的优势 以巧妙方式显示信息 提供有趣好玩的互动 既可以模拟现实世界 也可以带来全新体验
这些 App 充分利用了 专为 visionOS 构建的空间 API 而现在 visionOS 2 中 新增了许多功能 可以为你的视体 App 带来 更加生动鲜活的体验 我和 Troy 将介绍 如何构建一款名为“BOT-anist”的 全新视体 App 我将首先添加一个空间容器 然后在这基础上使用新的 API 进行构建 让这个 App 大放异彩
之后 Troy 会利用沉浸式空间 进行扩展 让 App 内容填充房间
我们从空间容器开始吧! 创建新的视体 App 或是将现有 App 与 visionOS 2 SDK 集成时 首先要注意的 是新的底板
当用户注视时 它会自动显示 从而突显空间容器的底部边缘
下面介绍底板在我的 全新空间容器中的实际应用 它会以温和的方式 引导我找到空间容器的边缘 因此 我可以立即知道 自己所使用的空间有多大
底板非常适合 位于空间容器内部 但不会填满边界的内容 底板让用户能够感知 空间容器的边缘 即使内容占据的内部空间较少 也适用 但是 如果你的 App 内容 已经顶到了空间容器的边界 或者 如果你已经绘制了自己的表面 那么最好停用底板 以免与你的 App 冲突 并且最好让内容本身 引导用户找到边缘
在 visionOS 2 中 底板 在默认情况下处于启用状态 并可通过空间容器 底板可见性修饰符进行控制 visionOS 2 中的 automatic 行为 会在用户注视底板时淡入底板 这与使用 visible 写入修饰符等效 你也可以将底板设置为 hidden 从而显式关闭底板
从底板开始 能让我即刻感知空间容器的边界 即使没有任何附加内容也无妨
现在 在我为“BOT-anist”App 添加圆形关卡时 底板可以帮助我找到 空间容器中窗口控件所在的 边缘和角落 这一点在 visionOS 2 中 尤其重要 因为空间容器的角落处 现在有了新的尺寸调整控制柄 就像窗口一样
请注意底板如何引导我找到 角落处的尺寸调整控制柄 但是当我尝试调整场景大小时 它会反弹回原来的大小 这是怎么回事? 默认情况下 空间容器尺寸的 最小值和最大值 继承自它的内容的大小 在 SwiftUI 中 这种行为 由 windowResizability 修饰符 通过 contentSize 行为提供 空间容器的 automatic 行为就像 在空间容器上编写了这个修饰符一样 这意味着空间容器尺寸的最小值 和最大值都将取决于它的视图大小
在 ExplorationView 上 我写了一个 具有特定宽度、高度和深度的框架 因此 空间容器具有与这个视图框架 相匹配的固定大小
如果我改为指定框架的最小值 则视图会指出可以将框架尺寸调大 因为空间容器会继承它的内容的大小 所以它现在也支持调整大小
如果指定尺寸最大值 则空间容器的大小也会以它为上限
现在 当我拖移尺寸调整控制柄时 空间容器的大小会平滑地变化
真不错!
这一行为意味着 你还可以 通过代码调整空间容器的大小 随着 App 更新其中内容 视图框架发生任何更改时 也会将它的尺寸告知空间容器 这意味着你可以轻松适应 不断变化的内容 而无需担心在场景边缘处发生裁剪
而且由于用户会在 靠近内容的角落处 寻找尺寸调整控制柄 这样也可以确保这些控件 永远不会离用户太远
为了以编程方式更改空间容器的尺寸 我为这一缩放比例添加了 一个新的状态变量
当我更改缩放比例变量的值时 它会更新视图框架的值 然后空间容器会自动调整尺寸 以适应框架的新尺寸
我还按照这一尺寸调整量 对圆形探索关卡的 RealityKit 实体进行了缩放 现在 我需要一个控件 来更改缩放比例
我添加了一个用于切换尺寸的按钮 暂时只是将它放置在覆盖图层中
现在 当我按下这个按钮时 关卡会在大小两种尺寸之间切换 空间容器也会相应调整自身边界 但是 这个按钮本身 在这里看起来有点格格不入 它非常适合放在工具栏中
对于空间容器 你可以在它下方的 装饰元素中设置悬浮显示的工具栏 通过这种简单而有效的方式 可以提供常用 App 控件的集合
这个工具栏会随着空间容器的 移动而自动缩放 所以无论将空间容器放置在什么位置 仍然易于查看其中内容 像这样的工具栏是将常用 App 控件 分组到一起的最佳方式 所以我要在 App 中添加一个工具栏
为了将控件放入这个工具栏 我在 App 的一个视图上 编写了一个工具栏修饰符
在工具栏中 我可以创建 工具栏项目和工具栏项目组
我为每个项目指定了 一个 .bottomOrnament 为了兼容 visionOS 1 这一步必不可少 但在 visionOS 2 中 自动放置功能也可以处理 底部装饰元素 如果 App 仅支持 visionOS 2 则可以省略这个参数
我在工具栏项目内添加了按钮 用于在游戏中执行不同的操作
这样一来 我的控件 就显示在了工具栏中!
visionOS 2 还新增了一项功能 工具栏会自动移到空间容器中 我的站立位置所在的一面 窗口控件也会一起移过去 这样可以鼓励用户体验新的视角 同时仍然能够确保所有工具 都正好在所需的位置上 现在 你可以在工具栏旁边 为空间容器添加额外的装饰元素 装饰元素非常适合 用来提供额外的控制选项 或是为当前内容提供更多详细信息
它们可以在 App 窗口上方和四周 悬浮显示辅助信息 从而有助于减少主窗口杂乱现象 窗口可在任意位置添加装饰元素 不仅仅是工具栏 还可以在窗口四周的任意位置添加 现在 空间容器也具有 一模一样的功能 包括能够控制装饰元素 在空间容器上放置的深度 装饰元素还可以动态缩放 从而能够在空间容器移到更远处时 确保尺寸始终合适
装饰元素提供了极大的灵活性 但也不要过度使用 这一点很重要
在空间容器四周放置许多装饰元素 可能会喧宾夺主 埋没你的精彩内容 而内容才是 App 的灵魂 单个装饰元素 可为一组控件和信息打造出色的容器 此外 系统自带的一些控件 例如工具栏和标签视图 也属于装饰元素 所以需要确保自定装饰元素 不会与这些控件冲突
在这个 App 中 我添加了这个视图来显示 玩家种植目标的完成进度 现在 这个视图位于 空间容器的主视图中
这意味着 当我围绕空间容器走动时 这个视图不会更新 而且 如果让空间容器离我远一点 视图就会变得越来越小 越难看清 如果能够将这个视图拿出来 放到装饰元素中 那就更好了 这样我的 App 就能获得那些自动行为 在这里 视图位于空间容器的正文内
为了将视图放入装饰元素 我将它移到装饰元素修饰符中
我提供了一个场景锚点 其中包含了一个 UnitPoint3D 用于根据空间容器的 宽度、高度和深度 来放置装饰元素 在本例中 topBack 放置方法 会将装饰元素放置在主关卡 上方靠后的居中位置
我们在 App 中试试看
就在这里 悬浮在主关卡后方 和工具栏一样 所有装饰元素也会 跟着你围绕空间容器移动 从而确保用户在任何方向上 都能轻松访问 装饰元素在场景中的位置 始终是相对于我的视线出发点 在容器上的那一面来确定的
空间容器的每一面都是一个视点 当你围绕空间容器走动时 窗口控件和装饰元素 会自动移到离你最近的视点
系统会根据当前的视点 自动更新装饰元素的位置 现在 我还添加了我的机器人小伙伴 它面朝正面 不知道我的位置在哪里 如果在我围绕空间容器走动时 这个机器人也能面朝我 就会让我的 App 更显生动
首先 为了获取视点的更新 我添加了一个新的修饰符 onVolumeViewpointChange 每当处于活动状态的视点更新时 系统都会调用这个修饰符 我使用这个修饰符在 appState 中 设置了可追踪活动视点的变量 当机器人更新时 它会使用这个值在现实场景中移动 并面朝当前视点
我使用了视点的 squareAzimuth 值 这个类型会将空间容器周围的位置 归一化为四个值中的一个 四个值分别表示空间容器的四个面
SquareAzimuth 的四个面 提供了 front、left、right 和 back 语义值 这些语义值还包含了 特定的 Rotation3D 值 这个值可以作为旋转 直接应用到视图和实体上
在处理机器人动作的代码中 我添加了一些代码 来让机器人在处于空闲模式时 转向这个位置 然后触发 挥手小动画
现在 机器人转身面向我
还向我挥了挥手! 这样一来 App 就变得更加生动鲜活 给人以充满活力的感觉
但是 并非所有 App 都需要支持每一个视点 我想将视点限制为正面和侧面
为了指定支持的视点 我要使用另一个新的视图修饰符 supportedVolumeViewpoints 默认情况下 App 支持所有视点
在我的示例中 我只想支持 空间容器的正面和侧面 而不想支持背面
为此 我传入了一个包含 front、 left 和 right 值的选项集
现在 当我走到空间容器背面时 装饰元素和窗口控件 不会跟着移到背面 现在 机器人也停下来了 由于到目前为止 机器人 一直对我的动作有反应 我觉得 它应该让我知道 我走到了错误的一面
我希望调用空间容器视点变化代码块 即使新值不在支持的视点集范围内 也仍然可以调用 于是 我为视点更新策略 添加了新的参数 通过指定为 all 我可以获得 所有视点的更新 包括不受支持的视点 我要检查一下新值 是否在支持的视点集范围内 如果不在 我就会为机器人 触发新的动画效果 提示我应该向后移动到 受支持的视点之一
现在 当我走到空间容器背面时 所有装饰元素都会 停在最后一个受支持的面上
机器人会生我的气 要我走回去
好了 这样就对了!
还有几个新选项 这些选项用于控制空间容器自身 在现实场景中的呈现方式 其中第一个选项是 world alignment 这个选项可以控制空间容器 是否始终与重力方向对齐 让它的底部与地面保持平行 或者在空间容器被抬起时 将自己向下倾斜 在大多数情况下 自适应倾斜行为 会让人感觉最舒适 这是 visionOS 2 中的默认选项 空间容器一开始与地面平行 但是在被抬起到地平线上方时 会自己开始倾斜
这样可以确保 空间容器的内容始终可用 即使空间容器处于倾斜位置 它的内容也可以使用 对于交互式内容来说 这样可以大大提升舒适感
但是 一些视体 App 不需要进行很多交互 或者旨在提供侧重于 营造环境氛围的内容
在这些情况下 与重力方向对齐的行为效果更好
volumeWorldAlignment 修饰符 允许覆盖自适应对齐 让空间容器始终与地面对齐
此外 空间容器现在 还可以进行动态缩放
如果 visionOS 中的窗口 在现实场景中发生了移动 它的缩放比例就会相应变化 当窗口远离用户时 它会放大 从而保持在你视野中的大小 这项功能非常实用 因为窗口往往包含文本和控件 这些内容如果离得比较远 就会难以辨认 不方便使用
这种行为与空间容器上的 工具栏和装饰元素相同
另一方面 空间容器本身 默认使用固定缩放比例 这有助于增强空间容器 在现实场景中的存在感
当空间容器远离用户时 它仍会保持固定的尺寸 所以空间容器的内容 从远处看起来比较小
对于许多视体 App 来说 这种视觉效果很棒 因为虚拟内容可以 在房间里得到直观呈现 仿佛就在你身边
但是 如果你的视体 App 体验 依赖于密集内容 并且具有许多个不同的 交互式区域 那么也可以利用动态缩放 来优化用户体验
要让空间容器支持动态缩放 请使用新的场景修饰符 名为 defaultWorldScalingBehavior 由于“BOT-anist”是一款交互式游戏 因此让关卡动态缩放 是比较合理的做法 所以我使用 .dynamic 选项 启用了这个行为
我们开了个好头 在空间容器中有了一款好玩的 App 关于空间容器 你已经 介绍了很多超棒的内容 好的 接下来呢? 我要利用沉浸式空间 让机器人从 App 走进现实场景中 听起来真是了不得
众多开发者正在利用沉浸式空间 在 Apple Vision Pro 上 打造丰富多彩的体验 我非常喜欢 Owen 进行的改进 让我们的植物学家 能够在共享空间中 更好地探索空间容器 接下来 我要在窗口之上更进一步 打造内容丰富的沉浸式体验 让温室能够填充整个房间
我要做的第一件事 是打造沉浸式空间本身 Xcode 的 New Project 对话框中 提供了一些适用的配置选项 但在这个案例中 我要自己添加
我有了一个沉浸式空间 但它现在空空如也
打开这个沉浸式空间后 我要将来自空间容器的 所有 RealityKit 内容 全部移植到沉浸式空间中 移植过程应该会非常顺畅 当植物学家 能够开始探索现实世界时 会让人感到非常惊喜
visionOS 1.1 推出了一个 专门用于实现这项功能的 坐标空间 名叫 Immersive Space
坐标空间是一种 用于以特定框架为参照 精确指出相对位置的工具
全新沉浸式坐标空间 可与 SwiftUI 现有的 局部和全局坐标空间协调搭配
局部是指当前视图的坐标空间 原点位于视图的左上角
全局是指窗口的坐标空间 原点位于窗口的左上角
对于空间容器 原点位于左上后角
沉浸式空间位于全局空间上方 原点定义为沉浸式空间 处于打开状态时 你脚下地面上的点
我要将坐标空间 与 RealityView 搭配使用 来实现转换为沉浸式空间的操作 RealityView 提供了 多种 convert 函数 可供我在 RealityKit 与 SwiftUI 坐标空间之间转换 首先 我要将机器人的变换 从空间容器的 RealityKit 场景空间转换为 SwiftUI 沉浸式空间 在 RealityView 的更新闭包中 调出第一个 convert 函数
在这里 我将机器人的变换 从空间容器的 局部 RealityKit 场景空间转换为 SwiftUI 的沉浸式空间 然后 将转换后的变换 储存在 App 模型中供以后使用
然后重新指定机器人的父项 从空间容器改为 沉浸式空间的根实体 为机器人重新指定父项 并计算出从空间容器中 将机器人取出所需的变换后 把来自空间容器视图的转换 标记为已完成 我要在沉浸式空间视图中 继续进行转换
在沉浸式空间视图中 调用另一个 convert 函数
在那里计算从 SwiftUI 沉浸式空间 到 RealityKit 场景空间 所需的变换 然后合成这两个变换 为此 我将刚刚计算出的变换 与刚才储存在 App 模型上的变换相乘 结果是从空间容器中的 局部坐标空间 转换为沉浸式空间中的局部坐标空间 我更新了机器人的变换 以便让机器人 在沉浸式空间中的放置位置 与它在空间容器中出现的位置相匹配
机器人的变换完成转换后 我让它跳起来
现在 转换可以实际应用了! 借助坐标转换 API 的强大功能 机器人能够从空间容器中跳出来 跳入沉浸式体验的世界中 稳稳落地!
接下来 我要为“BOT-anist”App 选择一种沉浸样式
默认情况下 沉浸式体验 一开始会采用 mixed 样式 以四周环境为背景显示 App progressive 样式在透视 与完全沉浸之间架起了一座桥梁 使用径向传送门来显示 App 同时可在传送门周围采用透视 在 full 样式中 沉浸式 App 会完全取代四周环境 我要选择 progressive 样式
它非常适合用于 让植物学家探索世界
在“BOT-anist”App 中采用 progressive 样式时 默认情况下传送门的初始尺寸 会占据玩家视野中 大约一半的区域 系统还会通过提供固定的 最小和最大沉浸程度 来定义支持的沉浸范围
我需要让“BOT-anist”App 从一开始就更具沉浸感 visionOS 2 还有一项新功能 支持采用自定沉浸程度来实现这一点 这样一来 我就可以转动数码旋钮 调低或调高沉浸程度 以展示植物学家 从空间容器中跳出来 进入沉浸式空间的动作 我们来深入了解一下 这个新的 API
首先使用新的构造器 来创建 progressive 沉浸样式 采用自定沉浸范围 以及一个表示初始沉浸程度的值 将 progressive 样式 应用于沉浸式空间时 系统将使用提供的值 来定义应用于场景的 progressive 效果的最小值、 最大值和初始值
我需要为“BOT-anist”App 营造沉浸式体验 以增强开始游戏时的沉浸感 所以 我选择了 80% 的初始沉浸强度 对于“BOT-anist”App 的 自定沉浸范围 我指定最小值为 20% 最大值为 100% 相当于完全沉浸式体验
我们来看看 自定沉浸程度的实际应用 增强开始游戏时的沉浸感 非常有助于突显 从空间容器中跳出来的植物学家 我所指定的整个范围 对应的效果都很棒 我可以转动数码旋钮 来调整我的体验
接下来 在玩家转动数码旋钮查看 受支持的各种沉浸程度时 我想让植物学家做出响应
使用 onImmersionChange 修饰符 来响应沉浸程度的变化 这会提供一个 context 值 其中包含新的沉浸程度
当沉浸程度变化时 我会读取 所提供的 context 中的值 对于“BOT-anist” 我会储存这个值 以便比较变化前后的值
我使用 onChange 来处理 储存的沉浸程度的变化 从闭包中提取新值和旧值进行传递
为了让机器人响应沉浸程度的变化 调用一个函数来触发 机器人在沉浸程度提高时 向外移动
还要调用一个函数来触发 机器人在沉浸程度降低时 向内移动 我们来试试看! 现在 植物学家会随着沉浸程度的 变化而做出响应 它会在我提高沉浸程度时靠近我 在我调低沉浸程度时远离我
我们来转动数码旋钮 重新调高沉浸程度 现在 植物学家会在 沉浸程度提高时跑出来 探索扩展的空间 但目前这个环境中的植被有点稀疏
为了解决这个问题 我将让地面 支持轻点操作 以便相对于环境的地面放置植物 供植物学家进行探索 为了将植物放置在地面上的特定位置 我需要相对于地面锚点来放置植物 为此 我需要能够使用 锚点的 3D 位置
你可以借助 RealityKit 中全新的 Spatial Tracking Session API 为 App 提供 对锚点 3D 位置的访问权限 以便玩家能够授权 他们想要追踪的锚点功能
为了使用这个 API 我需要 先创建一个空间追踪会话
然后 创建一个任务 在沉浸式空间打开时 调用一个函数来运行空间追踪会话
为了运行这个会话 我首先设置了 用于平面锚点追踪的配置
然后使用这个配置运行会话 以提示玩家授权平面锚点变换
现在 我已经注册了平面追踪 我需要将锚点添加到追踪
为目标平面指定水平对齐 并通过地面分类创建 要追踪的地面锚点实体
然后将地面锚点 添加到沉浸式空间中的 RealityView 内容 最后 我需要使用新锚点的 3D 位置 在房间内放置植物
我添加了 SpatialTapGesture 来检测用户在沉浸式空间中 针对目标实体的轻点操作
当手势结束时 我会将 gesture 值传递给一个函数 来处理轻点操作
为了处理轻点操作 我首先在 gesture 值上 使用 convert 函数来获取手势 相对于地面锚点的位置 这个步骤需要 App 能够获得 地面锚点的变换
最后 我可以通过将植物添加为 地面锚点的子项 并使用转换后的位置来设置 植物实体的位置 来摆放植物
现在 我要从列表中选择一种植物 地面上的悬停效果 指示了将要放置植物的位置 只需轻点一下 就能放置一株植物! 我很喜欢在房间内摆放植物的效果 这让“BOT-anist”App 更生动出彩
如需更深入地了解 Spatial Tracking Session API 请观看标题为 “使用 RealityKit 构建空间绘画 App”的讲座
沉浸式温室体验真正开始成形了
看看当植物学家看望这些植物时 植物长势喜人的 庆祝动画效果
我想为庆祝方式增加一点花样 目前 我们环境中的每株植物 都放在花盆中 而每个花盆 都与一种色调相关联 使用与花盆色调相匹配的颜色 对透视画面进行着色 是为庆祝方式增加花样的好办法
preferredSurroundingsEffect API 可用于对周围环境透视画面 进行着色 我要更新“BOT-anist”App 的 沉浸式体验 用正在进行庆祝的植物 所在花盆的色调 对透视画面进行着色
首先 挑选与花盆相配的色调
为自定 PlantComponent 添加 tintColor 属性 然后切换 plantType 选择色调 例如 为咖啡浆果选择浅蓝色
为了触发色调效果 我需要检测植物学家 什么时候位于花盆附近 RealityKit 的碰撞检测功能 非常适合这项任务
处理机器人随时间变化的运动时 我使用 collision 闭包 来处理相撞的实体 然后将 collision 值 传递给辅助函数
要进一步了解 RealityKit 的 碰撞检测功能 请观看“开发你的 第一款沉浸式 App”
在辅助函数中 先检查植物学家有没有 与植物发生碰撞 如果没有 则直接返回
如果发生了碰撞 则播放庆祝动画效果
最后 如果我位于沉浸式空间中 就将当前的色调 储存在 App 模型上 供以后使用
回到沉浸式视图 根据储存的当前色调 创建 SurroundingsEffect.colorMultiply 然后使用这个四周环境效果 对透视画面进行着色
我们来看看 更新后的庆祝色彩效果 现在 当植物学家看望植物时 对透视画面进行着色
虞美人对应洋红色
丝兰对应浅绿色
咖啡浆果对应浅蓝色 植物学家 干得漂亮!
在这个讲座中 我们介绍了许多 在 App 中构建空间容器 和沉浸式空间的方法 试试新的尺寸调整行为 对 App 中 空间容器的尺寸调整方法进行微调 使用视点 让 App 能够响应用户围绕空间容器 移动的动作 借助坐标转换的强大功能 冲出空间容器 进入沉浸式空间 响应沉浸式空间中沉浸程度的变化 空间 App 开辟了全新疆界 带给人们从前做梦都 想不到的神奇体验 借助 SwiftUI 和 RealityKit 强大而富有表现力的工具 你可以创造无限可能 再加上一点奇思妙想 就能做出令人惊叹的东西 谢谢大家!
-
-
3:09 - Baseplate
// Baseplate WindowGroup(id: "RobotExploration") { ExplorationView() .volumeBaseplateVisibility(.visible) // Default! } .windowStyle(.volumetric)
-
4:29 - Enabling resizability
// Enabling resizability WindowGroup(id: "RobotExploration") { let initialSize = Size3D(width: 900, height: 500, depth: 900) ExplorationView() .frame(minWidth: initialSize.width, maxWidth: initialSize.width * 2, minHeight: initialSize.height, maxHeight: initialSize.height * 2) .frame(minDepth: initialSize.depth, maxDepth: initialSize.depth * 2) } .windowStyle(.volumetric) .windowResizability(.contentSize) // Default!
-
6:10 - Programmatic resize
// Programmatic resize struct ExplorationView: View { @State private var levelScale: Double = 1.0 var body: some View { RealityView { content in // Level code here } update: { content in appState.explorationLevel?.setScale( [levelScale, levelScale, levelScale], relativeTo: nil) } .frame(width: levelSize.value.width * levelScale, height: levelSize.value.height * levelScale) .frame(depth: levelSize.value.depth * levelScale) .overlay { Button("Change Size") { levelScale = levelScale == 1.0 ? 2.0 : 1.0 } } } }
-
7:39 - Toolbar ornament
// Toolbar ornament ExplorationView() .toolbar { ToolbarItem { Button("Next Size") { levelScale = levelScale == 1.0 ? 2.0 : 1.0 } } ToolbarItemGroup { Button("Replay") { resetExploration() } Button("Exit Game") { exitExploration() openWindow(id: "RobotCreation") } } }
-
10:41 - Ornaments
// Ornaments WindowGroup(id: "RobotExploration") { ExplorationView() .ornament(attachmentAnchor: .scene(.topBack)) { ProgressView() } } .windowStyle(.volumetric)
-
12:08 - Volume viewpoint
// Volume viewpoint struct ExplorationView: View { var body: some View { RealityView { content in // Some RealityKit code } .onVolumeViewpointChange { oldValue, newValue in appState.robot?.currentViewpoint = newValue.squareAzimuth } } }
-
13:06 - Using volume viewpoint
// Volume viewpoint class RobotCharacter { func handleMovement(deltaTime: Float) { if self.robotState == .idle { characterModel.performRotation(toFace: self.currentViewpoint, duration: 0.5) self.animationState.transition(to: .wave) } else { // Handle normal movement } } }
-
13:43 - Supported viewpoints
// Supported viewpoints struct ExplorationView: View { let supportedViewpoints: Viewpoint3D.SquareAzimuth.Set = [.front, .left, .right] var body: some View { RealityView { content in // Some RealityKit code } .supportedVolumeViewpoints(supportedViewpoints) .onVolumeViewpointChange { _, newValue in appState.robot?.currentViewpoint = newValue.squareAzimuth } } }
-
14:30 - Viewpoint update strategy
// Viewpoint update strategy struct ExplorationView: View { let supportedViewpoints: Viewpoint3D.SquareAzimuth.Set = [.front, .left, .right] var body: some View { RealityView { content in // Some RealityKit code } .supportedVolumeViewpoints(supportedViewpoints) .onVolumeViewpointChange(updateStrategy: .all) { _, newValue in appState.robot?.currentViewpoint = newValue.squareAzimuth if !supportedViewpoints.contains(newValue) { appState.robot?.animationState.transition(to: .annoyed) } } } }
-
16:42 - World alignment
// World alignment WindowGroup { ExplorationView() .volumeWorldAlignment(.gravityAligned) } .windowStyle(.volumetric)
-
18:05 - Dynamic scale
// Dynamic scale WindowGroup { ContentView() } .windowStyle(.volumetric) .defaultWorldScalingBehavior(.dynamic)
-
19:16 - Starting with an empty immersive space
struct BotanistApp: App { var body: some Scene { // Volume WindowGroup(id: "Exploration") { VolumeExplorationView() } .windowStyle(.volumetric) // Immersive Space ImmersiveSpace(id: "Immersive") { EmptyView() } } }
-
20:52 - Callout to convert function from volume view
// Coordinate conversions // Convert from RealityKit entity in volume to SwiftUI space struct VolumeExplorationView: View { @Environment(ImmersiveSpaceAppModel.self) var appModel var body: some View { RealityView { content in content.add(appModel.volumeRoot) // ... } update: { content in guard appModel.convertingRobotFromVolume else { return } // Convert the robot transform from RealityKit scene space for // the volume to SwiftUI immersive space convertRobotFromRealityKitToImmersiveSpace(content: content) } } }
-
21:08 - Convert robot's transform to SwiftUI immersive space
// Coordinate conversions // Convert from RealityKit entity in volume to SwiftUI space func convertRobotFromRealityKitToImmersiveSpace(content: RealityViewContent) { // Convert the robot transform from RealityKit scene space for // the volume to SwiftUI immersive space appModel.immersiveSpaceFromRobot = content.transform(from: appModel.robot, to: .immersiveSpace) // Reparent robot from volume to immersive space appModel.robot.setParent(appModel.immersiveSpaceRoot) // Handoff to immersive space view to continue conversions. appModel.convertingRobotFromVolume = false appModel.convertingRobotToImmersiveSpace = true }
-
21:42 - Callout to convert function from immersive space view
// Coordinate conversions // Convert from SwiftUI immersive space back to RealityKit local space struct ImmersiveExplorationView: View { @Environment(ImmersiveSpaceAppModel.self) var appModel var body: some View { RealityView { content in content.add(appModel.immersiveSpaceRoot) } update: { content in guard appModel.convertingRobotToImmersiveSpace else { return } // Convert the robot transform from SwiftUI space for the immersive // space to RealityKit scene space convertRobotFromSwiftUIToRealityKitSpace(content: content) } } }
-
21:48 - Compute transform to place robot in matching position in immersive space
// Coordinate conversions // Calculate transform from SwiftUI to RealityKit scene space func convertRobotFromSwiftUIToRealityKitSpace(content: RealityViewContent) { // Calculate transform from SwiftUI immersive space to RealityKit // scene space let realityKitSceneFromImmersiveSpace = content.transform(from: .immersiveSpace, to: .scene) // Multiply with the robot's transform in SwiftUI immersive space to build a // transformation which converts from the robot's local // coordinate space in the volume and ends with the robot's local // coordinate space in an immersive space. let realityKitSceneFromRobot = realityKitSceneFromImmersiveSpace * appModel.immersiveSpaceFromRobot // Place the robot in the immersive space to match where it // appeared in the volume appModel.robot.transform = Transform(realityKitSceneFromRobot) // Start the jump! appModel.startJump() }
-
23:54 - Customizing immersion
// Customizing immersion struct BotanistApp: App { // Custom immersion amounts @State private var immersionStyle: ImmersionStyle = .progressive(0.2...1.0, initialAmount: 0.8) var body: some Scene { // Immersive Space ImmersiveSpace(id: "ImmersiveSpace") { ImmersiveSpaceExplorationView() } .immersionStyle(selection: $immersionStyle, in: .mixed, .progressive, .full) } }
-
25:17 - Callout to function to handle immersion amount changed
// Reacting to immersion struct ImmersiveView: View { @State var immersionAmount: Double? var body: some View { ImmersiveSpaceExplorationView() .onImmersionChange { context in immersionAmount = context.amount } .onChange(of: immersionAmount) { oldValue, newValue in handleImmersionAmountChanged(newValue: newValue, oldValue: oldValue) } } }
-
25:39 - Handle function to make robot react to changed immersion amount
// Reacting to immersion func handleImmersionAmountChanged(newValue: Double?, oldValue: Double?) { guard let newValue, let oldValue else { return } if newValue > oldValue { // Move the robot outward to react to increasing immersion moveRobotOutward() } else if newValue < oldValue { // Move the robot inward to react to decreasing immersion moveRobotInward() } }
-
26:57 - Create spatial tracking session
// Create and run spatial tracking session struct ImmersiveExplorationView { @State var spatialTrackingSession: SpatialTrackingSession = SpatialTrackingSession() var body: some View { RealityView { content in // ... } .task { await runSpatialTrackingSession() } } }
-
27:11 - Run spatial tracking session
// Create and run the spatial tracking session func runSpatialTrackingSession() async { // Configure the session for plane anchor tracking let configuration = SpatialTrackingSession.Configuration(tracking: [.plane]) // Run the session to request plane anchor transforms let _ = await spatialTrackingSession.run(configuration) }
-
27:32 - Create a floor anchor to track
// Create a floor anchor to track struct ImmersiveExplorationView { @State var spatialTrackingSession: SpatialTrackingSession = SpatialTrackingSession() let floorAnchor = AnchorEntity( .plane(.horizontal, classification: .floor, minimumBounds: .init(x: 0.01, y: 0.01)) ) var body: some View { RealityView { content in content.add(floorAnchor) } .task { await runSpatialTrackingSession() } } }
-
27:54 - Detect taps on entities in immersive space
// Detect taps on entities in immersive space RealityView { content in // ... } .gesture( SpatialTapGesture( coordinateSpace: .immersiveSpace ) .targetedToAnyEntity() .onEnded { value in handleTapOnFloor(value: value) } )
-
28:09 - Handle tap event to place plant
// Handle tap event func handleTapOnFloor(value: EntityTargetValue<SpatialTapGesture.Value>) { let location = value.convert(value.location3D, from: .immersiveSpace, to: floorAnchor) plantEntity.position = location floorAnchor.addChild(plantEntity) }
-
29:47 - Add tint color to custom plant component
// Add tint color to custom plant component struct PlantComponent: Component { var tintColor: Color { switch plantType { case .coffeeBerry: // Light blue return Color(red: 0.3, green: 0.3, blue: 1.0) case .poppy: // Magenta return Color(red: 1.0, green: 0.0, blue: 1.0) case .yucca: // Light green return Color(red: 0.2, green: 1.0, blue: 0.2) } } }
-
30:09 - Handle collisions with robot
// Handle collisions with robot // // Handle movement of the robot between frames func handleMovement(deltaTime: Float) { // Move character in the collision world appModel.robot.moveCharacter(by: SIMD3<Float>(...), deltaTime: deltaTime, relativeTo: nil) { collision in handleCollision(collision) } }
-
30:29 - Set active tint color when colliding with plant
// Set active tint color when colliding with plant // // Handle collision between robot and hit entity func handleCollision(_ collision: CharacterControllerComponent.Collision) { guard let plantComponent = collision.hitEntity.components[PlantComponent.self] else { return } // Play the plant growth celebration animation playPlantGrowthAnimation(plantComponent: plantComponent) if inImmersiveSpace { appModel.tintColor = plantComponent.tintColor } }
-
30:48 - Apply effect to tint passthrough
// Apply effect to tint passthrough struct ImmersiveExplorationView: View { var body: some View { RealityView { content in // ... } .preferredSurroundingsEffect(surroundingsEffect) } // The resolved surroundings effect based on tint color var surroundingsEffect: SurroundingsEffect? { if let color = appModel.tintColor { return SurroundingsEffect.colorMultiply(color) } else { return nil } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。