VisionGestureとは
visionOSのハンドトラッキングを使って空間ジェスチャーを作るオープンソースです。この記事ではVisionGestureについて紹介します。
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のカメラのフリをして指関節をトラッキングします。
HandTrackFakeはMacのカメラを使って手の動きを読取り、VisionKitのVNHumanHandPoseObservationで座標変換して、VisionProシミュレータ内で動作するvisionOSアプリにBluetooth送信します。
VNHumanHandPoseObservationで取得できる関節位置は2次元座標ですが、HandTrackFakeではZ方向の深さを与えることができますので、VisionProシミュレータ内で動作する疑似ハンドはZ方向に動かすことができます。
HandTrackFakeモジュール
// Public properties
var enableFake = true // false:実機 true:フェイクを使う
サンプルプロジェクト
フェイク座標の送信部 FakeTrackingSender
- Mac(またはiPhoneやiPad)のフロントカメラで手の動きをキャプチャ
- ハンドトラッキングの2次元座標をJsonにエンコード
- JsonデータをBluetoothでVisionGesture.appへ送信
MacのカメラはZ方向の奥行きを持っていませんが、FakeTrackingSenderでは奥行きを指定するスライダーがありますので、これで仮想ハンドの位置をZ方向に動かすことができます。
let handTrackFake = HandTrackFake()
// 送信部をアクティベート
handTrackFake.initAsAdvertiser()
// フェイク情報を送信
handTrackFake.sendHandTrackData(handJoints2D)
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シミュレータに表示
let handTrackFake = HandTrackFake()
// Activate fake data browser
if handTrackFake.enableFake == true {
handTrackFake.initAsBrowser()
}
// Check connection status
let nowState = handTrackFake.sessionState
// Receive 2D-->3D converted hand tracking data
let handJoint3D = handTrackFake.receiveHandTrackData()
Privacy - Local Network Usage Description
Bonjour services
- item 0 : _HandTrackFake._tcp
- item 1 : _HandTrackFake._udp
発売中のVisionProアプリから切り出されたオープンソース
VisionGestureは既に発売されているvisionOSアプリ"Air Poolbar"で使われているテクニックを切り出したオープンソースです。