大多数浏览器和
Developer App 均支持流媒体播放。
-
使用 Xcode 中的 Reality Composer Pro 内容
了解如何在 Xcode 中让 Reality Composer Pro 的内容变得生动。我们将向你展示如何将 3D 场景加载到 Xcode 中,如何将内容与代码集成,以及如何为 App 添加交互性。我们还将分享在开发工作流中一起使用这些工具的最佳实践和技巧。为了充分利用本节视频,我们建议你首先观看“了解 Reality Composer Pro”和“探索 Reality Composer Pro 中的材质”,以了解有关创建 3D 场景的更多信息。
章节
- 0:00 - Introduction
- 2:37 - Load 3D content
- 6:27 - Components
- 12:00 - User Interface
- 27:51 - Play audio
- 30:18 - Material properties
- 33:25 - Wrap-up
资源
相关视频
WWDC23
- 使用 RealityKit 增强你的空间计算 App
- 使用 RealityKit 构建空间体验
- 开发你的第一款沉浸式 App
- 探索 Reality Composer Pro 中的材质
- 认识 Reality Composer Pro
WWDC21
-
下载
♪ 悦耳的器乐嘻哈 ♪ ♪ Amanda:大家好 我是 Amanda 我是 RealityKit 和 Reality Composer Pro 的工程师 在本节讲座中 我们将学习如何使用 Reality Composer Pro 中 装配的 3D 内容进行空间体验 Reality Composer Pro 是一个开发工具 用于创建 RealityKit 内容 供开发者的空间计算 App 使用 本节讲座中 我们将继续迭代 我同事 Eric 和 Niels 在他们的讲座里创建的一个项目 然后我们将学习 如何使项目在代码中实现交互 如果你还没有看过他们的讲座 我建议你先熟悉他们的讲座中 关于编辑器 UI 和 Reality Composer Pro 功能的内容 首先 我们来看一下构建的成品 然后我将向你讲解 各个部分是如何创造的 我们看到的是 优胜美地国家公园的地形图 戴上头显设备观看 会有一种视野辽阔的感觉 这在以前只有身临其境才能体会到 在前面的 Reality Composer Pro 讲座中 Eric 组建了这个场景 Niels 创造了 我们正在使用的地形素材 这里我们加上了一个滑动条 在两个不同的 加利福尼亚地标之间产生变形 现在我们看到的是洛杉矶 海岸外的卡特琳娜岛 我们还在 3D 空间中放置了 悬停的 2D SwiftUI 按钮 让你进一步了解地图中 各兴趣点的更多信息 本节讲座中 我们将探索如何在 Reality Composer Pro 中 排列这些内容以提升体验感 我来展示一下如何添加 滑动条和兴趣点按钮 来改变我们在 Reality Composer Pro 中制作的场景 首先要讲的是 从 Reality Composer Pro 项目中 以编程的方式加载 3D 内容 我们将探索 RealityKit 组件 是如何工作的 以及如何在代码中使用它们 包括创建我们自己的自定义组件 我们将了解 SwiftUI 中 新的 RealityView API 并探索如何通过使用 Attachments API 将用户界面元素添加到场景中 我们还将学习如何使用 Reality Composer Pro 中 设置的音频 然后 我们将从 Niels 结束的地方开始 将我们用 Shader Graph 制作的 自定义材质连接起来 并从代码中驱动其中的元素 让我们开始吧 在 Eric 的讲座中 我们制作了 一个 Reality Composer Pro 项目 这包含了我们的立体模型 所需的全部资源 并且我们可以随心所欲地排列它们 顶部的这些选项卡 分别代表一个 root 实体 我们可以在运行时加载 我们能在一个场景中放入很多东西 并将其视为一个完整的场景 或者我们可以只放几个 然后把这个场景 当作一个可复用的小组合 我们想做多少都可以 我们看一下如何在运行时加载 这个名为 DioramaAssembled 的场景 我们使用实体的异步初始化定式 将 Reality Composer Pro 程序包中的内容创建一个实体 使用字符串名称指定要加载的实体 并为其提供程序包生成的 bundle 如果在我们的 Reality Composer Pro 项目中 找不到同名对象 它就会抛出异常 realityKitContentBundle 是我们在 Reality Composer Pro 程序包中 为你自动生成的常量值 这在 RealityView 中形成闭包 RealityView 是一种 新的 SwiftUI 视图 这是你进入 RealityKit 的入口 它是连接 SwiftUI 和 RealityKit 的桥梁 我们到讲座后期再深入讲解 RealityView 如果你在 Xcode 项目中 使用的 USD 资源 没有添加到 Reality Composer Pro 项目中 我们强烈建议你将这些资源 放入带有 .rkassets 目录的 Swift 程序包中 如图所示 Xcode 可以将 .rksassets 文件夹编译为 在运行时加载速度更快的格式 我们刚刚加载的实体实际上 是一个更高实体层次结构的 root 它有子实体 而子实体又有子实体 它们构成了 Reality Composer Pro 场景中的一切 如果我们想操作 层次结构较低的一个实体 可以在 Reality Composer Pro 中 为其命名 然后在运行时 要求场景根据名称查找该实体 实体是 ECS 的一部分 ECS 的意思是实体组件系统 ECS 为 RealityKit 和 Reality Composer Pro 赋能 我们先来了解一下 ECS ECS 与面向对象编程 有一些相似之处 但在一些关键方面有所不同 在面向对象编程的领域中 对象具有定义 其性质的属性 并且具有自己的功能 你可以在定义对象的类中 编写这些属性和功能 在 ECS 中 实体是你在场景中看到的任何东西 实体也可以是不可见的 但它们不包含属性或数据 相反 我们将数据放入组件中 在 App 执行期间 可以随时 向实体添加或从实体中删除组件 这提供了一种 动态更改实体性质的方法 系统是行为存在的地方 它有一个更新功能 每帧更新一次 在这里你可以 存放正在进行的逻辑数据 在你的系统中 你可以查询所有 具有特定组件或组件配置的实体 然后执行一些操作 并将更新后的数据存储回这些组件 有关 ECS 更深入的讨论 请查看 2021 年 “深入了解 RealityKit 2”的讲座 以及今年的 “使用 RealityKit 构建空间体验” 现在我们来了解一下组件 我们将展示如何在 Reality Composer Pro 项目中 向实体添加组件 然后介绍如何创建自定义组件 用于在立体模型上制作位置标记 在 Swift 中向实体添加组件 可以使用 entity.components.set() 并提供组件值 在 Reality Composer Pro 中 也是如此 在视口或层次结构中 选择需要的实体
然后在检查面板的底部 点击 Add Component 按钮 就会出现 RealityKit 全部可以使用的组件列表
我们可以向实体 添加任意数量的组件 但每种类型只能添加一个 这是一条规定 在这个列表中 也能看到之前创建的自定义组件 我们来看看如何利用 Reality Composer Pro 创建自己的自定义组件 我们要制作那些悬停在 地形上特定点的浮动按钮 这样你就可以选择 查看有关该点的更多信息 我们将在代码中 创建大量的 UI 和功能 但我想展示的是如何 在 Reality Composer Pro 中 将这些实体标记为 我们想要显示这些浮动按钮的位置 要做到这一点 我们要在地形图上方添加实体 这将向 App 表明 这些位置是我们想要显示 浮动按钮的位置 然后 我们创建一个兴趣点组件 来容纳每个地点的信息 然后在 Xcode 中打开 PointOfInterestComponent.swift 并进行编辑 添加诸如名称和 描述文件之类的属性 在 Reality Composer Pro 中 我们向每个新的实体添加新的 PointOfInterestComponent 组件 然后填充属性值 我们来制作第一个位置标记实体 丝带海滩是卡特琳娜岛的一个地方 我们点击加号菜单 选择转换 制作一个新的不可见实体
我们将实体命名为 Ribbon_Beach 我们把这个实体放在丝带海滩 位于岛上的真实位置 我们点击 Add Component 按钮 但这一次我们选择新组件 因为我们要制作自己的组件
我们将其命名为 PointOfInterest
现在它出现在了检查面板上 和其他组件是一样的
但这个计数属性是什么? 让我们在 Xcode 中 打开这个新组件 在 Xcode 中 我们看到 Reality Composer Pro 为我们创建了 PointOfInterestComponent.swift Reality Composer Pro 工程 都是 Swift 程序包 我们刚刚生成的 Swift 代码 就存在于这个程序包中 查看模板代码 我们可以看到 计数属性就是从这里来的 我们来更换一个属性 我们希望每个兴趣点 都知道它应与哪个地图相关 这样当你更改地图时 我们就可以渐出旧的兴趣点 渐入恰当的新兴趣点 所以我们要添加枚举属性 var region 我们在这上面创建枚举区域…… ……添加两个关键词 因为我们现在只有两个地图 卡特琳娜岛和优胜美地 枚举区域可以序列化为字符串 我们还要使它 符合 Codable 协议 这样 Reality Composer Pro 就可以看到它 并序列化它的实例 回到 Reality Composer Pro 计数属性已经没有了 出现了新的区域属性 它有一个默认值 yosemite 因为这是我们在代码中 初始化的内容 我们能为这个特定的实体覆盖该值 如果覆盖该值 那么这个值将 只对这个特定的实体生效 其余兴趣点组件的初始值 还是 yosemite 除非我们也将其覆盖 我们在使用 PointOfInterestComponent 时 将其当作能指一样的符号 一个我们粘贴在这些实体上的标记 这些实体就像在运行时 SwiftUI 按钮位置上的占位符 我们添加另一个 卡特琳娜岛兴趣点的方式 与添加丝带海滩的方式相同 现在我们运行 App 看看新的自定义组件能做什么
哦!什么也没做 这是因为我们还没有编写任何代码 来操作这些兴趣点组件 我们来编写一下 我们有一种新的方法 能将 SwiftUI 内容 放入 RealityKit 场景中 这被称为 Attachments API 我们将要把 attachment 与 PointOfInterestComponent 结合起来 在运行时创建 带有自定义数据的悬停按钮 我们先在代码中看一下 然后我将引导你了解数据流 Attachment 是 RealityView 一部分 我们首先来看一个简化的示例 展示一下 RealityView 的结构 这样我们就能看到 SwiftUI 视图 是如何进入 RealityKit 场景的 我们要使用的 RealityView 初始化程序 有三个参数: 一个 make 闭包 一个 update 闭包 还有一个 attachments ViewBuilder 简单地说 就是我们添加一个 创建 Attachment View 最低限度的编程工具 一个绿色 SwiftUI 按钮 并将它添加到我们的 RealityKit 场景中 在 Attachments ViewBuilder 中 我们制作一个常规的 SwiftUI 视图 可以使用视图修饰符和手势 以及其他所有 SwiftUI 给我们的功能 可以用任何独特的可哈希的数据类型 来标记我们的视图 我选择用鱼的表情符号 来标记这个按钮视图 随后 当 SwiftUI 调用我们的 update 闭包时 按钮视图就已经变成了一个实体 它存储在这个闭包的 attachments 参数中 我们利用之前给予的鱼标签 把它找出来 然后就可以将其视为 与其他实体一样了 我们可以将其添加为 场景中任何现有实体的子实体 也可以将其添加为 内容实体集合中新的顶级实体 由于它已经变成一个常规实体 我们就可以设置它的位置 以便在 3D 中显示我们想要的地方 我们也可以添加 任何我们想要的组件 这就是数据如何从 RealityView 的一部分 流向另一部分 我们来看一下这个 RealityView 初始化程序的三个参数 第一个是 make 是将 Reality Composer Pro bundle 中的 初始设置场景作为实体加载的地方 然后将其添加到 RealityKit 场景中 第二个是 update 它是在视图状态 发生改变时被调用的闭包 在这里 你可以更改有关实体的内容 如实体组件中的属性、位置 甚至可以在场景中添加或删除实体 这个 update 闭包 并不是每帧都执行 只有在 SwiftUI 视图状态 改变时才被调用 第三个是 attachments ViewBuilder 在这里 你可以制作 SwiftUI 视图 放入 RealityKit 场景中 你的 SwiftUI 视图从 attachments ViewBuilder 开始 然后在 attachments 参数的 update 闭包中交付给你 在这里 你可以询问 attachments 参数 是否含有使用与你在 attachments ViewBuilder 中给按钮的 相同标签的实体 如果有 它会向你提供一个 RealityKit 实体 在 update 闭包中 你可以设置它的 3D 位置 并将其加入你的 RealityKit 场景中 你就可以看到它 漂浮在任何你想要的地方 这里 我已经将按钮实体添加为 球体实体的子实体 我将它放置在 其父实体上方 0.2 米处 make 闭包也有一个 attachments 参数 这个参数是为了添加 首次评估视图时你已准备好的 attachments 因为 make 闭包只运行一次 现在我们理解了 RealityView 的常规流 我们再深入讲解一下 update 闭包 你的 make 和 update 闭包的参数 是一项 RealityKitContent 当你将一个实体添加到 RealityKit 内容中 它就会成为场景中的最高层级实体 同样 通过更新功能 在内容中添加实体也就会在场景中 生成一个新的最高层级实体 然而 make 闭包 只会被调用一次 update 闭包 则会被调用多次 如果你在 update 闭包中 创建一个新实体 并将它添加到内容中 就会得到该实体的副本 你可能并不想这样 为了防止这种情况 你应该 只将实体添加到仅运行一次的地方 你无需检查 content.entities 是否已经包含了你的实体 如果调用同一个实体两次 那就是一个空操作 像是一种规定 将实体作为 场景中现有实体的父对象时 也是如此 不会被添加两次 Attachment 实体 不是你创建的 它们是由 RealityView 为你在 attachments ViewBuilder 中 提供的每个 attachment 视图而创建的 这意味着可以安全地将它们添加到 update 闭包的内容中 而无需检查它是否已经存在 所以 如果我们想在 attachments ViewBuilder 中 对兴趣点进行硬编码 我们就要这样编写 attachment 代码 但既然我们想让 Reality Composer Pro 项目中的数据 来驱动体验感 那就要让它变得更加灵活 这样一来 设计者或制作者可以在 Reality Composer Pro 项目中创建兴趣点 而且我们的代码 可以容纳其添加的任何数据 为了做到数据驱动 就需要代码来读取 我们在 Reality Composer Pro 场景中 设置的数据 我们要动态地创建 attachment 视图 高级的做法是这样的 在 Reality Composer Pro 中 我们已经建立了 丝带海滩的占位符实体 然后我们对立体模型中 另一个想要强调的兴趣点也这样做 我们要填充每个实体所需全部信息 比如名称、属于哪一个地图 现在 在代码中 我们将查询这些实体 并为每个实体创建 一个新的 SwiftUI 按钮 为了每当我们向集合 添加新按钮时 SwiftUI 都能调用我们的 attachments ViewBuilder 我们将向该集合添加 @State 属性包装 我们将为 attachments ViewBuilder 启动这些按钮 最后 在 RealityView 的 update 闭包中 我们将把按钮作为实体接收 并将这些新按钮实体添加到场景中 我们将每一个实体都作为 Reality Composer Pro 中 设置的标记实体的子实体来添加 我们通过更详细的图表 来理解这六个步骤 然后我们来看一下代码 首先 我们在 Reality Composer Pro 场景中 添加不可见的实体 我们在 x、y 和 z 轴上将 不可见实体定位在 按钮需要显示的位置上 我们在这里使用转换组件 默认情况下所有实体都有转换组件 然后我们向每一个实体添加 PointOfInterestComponent 在代码中 通过查询场景中所有 具有 PointOfInterestComponent 的实体 我们获得了对这些实体的引用 查询返回了我们在 Reality Composer Pro 中 设置的三个不可见实体 我们为每一个实体创建 新的 SwiftUI 视图 并将它们存入集合中 为了让按钮进入 RealityView 我们要使用 SwiftUI 视图更新流程 也就是说将 属性包装器 @State 添加到 视图中的按钮集合中 @State 属性包装器 告诉 SwiftUI 当我们向这个集合添加项时 SwiftUI 应该触发 ImmersiveView 上的视图更新 这样会导致 SwiftUI 再次评估 我们的 attachments ViewBuilder 和 update 闭包 通过 RealityView 的 attachments ViewBuilder 可以向 SwiftUI 声明 我们希望这些按钮成为实体 接下来 RealityView 的 update 闭包将被调用 然后按钮将作为实体交付给我们 现在它们不再是 SwiftUI 视图了 因此我们就可以将它们 添加到实体层次结构中 在 update 闭包中 我们将 attachment 实体 添加到场景中 漂浮在每个不可见实体之上 现在 当我们观看立体模型场景时 它们就会在视觉上显示出来 我们来看一下每一步是如何做的 首先 在 Reality Composer Pro 场景中 标记我们的不可见实体 为了找到我们标记的实体 我们要生成 EntityQuery 用它来寻找所有 带有 PointOfInterestComponent 的实体 然后 我们将迭代 QueryResult 并为场景中的每个实体创建 一个新的 SwiftUI 视图 视图上带有一个 PointOfInterestComponent 我们将用从组件中 获取的信息来填充它 这些信息是我们在 Reality Composer Pro 中输入的数据 这个视图将成为我们的 attachments 之一 所以我们给它放置一个标签 在这种情况下 我们要严肃一些 所以我们用 ObjectIdentifier 而不是鱼的表情符号 这是我们制作 SwiftUI 视图集合的部分 我们称它为 attachmentsProvider 这是因为它要向 RealityView 的 attachments ViewBuilder 提供我们的 attachments 然后我们将视图存储在 attachmentsProvider 我们来看看集合类型 AttachmentsProvider 有一个 attachment 标签到视图的字典 我们打字删除了视图 这样就可以 在那里放置除 LearnMoreView 之外的 其他类型的视图 我们有一个名为 sortedTagViewPairs 的计算属性 它每次都以相同的顺序 返回一个元组数组 也就是标签及其对应的视图 然后 在 attachments ViewBuilder 中 我们将对我们制作的 attachment 集合进行 ForEach 这样就会告诉 SwiftUI 我们希望为给定的每对 提供一个视图 视图是从集合中提供的 我们让 ObjectIdentifier 在这里同时完成双重任务 既是视图的 attachment 标签 又是 ForEach 结构的标识符 所以为什么我们不直接在 PointOfInterestComponent 中 添加一个标签属性呢? Attachment 标签 必须是独一无二的 ForEach 结构和 attachment 机制才能运行 因为自定义组件中所有的属性 都会展示在 Reality Composer Pro 的检查面板上 所以当你向实体添加组件时 就意味着 attachmentTag 也会出现在那里 我们不想给自己增加负担 不想每次在 Reality Composer Pro 中 添加兴趣点时 都要记住统一所有标签 但对于我们来说方便的是 实体符合标识协议 因此它们自动具有唯一的标识符 当我们在 Reality Composer Pro 中 设计场景时 我们可以在运行时从实体中 获取此标识符 而无需提前记住 为了不让 attachmentTag 属性 显示在 Reality Composer Pro 中 我们使用一种技术 我称之为 “设计时 VS 运行时”的组件 我们将数据分为两组不同的组件 一组是我们在 Reality Composer Pro 中设计时 想要排列的数据 另一组是用于运行时的数据 我们在运行时动态附加到 相同的实体上 那些是我们不想在 Reality Composer Pro 检查面板上显示的属性 所以我们要定义一个新组件 PointOfInterestRuntimeComponent 将 attachment 标签移到组件内 Reality Composer Pro 基于 它在 Swift 程序包内读到的内容 自动为你构建组件 UI 它可以检查 程序包中的 Swift 代码 让发现的任何可编码的组件 都能供你在场景中使用 这里我们展示四个组件 组件 A 和组件 B 是在 Xcode 项目中的 但不在 Reality Composer Pro 的 程序包中 因此你不能将它附加到 Reality Composer Pro 中的实体 组件 C 在程序包中 但不可编码 所以 Reality Composer Pro 就会忽略它 以上四个组件中 只有组件 D 会出现在 Reality Composer Pro 的列表中 因为它存在于 Swift 程序包 并且是可编码的组件 这个就是我们的设计时组件 其他的可能用于运行时组件 设计时组件用于容纳更简单的数据 例如 int、字符串和 SIMD 值 这些都是 3D 艺术家和设计师 会使用的数据 如果你将 Reality Composer Pro 无法序列化类型的属性 添加到自定义组件中 你将在 Xcode 项目中 看到一个报错 现在 我们回头看看代码 首先 我们向实体添加 PointOfInterest 运行时组件 然后使用运行时组件 来帮助我们将 attachment 实体 与其在立体模型上 对应的兴趣点进行匹配 这就是我们的 运行时组件使用的地方 我们正在读入 PointOfInterest 实体 并创建我们的 attachment 视图 我们查询了所有的设计时组件 现在我们为每个组件制作一个新的 相对应的运行时组件 我们将 attachmentTag 存储在运行时组件 并且将运行时组件 存储在同一个实体上 通过这种方式 设计时组件就像一个能指 告诉我们的 App 需要为其制作 attachment 运行时组件处理 App 执行期间 我们需要的但不想存储在 设计时组件中的 任何其他类型的数据 在 RealityView 中 我们还有一个步骤 才能看到 attachment 实体 出现在场景中 一旦我们在 attachments ViewBuilder 中 提供了 SwiftUI 视图 SwiftUI 就会调用 RealityView update 闭包 并将 attachments 作为 RealityKit 实体提供给我们 但如果我们只是 将它们添加到内容中 而不定位它们 它们就会出现在 场景的原点 位置 我们并不想它们出现在那 我们想让它们漂浮在地形中 每个兴趣点之上 我们需要将 attachment 实体和 Reality Composer Pro 中建立的 不可见兴趣点实体相匹配 我们放置在不可见实体上的 运行时组件带有我们的标签 这样我们就可以将每个 attachmentEntity 与兴趣点实体相匹配 我们查询所有的 PointOfInterestRuntimeComponent 从查询返回的每个实体中 获得运行时组件 然后利用组件的 attachmentTag 属性 从 attachments 参数 获得 attachmentEntity 到 update 闭包中 现在我们向内容添加 attachmentEntity 并将其定位在 兴趣点实体上方半米处 我们再次运行 App 看看变成什么样子了 看起来很棒 可以看到每个地名都漂浮在 我们在 Reality Composer Pro 项目中 放置的位置上方 接下来 我们来了解如何播放 我们在 Reality Composer Pro 中 设置的音频 为了在 Reality Composer Pro 中 设置能够播放音频的程序 可以添加音频实体 通过点击加号按钮 选择音频 然后选择环境音频 这将创建一个带有 AmbientAudioComponent 的 常规不可见实体 我们将这个实体命名为 OceanEmitter 因为我们将用它为卡特琳娜岛 播放海洋的声音 你还需要将音频文件添加到场景中 我们来添加海洋的声音
你可以通过在检查面板 组件预览菜单中选择一种声音 来预览音频组件 但当实体加载到 App 中时 不会自动播放选定的声音 为此 我们需要加载音频资源 并要求它播放 为了播放这个声音 我们要获取对 放置音频组件的实体的引用 我们已经将实体命名为 OceanEmitter 所以我们通过名字找到实体 我们使用 AudioFileResource 初始化程序 加载声音文件 将场景中音频文件资源基础容器的 完整路径传递给它 我们在 Reality Composer Pro 项目中 为其提供了 .usda 文件的名称 在我们的案例中 这就是我们的主场景 名为 DioramaAssembled.usda 通过调用 entity.prepareAudio 我们创建一个 audioPlaybackController 我们就可以播放、 暂停和停止这段声音 现在我们已经准备好了 这就是我们在 App 中 播放的海洋声音 App 中的滑动条 在优胜美地和卡特琳娜岛 这两个不同的地形图之间变化
现在我们已经将音频引入到场景中 我们将在两个音频源之间交叉播放 添加森林音频发射器的方式与 添加海洋发射器实体的方式相同 我们来看看如何使用滑动条 来改变地形 在这个过渡中还包含了音频变化 我们将使用 Shader Graph 材质中的一个属性 在两个地形之间形成变形 我们来看看是如何做到的 在 Niels 的讲座中 他使用 Reality Composer Pro 的 Shader Graph 为我们创建了这个 美丽的几何图形修饰符 现在 我们可以将它 连接到我们的场景中 并在运行时驱动一些参数 我们想把这个 Shader Graph 材质 与滑动条连接 要做到这一点 我们需要提升一个输入节点 在节点上按住 Command 点击 然后选择提升 这样就会告诉项目 我们打算 在运行时为这部分材质提供数据 我们将此 提升节点命名为 Progress 这样就可以在运行时 用这个名称来操作它 我们现在可以 在代码中动态更改这个值 我们得到了该材质所在实体的参考 然后我们得到了 ModelComponent 也就是用于容纳材质的 RealityKit 组件 从 ModelComponent 中 我们得到了它的第一个材质 这个实体上只有一个材质 我们将其转换为 ShaderGraphMaterial 类型 现在 我们可以为 名为 Progress 的参数 设置一个新值 最后 我们将这个材质存储回 ModelComponent 并将 ModelComponent 存储回地形实体 现在我们将其连接到 我们的 SwiftUI 滑动条上 每当滑动条的值发生变化时 我们都会获取到 这个值在 0 到 1 的范围内 并将其持续更新到 ShaderGraphMaterial 中 接下来 我们在两个地形的 环境音轨之间进行交叉渐变 因为我们还在海洋和森林这两个 音频实体上放入了 AmbientAudioComponent 我们可以使用比例增益属性 来调整声音播放的音量 我们将查询所有 带有 AmbientAudioComponent 的实体 也就是目前海洋和森林两个实体 另外我们还添加了另一个自定义组件 名为 RegionSpecificComponent 这样我们就可以标记 与一个区域或另一区域相关的实体 我们得到一个 audioComponent 的可变副本 因为我们要做更改 并存储回我们的实体中 我们调用一个函数 来计算区域和滑动条值 应给的比例增益 我们将比例增益值设置在 AmbientAudioComponent 上 然后将该组件存储回实体中 我们来看看效果
非常好! 当我们移动滑动条时 可以看到 Shader Graph 材质 正在改变地形的几何图形 我们可以听到森林的声音逐渐消失 海洋的声音进入
今天我们讲到了很多信息 让我们回顾一下 我们学习了如何将 Reality Composer Pro 内容 加载到我们 在 Xcode 的 App 中 我们了解了如何在 Reality Composer Pro 中 创建自己的自定义组件 我们探索了 SwiftUI Attachments API 是如何工作的 以及它们是如何 作为实体交付给我们的 我们看到了如何建立音频 以及如何在代码中播放 最后我们了解了 如何获取提升的材质属性 并从代码中驱动它 这些工作流 将帮助你让空间体验更加生动 我期待看到开发者在我们的新平台上 构建的所有令人惊喜的内容 谢谢 ♪
-
-
3:12 - Loading an entity
RealityView { content in do { let entity = try await Entity(named: "DioramaAssembled", in: realityKitContentBundle) content.add(entity) } catch { // Handle error } }
-
6:39 - Adding a component
let component = MyComponent() entity.components.set(component)
-
12:21 - Attachments data flow
RealityView { _, _ in // load entities from your Reality Composer Pro package bundle } update: { content, attachments in if let attachmentEntity = attachments.entity(for: "🐠") { content.add(attachmentEntity) } } attachments: { Button { ... } .background(.green) .tag("🐠") }
-
15:48 - Adding attachments
let myEntity = Entity() RealityView { content, _ in if let entity = try? await Entity(named: "MyScene", in: realityKitContentBundle) { content.add(entity) } } update: { content, attachments in if let attachmentEntity = attachments.entity(for: "🐠") { content.add(attachmentEntity) } content.add(myEntity) } attachments: { Button { ... } .background(.green) .tag("🐠") }
-
20:43 - Adding point of interest attachment entities
static let markersQuery = EntityQuery(where: .has(PointOfInterestComponent.self)) @State var attachmentsProvider = AttachmentsProvider() 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) }
-
21:40 - AttachmentsProvider
@Observable final class AttachmentsProvider { var attachments: [ObjectIdentifier: AnyView] = [:] var sortedTagViewPairs: [(tag: ObjectIdentifier, view: AnyView)] { ... } } ... @State var attachmentsProvider = AttachmentsProvider() RealityView { _, _ in } update: { _, _ in } attachments: { ForEach(attachmentsProvider.sortedTagViewPairs, id: \.tag) { pair in pair.view } }
-
22:31 - Design-time and Run-time components
// Design-time component public struct PointOfInterestComponent: Component, Codable { public var region: Region = .yosemite public var name: String = "Ribbon Beach" public var description: String? } // Run-time component public struct PointOfInterestRuntimeComponent: Component { public let attachmentTag: ObjectIdentifier }
-
25:38 - Adding a run-time component for each design-time component
static let markersQuery = EntityQuery(where: .has(PointOfInterestComponent.self)) @State var attachmentsProvider = AttachmentsProvider() 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) let runtimeComponent = PointOfInterestRuntimeComponent(attachmentTag: attachmentTag) entity.components.set(runtimeComponent) }
-
26:19 - Adding and positioning the attachment entities
static let runtimeQuery = EntityQuery(where: .has(PointOfInterestRuntimeComponent.self)) RealityView { _, _ in } update: { content, attachments in x rootEntity.scene?.performQuery(Self.runtimeQuery).forEach { entity in guard let component = entity.components[PointOfInterestRuntimeComponent.self], let attachmentEntity = attachments.entity(for: component.attachmentTag) else { return } content.add(attachmentEntity) attachmentEntity.setPosition([0, 0.5, 0], relativeTo: entity) } } attachments: { ForEach(attachmentsProvider.sortedTagViewPairs, id: \.tag) { pair in pair.view } }
-
28:55 - Audio Playback
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() }
-
31:02 - Terrain material transition using the slider
@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:57 - Audio transition using the slider
@State private var sliderValue: Float = 0.0 static let audioQuery = EntityQuery(where: .has(RegionSpecificComponent.self) && .has(AmbientAudioComponent.self)) Slider(value: $sliderValue, in: (0.0)...(1.0)) .onChange(of: sliderValue) { _, _ in // ... Change the terrain material property ... 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) }) } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。