WWDC Quick Look 💓 By SwiftGGTeam
Meet ARKit for spatial computing

Meet ARKit for spatial computing

观看原视频

Highlight

Apple 为 visionOS 彻底重写了 ARKit,提供全新的 C 和 Swift API,支持世界追踪、场景重建、平面检测、图像追踪和手部追踪,让开发者能在保护隐私的前提下构建沉浸式空间应用。

核心内容

从 iOS 到 visionOS:ARKit 的蜕变

2017 年 iOS 11 发布时,ARKit 让开发者第一次在手机上构建增强现实体验。六年过去,ARKit 从手机上的一个框架,成长为 visionOS 的系统级服务。

在 visionOS 上,ARKit 深度融入操作系统。从窗口交互到沉浸式游戏,底层都由 ARKit 驱动。API 也经历了彻底的重设计,同时提供现代 Swift 接口和经典 C 接口。

隐私优先的架构

ARKit 需要摄像头和传感器数据来理解世界,但这些原始数据永远不会离开系统守护进程。传感器数据直接进入 ARKit 的守护进程,由 Apple 的算法安全处理后,才将 curated 结果转发给应用。

应用要获取 ARKit 数据,必须满足两个条件:进入 Full Space(Shared Space 中无法获取 ARKit 数据),并请求用户授权。

三大核心概念

ARKit API 由三个基本构件组成:

  • Anchor:表示真实世界中的位置和方向,包含唯一标识符和变换矩阵。部分 Anchor 是可追踪的(TrackableAnchor),追踪丢失时应隐藏关联的虚拟内容。
  • Data Provider:代表单个 ARKit 功能,允许轮询或观察数据更新。不同类型的 Data Provider 提供不同类型的数据。
  • Session:将一组 Data Provider 组合在一起运行。Session 启动后,各个 Data Provider 开始异步接收数据,更新频率因数据类型而异。

世界追踪:让虚拟内容锚定现实

WorldTrackingProvider 是放置虚拟内容的基础。它提供两种核心能力:

WorldAnchor:开发者添加的 WorldAnchor 会自动持久化,跨应用启动和设备重启后仍然有效。持久化的只有 Anchor 的标识符和变换矩阵,虚拟内容本身由开发者维护映射关系。

WorldAnchor 的持久化基于位置。从家到办公室,家的地图会被卸载,办公室的地图加载。返回时,ARKit 会自动重新定位并恢复之前的 Anchor。

设备姿态(Device Pose):查询设备相对于应用原点的位置和方向。这在用 Metal 和 CompositorServices 进行自定义渲染时是必需的。

场景理解:感知周围环境

  • 平面检测(Plane Detection):PlaneDetectionProvider 提供水平和垂直表面的 PlaneAnchor,包含对齐方式、几何形状和语义分类(如地板、桌面)。
  • 场景几何(Scene Geometry):SceneReconstructionProvider 将环境重建为细分网格(MeshAnchor),包含顶点、法线、面和每面的语义分类。可用于高保真物理模拟。
  • 图像追踪(Image Tracking):ImageTrackingProvider 检测真实世界中的 2D 图像,提供 ImageAnchor 和估计的缩放因子。

手部追踪:全新的交互维度

HandTrackingProvider 是 ARKit 在 visionOS 上的全新功能。它提供 HandAnchor,包含:

  • Chirality:左手或右手
  • Skeleton:手部骨骼,包含所有关节
  • Transform:手腕相对于应用原点的变换矩阵

每个关节包含父关节、名称、localTransform(相对于父关节)、rootTransform(相对于根关节)和 isTracked 标志。

开发者可以用手部追踪实现两种交互:将虚拟内容放置在手部相对位置,或检测自定义手势。

TimeForCube 示例:综合运用

演示应用 TimeForCube 展示了如何组合使用 ARKit 功能:

  1. 场景重建生成碰撞体,用于物理模拟和手势目标
  2. 手部追踪在指尖创建隐形碰撞体
  3. SpatialTapGesture 检测点击,在点击位置上方生成立方体
  4. 物理系统让立方体落到场景碰撞体上
  5. 手部碰撞体允许用户用手”推”立方体

详细内容

授权 API

05:20

在获取 ARKit 数据之前,需要请求用户授权。可以批量请求多种授权类型:

session = ARKitSession()

Task {
    let authorizationResult = await session.requestAuthorization(for: [.handTracking])

    for (authorizationType, authorizationStatus) in authorizationResult {
        print("Authorization status for \(authorizationType): \(authorizationStatus)")

        switch authorizationStatus {
        case .allowed:
            // All good!
            break
        case .denied:
            // Need to handle this.
            break
        // ...
        }
    }
}

