WWDC Quick Look 💓 By SwiftGGTeam
Meet ARKit for spatial computing

Meet ARKit for spatial computing

元の動画を見る

ハイライト

Apple は visionOS 向けに ARKit を全面的に書き直し、世界追跡、シーン再構築、平面検出、画像追跡、手部追跡をサポートする新しい C/Swift API を提供し、プライバシーを保ちながら没入型空間アプリを構築できます。

主要内容

iOS から visionOS へ: ARKit の変貌

2017 年の iOS 11 で ARKit は初めてスマホ上の AR 体験を可能にしました。6 年後、ARKit はスマホのフレームワークから visionOS のシステムレベルサービスへ成長しました。

visionOS では ARKit は OS に深く統合されます。ウィンドウ操作から没入ゲームまで、基盤は ARKit が駆動します。API も全面再設計され、モダンな Swift インターフェースと古典的な C インターフェースの両方を提供します。

プライバシー優先のアーキテクチャ

ARKit は世界を理解するためにカメラとセンサーデータが必要ですが、生データはシステムデーモンから出ません。センサーデータは ARKit デーモンに入り、Apple のアルゴリズムで安全に処理された後、キュレーションされた結果だけがアプリへ転送されます。

ARKit データ取得には 2 条件が必要: Full Space に入ること(Shared Space では取得不可)、ユーザー認可をリクエストすること。

3 つの中核概念

ARKit API は 3 つの基本部品で構成されます:

  • Anchor: 実世界の位置と向きを表し、一意 ID と変換行列を持つ。一部は追跡可能(TrackableAnchor)で、追跡喪失時は関連仮想コンテンツを隠す。
  • Data Provider: 単一の ARKit 機能を表し、ポーリングまたは更新の観察が可能。種類ごとに異なるデータを提供。
  • Session: 複数の Data Provider を組み合わせて実行。起動後、各 Provider は非同期でデータを受け取り、更新頻度はデータ型により異なる。

世界追跡: 仮想コンテンツを現実に固定

WorldTrackingProvider は仮想コンテンツ配置の基盤で、2 つの中核能力を提供します:

WorldAnchor: 開発者が追加した WorldAnchor は自動永続化され、アプリ起動やデバイス再起動後も有効。永続化されるのは ID と変換行列のみで、仮想コンテンツとの対応は開発者が維持します。

WorldAnchor の永続化は位置ベースです。自宅からオフィスへ移ると自宅マップはアンロードされオフィスマップがロードされます。戻ると ARKit が自動再定位し以前の Anchor を復元します。

デバイス姿勢(Device Pose): アプリ原点に対するデバイスの位置と向きを照会。Metal と CompositorServices でカスタムレンダリングする際に必須です。

シーン理解: 周囲環境の知覚

  • 平面検出: PlaneDetectionProvider が水平・垂直の PlaneAnchor を提供。整列、形状、意味分類(床、テーブルなど)を含む。
  • シーンジオメトリ: SceneReconstructionProvider が環境を細分化メッシュ(MeshAnchor)として再構築。頂点、法線、面、面ごとの意味分類を含み、高忠実度物理シミュレーションに使える。
  • 画像追跡: ImageTrackingProvider が実世界の 2D 画像を検出し、ImageAnchor と推定スケール係数を提供。

手部追跡: 新しいインタラクション次元

HandTrackingProvider は visionOS 向けの新機能で、HandAnchor を提供します:

  • Chirality: 左手または右手
  • Skeleton: 全関節を含む手の骨格
  • Transform: アプリ原点に対する手首の変換行列

各関節には親関節、名前、localTransform(親相対)、rootTransform(根相対)、isTracked フラグがあります。

手部追跡で可能な操作は 2 種: 手相対位置への仮想コンテンツ配置、カスタムジェスチャ検出。

TimeForCube 例: 総合活用

デモ App 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 カスタムレンダリングの没入 App では 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() で世界追跡 Provider 作成
  • ar_data_providers_create()ar_data_providers_add_data_provider() で 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() で手部追跡 Provider 作成
  • ar_hand_anchor_create() で手部アンカーを事前確保
  • ar_hand_tracking_provider_get_latest_anchors() で最新手部データをポーリング
  • ar_trackable_anchor_is_tracked() で追跡状態確認
  • ar_anchor_get_origin_from_anchor_transform() でアプリ原点相対のアンカー変換取得

TimeForCube App 構造

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 コンテンツ表示
  • 3 つの .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 Data Provider を管理
  • HandTrackingProviderSceneReconstructionProvider は具体的 Provider
  • 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: 新エンティティ作成、変換・衝突・物理・InputTarget を設定
  • 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 cm 上
  • ModelEntity でメッシュ、マテリアル、衝突形状、質量を同時作成
  • InputTargetComponent(allowedInputTypes: .indirect) は間接入力(注視+ピンチなど)のみ許可
  • PhysicsMaterialResource.generate(friction:restitution:) で物理マテリアルカスタマイズ
  • PhysicsBodyComponent(mode: .dynamic) で重力の影響を受ける動的物理体
  • シーン衝突体上に落下後、手の衝突体で押せる

重要ポイント

  • やること: 実際の部屋で家具をプレビューできる仮想家具配置 App を開発する。

  • 価値: ARKit の WorldAnchor は自動永続化され、複数回の装着後も同じ位置に留まる。PlaneDetectionProvider で机や床を識別し精密配置に使える。

  • 始め方: PlaneDetectionProvider で平面検出、WorldTrackingProviderWorldAnchor を追加して家具を固定し、RealityKit で USDZ を読み込む。

  • やること: ジェスチャで曲切り替え・音量調整する音楽プレイヤーを作る。

  • 価値: HandTrackingProvider は 26 関節を精密追跡し、ピンチ・開く・振るなどのカスタムジェスチャを認識。追加ハードウェア不要。

  • 始め方: HandTrackingProvider.anchorUpdates を監視し、Skeleton.JointrootTransform 変化パターンを分析して再生制御のジェスチャ状態機械を定義。

  • やること: 実際の本のページに 3D モデルとアニメーションを重ねる教育 App を構築する。

  • 価値: ImageTrackingProvider が特定参照画像を検出し ImageAnchor を提供。ReferenceImage はアセットカタログやコードから作成可能。

  • 始め方: ImageTrackingProviderReferenceImage を設定し、検出時に ImageAnchor 位置へ RealityKit エンティティを読み込み、ImageAnchor.estimatedScaleFactor でサイズ補正。

  • やること: ユーザー動作を追跡してフィードバックする没入型フィットネス App を開発する。

  • 価値: 手部追跡とシーンジオメトリの組み合わせで、手が仮想ターゲットに触れたか、身体が正しい空間位置にあるかを判断できる。

  • 始め方: HandTrackingProviderSceneReconstructionProvider を併用し、シーンメッシュ上に仮想ターゲットを配置、手関節とターゲットの衝突を検出。

関連セッション

コメント

GitHub Issues · utterances