大多数浏览器和
Developer App 均支持流媒体播放。
-
探索 RoomPlan 的增强功能
加入我们以了解 RoomPlan 中令人兴奋的更新内容,我们将探索 MultiRoom 支持和房间表示的增强功能。了解如何更详细地扫描区域、捕获多个房间信息以及将各个扫描结果合并到一个更大的结构中。我们还将与你分享将需要的 RoomPlan 结果合并到现有 3D 模型库中的工作流程和最佳实践。
资源
相关视频
WWDC23
WWDC22
-
下载
♪ ♪
Alex:大家好 我是 Alex 我来自 Video Engineering 团队 今天 我将和我的同事 Antoine 一起 与你分享 RoomPlan 的新功能 RoomPlan 使用 ARKit 提供的 先进机器学习算法 可用于检测墙、窗户、门、开口 以及其他定义房间的对象 RoomCaptureView API 可以帮助你将扫描体验 直接集成到 App 中 在完成扫描后 你的 App 就 可以展示生成的 3D 模型 并导出为 USDZ 文件 在本次讲座中 我们将介绍 RoomPlan 中的新功能 首先 我们会介绍一种 使用自定义 ARSession 来结合使用 RoomPlan 和 ARKit 的新方法 接着 我们会重点介绍 MultiRoom 支持 我们推出了新的 MultiRoom API 用于将单独的房间扫描 合并为更大的结构 然后 我们会向你演示 RoomCaptureView 中的 新旁白支持 最后 我们会介绍 RoomPlan 表示的改进 以及导出功能的增强 以实现新工作流程
接下来 我们首先从 自定义 ARSession 支持开始
正如我之前提到的 RoomPlan 依赖 ARKit 检测到的墙、窗户、门、开口 以及扫描期间其他对象的信息 为此 RoomCaptureSession 使用默认的 ARSession 运行 在 iOS 17 的新增功能中 RoomPlan 可以结合使用 自定义 ARSession 和 ARWorldTrackingConfiguration 从而 我们可以以新的方式 在同一工作流程中结合使用 RoomPlan 和 ARKit 我们来看几个例子 其中一种结合使用 RoomPlan 和自定义 ARSession 的方式 就是将 RoomPlan 的结果 与 ARKit 的场景几何 和平面检测进行整合 以在虚拟内容和真实世界几何之间 创造更加沉浸式的交互 此外 你还可以使用 ARKit 的高质量图像捕捉 来收集空间的照片表示 同时结合 RoomPlan 以创造更丰富的不动产列表 并且 如果你将 RoomPlan 作为现有 AR 体验的一部分 你也可以在不破坏 现有 ARAnchors 情况下 整合来自 RoomPlan 的结果 以上只是来自自定义 ARSession 的几个用例 接下来 我们来看一些代码 了解 如何将自定义 ARSession 传入 RoomPlan 这里是此前 RoomPlan 中的 init 和 stop 函数 这里是如何将自定义 ARSession 传入 init 函数的方法 任何带有 ARWorldTrackingConfiguration 的自定义 ARSession 在 RoomCaptureSession 中 都会得到支持 并且 stop 函数包含的新选项 可以决定是否暂停 底层的 ARSession 如果你希望 ARSession 在 RoomCaptureSession 停止后 继续运行 你可以将 Boolean 设置为 false 在接下来的部分中 我们将了解如何结合使用 ARSession 来实现新的工作流程 例如将多个扫描 合并为更大的结构 接下来 我们来了解一下 MultiRoom 支持 什么是 MultiRoom 支持呢? 在此前的 RoomPlan 中 你可以单独扫描房间 来获取其中的 3D 模型 假如你对房屋中的 不同房间进行了扫描 例如餐厅、厨房、客厅、 走廊以及卧室等 如果你希望合并这些扫描结果 你可能会遇到一些问题 首先 这些扫描结果全部位于 各自的坐标系统中 因此 对于每个房间而言 其世界坐标的原点和方向 都是不同的 其次 即便你将这些房间 手动拼接起来 最终还是会出现重复的墙 以及可能会重复的对象
首先 我们来解决 坐标系统不同的问题 我们最终的目标是 让所有的扫描结果都 位于相同的坐标系统中 为此 你可以采用以下两种方法 来扫描多个房间 第一种 使用连续的 ARSession 第二种 使用 ARSession 的重新定位 接下来 我们来介绍一下 如何使用连续的 ARSession 在此前的 RoomPlan 中 如果 RoomCaptureSession 停止 ARSession 就会暂停 并且 每次的扫描都 具有不同的坐标系统 新的 API 在 stop 函数中 推出了一个新参数 可用于将 pauseARSession 设置为 false 这样 ARSession 可以继续 进行下一次扫描 甚至是之后的扫描 直到我们再次暂停 ARSession 为止
使用这种方法 我们可以确保各次扫描中 运行的是同一个 ARSession 这样 我们就可以保证 所有的扫描结果 共用一个世界坐标系统 接下来 我们来看一下 如何在代码中实现这一点 这个示例演示的是 如何使用连续的 ARSession 运行 RoomCaptureSession 首先 我们使用 RoomCaptureSession.run 开始第一次扫描 接着 就到了关键部分 有了 RoomCaptureSession.stop 中的新 API 我们需要将 pauseARSession 设置为 false 这样可以使 ARSession 在下一次扫描中继续运行 之后 我们使用同一个 roomCaptureSession 实例 进行第二次扫描 接着 停止第二次扫描 最终 我们就可以 在同一个坐标系统中 得到第一次和第二次的扫描结果 另一个在同一个坐标空间中 对单个房间进行扫描的方法 就是使用 ARSession 的重新定位 这个方法非常适用于 你需要分几次 对单个房间进行扫描的情况 例如 隔天或隔周 再对同一位置进行扫描 我们来了解一下这个方法的 工作原理是什么 同样的 这里也是使用单个扫描 来获取单个房间的 3D 模型 由于我们停止了 RoomCaptureSession 并暂停了 ARSession 为了使重新定位适用于之后的扫描 我们需要将 ARWorldMap 保存到磁盘上 如果你想在 ARSession 暂停时 继续之前的扫描 你可以从暂停的 ARSession 中 加载 ARWorldMap 来恢复扫描
使用该 ARWorldMap 你可以 根据此前扫描的环境进行重新定位 以确保一系列的扫描结果都 共享同一个坐标系统 我们来看一下使用重新定位的 扫描工作流程的示例代码 首先 我们运行第一次扫描 接着 我们暂停 ARSession 停止第一次扫描 之后 为了启用重新定位 我们需要在 ARSession 暂停时 保存 ARWorldMap
在运行第二次扫描前 我们需要恢复先前的 ARWorldMap 首先 我们加载 ARWorldMap 然后 我们将加载的 ARWorldMap 分配给 ARWorldTrackingConfiguration. initialWorldMap 接着 我们运行 ARSession 来触发重新定位 在重新定位完成后 加载此前的 ARSession 现在 当前的世界坐标就与 先前的世界坐标对齐了 然后 我们运行第二次扫描 最后 我们停止第二次扫描 这样 经过以上这些步骤 第一次和第二次的扫描结果 就位于同一个 3D 世界坐标中了 刚刚我们了解了两种 在同一个 3D 坐标系统 链接多个扫描结果的方法 接下来 我们来了解一下 如何使用新的 MultiRoom API 将扫描结果合并为一个组合结构 在每次扫描中 我们都会 运行 RoomBuilder API 来生成单独的 CapturedRoom 正如我们之前所展示的 使用连续的 ARSession 以及 ARSession 的重新定位 所有 CapturedRoom 都会 位于同一个 3D 世界空间中 这里是 RoomBuilder 的输出 其中包含了 3 个 CapturedRooms 现在 我们使用一个新的合并 API StructureBuilder 来将这些 CapturedRooms 合并为一个大结构 CapturedStructure 接下来 我们来看一下 StructureBuilder API 的示例代码 这里展示的是如何使用 StructureBuilder API 合并多个扫描结果 首先 我们使用配置选项创建 一个 StructureBuilder 实例 接着 我们创建一个数组来加载 此前扫描的多个 CapturedRoom 之后 我们调用 StructureBuilder API 来获取合并结果 capturedStructure 最后 我们可以将 capturedStructure 导出为 USDZ 文件 这里是 CapturedStructure 的定义 首先 其有一个 rooms 属性 该属性是一个 CapturedRoom 实例的数组 接着 其还有用于合并的 walls、doors、 windows、openings 以及 objects 属性 最后 其还有一个用于 导出到 USDZ 文件的函数
接下来 我们来看一看 MultiRoom 的实际应用效果 我们为你提供了一个示例 App 可供你 使用 StructureBuilder API 合并 多个扫描结果并导出到 USDZ 文件 接着 你就可以在 iOS 和 macOS 上预览 USDZ 文件 但你也可以更进一步 将该 USDZ 加载到 Blender 等数字内容创作工具中
在对 3D 模型进行一些美化处理后 其外观效果会更好
最后 我们来讲一下 在使用 MultiRoom 支持 获取出色的 MultiRoom 体验时 需要注意的内容 MultiRoom 最适合单层住宅 即通常包括一到四个卧室、起居室、 厨房及餐厅的住宅 对于需要扫描和合并的单个房间 我们建议最大总面积 不超过 2,000 平方英尺 大约 186 平方米 此外 我们建议你使用 50 勒克斯 及更高的良好照明 以确保 RoomPlan 在扫描时 视频流保持清晰 并且 AR 跟踪表现出良好的性能 接下来 Antoine 会向你介绍 iOS 17 中 RoomPlan 的 更多增强功能 Antoine:谢谢你 Alex 接下来 我们来介绍以下辅助功能 在我们说到渲染时 大部分人想到的都是视觉模态 但对于低视力人群而言 这种模态是极为不实用的 今年 RoomPlan 添加了启用旁白时 提供音频反馈的功能 让你的手机可以 提供扫描的指导 并对看到的内容进行描述 旁白:移动设备以开始 将相机对准墙的底边 例如 壁炉、墙 和窗户 Antoine:接下来 我们来了解一下 RoomPlan 可以从房间中 收集到的新信息 以及渲染这些信息的方式 RoomPlan 可以轻松扫描各种房间 但到目前为止 它在表示有限的房间情况时 精度有限 RoomPlan 现已支持更多种类的房间 包括倾斜和弯曲的墙壁 以及洗碗机、烤箱或水槽等 嵌入式厨房元素 同时 RoomPlan 在检测 给定类别的配置对象方面 也得到了改进 例如 沙发存在很多类型 包括单座、L 型 以及简单的方形沙发等 新版本的 RoomPlan 对此都能实现检测 在介绍 RoomPlan 时 我们提到了两个 可以得到检测的元素:表面和对象 现在 我们还添加了 用于描述房间内区域的新元素 我们称之为部分 现在 我们可以将墙描述为多边形 以处理倾斜的墙以及 有横梁的墙等不规则的墙 到目前为止 弯曲的墙和窗户 只是仅限数据的 API 的一部分 现在 RoomCaptureView 的 最终结果也可以呈现弯曲的墙 除了表面类别 地板类别是另一个新增的 可被描述为多边形的类型 并且 对象也新增了属性 以对类别内不同的配置 进行更好地描述 现在 表面和对象 具有了新的父变量 其中包含父级的标识符 例如 窗户的父级是墙 椅子的父级是桌子 以及洗碗机的父级是储物柜等 接下来 我们用示例来 更详细地了解一下这些改进 每个部分都描述了 房间或房屋中的不同区域 部分可以使用以下标签: livingRoom、bedroom、bathroom、 kitchen 和 diningRoom 并且 它具有给定楼层的给定位置
不规则墙可以使用 polygonCorner 变量 渲染为多边形 地板在扫描时可被表示为矩形 并会在扫描结束时美化为多边形 洗碗机、烤箱和水槽的父级 会嵌入到渲染的画面中 在介绍 RoomPlan 时 我们使用类别来描述对象 但是 这种表示存在一些局限 我们以椅子为例 表示该类别的椅子存在多种类型 例如 凳子、餐椅以及办公椅等 并且 这些椅子的功能也各不相同 为了更好地表示对象 我们现在添加了属性 在本示例中 我们可以借助属性 更加准确地了解扫描的内容 在 RoomPlan API 中 属性可以通过多态枚举数组获得 然而 枚举并不是 理解属性的最佳方式 在下一部分 我们会探讨新的方法 来实现更好的描述 目前 我们的导出结果包括了 属性以及扫描期间捕获的新信息 并且 我们还新增了两类 可以导出的数据: 用于从 USDZ 节点中 找回元数据的文件 以及使用模型丰富 导出的 USDZ 文件的结构 在将房间导出为网格时 我们会创建一个 包含表面节点树 和对象节点数的 USDZ 文件 今年 我们还添加了一个 包含部分中心的部分组 但是 我们在这么做时会丢失 大量扫描信息 例如 墙和对象的尺寸 以及对象的属性 所以 我们现在在导出房间时 会创建一个映射文件 该文件是一个从字符串 到 UUID 的编码字典 其中 UUID 在 USDZ 唯一节点名 和通过其标识符唯一识别的 CapturedRoom 元素 之间建立起了联系
接下来 我们来了解一下该文件在 RoomPlan API 中的工作方式 在此前版本的 RoomPlan 中 房间可以以两种方式导出: 通过导出函数作为 USDZ 格式导出 或对 CapturedRoom 结构进行编码 作为 JSON 或 Plist 格式导出 iOS 17 最新推出的功能 可以指定元数据 URL 来在导出函数中 将房间映射到 USDZ 文件 从而 你便可以对 两个导出信息进行关联 这样 你在渲染已扫描的房间时 就可以查询表面或对象的附加信息 除了新的映射文件 我们还引入了 Model Provider 并且 Model Provider 会使用 与扫描属性匹配的模型 来替代此前以盒子形式表示的对象 所以 我们在这里会将 对象和 3D 模型进行关联 更确切地说 就是与其 URL 进行关联 这样 我们就可以 使用更准确、更真实的渲染 来替换此前用于表示对象的边界框 为此 RoomPlan 推出了 一个新的结构: Model Provider Model Provider 会将类别和属性集 映射到 Model URL 从具有类别和属性的对象出发 你可以要求 Model Provider 提供相应的 Model URL 接着 将其推广到整个房间 即包含部分、表面、 以及具有类别和属性的对象的集合 导出函数中指定的 Model Provider 实例 可以将 3D Model URL 与房间中的每个对象进行关联 现在 我们已经有了一个 处理模型关联的结构 接下来 我们来了解一下 如何使用 Model Provider 你可以使用多种方法 填充 Model Provider 例如 你可以从数据库获取 与一组属性匹配的模型 或注释现有的目录 这里我会通过一个简单的示例 向你介绍 如何创建自己的小型资源目录 创建并使用目录共需要四步 首先 解析 RoomPlan 支持的 类别和属性 接着 为每个类别和属性集 关联一个模型 然后 实例化一个 Model Provider 最后 使用该实例导出房间 接下来 我们来看看如何使用模型 发现属性并创建 Model Provider 首先 我们会遍历 RoomPlan 支持的 所有类别 接着 我们为每个支持的类别 创建一个文件夹 稍后 我们会将模型 添加到每个文件夹 然后 我们向每个类别 请求 RoomPlan 支持的属性集 我们会为每个支持的属性集 创建一个文件夹 你可以在每个文件夹中 添加与类别或属性组对应的 3D 模型 至此 目录内容已经准备就绪 接下来 我们需要为其 创建一个索引文件 这里展示的是一个 用于处理目录索引的示例结构 该结构包含一个元素数组 每个元素都包含了 一个类别或属性集 并且 每个元素都引用了 对应模型的路径 现在 我们可以将索引文件 保存为 Plist 文件 并将目录保存为 Bundle 文件 每当我们希望导出房间模型 或希望使用 Model Provider 将对象关联到模型 我们就可以使用该目录 Bundle 文件 生成一个 Model Provider 而我们所需要做的就是 遍历目录类别和属性 找到相应的 modelURL 接着 在没有属性的情况下 将 modelURL 关联到目录 在有属性的情况下 将 modelURL 关联到属性集 最后一步就是调用导出函数 指定输出 URL、 model Provider 实例 以及 .model 选项 就是这样! 我们得到一个 USDZ 文件 其中的 3D 模型与扫描完全对应 如果还想效果更棒 你可以选择将 USDZ 文件 导入 Blender 等 DCC 工具中 以添加光照和阴影 从而 你便可在几分钟内 得到更加逼真的结果 为了帮助你创造更真实的结果 我们在示例代码中添加了 一个预先填充的目录 时间交还给你了 Alex! Alex:谢谢你 Antoine 接下来 我们来回顾一下 今天分享的内容 首先 自定义 ARSession 支持 来实现新的用例 例如在扫描过程中 捕获高质量图像和音频 来优化不动产清单 现在 你还可以对多个房间 进行扫描 并使用新的 StructureBuilder API 对整个房屋生成合成的 3D 模型 为了改善低视力用户的扫描体验 RoomPlan 现已在使用 RoomCaptureView 时支持旁白 RoomPlan 的新对象属性 可以让你更准确地描述扫描房间 最后 你现在可以使用 最新的导出 API 将自定义目录中的 3D 模型分配给 相应的扫描对象 这就是 iOS 17 中的 RoomPlan 我们很期待看到你 使用该框架创造出的精彩作品
-
-
3:00 - RoomPlan with custom ARSession
// RoomCaptureSession public class RoomCaptureSession { // Init: ARSession is an optional input for RoomCaptureSession public init(arSession: ARSession? = nil) { ... } // Stop: pauseARSession is used for whether to continue ARSession experience public func stop(pauseARSession: Bool = true) { ... } }
-
5:50 - MultiRoom support with Continuous ARSession
// Continuous ARSession // start 1st scan roomCaptureSession.run(configuration: captureSessionConfig) // stop 1st scan with continuing ARSession roomCaptureSession.stop(pauseARSession: false) // start 2nd scan roomCaptureSession.run(configuration: captureSessionConfig) // stop 2nd scan (pauseARSession = true by default) roomCaptureSession.stop()
-
7:30 - MultiRoom capture with loading ARWorldMap
// Capture with loading ARWorldMap // load ARWorldMap let arWorldMap = try NSKeyedUnarchiver.unarchivedObject(ofClass: ARWorldMap.self, from: data) // run ARKit relocalization let arWorldTrackingConfig = ARWorldTrackingConfiguration() arWorldTrackingConfig.initialWorldMap = arWorldMap roomCaptureSession.init() roomCaptureSession.arSession.run(arWorldTrackingConfig, options: []) // Wait for relocalization to complete // start 2nd scan roomCaptureSession.run(configuration: captureSessionConfig) // stop 2nd scan roomCaptureSession.stop()
-
9:40 - StructureBuilder
// StructureBuilder // create structureBuilder instance let structureBuilder = StructureBuilder(option: [.beautifyObjects]) // load multiple capturedRoom results to capturedRoomArray var capturedRoomArray: [CapturedRoom] = [] // run structureBuilder API to get capturedStructure let capturedStructure = try await structureBuilder.capturedStructure(from: capturedRoomArray) // export capturedStructure to usdz try capturedStructure.export(to: destinationURL)
-
10:11 - CapturedStructure
// CapturedStructure public struct CapturedStructure: Codable, Sendable { public var rooms: [CapturedRoom] public var walls: [Surface] public var doors: [Surface] public var windows: [Surface] public var openings: [Surface] public var objects: [Object] public var floors: [Surface] public var sections: [Section] public func export(to url: URL, metadataURL: URL? = nil, modelProvider: ModelProvider? = nil, exportOptions: USDExportOptions = .mesh) throws }
-
19:20 - Parse attributes and categories to create folder hierarchy
// Parse attributes and categories to create folder hierarchy for category in CapturedRoom.Object.Category.allCases { let url = generateFolderURL(category: category, attributes: []) FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) for attributes in category.supportedCombinations { let url = generateFolderURL(category: category, attributes: attributes) FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) } }
-
20:00 - Create a Catalog index
// Create a Catalog index struct RoomPlanCatalog: Codable { let categoryAttributes: [RoomPlanCatalogCategoryAttribute] } struct RoomPlanCatalogCategoryAttribute: Codable { enum CodingKeys: String, CodingKey { case folderRelativePath case category case attributes case modelFilename } let category: CapturedRoom.Object.Category let attributes: [any CapturedRoomAttribute] let folderRelativePath: String private(set) var modelFilename: String? = nil func encode(to encoder: Encoder) throws { … } }
-
20:15 - Create a Catalog bundle
// Create a Catalog bundle let catalog = RoomPlanCatalog(categoryAttributes: categoryAttributes) let plistEncoder = PropertyListEncoder() let data = try plistEncoder.encode(catalog) let catalogURL = inputURL.appending(path: "catalog.plist") try data.write(to: catalogURL) let fileWrapper = try FileWrapper(url: inputURL) try fileWrapper.write(to: outputURL, options: [.atomic, .withNameUpdating], originalContentsURL: nil)
-
20:22 - Instantiate a Model Provider from a Catalog
// Instantiate a Model Provider from a Catalog for categoryAttribute in catalog.categoryAttributes { guard let modelFilename = categoryAttribute.modelFilename else { continue } let folderRelativePath = categoryAttribute.folderRelativePath let modelURL = url.appending(path: folderRelativePath).appending(path: modelFilename) if categoryAttribute.attributes.isEmpty { try modelProvider.setModelFileURL(modelURL, for: categoryAttribute.category) } else { try modelProvider.setModelFileURL(modelURL, for: categoryAttribute.attributes) } }
-
20:47 - Exporting a captured room to usdz with models
// Exporting a captured room to usdz with models try capturedRoom.export(to: outputURL, modelProvider: modelProvider, exportOptions: .model)
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。