Swift初心者がApple Vision Proアプリ作成.その1 / ジェスチャ編

こんにちは。
クラウドソリューショングループのsatokです。
最近Apple Vision Pro(以下AVP)を用いてアプリを作成し始めました。
今回はそのアプリのジェスチャ部分まで実装が完成したので紹介します。
iOS製品に触ったことが無ければ開発でswiftに触ったことも無かった初学者のため、温かい目で見て頂ければと幸いです。
 
目次
1. アプリの概要
まず始めに作成しようとしているアプリの概要です。
今回私が作成しようとしているものは、ゲームライクなメニュー画面呼出しアプリです。
人差し指と中指をくっつけて伸ばし、その状態で上から下に振り下ろす(スワイプする)ことでメニュー画面を呼び出します。
(某フルダイブデスゲームアニメのメニュー画面のイメージです。)
そのため、実装する内容は大きく以下のように分かれています。
- ジェスチャの検知
- 検知後のイベント処理
- 処理後に表示する画面
今回の記事では「ジェスチャの検知」の部分を紹介します。
 
2. 実装内容
続いて実装についてです。
AVPのコードに触れるのは初めてだったため、サンプルコードを参考にして実装することにしました。
今回参考にしたのはAppleから出されているHappy Beamです。
ここからは最低限ジェスチャを取得、処理できるために必要な要素を挙げていきます。
ジェスチャ使用の許可
まず、ユーザのジェスチャを検知するため、AVPに対して許可が必要です。
Information Property Listに NSHandsTrackingUsageDescription を追加し、Valueに警告用の文字列を入れることでユーザに許可を求めることができます。

ここで求めた許可は初回起動時のみ表示されるものなので、あまり頻繁に目にするものではありませんがしっかり設定しておきましょう。
イマーシブスペース定義
続いてコンテンツを表示したりジェスチャを検知するためのイマーシブスペースの定義です。
ここでは主にジェスチャに関する処理をinit、updateさせており、ジェスチャが発生し次第画面を開く処理をしています。
struct ImmersiveView: View {
    @ObservedObject var gestureModel: SwipeGestureModel
    @Environment(AppModel.self) var appModel
    @Environment(\.openWindow) private var openWindow
 
    var body: some View {
        RealityView { content in
            
        } update: { updateContent in
            let handsCenterTransform = gestureModel.checkSwipeGestured()
            if handsCenterTransform != nil {
                gestured()
            }
        }
        .task {
            await gestureModel.start()
        }
        .task {
            await gestureModel.publishHandTrackingUpdates()
        }
        .task {
            await gestureModel.monitorSessionEvents()
        }
    }
    
    private func gestured() {
        openWindow(id: "HPView")
        openWindow(id: "TimeView")
    }
}
初期化周りの
- start()
- publishHandTrackingUpdates()
- monitorSessionEvents()
はHappyBeamから拝借しているため、詳しい内容は割愛します。
本処理
最後にメインの処理となるSwipeGestureModelについてです。
やっていることとしては単純で
①必要な指の関節の座標を取得
②取得した座標からスワイプに必要な手の形かを判定
③スワイプの形になっていれば、振り下ろされたかを判定
という流れです。
それぞれコードを載せながら軽く解説します。
①必要な指の関節の座標を取得
必要なコードはHappyBeamからおおよそ流用しています。
今回は「右手の人差し指の先端」「右手の人差し指の付け根(MP関節)」「右手の中指の先端」の3箇所を使用しています。
(まだ使用していませんが、左手のトラッキングを行うためのコードも含まれています。)
		guard let leftHandAnchor = latestHandTracking.left,
			  let rightHandAnchor = latestHandTracking.right,
			  leftHandAnchor.isTracked, rightHandAnchor.isTracked else {
			return nil
		}
		
		guard
			let rightHandIndexFingerTip = rightHandAnchor.handSkeleton?.joint(.indexFingerTip),
			let rightHandIndexFingerKnuckle = rightHandAnchor.handSkeleton?.joint(.indexFingerKnuckle),
			let rightHandMiddleFingerTip = rightHandAnchor.handSkeleton?.joint(.middleFingerTip),
			rightHandIndexFingerTip.isTracked && rightHandIndexFingerKnuckle.isTracked &&
				rightHandMiddleFingerTip.isTracked
		else {
			return nil
		}
		
		let originFromRightHandIndexFingerTipTransform = matrix_multiply(
			rightHandAnchor.originFromAnchorTransform, rightHandIndexFingerTip.anchorFromJointTransform
		).columns.3.xyz
		let originFromRightHandIndexFingerKnuckleTransform = matrix_multiply(
			rightHandAnchor.originFromAnchorTransform, rightHandIndexFingerKnuckle.anchorFromJointTransform
		).columns.3.xyz
		let originalFromRightHandMiddleFingerTipTransform = matrix_multiply(
			rightHandAnchor.originFromAnchorTransform, rightHandMiddleFingerTip.anchorFromJointTransform
		).columns.3.xyz
手の各関節にはそれぞれ名前が定義されているため、必要に応じて取得することができます。

②取得した座標からスワイプに必要な手の形かを判定
ここからはオリジナルの部分です。
「右手の人差し指の先端」「右手の人差し指の付け根」「右手の中指の先端」の座標を使用し、今回は「人差し指の先端と中指の先端がくっついていること」と「人差し指が伸びた状態になっていること」を「必要な手の形」として定義しました。
		let distanceBetweenIndexAndMiddleFingerTips:Float = distance(
			originFromRightHandIndexFingerTipTransform,
			originalFromRightHandMiddleFingerTipTransform
		)
		let distanceBetweenIndexTipsAndKnuckles:Float = distance(
			originFromRightHandIndexFingerTipTransform,
			originFromRightHandIndexFingerKnuckleTransform
		)
		
		// スワイプ時の手の形になっているか判定(人差し指と中指がくっついているか、人差し指が伸びているか)
		let isSwipePosed = distanceBetweenIndexAndMiddleFingerTips  0.085
(余談)
ここの閾値に使用している値は私の手で試した際にちょうど良かった値でハードコーディングされてしまっていますが、今後は人によってここの値が変わるかを検証し、必要があればキャリブレーションできるよう改良していきたいですね。
③スワイプの形になっていれば、振り下ろされたかを判定
②でスワイプに必要な手の形になっていることが確認されたら、その形のまま手が振り下ろされたかを判定します。
今回は「スワイプの形で手が存在した最高y座標」を記録し、その値より一定距離振り下ろされていれば「スワイプされた」と判定しています。
		if !isSwipePosed {
			topYpoint = minusLimitYPoint	//スワイプの形じゃなければ指の最高座標をリセット
			return nil
		}
		
		// 最高座標更新
		if originFromRightHandIndexFingerTipTransform.y > topYpoint {
			topYpoint = originFromRightHandIndexFingerTipTransform.y
		}
		
		// 最高座標よりある程度振り下ろされているかを確認
		let isSwipeGestured = (topYpoint - originFromRightHandIndexFingerTipTransform.y) > 0.25
3. 動作風景
以上のコード(+α)で動作させている動画を載せておきます。
イベントが発火した先はまだ何も作れていないので、今回は新しくウィンドウが生成されるのみにしています。
4. 最後に
今回はSwift初学者がApple Vision Pro向けのアプリを作成する様子をお届けしました。
イマーシブ空間におけるアプリ作成はスマホやPC向けとは違った自由度の高さがあり、作成していてとてもワクワクしました。
次回は発火したイベントにて表示されるヴィジュアル部分を作成していきます。
また近いうちにお会いしましょう。
 
	             
  







