visionOSにおけるARKitのハンドトラッキングAPIの基本は、手の各ジョイントの座標や回転などのデータを取得する事です。
今回は、たった70行ほどで動作する最小構成のハンドトラッキングAPIのサンプルコードを紹介します。
基礎知識
まず、このセッションビデオを見てください。visionOSのARKitやハンドトラッキングについて概ね分かります。
日本語字幕があります。15:05からハンドトラッキングについて説明しています。
2つのAPI
ジョイントデータを取得するAPIは2つあります。どちらもHandTrackingProviderインスタンスから取得します。
- AsyncSequenceで最新の値を受け取るanchorUpdates
- 最新の値が格納されているlatestAnchors
今回はlatestAnchorsで実装しました。
AsyncSequenceで実装する場合はこちらの記事を参照してください。
サンプルコードの概要
- アプリの起動と同時にハンドトラッキングを開始する。
- 両手の全てのジョイントにシンプルな球体オブジェクトを配置する。
- 常に最新のジョイントの位置になるように各オブジェクトを更新する。
- オブジェクトが手で隠れないようにする。
ソースコード全体
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」を設定
解説
前述のセッションビデオ内で説明されている内容は省略します。また、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」と直接入力してください。
フルスペースでアプリを起動
@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
リンク