Vision Pro 空間ジェスチャーを作る

# VisionGestureとは
[visionOSのハンドトラッキング](https://developer.apple.com/documentation/arkit/handtrackingprovider/)を使って空間ジェスチャーを作るオープンソースです。この記事ではVisionGestureについて紹介します。

![handtracking_01.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1839672/5d031ce6-5dbd-781f-e1ee-ae094018c552.jpeg)

VisionGestureの[Githubレポジトリはこちら](https://github.com/AlohaYos/VisionGesture)

# VisionGestureに含まれるもの
オープンソース[VisionGesture](https://github.com/AlohaYos/VisionGesture/)には以下のサンプルが含まれています。
 - visionOSで手の関節の動きをトラッキングするHandTrackingを使うサンプル
 - イマーシブ空間に浮かぶ仮想ハンドの制作方法
 - 空間ジェスチャーを簡単に自作できるテンプレート
 - 実機のVisionProが無くてもvisionOSシミュレータでハンドトラッキングを試せる"[HandTrackFake（疑似ハンドトラッキング）](https://github.com/AlohaYos/VisionGesture/blob/main/README.md#handtrackfake)"

## VisionGestureプレイグラウンド
実際にどんなモノが作れるのか、簡単に試してみることができるサンプル（プレイグラウンド）が付いています。遊んでみて、独自の空間ジェスチャーや3Dオブジェクトを作ってみましょう。

<iframe width="560" height="315" src="https://www.youtube.com/embed/-j5wBexSdOw?si=tYR65PoN0y0yRSyH" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>


VisionGestureをシミュレータで動作させる場合は[HandTrackFake.swift](https://github.com/AlohaYos/VisionGesture/blob/main/HandTrackFake/HandTrackFake.swift)の先頭付近にあるenableFakeをtrueにしてください。

```swift
class HandTrackFake: NSObject {
	var enableFake = true  // <--- true:シミュレータ, false:実機
	var rotateHands = false
	var zDepth: Float = 0.0
```

## 空間ジェスチャーの自作方法
VisionGestureにはジェスチャーのテンプレートコードが付いています。XcodeでVisionGestureプロジェクトを開いて"TODO: MyGesture"を検索すると、ジェスチャーを作るためにコーディングが必要な部分がピックアップできます。
プロジェクト内にあるGestrue_Draw.swift と Gesture_Aloha.swiftを参考にすれば空間ジェスチャーをどのように作れば良いかが分かりますね。

### ジェスチャーのロジックを[Gesture_MyGesture.swift](https://github.com/AlohaYos/VisionGesture/blob/main/VisionGesture/VisionGesture/Gesture/Gesture_MyGesture.swift)に作ります
- いくつかの指の関節の位置関係を計算します
- 特定のポーズになったら、ジェスチャーが開始されたと判断します
- ジェスチャーが有効な間、デリゲートをコールします。この時、デリゲートメソッドにはイベント種類と空間でのジェスチャー位置を渡します
- 別の特定のポーズになったら、ジェスチャーが終了したと判断します

```swift
class Gesture_MyGesture: GestureBase
{
	override init() {
		super.init()
	}

	convenience init(delegate: Any) {
		self.init()
		self.delegate = delegate as? any GestureDelegate
	}

	// ジェスチャー判定ループ
	override func checkGesture(handJoints: [[[SIMD3<Scalar>?]]]) {
		self.handJoints = handJoints
		switch state {
		case .unknown:	// 初期状態
			// TODO: MyGesture: 最初のポーズを待つ
			if(isMyGesturePose()) {
				delegate?.gesture(gesture: self, event: GestureDelegateEvent(type: .Began, location: [CGPointZero]))
				state = State.waitForRelease
			}
			break
		case .waitForRelease:	// ジェスチャーが解除されるポーズを待つ
			// TODO: MyGesture: ジェスチャー中に行う処理をデリゲート（コールバック）
			let position:SIMD3<Float> = [0,0,0]
			delegate?.gesture(gesture: self, event: GestureDelegateEvent(type: .Moved3D, location: [position]))

			if(!isMyGesturePose()) {	// ジェスチャーが解除されるポーズ
				delegate?.gesture(gesture: self, event: GestureDelegateEvent(type: .Ended, location: [CGPointZero]))
				state = State.unknown
			}
			break
		default:
			break
		}
	}
	
	// TODO: MyGesture: ジェスチャーポーズを判定する
	func isMyGesturePose() -> Bool {
		if HandTrackProcess.handJoints.count > 0 {
			let check = 0
			// 下記のようなジェスチャー判定のための計測関数は"GestureBase.swift"内に用意されています
//			if isStraight(hand: .right, finger: .thumb){ check += 1 }
//			if isBend(hand: .right, finger: .index){ check += 1 }
//			if isBend(hand: .right, finger: .middle){ check += 1 }
//			if isBend(hand: .right, finger: .ring){ check += 1 }
//			if isStraight(hand: .right, finger: .little){ check += 1 }
			if check == 5 { return true }
		}
		return false
	}
}
```

### GestureDelegate([ImmersiveView.swift](https://github.com/AlohaYos/VisionGesture/blob/main/VisionGesture/VisionGesture/ImmersiveView.swift)内)にジェスチャーで行いたい処理を記述します
- Gesture_MyGesture.swiftのロジックで特定のジェスチャーと判断された場合、 GestureDelegateが呼び出されます  
- GestrueDelegateEventを使って、発生したイベントと空間座標を取得します

```swift
	// TODO: MyGesture: ジェスチャーを使った作業
	func handle_myGesture(event: GestureDelegateEvent) {
		switch event.type {
		case .Moved3D:
			if let pnt = event.location[0] as? SIMD3<Scalar> {
				// pnt ... イマーシブ空間でのジェスチャー位置
			}
		case .Fired:
			break
		case .Moved2D:
			break
		case .Began:
			break
		case .Ended:
			break
		case .Canceled:
			break
		default:
			break
		}
	}
```

### [GestureBase.swift](https://github.com/AlohaYos/VisionGesture/blob/main/VisionGesture/VisionGesture/Gesture/GestureBase.swift)で、ジェスチャー判断のために関節位置を計測するための関数を作ります
- まずは既にサンプルとして実装されてる関数を使います  
- 自分の制作するジェスチャー判定のための計測機能が足りない場合は、自作しましょう
- サンプルコードを見て関節位置をどのように比較するかを理解してください 
以下のサンプルでは isBend（特定の指は曲がっているか）、isStraight（特定の指は伸びているか）といった計測関数が実装されています。

```swift
class GestureBase {
	// MARK: 関節位置を比較する

	func cv2(_ pos: SIMD3<Scalar>?) -> CGPoint? {
		guard let p = pos else { return CGPointZero }
		return CGPointMake(CGFloat(p.x),CGFloat(p.y))
	}
	
	// 指は曲がっているか、伸びているか
	func isBend(pos1: CGPoint?, pos2: CGPoint?, pos3: CGPoint? ) -> Bool {
		guard let p1 = pos1, let p2 = pos2, let p3 = pos3 else { return false }
		if p1.distance(from: p2) > p1.distance(from: p3) { return true }
		return false
	}
	func isBend(hand: HandTrackProcess.WhichHand, finger: HandTrackProcess.WhichFinger) -> Bool {
		let posTip: CGPoint? = cv2(jointPosition(hand:hand, finger:finger, joint: .tip))
		let pos2nd: CGPoint? = cv2(jointPosition(hand:hand, finger:finger, joint: .pip))
		let posWrist: CGPoint? = cv2(jointPosition(hand:hand, finger:.wrist, joint: .tip))
		guard let posTip, let pos2nd, let posWrist else { return false }

		if posWrist.distance(from: pos2nd) > posWrist.distance(from: posTip) { return true }
		return false
	}
	func isStraight(pos1: CGPoint?, pos2: CGPoint?, pos3: CGPoint? ) -> Bool {
		guard let p1 = pos1, let p2 = pos2, let p3 = pos3 else { return false }
		if p1.distance(from: p2) < p1.distance(from: p3) { return true }
		return false
	}
	func isStraight(hand: HandTrackProcess.WhichHand, finger: HandTrackProcess.WhichFinger) -> Bool {
		let posTip: CGPoint? = cv2(jointPosition(hand:hand, finger:finger, joint: .tip))
		let pos2nd: CGPoint? = cv2(jointPosition(hand:hand, finger:finger, joint: .pip))
		let posWrist: CGPoint? = cv2(jointPosition(hand:hand, finger:.wrist, joint: .tip))
		guard let posTip, let pos2nd, let posWrist else { return false }

		if posWrist.distance(from: pos2nd) < posWrist.distance(from: posTip) { return true }
		return false
	}
}
```

---

## HandTrackFake（疑似ハンドトラッキング）
VisionPro実機ではなく、VisionProシミュレータでハンドトラッキングをデバッグするための仕組みです。

VisionPro実機は必要なく、Mac（またはiPhoneやiPad）のフロントカメラがあればVisionProのカメラのフリをして指関節をトラッキングします。

![handtrackfakefig.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1839672/8b129bdc-2d56-db1c-5c57-99bcc8bf28cd.jpeg)


HandTrackFakeはMacのカメラを使って手の動きを読取り、VisionKitの[VNHumanHandPoseObservation](https://developer.apple.com/documentation/vision/vnhumanhandposeobservation)で座標変換して、VisionProシミュレータ内で動作するvisionOSアプリにBluetooth送信します。

VNHumanHandPoseObservationで取得できる関節位置は2次元座標ですが、HandTrackFakeではZ方向の深さを与えることができますので、VisionProシミュレータ内で動作する疑似ハンドはZ方向に動かすことができます。
### HandTrackFakeモジュール
[HandTrackFake.swift](https://github.com/AlohaYos/VisionGesture/blob/main/HandTrackFake/HandTrackFake.swift)
```swift
// Public properties
var enableFake = true // false:実機 true:フェイクを使う
```

### サンプルプロジェクト
#### フェイク座標の送信部 [FakeTrackingSender](https://github.com/AlohaYos/VisionGesture/tree/main/FakeTrackingSender)
 - Mac（またはiPhoneやiPad）のフロントカメラで手の動きをキャプチャ
 - ハンドトラッキングの2次元座標をJsonにエンコード
 - JsonデータをBluetoothでVisionGesture.appへ送信

<iframe width="560" height="315" src="https://www.youtube.com/embed/cHgTY3yTLuw?si=U69exrT1mOM8v2_l" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

MacのカメラはZ方向の奥行きを持っていませんが、FakeTrackingSenderでは奥行きを指定するスライダーがありますので、これで仮想ハンドの位置をZ方向に動かすことができます。

![zaxis.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1839672/bd2d8cc1-d340-59d5-1e00-75a50a83aaf5.png)


[AppDelegate.swift](https://github.com/AlohaYos/VisionGesture/blob/main/FakeTrackingSender/TrackingSender/AppDelegate.swift)

```swift
let handTrackFake = HandTrackFake()
```

[HandTrackingProvider.swift](https://github.com/AlohaYos/VisionGesture/blob/main/FakeTrackingSender/TrackingSender/HandTrackingProvider.swift)
```swift
// 送信部をアクティベート
handTrackFake.initAsAdvertiser()

// フェイク情報を送信
handTrackFake.sendHandTrackData(handJoints2D)
```

[Info.plist](https://github.com/AlohaYos/VisionGesture/blob/main/FakeTrackingSender/TrackingSender/Info.plist)
```
Privacy - Camera Usage Description
Privacy - Local Network Usage Description  
Bonjour services  
 - item 0 : _HandTrackFake._tcp  
 - item 1 : _HandTrackFake._udp  
```

#### フェイク座標の受信部 [VisionGesture](https://github.com/AlohaYos/VisionGesture/tree/main/VisionGesture)
 - FakeTrackingSender.appからのハンドトラッキング2次元JsonデータをBluetoothで受信
 - Jsonデータをデコードして3次元座標を生成
 - 関節位置を反映した疑似ハンドをVisionProシミュレータに表示

<iframe width="560" height="315" src="https://www.youtube.com/embed/Rd27NMTmHlA?si=X5ttMHCVnILwcTx1" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

[TrackingReceiverApp.swift](https://github.com/AlohaYos/VisionGesture/blob/main/VisionGesture/VisionGesture/TrackingReceiverApp.swift)
```swift
let handTrackFake = HandTrackFake()
```

[ImmersiveView.swift](https://github.com/AlohaYos/VisionGesture/blob/main/VisionGesture/VisionGesture/ImmersiveView.swift)
```swift
// Activate fake data browser
if handTrackFake.enableFake == true {
    handTrackFake.initAsBrowser()
}

// Check connection status
let nowState = handTrackFake.sessionState
```

[HandTrackProcess.swift](https://github.com/AlohaYos/VisionGesture/blob/main/VisionGesture/VisionGesture/HandTrackProcess.swift)
```swift
// Receive 2D-->3D converted hand tracking data
let handJoint3D = handTrackFake.receiveHandTrackData()
```

[Info.plist](https://github.com/AlohaYos/VisionGesture/blob/main/VisionGesture/VisionGesture/Info.plist)
```
Privacy - Local Network Usage Description  
Bonjour services  
 - item 0 : _HandTrackFake._tcp  
 - item 1 : _HandTrackFake._udp  
```

# 発売中のVisionProアプリから切り出されたオープンソース
[VisionGesture](https://github.com/AlohaYos/VisionGesture/)は既に発売されているvisionOSアプリ["Air Poolbar"](https://apps.apple.com/us/app/air-poolbar/id6473489849)で使われているテクニックを切り出したオープンソースです。
<iframe width="560" height="315" src="https://www.youtube.com/embed/-KGQhV8TA-Q?si=fH4CPSNbiROo6zBA" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

## アプリとオープンソースができるまでの物語
[Vision Pro実機なしで空間ジェスチャーを作った話](https://qiita.com/AlohaYos/items/938b8f5c281946cb05c2)