WWDC Quick Look 💓 By SwiftGGTeam
Explore enhancements to RoomPlan

Explore enhancements to RoomPlan

观看原视频

Highlight

RoomPlan 在 iOS 17 支持多房间扫描合并、自定义 ARSession 集成、VoiceOver 无障碍访问,以及用 3D 模型替换扫描结果中的方块物体,让房产测绘、室内设计和空间规划应用从单房间工具升级为完整的空间解决方案。

核心内容

RoomPlan 从 iOS 16 开始让 iPhone 和 iPad 具备了扫描房间并生成 3D 模型的能力。但第一版有明显限制:只能扫一个房间,扫描时不能和其他 AR 功能混用,扫描结果中的家具只是彩色方块。

iOS 17 的四项更新解决了这些痛点。

自定义 ARSession

01:36)以前 RoomPlan 自己管理 ARSession,开发者插不进去。现在可以在初始化时传入自定义的 ARSession:

// RoomCaptureSession 现在接受可选的 ARSession
public class RoomCaptureSession {
    public init(arSession: ARSession? = nil) {
        // ...
    }

    public func stop(pauseARSession: Bool = true) {
        // ...
    }
}

这打开了几个新场景:

  • AR 互动增强:把 RoomPlan 扫描结果和 ARKit 的场景几何、平面检测结合,让虚拟内容更精确地贴合真实空间
  • 房产展示升级:扫描房间的同时用 ARKit 采集高质量照片和视频,生成带 3D 模型和实景照片的房源页面
  • 不干扰现有 AR:在已有 AR 体验中加入 RoomPlan,不会破坏已有的 ARAnchor

03:20stop(pauseARSession:)pauseARSession 参数控制扫描结束后 ARSession 是否继续运行。设为 false 时,ARSession 保持活跃,为下一个功能或下一次扫描做准备。

多房间扫描

03:53)以前扫完客厅再扫卧室,两个房间各自独立,坐标系不同,手动合并时还会遇到重复墙壁和重复家具的问题。

iOS 17 提供了两种方案让多个房间共享同一个坐标系:

方案一:连续 ARSession

05:02)扫完第一个房间后不暂停 ARSession,直接开始扫下一个:

// 开始第一次扫描
roomCaptureSession.run(configuration: captureSessionConfig)

// 结束第一次扫描,但保持 ARSession 运行
roomCaptureSession.stop(pauseARSession: false)

// 开始第二次扫描
roomCaptureSession.run(configuration: captureSessionConfig)

// 结束第二次扫描(默认暂停 ARSession)
roomCaptureSession.stop()

关键点:

  • pauseARSession: false 让 ARSession 在扫描间隔期保持运行
  • 同一个 roomCaptureSession 实例复用同一个 ARSession
  • 所有扫描结果自动共享同一个世界坐标系

方案二:ARSession 重定位

06:38)适合跨时段扫描,比如今天扫客厅,明天扫卧室:

// 加载之前保存的 ARWorldMap
let arWorldMap = try NSKeyedUnarchiver.unarchivedObject(
    ofClass: ARWorldMap.self,
    from: data
)

// 配置 ARKit 重定位
let arWorldTrackingConfig = ARWorldTrackingConfiguration()
arWorldTrackingConfig.initialWorldMap = arWorldMap

roomCaptureSession.init()
roomCaptureSession.arSession.run(arWorldTrackingConfig, options: [])

// 等待重定位完成...

// 开始第二次扫描
roomCaptureSession.run(configuration: captureSessionConfig)

// 结束第二次扫描
roomCaptureSession.stop()

关键点:

  • 第一次扫描结束后保存 ARWorldMap
  • 第二次扫描前加载 ARWorldMap 触发重定位
  • 重定位完成后,当前坐标系与之前对齐
  • 适合分时段、分次扫描的场景

合并房间:StructureBuilder

09:40)多个房间扫完后,用 StructureBuilder 合并:

// 创建 StructureBuilder 实例
let structureBuilder = StructureBuilder(option: [.beautifyObjects])

// 加载多个 CapturedRoom
var capturedRoomArray: [CapturedRoom] = []

// 合并为 CapturedStructure
let capturedStructure = try await structureBuilder.capturedStructure(
    from: capturedRoomArray
)

// 导出为 USDZ
try capturedStructure.export(to: destinationURL)

关键点:

  • StructureBuilder 自动处理重复墙壁和重复物体
  • .beautifyObjects 选项美化物体表示
  • 输出 CapturedStructure 包含合并后的完整空间信息

10:11CapturedStructure 的结构:

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
}

关键点:

  • rooms 数组保留每个房间的原始信息
  • wallsdoorswindows 等是合并去重后的结果
  • sections 描述房间的不同功能区域(客厅、卧室等)
  • 支持导出为 USDZ,可选附带元数据和 3D 模型

扫描体验优化

11:11)多房间扫描的最佳实践:

  • 适合单层住宅,1-4 个卧室的典型户型
  • 总扫描面积建议不超过 2000 平方英尺(约 186 平方米)
  • 光照条件建议 50 lux 以上
  • 扫描段之间保持图像重叠,帮助系统对齐

