🤿

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

2024/03/21に公開5

VisionGestureとは

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

handtracking_01.jpeg

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

VisionGestureに含まれるもの

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

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

VisionGestureプレイグラウンド

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

https://www.youtube.com/watch?v=-j5wBexSdOw

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へ送信

https://www.youtube.com/watch?v=cHgTY3yTLuw

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シミュレータに表示

https://www.youtube.com/watch?v=Rd27NMTmHlA

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"で使われているテクニックを切り出したオープンソースです。
https://www.youtube.com/watch?v=-KGQhV8TA-Q

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

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

Discussion

AsahiAsahi

初めまして!VisionOSの開発を勉強しているものです!
とても参考になる記事ありがとうございます。

一点質問ですが、
RealityKitContentのImmersiveの中にある、青い立方体や赤色のオブジェクトを
ハンドジェスチャーで掴もうとすると、スリップしてしまい、上手く掴めないのですが
これはオブエクト側の摩擦係数等が原因でしょうか?

yosyos

こんにちは!
Packagesの中のPackage.realitycomposerproをReality Composer Proで開いて、オブジェクトの摩擦係数(Dynamic, Static)を変更してみてください。ViewModel.swiftで生成しているtinyBall(小さい豆粒)オブジェクトは、friction=0.5に設定にしているので掴めると思います。

AsahiAsahi

ご返信ありがとうございます!
Reality Composer Proで
緑色のSphereオブジェクトの摩擦係数をStatic = 100, Dynamic=10
右手の各関節をトラッキングするオブジェクトの摩擦係数をStatic = 100, Dynamic=10
にしたのですが、以下の投稿の動画のようにオブジェクトを上手く掴むことができず、
何が原因か分かりますでしょうか、、?
オブジェクトの反発係数や質量等が原因でしょうか?
https://x.com/kbrash_/status/1779784947987059014

お手数おかけしますが、宜しくお願いいたします。

yosyos

VisionGestureで配置しているのは固いオブジェクトですから、例えると「プラスチック素材の手袋でピンポン玉をつかもう」としている感じ。人間の手は表面の柔らかい凹凸を利用してモノを掴んでいるので、物理設定値の調整だけでは実際の手のようにオブジェクトを掴むことはできないと思います。
反発係数というよりもマテリアル弾性のような設定が必要になるので、そのあたりは自分で物理コーディングが必要になると思います(やり方は分からないですw)。

それに加えてハンドトラッキング精度も影響しています。関節位置は安定しておらず、微妙に震えています(関節位置のログを取ってみると分かります)。固い材質の手が震えているところに固いオブジェクトが小刻みに衝突するので、物体は弾けるように飛んでいってしまうんです。
トラッキング精度はVision Proを使う環境の照明の具合も影響していて、左右の目で光度差があるような場所では特に精度が悪くなる傾向があるようですよ。

AsahiAsahi

ご丁寧に返信ありがとうございます!
自分で色々なパラメーター調整、物理コーディングをチャレンジしてみます!!
また、何か質問したいことがあったら、コメントするかもしれませんが、宜しくお願いいたします!