👐

【Swift】visionOSでかめはめ波を撃つ

2024/03/19に公開

はじめに

前回、【Swift】visionOSのカスタムジェスチャーで玉を飛ばすでカスタムジェスチャーを使って手からオブジェクトを生成することができました。
今回はそれを応用してかめはめ波を撃ってみたいと思います。

つくったもの

環境

  1. Xcode Version 15.3
  2. visionOS 1.1

前提条件

  1. アプリはFullSpaceに入る必要があります。
  2. 一部のARKitデータにはアクセス権限が必要です。

かめはめ波のポーズを検出する

前回作成したprocessHandUpdatesでグローバル変数に左右それぞれのAnchorを保存。

ViewModel.swift
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 {
                 self.leftHandAnchor = anchor // 左手のアンカーを保存
             } else if anchor.chirality == .right {
                 self.rightHandAnchor = anchor // 右手のアンカーを保存
             }
            
            detectGestureTransform(handAnchor: anchor)
            updateTranslationAndShootEnergyBall(handAnchor: anchor)
        default:
            break
        }
    }
}

左右の手首の距離を測る。

ViewModel.swift
func calculateHandWristDistance(leftHandAnchor: HandAnchor?, rightHandAnchor: HandAnchor?) -> Float? {
    guard let leftHandAnchor = leftHandAnchor else { return 0 }
    guard let rightHandAnchor = rightHandAnchor else { return 0 }
    let leftWristPosition = leftHandAnchor.originFromAnchorTransform.columns.3.xyz
    let rightWristPosition = rightHandAnchor.originFromAnchorTransform.columns.3.xyz
      
      return distance(leftWristPosition, rightWristPosition)
}

「かーめーはーめー」時にエネルギーボールを生成する

左右の手首が0.03cm以下になったらエネルギーボールを生成する。

ViewModel.swift
func detectGestureTransform(handAnchor: HandAnchor?) {
    guard let handAnchor = handAnchor,
          (handAnchor.chirality == .left ? leftHandEnergyBall : rightHandEnergyBall) == nil,
          let handDistance = calculateHandWristDistance(handAnchor: handAnchor),
          handDistance < 0.03
    else {
        return
    }
    
    spawnEnergyBall(handAnchor: handAnchor)
}

エネルギーボールは発光させてParticleを出してかめはめ波っぽくしたい。
エネルギーボールを発光させるMaterialを追加。

ViewModel.swift
var material = PhysicallyBasedMaterial()
material.emissiveColor = PhysicallyBasedMaterial.EmissiveColor(color: .cyan)
material.emissiveIntensity = 2.0

エネルギーボールにParticleを追加。

ViewModel.swift
func particleComponent() -> ParticleEmitterComponent {
    var particles = ParticleEmitterComponent()
    
    particles.spawnOccasion = .onDeath
    particles.emitterShape = .sphere
    particles.emitterShapeSize = [0.2, 0.2, 0.2]
    
    particles.speed = -0.1
    particles.birthLocation = .surface
    particles.mainEmitter.birthRate = 1000
    particles.mainEmitter.size = 0.2
    particles.mainEmitter.billboardMode = .billboard
    particles.mainEmitter.color = .evolving(start: .single(.white), end: .single(.cyan))
    
    return particles
}

また、重力の影響を受けないようにする。

ViewModel.swift
energyBall.physicsBody?.isAffectedByGravity = false

spawnEnergyBallの全体のコード。

ViewModel.swift
// エネルギーボールを生成
func spawnEnergyBall(handAnchor: HandAnchor?) {
    guard let handAnchor = handAnchor else { return }
    let radius: Float = 0.1
    
    // enegyBallを発光させる
    var material = PhysicallyBasedMaterial()
    material.emissiveColor = PhysicallyBasedMaterial.EmissiveColor(color: .cyan)
    material.emissiveIntensity = 2.0
    
    let energyBall = ModelEntity(
        mesh: .generateSphere(radius: radius),
        materials: [material]
    )

    // エネルギーボールの位置を計算
    energyBall.transform.translation = Transform(matrix: handAnchor.originFromAnchorTransform).translation

    energyBall.name = "EnergyBall"
    // エネルギーボールの衝突形状を設定
    energyBall.collision = CollisionComponent(
        shapes: [.generateSphere(radius: radius)],
        mode: .trigger
    )
    // Particleをセット
    energyBall.components.set(particleComponent())
    
    // エネルギーボールの物理演算処理を設定
    energyBall.physicsBody = PhysicsBodyComponent(
        shapes: [ShapeResource.generateSphere(radius: radius)],
        mass: 0.1,
        material: nil,
        mode: .dynamic
    )
    
    // 重力の影響を受けないようにする
    energyBall.physicsBody?.isAffectedByGravity = false
    // 継続的な衝突判定をする
    energyBall.physicsBody?.isContinuousCollisionDetectionEnabled = true
    
    // エネルギーボールへの参照を左手に保持
    handEntity.addChild(energyBall)
    handEnergyBall = energyBall
}

エネルギーボールを手の中に保持し「波ー!」で発射する

「かーめーはーめー」とやってるときはエネルギーボールは発射せず、手の中で保持し、「波ー!」のときに発射したい。

ViewModel.swift
// エネルギーボールの位置を更新し手を離したときは発射する
func updateTranslationAndShootEnergyBall(handAnchor: HandAnchor?) {
    guard let handAnchor = handAnchor,
          let energyBall = handEnergyBall,
          let handDistance = calculateHandWristDistance(handAnchor: handAnchor)
    else {
        return
    }
    
    // 手のアンカーに基づいてエネルギーボールの位置を更新
    energyBall.transform.translation = Transform(matrix: handAnchor.originFromAnchorTransform).translation
    + calculateHandTranslationOffset(handAnchor: handAnchor)
    
    // 両手が離れた場合(閾値以上の距離)にエネルギーボールを発射
    if handDistance > 0.03 {
        let forceDirection = calculateForceDirection(handAnchor: handAnchor)
        energyBall.addForce(forceDirection * 150, relativeTo: nil)
        // 発射後はエネルギーボールをnilに設定
        handEnergyBall = nil
    }
}

contentEntity.addChild(handEntity)するのを忘れないように!

おわりに

以上、「visionOSでかめはめ波を撃つ」でした。
基本的なHandGestureProviderの使い方やその他の関数は前回の記事で紹介しています。
動作確認は行っていますが、多少コードが変わってる可能性もあり、動かない場合はコメント等でご教示いただけると嬉しいです。

今後もvisionOSについて発信していきますので、この記事が参考になったと思ったらぜひ♡をお願いします。

Discussion