0
0

【visionOS/ARKit】ハンドトラッキングの最小構成サンプルコード【latestAnchors編】

Last updated at Posted at 2024-07-22

visionOSにおけるARKitのハンドトラッキングAPIの基本は、手の各ジョイントの座標や回転などのデータを取得する事です。

Joints Example SessionVideo

Joints List SessionVideo

今回は、たった70行ほどで動作する最小構成のハンドトラッキングAPIのサンプルコードを紹介します。

基礎知識

まず、このセッションビデオを見てください。visionOSのARKitやハンドトラッキングについて概ね分かります。

日本語字幕があります。15:05からハンドトラッキングについて説明しています。

2つのAPI

ジョイントデータを取得するAPIは2つあります。どちらもHandTrackingProviderインスタンスから取得します。

  • AsyncSequenceで最新の値を受け取るanchorUpdates
  • 最新の値が格納されているlatestAnchors

Observing hand anchor data

今回はlatestAnchorsで実装しました。

AsyncSequenceで実装する場合はこちらの記事を参照してください。

サンプルコードの概要

  • アプリの起動と同時にハンドトラッキングを開始する。
  • 両手の全てのジョイントにシンプルな球体オブジェクトを配置する。
  • 常に最新のジョイントの位置になるように各オブジェクトを更新する。
  • オブジェクトが手で隠れないようにする。

Example 1

Example 2

ソースコード全体

import SwiftUI
import RealityKit
import ARKit

@main
struct MyApp: App {
    var body: some SwiftUI.Scene {
        ImmersiveSpace {
            RealityView { content in
                for chirality in [HandAnchor.Chirality.left, .right] {
                    for jointName in HandSkeleton.JointName.allCases {
                        let jointEntity = ModelEntity(mesh: .generateSphere(radius: 0.006),
                                                      materials: [SimpleMaterial()])
                        
                        jointEntity.components.set(JointComponent(chirality: chirality,
                                                                  jointName: jointName))
                        
                        content.add(jointEntity)
                    }
                }
            }
        }
        .upperLimbVisibility(.hidden)
    }
    
    init() {
        JointComponent.registerComponent()
        JointSystem.registerSystem()
    }
}

struct JointComponent: Component {
    let chirality: HandAnchor.Chirality
    let jointName: HandSkeleton.JointName
}

struct JointSystem: System {
    private let session = ARKitSession()
    private let provider = HandTrackingProvider()
    
    init(scene: RealityKit.Scene) {
        setUpSession()
    }
    
    private func setUpSession() {
        Task {
            try! await session.run([provider])
        }
    }
    
    func update(context: SceneUpdateContext) {
        let entities = context.entities(matching: .init(where: .has(JointComponent.self)),
                                        updatingSystemWhen: .rendering)
        
        let (leftHandAnchor, rightHandAnchor) = provider.latestAnchors
        
        for handAnchor in [leftHandAnchor, rightHandAnchor] {
            guard let handAnchor else { continue }
            
            for entity in entities {
                let component = entity.components[JointComponent.self]!
                
                guard component.chirality == handAnchor.chirality,
                      let joint = handAnchor.handSkeleton?.joint(component.jointName) else {
                    continue
                }
                
                entity.setTransformMatrix(handAnchor.originFromAnchorTransform * joint.anchorFromJointTransform,
                                          relativeTo: nil)
            }
        }
    }
}

ソースコードはこれだけです。コピペしてください。

コードコピペ以外に必要な作業

  • Info.plistで「NSHandsTrackingUsageDescription」に任意のテキストを設定
  • Info.plistで「Preferred Default Scene Session Role」に「Immersive Space」を設定

Info.plist Screenshot

解説

前述のセッションビデオ内で説明されている内容は省略します。また、SwiftUIやRealityKitの基本的な知識についての説明も省略します。

latestAnchorsとは

The most recent hand anchors for each hand.
Accessing this tuple consumes its values and sets them to nil until the next anchor update.
【DeepL翻訳】 各ハンドの最新のアンカー。
このタプルにアクセスすると、その値が消費され、次のアンカー更新までnilに設定されます。

更新のポーリングをして最新のハンドアンカーの値を随時取得するというのが想定された実装パターンのようです。

ECSで構成

struct JointComponent: Component {
    let chirality: HandAnchor.Chirality
    let jointName: HandSkeleton.JointName
}

struct JointSystem: System {
    ...
    
    func update(context: SceneUpdateContext) {
        let entities = context.entities(matching: .init(where: .has(JointComponent.self)),
                                        updatingSystemWhen: .rendering)
        
        let (leftHandAnchor, rightHandAnchor) = provider.latestAnchors
        
        ...
    }
}

今回は、RealityKitでの典型的な実装パターンであるECS(Entity Component System)でポーリングを実装しました。

各エンティティの管理

struct JointComponent: Component {
    let chirality: HandAnchor.Chirality
    let jointName: HandSkeleton.JointName
}

今回は独自のComponentにHandAnchor.ChiralityやHandSkeleton.JointNameのプロパティを定義して管理しました。

latestAnchorsの取り出し方に注意

provider.latestAnchorsを呼び出すと値が「消費」されるため実装方法には注意が必要です。

//↓これはOK↓
let (leftHandAnchor, rightHandAnchor) = provider.latestAnchors

for handAnchor in [leftHandAnchor, rightHandAnchor] {
    ...
}

//↓これはNG↓
for handAnchor in [provider.latestAnchors.leftHand, provider.latestAnchors.rightHand] {
    ...
}

このNGの例ではprovider.latestAnchorsを2回呼び出すことになるので、2回目に取得される値が想定と異なる値(恐らくnil)になってしまいます。

手のオクルージョンを切り替える

両手のオクルージョン

visionOSでは両手のオクルージョンがデフォルトで有効になっています。これをオフにするにはSwiftUIのupperLimbVisibilityでhiddenと設定してください。

.upperLimbVisibility(.hidden)

アクセス権限リクエストの設定

アクセス権限リクエストのためにInfo.plistで「NSHandsTrackingUsageDescription」に任意のテキストを設定する必要があります。

多くの場合、Info.plistで何らかの値を設定するにはプルダウンメニューからkeyを指定します。しかし今回の場合はプルダウンメニューに表示されないので、「NSHandsTrackingUsageDescription」と直接入力してください。

Xcode Info.plist pull-down-menu

フルスペースでアプリを起動

@main
struct MyApp: App {
	...
    var body: some SwiftUI.Scene {
        ImmersiveSpace {
        	...
        }
        ...
	}
}

XcodeでvisionOSアプリプロジェクトを新規作成すると、ウインドウで起動するコードが生成されます。今回はシンプルな実装にするために、フルスペースで起動するようにしました。

Info.plistの「Preferred Default Scene Session Role」に「Immersive Space」を設定する必要があります。この設定をしないとアプリ起動直後にクラッシュします。

【注意】Apple Vision Pro実機が必要

ARKitのハンドトラッキングAPIを試すにはApple Vision Pro実機が必要です。シミュレーターでは全く動きません。

次のステップ

  • 現在のアクセス権限の状態を確認: session.queryAuthorization(for:)
  • アクセス権限許可を明示的にリクエスト: session.requestAuthorization(for:)
  • アンカーの状態を確認: AnchorUpdate.Event
  • 各アンカーや各ジョイントがトラッキング中かどうかを確認: TrackableAnchor.isTracked
  • DataProviderの状態変化を監視: ARKitSession.Events
  • 現在の実行環境がサポートしているかを確認: HandTrackingProvider.isSupported

リンク


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0