关键点:

  • ARKitSession() 创建会话实例
  • requestAuthorization(for:) 异步请求授权,可传入数组批量请求多种类型
  • 返回结果按授权类型枚举,.denied 时需要应用自行处理降级逻辑
  • 如果不主动请求,ARKit 会在运行会话时自动弹窗提示

世界追踪与设备姿态(C API)

10:20

对于使用 Metal 自定义渲染的沉浸式应用,需要用 C API 查询设备姿态:

#include <ARKit/ARKit.h>
#include <CompositorServices/CompositorServices.h>

struct Renderer {
    ar_session_t                 session;
    ar_world_tracking_provider_t world_tracking;
    ar_pose_t                    pose;
    // ...
};

void renderer_init(struct Renderer *renderer) {
    renderer->session = ar_session_create();

    ar_world_tracking_configuration_t config = ar_world_tracking_configuration_create();
    renderer->world_tracking = ar_world_tracking_provider_create(config);

    ar_data_providers_t providers = ar_data_providers_create();
    ar_data_providers_add_data_provider(providers, renderer->world_tracking);
    ar_session_run(renderer->session, providers);

    renderer->pose = ar_pose_create();
    // ...
}

关键点:

  • ar_session_create() 创建 ARKit 会话
  • ar_world_tracking_provider_create() 创建世界追踪数据提供者
  • ar_data_providers_create()ar_data_providers_add_data_provider() 将数据提供者添加到集合
  • ar_session_run() 启动会话
  • ar_pose_create() 预分配姿态对象,避免在渲染循环中分配内存

在渲染函数中查询姿态:

10:21

void render(struct Renderer *renderer,
            cp_layer_t       layer,
            cp_frame_t       frame_encoder,
            cp_drawable_t    drawable) {
    const cp_frame_timing_t timing_info = cp_drawable_get_frame_timing(drawable);
    const cp_time_t presentation_time = cp_frame_timing_get_presentation_time(timing_info);
    const CFTimeInterval target_render_time = cp_time_to_cf_time_interval(presentation_time);

    simd_float4x4 pose = matrix_identity_float4x4;

    const ar_pose_status_t status =
        ar_world_tracking_provider_query_pose_at_timestamp(renderer->world_tracking,
                                                           target_render_time,
                                                           renderer->pose);

    if (status == ar_pose_status_success) {
        pose = ar_pose_get_origin_from_device_transform(renderer->pose);
    }

    // ...

    cp_drawable_set_ar_pose(drawable, renderer->pose);
    // ...
}

关键点:

  • cp_drawable_get_frame_timing() 从 CompositorServices 获取帧时间信息
  • cp_frame_timing_get_presentation_time() 获取目标呈现时间
  • ar_world_tracking_provider_query_pose_at_timestamp() 在指定时间点查询设备姿态
  • ar_pose_get_origin_from_device_transform() 提取设备相对于应用原点的变换矩阵
  • cp_drawable_set_ar_pose() 将姿态设置到 drawable 上,告知合成器渲染使用的姿态
  • 姿态查询相对昂贵,不建议用于内容放置等非渲染逻辑

手部追踪骨骼结构

16:00

Swift API 中的 Skeleton 结构:

@available(xrOS 1.0, *)
public struct Skeleton : @unchecked Sendable, CustomStringConvertible {

    public func joint(named: SkeletonDefinition.JointName) -> Skeleton.Joint

    public struct Joint : CustomStringConvertible, @unchecked Sendable {
        public var parentJoint: Skeleton.Joint? { get }
        public var name: String { get }
        public var localTransform: simd_float4x4 { get }
        public var rootTransform: simd_float4x4 { get }
        public var isTracked: Bool { get }
    }
}

关键点:

  • joint(named:) 按名称查询关节,如 .handIndexFingerTip
  • parentJoint 获取父关节引用,构成层级结构
  • localTransform 是相对于父关节的变换
  • rootTransform 是相对于手腕(根关节)的变换
  • isTracked 指示该关节当前是否被追踪到

手部追踪的 C API 初始化:

17:00

struct Renderer {
    ar_hand_tracking_provider_t  hand_tracking;
    struct {
        ar_hand_anchor_t left;
        ar_hand_anchor_t right;
    } hands;
    // ...
};

