【visionOS/ARKit】ハンドトラッキングの最小構成サンプルコード【latestAnchors編】

visionOSにおけるARKitのハンドトラッキングAPIの基本は、手の各ジョイントの座標や回転などのデータを取得する事です。

![Joints Example SessionVideo](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3564662/d705fb6d-3762-9cc0-e09d-caac32521992.jpeg)

![Joints List SessionVideo](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3564662/4085032a-9ac0-70b5-f7fb-4d2530ddab98.jpeg)

今回は、たった70行ほどで動作する最小構成のハンドトラッキングAPIのサンプルコードを紹介します。


基礎知識
-------------------------------------------------------
まず、このセッションビデオを見てください。visionOSのARKitやハンドトラッキングについて概ね分かります。

https://developer.apple.com/wwdc23/10082

日本語字幕があります。15:05からハンドトラッキングについて説明しています。


2つのAPI
-------------------------------------------------------
ジョイントデータを取得するAPIは2つあります。どちらもHandTrackingProviderインスタンスから取得します。

- AsyncSequenceで最新の値を受け取るanchorUpdates
- 最新の値が格納されているlatestAnchors

![Observing hand anchor data](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3564662/22e7971f-168c-38d3-5aef-2ee4371956da.png)

今回はlatestAnchorsで実装しました。