VoiceOver 支持

12:00)RoomCaptureView 新增 VoiceOver 音频反馈,让视障用户也能使用扫描功能:

  • “Move device to start”
  • “Point camera at bottom edge of wall”
  • “A fireplace. A wall.”
  • “A window.”

系统会语音描述检测到的物体和扫描进度,低视力用户不再需要依赖视觉反馈。

更丰富的房间表示

12:32)RoomPlan 现在支持更多房间类型和物体配置:

  • 斜墙和曲墙:以前只能处理直墙,现在支持多边形描述的异形墙面
  • 嵌入式厨房设备:洗碗机、烤箱、水槽等嵌入式设备能被检测并正确渲染
  • 沙发配置:单人沙发、L 型沙发、方形沙发等不同形状都能识别
  • Section 区域:房间可以划分为客厅、卧室、厨房、餐厅等功能区域
  • 父子关系:窗户属于某面墙,椅子属于某张桌子,洗碗机属于某个橱柜

用 3D 模型替换方块

17:26)以前扫描结果中的家具显示为彩色方块。现在可以用真实 3D 模型替换:

第一步:创建模型目录

// 遍历所有支持的物体类别
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)
    }
}

第二步:创建目录索引

struct RoomPlanCatalog: Codable {
    let categoryAttributes: [RoomPlanCatalogCategoryAttribute]
}

struct RoomPlanCatalogCategoryAttribute: Codable {
    let category: CapturedRoom.Object.Category
    let attributes: [any CapturedRoomAttribute]
    let folderRelativePath: String
    private(set) var modelFilename: String? = nil
}

第三步:打包为 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
)

第四步:从 catalog 创建 ModelProvider

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)
    }
}

第五步:导出带模型的 USDZ

try capturedRoom.export(
    to: outputURL,
    modelProvider: modelProvider,
    exportOptions: .model
)

关键点:

  • ModelProvider 把物体类别和属性映射到 3D 模型 URL
  • 没有属性的物体按类别匹配,有属性的按属性组合匹配
  • 导出时指定 .model 选项替换方块为真实模型
  • Sample code 包含预填充的模型目录作为起点

元数据映射

16:08)导出 USDZ 时可以附带元数据映射文件,把 USDZ 节点和 CapturedRoom 元素关联起来:

try capturedRoom.export(
    to: outputURL,
    metadataURL: metadataURL
)

生成的映射文件是 String 到 UUID 的字典,让你可以在渲染扫描结果时查询墙面尺寸、物体属性等额外信息。

详细内容

Object Attributes 详解

RoomPlan 现在用 attributes 更精确地描述物体。以椅子为例:

  • Category:chair
  • Attributes:可以是 stool(凳子)、dining chair(餐椅)、office chair(办公椅)

Attributes 通过多态枚举数组暴露,但处理起来不够直观。建议结合 ModelProvider 把 attributes 映射到具体的 3D 模型。

墙壁和地板的多边形表示

非均匀墙壁现在用 polygonCorners 描述为多边形。地板在扫描期间显示为矩形,扫描结束后美化为多边形。曲墙和曲窗在 RoomCaptureView 的最终结果中也能渲染。

导出选项对比

选项输出用途
.mesh网格 USDZ通用 3D 预览
.model带模型的 USDZ需要真实家具模型的场景
metadataURL映射文件查询额外信息

核心启发

1. 做一个房产展示应用

  • 做什么:扫描整套房子生成 3D 模型,结合高质量照片创建沉浸式房源展示
  • 为什么值得做:多房间合并 + 自定义 ARSession 让扫描和拍照可以同时进行,ModelProvider 让家具显示为真实模型
  • 怎么开始:用连续 ARSession 扫描各个房间,StructureBuilder 合并,导出带 metadata 的 USDZ,在网页中用 Quick Look 展示

2. 做一个室内设计规划工具

  • 做什么:扫描客户房间,在 3D 模型中预览家具摆放效果
  • 为什么值得做:RoomPlan 自动检测墙壁、门窗位置,Section 区分功能区域,ModelProvider 用真实家具模型替换方块
  • 怎么开始:扫描房间获取结构,用 ModelProvider 加载家具目录,在 AR 中实时预览摆放效果

3. 做一个无障碍空间导航应用

  • 做什么:帮助视障用户了解陌生空间的布局
  • 为什么值得做:VoiceOver 支持让扫描过程本身无障碍,扫描结果可以语音描述房间结构和物体位置
  • 怎么开始:集成 RoomCaptureView,扫描后用语音播报检测到的墙壁、门窗、家具及其相对位置

4. 做一个家具电商的 AR 预览功能

  • 做什么:用户扫描自己的房间,在真实空间中预览电商平台的家具
  • 为什么值得做:RoomPlan 提供精确的房间几何,自定义 ARSession 可以同时运行平面检测放置虚拟家具
  • 怎么开始:用 RoomPlan 获取房间结构,用 ARKit 平面检测在地板/墙面上放置家具模型,结合 ModelProvider 使用真实产品模型

关联 Session

评论

GitHub Issues · utterances