🥽

visionOSでHandTrackingProviderを使用した3Dオブジェクト操作の実装

2024/12/20に公開

はじめに

この記事はxRギルド Advent Calendar 2024 の記事です。他にもXR系の記事が上がりますので是非読んでみてください。

今回は、HandTrackingProviderを使用して手の動きを検知し、3Dオブジェクトを操作する実装方法について解説します。

作成するアプリは以下のような感じです
作成したアプリ

HandTrackingProviderの基本

HandTrackingProviderでは、以下のような機能を利用することができます:

  • 手の位置と向きの検出
  • 各指の関節位置の取得
  • 左右の手の識別
  • リアルタイムトラッキング

これらの機能を組み合わせることで、直感的なオブジェクト操作を実現できます。

基本実装

HandTrackingManagerの実装

まずは、ハンドトラッキングの基本となる管理クラスを実装します。

@Observable
class HandTrackingManager {
    var leftHandPosition: SIMD3<Float>?
    var rightHandPosition: SIMD3<Float>?
    var errorMessage: String?
    let session = ARKitSession()
    let provider = HandTrackingProvider()
    
    func startHandTracking() async {
        do {
            try await session.requestAuthorization(for: [.handTracking])
            try await session.run([provider])
        } catch {
            self.errorMessage = error.localizedDescription
        }
    }
}

@Observableで状態管理をします。このクラスのプロパティが変更されると、UIが自動的に更新されます

手の位置検出

手の位置を正確に検出する処理は、この実装の要となります。

private func calculatePalmPosition(skeleton: HandSkeleton, anchor: HandAnchor) -> SIMD3<Float> {
    let metacarpalJoints: [HandSkeleton.JointName] = [
        .thumbTip, .indexFingerTip, .middleFingerTip, .ringFingerTip, .littleFingerTip
    ]
    
    let positions = metacarpalJoints.map { jointName -> SIMD3<Float> in
        let joint = skeleton.joint(jointName)
        let transform = anchor.originFromAnchorTransform * joint.anchorFromJointTransform
        return SIMD3<Float>(transform.columns.3.x, transform.columns.3.y, transform.columns.3.z)
    }
    
    return positions.reduce(SIMD3<Float>(repeating: 0), +) / Float(positions.count)
}

実装のポイント

  • 指先の位置の利用:
    • thumbTip: 親指
    • indexFingerTip: 人差し指
    • middleFingerTip: 中指
    • ringFingerTip: 薬指
    • littleFingerTip: 小指

今回は以上のポイントの利用をしていますが、HandSkeleton.JoinName細かく指定されており以下の公式ドキュメントから確認ができます。
https://developer.apple.com/documentation/arkit/handskeleton/jointname

  • 複数指の平均による安定性の確保: 複数の指先の位置から平均を計算することで、手の全体的な位置を安定して検出します。これにより、ノイズや一部の指の動きによる不安定さを軽減しています。

  • 適切な座標変換処理: 指先の位置を世界座標に変換するための計算です。オブジェクトの操作は世界座標で行われるため、正確な座標変換が不可欠です。

オブジェクト操作の実装

検出した手の位置を基に、3Dオブジェクトを操作する核となる実装です。

private func updateSpherePosition() async {
    guard let leftPos = handTrackingManager.leftHandPosition,
          let rightPos = handTrackingManager.rightHandPosition,
          let sphereEntity = sphereEntity else {
        return
    }
    
    let distance = length(leftPos - rightPos)
    let midPoint = (leftPos + rightPos) * 0.5
    let size = min(max(distance * Constants.sphereSizeMultiplier, Constants.minSphereSize), Constants.maxSphereSize)
    
    await MainActor.run {
        sphereEntity.position = midPoint
        sphereEntity.scale = SIMD3<Float>(repeating: size)
    }
}

この updateSpherePosition メソッドは、両手の位置をもとに3D空間内のオブジェクトを動的に操作するためのものです。手の位置と距離を利用して、オブジェクトの位置とサイズを調整しています。

実装のポイント

  1. 両手の距離の計算:
    let distance = length(leftPos - rightPos) では、両手の位置のベクトル差を計算し、その長さを求めています。これにより、手の広がり具合(主に手の開閉の度合い)が得られます。この情報を元に、オブジェクトのサイズを制御します。

  2. 中間点の計算:
    let midPoint = (leftPos + rightPos) * 0.5 では、両手の位置の平均を計算し、中間点を求めています。これを使用することで、オブジェクトを正確に両手の中間地点に配置することができます。

  3. オブジェクトサイズの計算と制限:
    let size = min(max(distance * Constants.sphereSizeMultiplier, Constants.minSphereSize), Constants.maxSphereSize) では、手の距離に基づいて球体のサイズを計算し、最小および最大のサイズ制限を適用しています。この制限により、オブジェクトが極端に大きくなったり小さくなったりして、操作が困難になることを防ぎます。

まとめ

今回はHandTrackingProviderを利用してハンドトラッキング人関するアプリを作成してみました。ハンドトラッキングのデバックには実機がなかったため今まで行えませんでしたが、XRギルドの備品を借りることができ検証を行うことができました。
ハンドトラッキングはvisionOSでの操作性を上げるポイントと今後はなってきそうだと思うので、今後もキャッチアップしていきたいです

参考資料

Discussion