AsyncSequenceで実装する場合は[こちらの記事](https://qiita.com/mjnfhbuvwebwfiejcnw/items/47d279c812ae9ee3b87f)を参照してください。


サンプルコードの概要
-------------------------------------------------------
- アプリの起動と同時にハンドトラッキングを開始する。
- 両手の全てのジョイントにシンプルな球体オブジェクトを配置する。
- 常に最新のジョイントの位置になるように各オブジェクトを更新する。
- オブジェクトが手で隠れないようにする。

![Example 1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3564662/75b2c806-8302-1192-f943-3e1df8336f5f.jpeg)

![Example 2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3564662/e207f635-6cfe-9dc3-b4d4-2a18eb0324d9.jpeg)



ソースコード全体
-------------------------------------------------------
```swift
import SwiftUI
import RealityKit
import ARKit

@main
struct MyApp: App {
    var body: some SwiftUI.Scene {
        ImmersiveSpace {
            RealityView { content in
                for chirality in [HandAnchor.Chirality.left, .right] {
                    for jointName in HandSkeleton.JointName.allCases {
                        let jointEntity = ModelEntity(mesh: .generateSphere(radius: 0.006),
                                                      materials: [SimpleMaterial()])
                        
                        jointEntity.components.set(JointComponent(chirality: chirality,
                                                                  jointName: jointName))
                        
                        content.add(jointEntity)
                    }
                }
            }
        }
        .upperLimbVisibility(.hidden)
    }
    
    init() {
        JointComponent.registerComponent()
        JointSystem.registerSystem()
    }
}

struct JointComponent: Component {
    let chirality: HandAnchor.Chirality
    let jointName: HandSkeleton.JointName
}

struct JointSystem: System {
    private let session = ARKitSession()
    private let provider = HandTrackingProvider()
    
    init(scene: RealityKit.Scene) {
        setUpSession()
    }
    
    private func setUpSession() {
        Task {
            try! await session.run([provider])
        }
    }
    
    func update(context: SceneUpdateContext) {
        let entities = context.entities(matching: .init(where: .has(JointComponent.self)),
                                        updatingSystemWhen: .rendering)
        
        let (leftHandAnchor, rightHandAnchor) = provider.latestAnchors
        
        for handAnchor in [leftHandAnchor, rightHandAnchor] {
            guard let handAnchor else { continue }
            
            for entity in entities {
                let component = entity.components[JointComponent.self]!
                
                guard component.chirality == handAnchor.chirality,
                      let joint = handAnchor.handSkeleton?.joint(component.jointName) else {
                    continue
                }
                
                entity.setTransformMatrix(handAnchor.originFromAnchorTransform * joint.anchorFromJointTransform,
                                          relativeTo: nil)
            }
        }
    }
}
```

ソースコードはこれだけです。コピペしてください。


コードコピペ以外に必要な作業
-------------------------------------------------------
- Info.plistで「NSHandsTrackingUsageDescription」に任意のテキストを設定
- Info.plistで「Preferred Default Scene Session Role」に「Immersive Space」を設定

![Info.plist Screenshot](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3564662/d74a4959-21e3-ac50-3fb3-32ebe50e89be.png)


解説
-------------------------------------------------------
__前述のセッションビデオ内で説明されている内容は省略します。また、SwiftUIやRealityKitの基本的な知識についての説明も省略します。__


### latestAnchorsとは
https://developer.apple.com/documentation/arkit/handtrackingprovider/4189752-latestanchors

> The most recent hand anchors for each hand.
> Accessing this tuple consumes its values and sets them to nil until the next anchor update.
> 【DeepL翻訳】 各ハンドの最新のアンカー。
> このタプルにアクセスすると、その値が消費され、次のアンカー更新までnilに設定されます。

更新のポーリングをして最新のハンドアンカーの値を随時取得するというのが想定された実装パターンのようです。


### ECSで構成
```swift
struct JointComponent: Component {
    let chirality: HandAnchor.Chirality
    let jointName: HandSkeleton.JointName
}

struct JointSystem: System {
    ...
    
    func update(context: SceneUpdateContext) {
        let entities = context.entities(matching: .init(where: .has(JointComponent.self)),
                                        updatingSystemWhen: .rendering)
        
        let (leftHandAnchor, rightHandAnchor) = provider.latestAnchors
        
        ...
    }
}
```

今回は、RealityKitでの典型的な実装パターンであるECS(Entity Component System)でポーリングを実装しました。


### 各エンティティの管理
```swift
struct JointComponent: Component {
    let chirality: HandAnchor.Chirality
    let jointName: HandSkeleton.JointName
}
```

今回は独自のComponentにHandAnchor.ChiralityやHandSkeleton.JointNameのプロパティを定義して管理しました。


### latestAnchorsの取り出し方に注意
provider.latestAnchorsを呼び出すと値が「消費」されるため実装方法には注意が必要です。

```swift
//↓これはOK↓
let (leftHandAnchor, rightHandAnchor) = provider.latestAnchors

for handAnchor in [leftHandAnchor, rightHandAnchor] {
    ...
}

//↓これはNG↓
for handAnchor in [provider.latestAnchors.leftHand, provider.latestAnchors.rightHand] {
    ...
}
```

このNGの例ではprovider.latestAnchorsを2回呼び出すことになるので、2回目に取得される値が想定と異なる値(恐らくnil)になってしまいます。


### 手のオクルージョンを切り替える
![両手のオクルージョン](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3564662/a9e5e919-535d-d604-e54e-be459c6fde6f.jpeg)

visionOSでは両手のオクルージョンがデフォルトで有効になっています。これをオフにするにはSwiftUIのupperLimbVisibilityでhiddenと設定してください。

```swift
.upperLimbVisibility(.hidden)
```


### アクセス権限リクエストの設定
アクセス権限リクエストのためにInfo.plistで「NSHandsTrackingUsageDescription」に任意のテキストを設定する必要があります。

多くの場合、Info.plistで何らかの値を設定するにはプルダウンメニューからkeyを指定します。しかし今回の場合はプルダウンメニューに表示されないので、「NSHandsTrackingUsageDescription」と直接入力してください。

![Xcode Info.plist pull-down-menu](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3564662/7a61428f-267d-729b-c661-ad6972c6170a.png)


### フルスペースでアプリを起動
```swift
@main
struct MyApp: App {
	...
    var body: some SwiftUI.Scene {
        ImmersiveSpace {
        	...
        }
        ...
	}
}
```

XcodeでvisionOSアプリプロジェクトを新規作成すると、ウインドウで起動するコードが生成されます。今回はシンプルな実装にするために、フルスペースで起動するようにしました。

Info.plistの「Preferred Default Scene Session Role」に「Immersive Space」を設定する必要があります。この設定をしないとアプリ起動直後にクラッシュします。


【注意】Apple Vision Pro実機が必要
-------------------------------------------------------
ARKitのハンドトラッキングAPIを試すにはApple Vision Pro実機が必要です。シミュレーターでは全く動きません。


次のステップ
-------------------------------------------------------
- 現在のアクセス権限の状態を確認: _session.queryAuthorization(for:)_
- アクセス権限許可を明示的にリクエスト: _session.requestAuthorization(for:)_
- アンカーの状態を確認: _AnchorUpdate.Event_
- 各アンカーや各ジョイントがトラッキング中かどうかを確認: _TrackableAnchor.isTracked_
- DataProviderの状態変化を監視: _ARKitSession.Events_
- 現在の実行環境がサポートしているかを確認: _HandTrackingProvider.isSupported_


リンク
-------------------------------------------------------
https://developer.apple.com/wwdc23/10082
<!--空間コンピューティング向けのARKitについて - WWDC23 - Videos - Apple Developer-->

https://developer.apple.com/documentation/arkit/arkit_in_visionos
<!--ARKit in visionOS | Apple Developer Documentation-->

https://developer.apple.com/documentation/arkit/handtrackingprovider
<!--HandTrackingProvider | Apple Developer Documentation-->

https://developer.apple.com/documentation/arkit/handtrackingprovider/4180435-anchorupdates
<!--anchorUpdates | Apple Developer Documentation-->

https://developer.apple.com/documentation/arkit/handtrackingprovider/4189752-latestanchors
<!--latestAnchors | Apple Developer Documentation-->

https://developer.apple.com/documentation/arkit/handanchor
<!--HandAnchor | Apple Developer Documentation-->

https://developer.apple.com/documentation/arkit/handskeleton
<!--HandSkeleton | Apple Developer Documentation-->

https://developer.apple.com/documentation/swiftui/scene/upperlimbvisibility(_:)
<!--upperLimbVisibility(_:) | Apple Developer Documentation-->

https://developer.apple.com/documentation/visionos/happybeam
<!--Happy Beam | Apple Developer Documentation-->

* * *

https://qiita.com/mjnfhbuvwebwfiejcnw/items/90d6e1b312d36cf085c4
<!--Apple Vision Proアプリを発売日に3つリリースした話 #Swift - Qiita-->

https://qiita.com/mjnfhbuvwebwfiejcnw/items/498ad1878dd48ea53412
<!--空間ペルソナのSharePlayに対応したアプリを2つリリースした話 #SwiftUI - Qiita-->

https://zenn.dev/huiygfutfgvjknj/articles/a48848b2f0ccd6
<!--手で距離を測るメジャーアプリを作る【visionOS】-->

https://qiita.com/mjnfhbuvwebwfiejcnw/items/96b64d2474b6886ddd88
<!--visionOSアプリ開発者向け公式ドキュメントまとめ #AR - Qiita-->

https://zenn.dev/huiygfutfgvjknj/articles/134014540063e9
<!--Appleが教えてくれないvisionOSアプリ開発に必要な知識集(2024春)-->