【Swift】visionOSのカスタムジェスチャーで玉を飛ばす
はじめに
visionOSは基本的にハンドトラッキングとアイトラッキングで操作を行います。
Appleの公式では以下のように機能するよう設計されています。
そして、上記以外の入力もカスタムジェスチャーを用いてオブジェクトを操作することができます。
今回はカスタムジェスチャーを用いて現実世界に仮想の玉を飛ばしてみました!
つくったもの
ARKitSession
やHandTrackingProvider
を使って銃を撃つジェスチャーから玉を飛ばしてみました。
また、後述のSceneReconstructionProvider
を使って実世界をスキャンし、玉が現実世界に存在するようにしました。
ARKitSessionの概要
ARKitSession
- AR体験のセッションを管理するためのもの。
DataProvider
- Anchorの変更やデータの更新のポーリングなどの観察を可能にするもの。
Anchor
- 実世界の位置と方向を表すもの。
前提条件
- アプリは
FullSpace
に入る必要があります。 - 一部のARKitデータにはアクセス権限が必要です。
ARKitSessionの実行
SessionはDataProviderのセットを提供することで実行します。
DataProviderはDataを受信します。
ARKitSessionの使い方
使用したいDataProviderをrun
でつっこむだけ。
RealityView { content in
content.add(model.setupContentEntity())
}
.task {
do {
if model.dataProvidersAreSupported && model.isReadyToRun {
try await model.session.run([model.sceneReconstruction, model.handTracking])
} else {
await dismissImmersiveSpace()
}
} catch {
print("Failed to start session: \(error)")
await dismissImmersiveSpace()
openWindow(id: "error")
}
}
let session = ARKitSession()
let handTracking = HandTrackingProvider()
let sceneReconstruction = SceneReconstructionProvider()
SceneReconstructionProvider
SceneReconstructionProvider
は周りの世界をスキャンするとともに細分化されたAnchor
を提供します。
Anchorの取得
anchorUpdates
からanchor
を取得してcollision
などを設置しています。
これだけで衝突判定を持つMeshEntityを作成することができます。
func processReconstructionUpdates() async {
for await update in sceneReconstruction.anchorUpdates {
let meshAnchor = update.anchor
guard let shape = try? await ShapeResource.generateStaticMesh(from: meshAnchor) else { continue }
switch update.event {
case .added:
let entity = ModelEntity()
entity.transform = Transform(matrix: meshAnchor.originFromAnchorTransform)
entity.collision = CollisionComponent(shapes: [shape], isStatic: true)
entity.components.set(InputTargetComponent())
entity.physicsBody = PhysicsBodyComponent(mode: .static)
meshEntities[meshAnchor.id] = entity
contentEntity.addChild(entity)
case .updated:
guard let entity = meshEntities[meshAnchor.id] else { continue }
entity.transform = Transform(matrix: meshAnchor.originFromAnchorTransform)
entity.collision?.shapes = [shape]
case .removed:
meshEntities[meshAnchor.id]?.removeFromParent()
meshEntities.removeValue(forKey: meshAnchor.id)
}
}
}
HandTrackingProvider
HandTrackingProvider
はそれぞれの手の骨格データを含むAnchor
を提供します。
以下は、取得できる関節です。ただし、現在は名称が変わっています。
Anchorの取得
こちらもanchorUpdates
からanchor
を取得しています。anchor
からchirality
で左右を判別し、メソッドを実行します。
func processHandUpdates() async {
for await update in handTracking.anchorUpdates {
switch update.event {
case .updated:
let anchor = update.anchor
guard anchor.isTracked else { continue }
if anchor.chirality == .left {
latestHandTracking.left = anchor
spawnSphereOnGunGesture(handAnchor: latestHandTracking.left)
} else if anchor.chirality == .right {
latestHandTracking.right = anchor
spawnSphereOnGunGesture(handAnchor: latestHandTracking.right)
}
default:
break
}
}
}
銃を撃つジェスチャーの計算
親指と人差指の根本のTransformを取得して4cm以下になったら手首のTransformを返します。
// 銃を撃つポーズの計算
func detectGunGestureTransform(handAnchor: HandAnchor?) -> simd_float4x4? {
guard let handAnchor = handAnchor else { return nil }
guard
let handThumbTip = handAnchor.handSkeleton?.joint(.thumbTip),
let handIndexFingerKnuckle = handAnchor.handSkeleton?.joint(.indexFingerKnuckle),
handThumbTip.isTracked &&
handIndexFingerKnuckle.isTracked
else {
return nil
}
let originFromHandThumbTipTransform = matrix_multiply(
handAnchor.originFromAnchorTransform, handThumbTip.anchorFromJointTransform
).columns.3.xyz
let originFromHandIndexFingerKnuckleTransform = matrix_multiply(
handAnchor.originFromAnchorTransform, handIndexFingerKnuckle.anchorFromJointTransform
).columns.3.xyz
let thumbToIndexFingerDistance = distance(
originFromHandThumbTipTransform,
originFromHandIndexFingerKnuckleTransform
)
// 親指と人差し指の根本が接触しているか判断
if thumbToIndexFingerDistance < 0.04 { // 接触していると見なす距離の閾値
return handAnchor.originFromAnchorTransform
} else {
return nil
}
}
玉の生成
玉を生成し手首から指先までのオフセット+力を加える方向を計算してaddForce
しています。
(本当はHandAnchorのEntity
の子に球体のEntity
を生成して前に飛ばすようにしたかったけど、難しかった。)
func spawnSphereOnGunGesture(handAnchor: HandAnchor?) {
guard let handAnchor = handAnchor,
let handLocation = detectGunGestureTransform(handAnchor: handAnchor) else { return }
// 球体のModelEntity
let entity = ModelEntity(
mesh: .generateSphere(radius: 0.05),
materials: [SimpleMaterial(color: .white, isMetallic: true)],
collisionShape: .generateSphere(radius: 0.05),
mass: 1.0
)
// 球体を生成する位置
entity.transform.translation = Transform(matrix: handLocation).translation + calculateTranslationOffset(handAnchor: handAnchor)
// 球体を飛ばす方向
let forceDirection = calculateForceDirection(handAnchor: handAnchor)
entity.addForce(forceDirection * 300, relativeTo: nil)
// 球体をcontentEntityの子として追加
contentEntity.addChild(entity)
}
// 指の長さに相当するoffsetを定義
func calculateTranslationOffset(handAnchor: HandAnchor) -> SIMD3<Float> {
let handRotation = Transform(matrix: handAnchor.originFromAnchorTransform).rotation
return handRotation.act(handAnchor.chirality == .left ? SIMD3(0.25, 0, 0) : SIMD3(-0.25, 0, 0))
}
// 手の向きに基づいて力を加える方向を計算
func calculateForceDirection(handAnchor: HandAnchor) -> SIMD3<Float> {
let handRotation = Transform(matrix: handAnchor.originFromAnchorTransform).rotation
return handRotation.act(handAnchor.chirality == .left ? SIMD3(1, 0, 0) : SIMD3(-1, 0, 0))
}
すべてのコード
※実機のみ動作します
参考
終わりに
今回はカスタムジェスチャーで玉を飛ばすサンプルアプリを作成しました。
カスタムジェスチャーを使うことでさらにvisionOSの幅が広がり魔法や忍術のような漫画の世界を実現できそうだなと感じました!
今後もvisionOSについて発信していきますので、この記事が参考になったと思ったらぜひ♡をお願いします。
Discussion