Loading
BLOG 開発者ブログ

2024年6月27日

Apple Vision Proでカスタムジャスチャを利用してみた

custom_gesture

はじめに

こんにちは。
クラウドソリューショングループのyamasaki.sです。

ついにApple Vision Proが今週末、日本で発売されますね!
同時にVisionOS 2もリリースされ、コントロールセンターへのアクセスもより快適に行えるようになりました。
企業向けではあるものの、カメラ機能へのアクセスなど様々な機能が利用できるようになってきており今後も楽しみですね!

今回は、標準で用意されているジェスチャ以外を利用することができるカスタムジェスチャを実装しようと思います。

目次

カスタムジェスチャについて

vision OSでは、基本的にハンドトラッキングとアイトラッキングを利用して操作を行います。
標準で用意されているジェスチャとして以下が用意されています。

基本的には、上記で十分ですがアプリケーションに独自性を持たせたい時や標準のジェスチャ以外で操作させたい時に今回のカスタムジェスチャを利用することで実現可能です。
カスタムジェスチャを利用するためには、各関節に対して以下のようにそれぞれ名前が定義されているのでその名前を元に情報を取得することが可能になります。

カスタムジェスチャの実装

カスタムジェスチャ実装にあたって、Appleが提供しているサンプルコード「Happy Beam」を解析して実装を進めます。
「Happy Beam」では、カスタムジェスチャに関する必要な処理は「HeartGestureModel」にまとめられています。

func start() async {
    do {
        if HandTrackingProvider.isSupported {
            print("ARKitSession starting.")
            try await session.run([handTracking])
        }
    } catch {
        print("ARKitSession error:", error)
    }
}

上記は、ハンドトラッキングを利用するにあたってそもそもハンドトラッキングが利用可能な状態か、
利用可能であれば接続しトラッキングした情報が取得できるようにしているようですね。

func publishHandTrackingUpdates() async {
    for await update in handTracking.anchorUpdates {
        switch update.event {
        case .updated:
            let anchor = update.anchor
            
            // Publish updates only if the hand and the relevant joints are tracked.
            guard anchor.isTracked else { continue }
            
            // Update left hand info.
            if anchor.chirality == .left {
                latestHandTracking.left = anchor
            } else if anchor.chirality == .right { // Update right hand info.
                latestHandTracking.right = anchor
            }
        default:
            break
        }
    }
}

上記は、ハンドトラッキングした情報に更新があった場合に呼び出す処理のようです。
トラックングした情報を右手と左手に分けて変数に保持しているようですね。

次にカスタムジェスチャのメイン処理をしているメソッド「computeTransformOfUserPerformedHeartGesture()」ですが、長いので分割して解析しようと思います。

    // Get the latest hand anchors, return false if either of them isn't tracked.
    guard let leftHandAnchor = latestHandTracking.left,
          let rightHandAnchor = latestHandTracking.right,
          leftHandAnchor.isTracked, rightHandAnchor.isTracked else {
        return nil
    }
    
    // Get all required joints and check if they are tracked.
    guard
        let leftHandThumbKnuckle = leftHandAnchor.handSkeleton?.joint(.thumbKnuckle),
        let leftHandThumbTipPosition = leftHandAnchor.handSkeleton?.joint(.thumbTip),
        let leftHandIndexFingerTip = leftHandAnchor.handSkeleton?.joint(.indexFingerTip),
        let rightHandThumbKnuckle = rightHandAnchor.handSkeleton?.joint(.thumbKnuckle),
        let rightHandThumbTipPosition = rightHandAnchor.handSkeleton?.joint(.thumbTip),
        let rightHandIndexFingerTip = rightHandAnchor.handSkeleton?.joint(.indexFingerTip),
        leftHandIndexFingerTip.isTracked && leftHandThumbTipPosition.isTracked &&
        rightHandIndexFingerTip.isTracked && rightHandThumbTipPosition.isTracked &&
        leftHandThumbKnuckle.isTracked && rightHandThumbKnuckle.isTracked
    else {
        return nil
    }

まずは、上記部分ですが右手左手のトラッキング情報を取得しているようです。
その後、取得したトラッキング情報から、さらに各関節のトラッキング情報を取得しているようですね。

    // Get the position of all joints in world coordinates.
    let originFromLeftHandThumbKnuckleTransform = matrix_multiply(
        leftHandAnchor.originFromAnchorTransform, leftHandThumbKnuckle.anchorFromJointTransform
    ).columns.3.xyz
    let originFromLeftHandThumbTipTransform = matrix_multiply(
        leftHandAnchor.originFromAnchorTransform, leftHandThumbTipPosition.anchorFromJointTransform
    ).columns.3.xyz
    let originFromLeftHandIndexFingerTipTransform = matrix_multiply(
        leftHandAnchor.originFromAnchorTransform, leftHandIndexFingerTip.anchorFromJointTransform
    ).columns.3.xyz
    let originFromRightHandThumbKnuckleTransform = matrix_multiply(
        rightHandAnchor.originFromAnchorTransform, rightHandThumbKnuckle.anchorFromJointTransform
    ).columns.3.xyz
    let originFromRightHandThumbTipTransform = matrix_multiply(
        rightHandAnchor.originFromAnchorTransform, rightHandThumbTipPosition.anchorFromJointTransform
    ).columns.3.xyz
    let originFromRightHandIndexFingerTipTransform = matrix_multiply(
        rightHandAnchor.originFromAnchorTransform, rightHandIndexFingerTip.anchorFromJointTransform
    ).columns.3.xyz
    
    let indexFingersDistance = distance(originFromLeftHandIndexFingerTipTransform, originFromRightHandIndexFingerTipTransform)
    let thumbsDistance = distance(originFromLeftHandThumbTipTransform, originFromRightHandThumbTipTransform)
    
    // Heart gesture detection is true when the distance between the index finger tips centers
    // and the distance between the thumb tip centers is each less than four centimeters.
    let isHeartShapeGesture = indexFingersDistance < 0.04 && thumbsDistance < 0.04
    if !isHeartShapeGesture {
        return nil
    }

上記の部分では、手や間接で取得したトラッキング情報を空間座標に変換している処理のようです。
その後、左右の人差し指先、左右の親指先の距離の取得し指がくっついているかどうかを見ているようですね。
ここで、ハートマークを検出しているようですが、指先しか見ていないのでくっついていれば良さそうですね。

実装してみた

今回実装したカスタムジェスチャは、両手でつまむ動作を行うと玉が生成される仕組みです!
親指と人差し指でしか反応しないようにしているので、ちゃんと他の指では玉は生成されないのも確認できますね!

さいごに

Apple Vision Proの開発にあたって、先人のコードや記事をよく見たりしますが、
やはり実際にコードを解析しながら開発を進めた方が記事を見るだけより圧倒的に理解度が高くなるのでよいですね!

Apple Vision Proのアプリ開発で、まだ色々機能的な制限や端末的な制限がありできないことも多いですが、
これからの日本発売に向けてまた熱が上がるようなアプリケーションを開発していきたいですね!

yamasakiのブログ