LoginSignup
6
6

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

Last updated at Posted at 2024-03-14

VisionGestureとは

visionOSのハンドトラッキングを使って空間ジェスチャーを作るオープンソースです。この記事ではVisionGestureについて紹介します。

handtracking_01.jpeg

VisionGestureのGithubレポジトリはこちら

VisionGestureに含まれるもの

オープンソースVisionGestureには以下のサンプルが含まれています。

  • visionOSで手の関節の動きをトラッキングするHandTrackingを使うサンプル
  • イマーシブ空間に浮かぶ仮想ハンドの制作方法
  • 空間ジェスチャーを簡単に自作できるテンプレート
  • 実機のVisionProが無くてもvisionOSシミュレータでハンドトラッキングを試せる"HandTrackFake(疑似ハンドトラッキング)"

VisionGestureプレイグラウンド

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

VisionGestureをシミュレータで動作させる場合はHandTrackFake.swiftの先頭付近にあるenableFakeをtrueにしてください。

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に作ります

  • いくつかの指の関節の位置関係を計算します
  • 特定のポーズになったら、ジェスチャーが開始されたと判断します
  • ジェスチャーが有効な間、デリゲートをコールします。この時、デリゲートメソッドにはイベント種類と空間でのジェスチャー位置を渡します
  • 別の特定のポーズになったら、ジェスチャーが終了したと判断します
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内)にジェスチャーで行いたい処理を記述します

  • Gesture_MyGesture.swiftのロジックで特定のジェスチャーと判断された場合、 GestureDelegateが呼び出されます
  • GestrueDelegateEventを使って、発生したイベントと空間座標を取得します
	// 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で、ジェスチャー判断のために関節位置を計測するための関数を作ります

  • まずは既にサンプルとして実装されてる関数を使います
  • 自分の制作するジェスチャー判定のための計測機能が足りない場合は、自作しましょう
  • サンプルコードを見て関節位置をどのように比較するかを理解してください
    以下のサンプルでは isBend(特定の指は曲がっているか)、isStraight(特定の指は伸びているか)といった計測関数が実装されています。
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

HandTrackFakeはMacのカメラを使って手の動きを読取り、VisionKitのVNHumanHandPoseObservationで座標変換して、VisionProシミュレータ内で動作するvisionOSアプリにBluetooth送信します。

VNHumanHandPoseObservationで取得できる関節位置は2次元座標ですが、HandTrackFakeではZ方向の深さを与えることができますので、VisionProシミュレータ内で動作する疑似ハンドはZ方向に動かすことができます。

HandTrackFakeモジュール

HandTrackFake.swift

// Public properties
var enableFake = true // false:実機 true:フェイクを使う

サンプルプロジェクト

フェイク座標の送信部 FakeTrackingSender

  • Mac(またはiPhoneやiPad)のフロントカメラで手の動きをキャプチャ
  • ハンドトラッキングの2次元座標をJsonにエンコード
  • JsonデータをBluetoothでVisionGesture.appへ送信

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

zaxis.png

AppDelegate.swift

let handTrackFake = HandTrackFake()

HandTrackingProvider.swift

// 送信部をアクティベート
handTrackFake.initAsAdvertiser()

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

Info.plist

Privacy - Camera Usage Description
Privacy - Local Network Usage Description  
Bonjour services  
 - item 0 : _HandTrackFake._tcp  
 - item 1 : _HandTrackFake._udp  

フェイク座標の受信部 VisionGesture

  • FakeTrackingSender.appからのハンドトラッキング2次元JsonデータをBluetoothで受信
  • Jsonデータをデコードして3次元座標を生成
  • 関節位置を反映した疑似ハンドをVisionProシミュレータに表示

TrackingReceiverApp.swift

let handTrackFake = HandTrackFake()

ImmersiveView.swift

// Activate fake data browser
if handTrackFake.enableFake == true {
    handTrackFake.initAsBrowser()
}

// Check connection status
let nowState = handTrackFake.sessionState

HandTrackProcess.swift

// Receive 2D-->3D converted hand tracking data
let handJoint3D = handTrackFake.receiveHandTrackData()

Info.plist

Privacy - Local Network Usage Description  
Bonjour services  
 - item 0 : _HandTrackFake._tcp  
 - item 1 : _HandTrackFake._udp  

発売中のVisionProアプリから切り出されたオープンソース

VisionGestureは既に発売されているvisionOSアプリ"Air Poolbar"で使われているテクニックを切り出したオープンソースです。

アプリとオープンソースができるまでの物語

Vision Pro実機なしで空間ジェスチャーを作った話

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6