void renderer_init(struct Renderer *renderer) {
    // ...
    ar_hand_tracking_configuration_t hand_config = ar_hand_tracking_configuration_create();
    renderer->hand_tracking = ar_hand_tracking_provider_create(hand_config);

    ar_data_providers_t providers = ar_data_providers_create();
    ar_data_providers_add_data_provider(providers, renderer->world_tracking);
    ar_data_providers_add_data_provider(providers, renderer->hand_tracking);
    ar_session_run(renderer->session, providers);

    renderer->hands.left = ar_hand_anchor_create();
    renderer->hands.right = ar_hand_anchor_create();
    // ...
}

在渲染循环中轮询手部数据:

17:25

void render(struct Renderer *renderer, ... ) {
    // ...
    ar_hand_tracking_provider_get_latest_anchors(renderer->hand_tracking,
                                                 renderer->hands.left,
                                                 renderer->hands.right);

    if (ar_trackable_anchor_is_tracked(renderer->hands.left)) {
        const simd_float4x4 origin_from_wrist 
            = ar_anchor_get_origin_from_anchor_transform(renderer->hands.left);
        // ...
    }
    // ...
}

关键点:

  • ar_hand_tracking_provider_create() 创建手部追踪提供者
  • ar_hand_anchor_create() 预分配手部锚点对象
  • ar_hand_tracking_provider_get_latest_anchors() 轮询最新手部数据
  • ar_trackable_anchor_is_tracked() 检查锚点是否被追踪
  • ar_anchor_get_origin_from_anchor_transform() 获取锚点相对于应用原点的变换

TimeForCube 应用结构

18:00

@main
struct TimeForCube: App {
   @StateObject var model = TimeForCubeViewModel()

    var body: some SwiftUI.Scene {
        ImmersiveSpace {
            RealityView { content in
                content.add(model.setupContentEntity())
            }
            .task {
                await model.runSession()
            }
            .task {
                await model.processHandUpdates()
            }
            .task {
                await model.processReconstructionUpdates()
            }
            .gesture(SpatialTapGesture().targetedToAnyEntity().onEnded({ value in
                let location3D = value.convert(value.location3D, from: .global, to: .scene)
                model.addCube(tapLocation: location3D)
            }))
        }
    }
}

关键点:

  • ImmersiveSpace 是必需的,ARKit 数据只能在 Full Space 中获取
  • RealityView 呈现 3D 内容
  • 三个 .task 分别运行会话、处理手部更新、处理场景重建更新
  • SpatialTapGesture 检测空间中的点击手势

ViewModel 结构:

18:50

@MainActor class TimeForCubeViewModel: ObservableObject {
    private let session = ARKitSession()
    private let handTracking = HandTrackingProvider()
    private let sceneReconstruction = SceneReconstructionProvider()

    private var contentEntity = Entity()
    private var meshEntities = [UUID: ModelEntity]()
    private let fingerEntities: [HandAnchor.Chirality: ModelEntity] = [
        .left: .createFingertip(),
        .right: .createFingertip()
    ]
    // ...
}

关键点:

  • ARKitSession 管理所有 ARKit 数据提供者
  • HandTrackingProviderSceneReconstructionProvider 是具体的数据提供者
  • meshEntities 用字典映射 MeshAnchor ID 到 ModelEntity
  • fingerEntities 为左右手各创建一个指尖实体

处理手部更新

20:00

func processHandUpdates() async {
    for await update in handTracking.anchorUpdates {
        let handAnchor = update.anchor

        guard handAnchor.isTracked else { continue }

        let fingertip = handAnchor.skeleton.joint(named: .handIndexFingerTip)
        guard fingertip.isTracked else { continue }

        let originFromWrist = handAnchor.transform
        let wristFromIndex = fingertip.rootTransform
        let originFromIndex = originFromWrist * wristFromIndex

        fingerEntities[handAnchor.chirality]?.setTransformMatrix(originFromIndex, relativeTo: nil)
    }
}

关键点:

  • handTracking.anchorUpdates 是异步序列,持续产生手部锚点更新
  • 先检查 handAnchor.isTracked,丢失追踪时跳过
  • 获取食指指尖关节 .handIndexFingerTip
  • handAnchor.transform 是手腕相对于应用原点的变换
  • fingertip.rootTransform 是指尖相对于手腕的变换
  • 两者相乘得到指尖相对于应用原点的完整变换
  • 根据 handAnchor.chirality(.left 或 .right)更新对应指尖实体

处理场景重建更新

21:20

