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 功能:
- 场景重建生成碰撞体,用于物理模拟和手势目标
- 手部追踪在指尖创建隐形碰撞体
- SpatialTapGesture 检测点击,在点击位置上方生成立方体
- 物理系统让立方体落到场景碰撞体上
- 手部碰撞体允许用户用手”推”立方体
详细内容
授权 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:)按名称查询关节,如.handIndexFingerTipparentJoint获取父关节引用,构成层级结构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 数据提供者HandTrackingProvider和SceneReconstructionProvider是具体的数据提供者meshEntities用字典映射 MeshAnchor ID 到 ModelEntityfingerEntities为左右手各创建一个指尖实体
处理手部更新
(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.Joint的rootTransform变化模式,定义手势状态机触发播放控制。 -
做什么:构建一个教育类应用,在真实书籍页面上叠加 3D 模型和动画。
-
为什么值得做:ImageTrackingProvider 可以检测特定的参考图像,并在检测到图像时提供 ImageAnchor。ReferenceImage 可以从资源目录或代码中创建。
-
怎么开始:创建
ImageTrackingProvider并配置ReferenceImage,在检测到ImageAnchor时在其位置加载 RealityKit 实体,用ImageAnchor.estimatedScaleFactor校正内容大小。 -
做什么:开发一个沉浸式健身应用,追踪用户的动作并给出反馈。
-
为什么值得做:手部追踪和场景几何结合,可以判断用户手是否触碰到虚拟目标物,或者身体是否处于正确的空间位置。
-
怎么开始:同时使用
HandTrackingProvider和SceneReconstructionProvider,在场景网格上放置虚拟目标,检测手部关节与目标的碰撞。
关联 Session
- Build spatial experiences with RealityKit — RealityKit 在 visionOS 上的基础介绍,配合 ARKit 数据构建沉浸式场景。
- Bring your ARKit app to spatial computing — 将现有 iOS ARKit 应用迁移到 visionOS 的专项指南。
- Create immersive Unity apps — 使用 Unity 为 visionOS 创建沉浸式应用,涉及 ARKit 数据集成。
- Optimize your 3D assets for spatial computing — 优化 3D 资源以在 visionOS 上获得更好性能。
评论
GitHub Issues · utterances