ハンドトラッキングを読み解く
はじめに
visionOSのハンドトラッキングは、ARKitを使用して実装されています。Appleが提供しているHappy Beamでは、ユーザーがハート型のジェスチャーを作ると、そこからビームが発射されるという仕組みになっています。このコードを参考にしながらハンドトラッキングを読み解きます。
実装
具体的なコードは下記のハンドトラッキングのサンプルコードを参照してください。
https://github.com/tochi/HandGestureExample/tree/handtracking_origin
まずは、ハンドトラッキングの処理をまとめたHandGestureModel.swiftについて見てみます。
# HandGestureModel.swift
let session = ARKitSession()
var handTracking = HandTrackingProvider()
func start() async {
do {
if HandTrackingProvider.isSupported {
print("ARKitSession starting.")
try await session.run([handTracking])
}
} catch {
print("ARKitSession error:", error)
}
}
visionOSでハンドトラッキングを利用するにはHandTrackingProviderを利用します。HandTrackingProviderを利用すると、リアルタイムにImmersive Space上での手や指の位置を取得することができます。HandTrackingProviderはARKitの一つなので、start()メソッドのsession.run([handTracking])でARKitのセッションとしてハントトラッキングが開始されます。
# HandGestureModel.swift
@Published var latestHandTracking: HandsUpdates = .init(left: nil, right: nil)
var handTracking = HandTrackingProvider()
struct HandsUpdates {
var left: HandAnchor?
var right: HandAnchor?
}
func publishHandTrackingUpdates() async {
for await update in handTracking.anchorUpdates {
switch update.event {
case .updated:
let anchor = update.anchor
guard anchor.isTracked else { continue }
if anchor.chirality == .left {
latestHandTracking.left = anchor
} else if anchor.chirality == .right {
latestHandTracking.right = anchor
}
default:
break
}
}
}
ishHandTrackingUpdates()メソッドでは、HandTrackingProviderの.anchorUpdatesを呼び出しています。.anchorUpdatesは、左右の手の状況が変化する度に呼び出されます。
呼び出されたupdate.eventからanchorを取り出しanchor.chiralityで左右どちらの手なのかを判断して、latestHandTrackingに取得したanchorをセットしています。
@Published var latestHandTracking: HandsUpdates = .init(left: nil, right: nil)
var rightHandAnchorOriginFromAnchorTransform: simd_float4x4? {
guard let rightHandAnchor = latestHandTracking.right, rightHandAnchor.isTracked else { return nil }
return rightHandAnchor.originFromAnchorTransform
}
var leftHandAnchorOriginFromAnchorTransform: simd_float4x4? {
guard let leftHandAnchor = latestHandTracking.left, leftHandAnchor.isTracked else { return nil }
return leftHandAnchor.originFromAnchorTransform
}
struct HandsUpdates {
var left: HandAnchor?
var right: HandAnchor?
}
後は、latestHandTrackingからlet rightHandAnchor = latestHandTracking.rightのように左右のHandAnchorを取り出します。取り出したHandAnchorからrightHandAnchor.originFromAnchorTransformで座標系が入ったsimd_float4x4を取り出します。
# ImmersiveView.swift
@ObservedObject var gestureModel: HandGestureModel
var body: some View {
RealityView { content in
...
}
.task {
await gestureModel.start()
}
.task {
await gestureModel.publishHandTrackingUpdates()
}
}
RealityViewの.taskでgestureModel.start()でARKitのセッションを開始して、gestureModel.publishHandTrackingUpdates()でトラッキング情報をリアルタイムに取得できるようにします。
# ContentView.swift
@ObservedObject var gestureModel: HandGestureModel
var body: some View {
VStack() {
Spacer()
HStack() {
handAnchorLogView(title: "Left", handAnchorOriginFromAnchorTransform: gestureModel.leftHandAnchorOriginFromAnchorTransform)
.padding()
handAnchorLogView(title: "Right", handAnchorOriginFromAnchorTransform: gestureModel.rightHandAnchorOriginFromAnchorTransform)
}
.padding(padding)
Spacer()
ToggleImmersiveSpaceButton()
Spacer()
}
}
func handAnchorLogView(title: String, handAnchorOriginFromAnchorTransform: simd_float4x4?) -> some View {
VStack(alignment: .leading) {
Text("\(title) Hand Anchor")
.font(.title)
if let handAnchorOriginFromAnchorTransform = handAnchorOriginFromAnchorTransform {
Text("x: \(String(describing: roundUp(handAnchorOriginFromAnchorTransform.columns.3.x)))")
Text("y: \(String(describing: roundUp(handAnchorOriginFromAnchorTransform.columns.3.y)))")
Text("z: \(String(describing: roundUp(handAnchorOriginFromAnchorTransform.columns.3.z)))")
} else {
Text("x:")
Text("y:")
Text("z:")
}
}
}
func roundUp(_ value: Float) -> Float {
floor(value * 1000) / 1000
}
ContentView.swiftでgestureModel.leftHandAnchorOriginFromAnchorTransformで手の座標系を取得します。Immersice Space上での手のXYZの座標系は.columns.3で取得可能です。
取得した値をWindowに表示してみると下記のようになります。
右手 X:0.083、Y:1.432、Z:-0.329
左手 X:-0.117、Y:1.435、Z:-0.314
これがImmersive Space内での両手の位置を表す座標です。
Immersive Spaceの原点
先ほど両手の座標が分かりましたが、これはどこを原点とした座標なのでしょうか。この座標は、Immersive Spaceの原点からの座標です。原点は、頭の頂点からまっすぐ下に降ろした線が地面と接する点に設定されています。visionOSのImmersive Spaceでは、座標系として右手系が採用されています。そのため、先程の手の座標も、まっすぐ前に手を挙げたとき、以下のようになります。
右手のX座標:正の値
左手のX座標:負の値
Immersive Spaceの原点は、session.run([handTracking])を実行したタイミングで決定されます。今回のコードの場合、以下の2つのタイミングで原点が決定されます。
Immersive Spaceに入った時
Vision Proの右上にあるジョグダイヤルを長押しして、ホームポジションのリセットを行った時
これらのタイミングで原点が再設定されるため、ユーザーは必要に応じて空間内での位置を調整することができます。
ハンドトラッキングの原点
では、ハンドトラッキングの原点はどこにあるのでしょう?実は、手首のところにあります。
右手 X:0.083、Y:1.432、Z:-0.329
左手 X:-0.117、Y:1.435、Z:-0.314
つまり、先ほどのコードで出力された座標は、Immersive Spaceの原点から手首までの距離を表しています。例えば、右手の場合、以下のように解釈できます。
X軸:体の中心から8.3cm右側
Y軸:地面から1.4mの高さ
Z軸:体の手前側32.9cmの位置
これらの数値から、右手の手首がImmersive Space内のどの位置にあるかを正確に把握することができます。
まとめ
visionOSのImmersive Spaceにおけるハンドトラッキングは、非常に高精度かつリアルタイムで行われています。
座標系: 右手系を採用し、原点はユーザーの足元に設定されています。
精度: かなり小さな単位で手の動きを追跡し、リアルタイムに座標を更新します。
原点の決定: session.run([handTracking])の実行時や、ホームポジションのリセット時に原点が設定されます。
座標の意味: 出力される座標は、原点から手首までの距離を表し、メートル単位で表示されます。
この高精度なハンドトラッキングシステムにより、ユーザーの手の位置を正確に把握することでき、Immersive Space内での没入感のある体験を提供することができます。