func processReconstructionUpdates() async {
    for await update in sceneReconstruction.anchorUpdates {
        let meshAnchor = update.anchor
        
        guard let shape = try? await ShapeResource.generateStaticMesh(from: meshAnchor) else { continue }
        
        switch update.event {
        case .added:
            let entity = ModelEntity()
            entity.transform = Transform(matrix: meshAnchor.transform)
            entity.collision = CollisionComponent(shapes: [shape], isStatic: true)
            entity.physicsBody = PhysicsBodyComponent()
            entity.components.set(InputTargetComponent())

            meshEntities[meshAnchor.id] = entity
            contentEntity.addChild(entity)
        case .updated:
            guard let entity = meshEntities[meshAnchor.id] else { fatalError("...") }
            entity.transform = Transform(matrix: meshAnchor.transform)
            entity.collision?.shapes = [shape]
        case .removed:
            meshEntities[meshAnchor.id]?.removeFromParent()
            meshEntities.removeValue(forKey: meshAnchor.id)
        @unknown default:
            fatalError("Unsupported anchor event")
        }
    }
}

关键点:

  • sceneReconstruction.anchorUpdates 异步序列产生网格锚点更新
  • ShapeResource.generateStaticMesh(from:) 从 MeshAnchor 生成碰撞形状
  • .added 事件:创建新实体,设置变换、碰撞体、物理体和输入目标组件
  • isStatic: true 表示场景几何是静态碰撞体
  • InputTargetComponent() 使实体可以接收手势
  • .updated 事件:更新现有实体的变换和碰撞形状
  • .removed 事件:从父节点移除并清理字典

添加立方体

22:20

func addCube(tapLocation: SIMD3<Float>) {
    let placementLocation = tapLocation + SIMD3<Float>(0, 0.2, 0)

    let entity = ModelEntity(
        mesh: .generateBox(size: 0.1, cornerRadius: 0.0),
        materials: [SimpleMaterial(color: .systemPink, isMetallic: false)],
        collisionShape: .generateBox(size: SIMD3<Float>(repeating: 0.1)),
        mass: 1.0)

    entity.setPosition(placementLocation, relativeTo: nil)
    entity.components.set(InputTargetComponent(allowedInputTypes: .indirect))

    let material = PhysicsMaterialResource.generate(friction: 0.8, restitution: 0.0)
    entity.components.set(PhysicsBodyComponent(shapes: entity.collision!.shapes,
                                               mass: 1.0,
                                               material: material,
                                               mode: .dynamic))

    contentEntity.addChild(entity)
}

关键点:

  • 放置位置在点击点上方 20 厘米处
  • ModelEntity 同时创建网格、材质、碰撞形状和质量
  • InputTargetComponent(allowedInputTypes: .indirect) 只允许间接输入(如眼睛注视+捏合)
  • PhysicsMaterialResource.generate(friction:restitution:) 自定义物理材质
  • PhysicsBodyComponent(mode: .dynamic) 创建动态物理体,受重力影响
  • 立方体落到场景碰撞体上后,可以用手部碰撞体推动

核心启发

  • 做什么:开发一个虚拟家具摆放应用,让用户在真实房间里预览家具。

  • 为什么值得做:ARKit 的 WorldAnchor 自动持久化,用户放置的虚拟家具在多次佩戴设备后仍保持在同一位置。PlaneDetectionProvider 可以识别桌面和地板,辅助精准放置。

  • 怎么开始:使用 PlaneDetectionProvider 检测平面,用 WorldTrackingProvider 添加 WorldAnchor 锚定家具位置,结合 RealityKit 加载 USDZ 模型。

  • 做什么:创建一个手势控制的音乐播放器,用手势切换歌曲、调节音量。

  • 为什么值得做:HandTrackingProvider 提供 26 个关节的精确追踪,可以识别捏合、张开、挥动等自定义手势。不需要额外的硬件控制器。

  • 怎么开始:监听 HandTrackingProvider.anchorUpdates,分析 Skeleton.JointrootTransform 变化模式,定义手势状态机触发播放控制。

  • 做什么:构建一个教育类应用,在真实书籍页面上叠加 3D 模型和动画。

  • 为什么值得做:ImageTrackingProvider 可以检测特定的参考图像,并在检测到图像时提供 ImageAnchor。ReferenceImage 可以从资源目录或代码中创建。

  • 怎么开始:创建 ImageTrackingProvider 并配置 ReferenceImage,在检测到 ImageAnchor 时在其位置加载 RealityKit 实体,用 ImageAnchor.estimatedScaleFactor 校正内容大小。

  • 做什么:开发一个沉浸式健身应用,追踪用户的动作并给出反馈。

  • 为什么值得做:手部追踪和场景几何结合,可以判断用户手是否触碰到虚拟目标物,或者身体是否处于正确的空间位置。

  • 怎么开始:同时使用 HandTrackingProviderSceneReconstructionProvider,在场景网格上放置虚拟目标,检测手部关节与目标的碰撞。

关联 Session

评论

GitHub Issues · utterances