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

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)

今回は、たった41行で動作する最小構成のハンドトラッキング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)

今回はAsyncSequenceのanchorUpdatesで実装しました。

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


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

![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 {
    private let session = ARKitSession()
    private let provider = HandTrackingProvider()
    private let rootEntity = Entity()
    
    var body: some SwiftUI.Scene {
        ImmersiveSpace {
            RealityView { content in
                content.add(rootEntity)
                for chirality in [HandAnchor.Chirality.left, .right] {
                    for jointName in HandSkeleton.JointName.allCases {
                        let jointEntity = ModelEntity(mesh: .generateSphere(radius: 0.006),
                                                      materials: [SimpleMaterial()])
                        jointEntity.name = "\(jointName)\(chirality)"
                        rootEntity.addChild(jointEntity)
                    }
                }
            }
            .task { try! await session.run([provider]) }
            .task {
                for await update in provider.anchorUpdates {
                    let handAnchor = update.anchor
                    for jointName in HandSkeleton.JointName.allCases {
                        guard let joint = handAnchor.handSkeleton?.joint(jointName),
                              let jointEntity = rootEntity.findEntity(named: "\(jointName)\(handAnchor.chirality)") else {
                            continue
                        }
                        jointEntity.setTransformMatrix(handAnchor.originFromAnchorTransform * joint.anchorFromJointTransform,
                                                       relativeTo: nil)
                    }
                }
            }
        }
        .upperLimbVisibility(.hidden)
    }
}
```

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


コードコピペ以外に必要な作業
-------------------------------------------------------
- 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の基本的な知識についての説明も省略します。__


### AsyncSequenceで最新の値を受け取る
```
for await update in provider.anchorUpdates {
    let handAnchor = update.anchor
    ...
}
```

AsyncSequence(for await構文)で最新のデータを随時受け取るようにします。そして、受け取ったupdateからHandAnchorの値を取り出します。


### 各エンティティの管理
```
jointEntity.name = "\(jointName)\(chirality)"
```

```
let jointEntity = rootEntity.findEntity(named: "\(jointName)\(handAnchor.chirality)")
```

今回はエンティティのnameで各エンティティを管理しました。

各ジョイントの名称を表すHandSkeleton.JointNameと、左手か右手かを表すHandAnchor.Chiralityを用いてエンティティのnameを指定しました。


### 手のオクルージョンを切り替える
![両手のオクルージョン](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)


### フルスペースでアプリを起動
```
@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://zenn.dev/huiygfutfgvjknj/articles/a48848b2f0ccd6
<!--手で距離を測るメジャーアプリを作る【visionOS】-->

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

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