3Dプリンターで印刷したクリスマスツリーにApple Vision ProでAR飾り付けをする話

# はじめに
クリスマス！なのでクリスマスツリーを飾りたい。
せっかくなので最近勉強中のVisionPro + ObjectTrackingを使って、
ARでツリーを表示するアプリを作ってみました。

https://developer.apple.com/jp/videos/play/wwdc2024/10101/

ObjectTrackingとは、現実空間の物体をAR用マーカーとし
VisionProでその物体を検出し、その物体に演出を重ね合わせる
ARコンテンツを開発できる機能です。
「スマートフォンでQRマーカーを読みAR表示を行う」という物と同じで、
マーカーが画像やQRではなく立体物にできるというのが特徴です。
現実の立体物をマーカーにできるため、
より没入感のある体験・ストーリーテリングを作れる可能性があります。

（ちなみに今回使ってるツリーは前後左右を識別できない
回転対称であることに加え、印刷カラーが黒のため
識別精度が低く、題材としてはあまりよろしくありません。
白い背景だと正常動作するのでご了承ください・・・）

# TL;DR
サンプルプロジェクトはこちら↓

https://github.com/GONBEEEproject/ChristmARsTree

3Dプリントデータはこちら↓
PLAの黒、スケール80%で印刷するとそのまま使えるかもしれません。

https://www.printables.com/model/338288-christmas-tree


## 参考元プロジェクト
今回のプロジェクトは下記リンクから派生する形で、
ロード用モジュールなどをお借りして実装しています。
元リンクの方が出来ることが多い（その代わり難しい）ので、
この記事を理解した後、下記のプロジェクトも試してみてください。

https://vision.engineer/posts/object-tracking-in-visionOS-2/

# 制作環境・バージョン

|  | バージョン |
|:-|:-|
| MacOS | Sequoia 15.2 Beta (24C5089c)|
|  Xcode | 16.1 (16B40)  |
| RealityComposerPro  |  2.0 (448.0.16.0.3) |


# 物理ツリー側制作
## 0. ツリーを3Dプリント
まず初めにトラッキング用マーカーとなる立体物を作っていきます。
どのご家庭でもお試しいただけるように、
Bambu Lab A1miniで印刷できるサイズ・材質・データで制作します。
（真面目な話、3Dプリントさえあれば
データ的に検証環境を統一できるため大変オススメ）
以降の手順でマーカーの機械学習も行う場合は、
お好みの立体物で問題ありません。

今回はこちらのクリスマスツリーを使わせていただきます。

https://www.printables.com/model/338288-christmas-tree

今回のプロジェクトではフィラメント節約もかねて
サイズを80%で印刷しています。
サンプルプロジェクトをそのまま利用する場合は80%設定にして
印刷するようにしてください。

出来上がったものがこちらです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/e9b5788f-9353-f9e2-f338-d79466ef21cb.png" width=40%>

## 1. ツリーを3Dスキャン
次に用意した立体物を3Dスキャンし、
トラッキングマーカー用のデータを準備します。
次ステップにて**USDZ形式の3Dモデルデータ**が必要になりますので、
USDZ出力ができる3Dスキャンアプリを用意しましょう。

最近はスマホ向け3Dスキャンアプリが結構多く出てきていますが、
手軽さやUIのシンプルさを考え**Scaniverse**がオススメです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/9f5a08ea-d561-912b-f801-62d5ef5b21e0.png" width=40%>

スキャン時はなるべく明るい環境下で実施し、
撮影者の影が対象物に被らないように立ち回ることを意識しましょう。
Scaniverseの結像は相当優秀なので、多少雑でも成功します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/de75ad2a-7901-e382-2383-baa4af5273fc.png" width=40%>

結像に成功したら画面下部の編集ボタンからトリミングを行います。
スキャン時に映り込んだ背景などを切り取ります。
コツとしては横からの視点にし、床面を若干削り取ることです。
トリミングが終わったら保存して元の画面に戻ります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/9a0d5578-e3d5-c871-13f1-fa19cef174f8.png" width=40%>

背景をキレイに刈り取った後の様子がこちらです。
人間が見て「だいたいそれっぽい」という形状にできたら、
VisionProもだいたい検出してくれます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/a251ed39-57af-c623-020c-b9f80045a5ea.png" width=40%>

編集が終わったら画面下部の共有ボタンから
モデルをエクスポートします。
この際**USDZ形式のデータとしてエクスポート**してください。
出力後は何らかの方法（AirDropなど）でMac環境に転送しましょう。

### CADデータをそのまま使えば？
~~それはそう。~~
次ステップで行う、立体物をマーカーとして利用できるようにする工程が
USDZ形式のデータしかロードできません。
そのため、一度印刷してそれをスキャンし直しUSDZで出力するという
回りくどいフローを採用しています。

CAD用のSTLデータなどをUSDZに変換してもよいのですが、
スケール統一や「カメラから見たオブジェクトの状態」として扱うなど、
いくつかの観点で意図的に3Dスキャンをし直しています。


## 2. スキャンデータを機械学習
Scaniverseから出力されたUSDZデータを機械学習にかけて
トラッキングマーカーとして利用できるように準備します。

機械学習を行うにはXcodeに同梱されている
`CreateML`というアプリを使います。
`上部メニュー/Xcode/Open Developer Tool/Create ML`からアクセスします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/878a0117-6a3e-8610-3fef-e3a27e715cb3.png" width=60%>

CreateMLを立ち上げたらNew Documentから新規プロジェクトを作成し、
左メニューのSpatialからObject Trackingテンプレートを選びます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/73e8b446-d17c-aa19-fecb-d4a46aae7ab6.png" width=60%>

プロジェクト名などを指定し保存先を選ぶと以下のような画面に遷移し、
機械学習のターゲットを選んだりオプションをいじる画面になります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/8c2d367a-d184-3624-22e5-49549e95dbbe.png" width=60%>

画面中央エリア左上から`Select an object`を選ぶと、
トレーニング対象のファイルを指定するダイアログが現れるので、
先ほどScaniverseから転送してきたスキャンデータを選択します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/e5056f00-cbd6-bcb1-303a-3c8fadff7c8b.png" width=60%>

（上記画像はトレーニング終了後なので厳密にはちょっと違いますが）
データを選択すると上のように中央に3Dモデルと、実寸スケールが表示されます。
ここで現実の立体物と概ね同じサイズになっていることを確認しましょう。
Scaniverseでの編集時に底面を若干削っているので、
高さ方向が少しズレているかもしれません。


### View Anglesについて
画面下部エリアの`View Angles`から
「そのオブジェクトをどのアングルから見る可能性があるか」を定義します。

`All Angles`は文字通り立体物の全方位から見る可能性があるものです。
地球儀などの球体を利用する際はこちらのオプションを選びます。

`Upright`は~~右上~~ 上から見下ろすアングルで利用する場合に選びます。
今回はクリスマスツリーを見下ろす（見上げるアングルは無いという意味）形になるので
このオプションで進めます。

`Front`は更に範囲を絞り、上からに加え見る角度も立体物の手前側のみという設定です。
机に固定されている物体をトラッキングマーカーとするケースや、
自動車のハンドルやメーターをマーカーとするケースに向いている・・・っぽいですが、
使ったことは今のところありません。

-------------

`View Angles`を設定したら画面上部エリア左上にある
`Train`ボタンから機械学習を開始します。
トレーニング開始後は`Training`タブから進行状況を確認することができます。

この工程がまァ〜〜〜〜〜〜〜〜それはそれは時間がかかります。
電力もアホほど食うので、コンセントに繋いで待つしかありません。
「寝る前に仕掛けて、朝起きて終わってたらラッキー」というくらい時間がかかります。
12時間待つのだぞ。

ちなみに学習中はCPUもメモリも食いつぶしてブン回るので、
基本的に他タスクは終了して、Mac自体もなるべく触らないようにしましょう。
なんなら低温やけどするレベルで熱くなります。触らないというか、触れない。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/60db3745-d0c4-2b3b-b92e-a451471244e3.png" width=60%>


## 3. ReferenceObjectの出力

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/08543f42-be80-a224-a7b4-c6f9807c75bb.png" width=60%>

じっと我慢の子であった・・・。（ホンマに）
トレーニングが完了すると画面上部エリアの`Output`タブから学習結果の出力ができます。
`Get`ボタンで確実に学習結果データを保存しましょう。

ここで出力されるデータは`ReferenceObject`という独自形式のデータです。
内部には学習元のUSDZデータやトラッキング用データが入ってるらしい・・・。
この`ReferenceObject`データは後ほどXcodeで利用するので、
わかりやすい場所に保存しておきます。

ちなみに今回利用する黒いクリスマスツリーのトレーニングは
約11時間かかっています。勘弁してヨ・・・。
（過去16時間かかった物体があったので、なんなら短い方）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/7ef19a0c-87bc-e253-5604-0c57b957b5a7.png" width=40%>


# アプリケーション側制作

## 0. Xcodeプロジェクトの立ち上げ
トラッキング用マーカーを用意したら、アプリ実装に進みます。
Xcodeから新規にプロジェクトを立ち上げます。
テンプレートは`visionOS/App`を選びます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/3503ad80-ed2d-d6d5-c5bf-c1844a43738f.png" width=60%>

プロジェクトのオプションは以下のとおりです
|  |  |
|:-|:-|
| Initial Scene |Volume   |
| Immersive Space Renderer  | RealityKit  |
| Immersive Space   | Mixed  |

その他は特に指定はありません。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/7e11fc12-f3fb-c2da-01cc-5a8b07376eb4.png" width=60%>


## 1. AR演出の制作
プロジェクトを立ち上げたらまず初めにAR表示用の演出シーンを制作します。
プロジェクトディレクトリ内にPackagesというフォルダがあるはずです。
`プロジェクト名/Packages/RealityKitContent/Package.realitycomposerpro`
というファイルを選択すると以下のような画面になるので、
右上の`Open in Reality Composer Pro`のボタンからRealityComposerProを起動します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/06a3dbe6-87a6-5b24-0061-493b4c38b963.png" width=70%>

RealityComposerProが起動したら画面下部のプロジェクトビューで
`command + n`キーで今回表示する用の新規シーンを作ります。
任意のファイル名で大丈夫です。
（サンプルコード上では`ChristmasTreeScene`としています。）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/d311992f-8839-6b08-0e8c-e46ba72753d1.png" width=70%>

次に`.referenceobject`の学習元となったスキャンデータのUSDZファイルを
RealityComposerProに導入します。
USDZファイルをプロジェクトビューにドラッグ・アンド・ドロップするだけで
インポートできますので、適当に持ってきます。

USDZファイルを持ってきたら先程作成したシーン内にドラッグ・アンド・ドロップして
オブジェクトを設置します。
画面右のインスペクタビューからTransformコンポーネントを確認し、
Position,Rotationともに0,0,0になっていればOKです。
こんな感じ↓

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/bf45537f-2a24-faeb-f4df-0910bff3b502.png" width=70%>


### 飾り付けをする
ツリー本体が配置できたら、ツリー周辺に飾りを配置していきます。
今回はシンプルにRealityComposerProに同梱されているプリミティブデータのみで
飾り付けを行っていきます。

画面右上のプラスボタンからプロジェクト内に追加するデータを選択します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/9eba60e7-7abd-fe6a-b297-f68e16cad8d5.png" width=70%>

例えばプリミティブデータの中からSphereをダブルクリックすると、
シーン内原点にSphereが配置されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/ea1eb460-7c76-a7c5-6952-4aa6c655b4fa.png" width=70%>

追加されたオブジェクトを選択すると画面右側のインスペクタビューから
座標やスケールなどを操作することができます。
UnityやUnrealEngineなど、他のゲーム制作ツールを触ったことがある人なら
比較的スムーズに操作できると思います。
シーン内によしなに配置していきましょう。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/2892dba6-8f39-63af-a1d8-cc3d2bfdc919.png" width=60%>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/42265034-90ee-a96e-9a16-ea1e62e8fae7.png" width=30%>

オブジェクトの色を変更したい場合は、
画面左側ヒエラルキービューの左下、プラスボタンから
`Physically Based Material`を作成します。
マテリアル作成後は画面右側インスペクタビューから
`Diffuse Color`を始め各種パラメータを変更していきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/e6edcdfd-412a-dc77-7a26-56badbc842b8.png" width=20%>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/907e566c-aa65-ebb3-9b22-6e81e59f55a6.png" width=70%>

マテリアルを作成したら先ほど配置したオブジェクトに適用します。
色を変えたいオブジェクトを選択し、インスペクタビューの
`Material Bindings`からマテリアルを変更します。
今回は`Root/Material`を選択します。
以下のような形で色が変わったら成功です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/f53c235b-cc64-5906-037c-9758514a50ef.png" width=40%>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/4ade0dc0-92fe-baee-9ad2-a4fc73a30456.png" width=50%>


### パーティクルを飛ばす
次にツリーの周辺に雪が降るようなパーティクルを飛ばします。
画面左側ヒエラルキービューの左下、プラスボタンから
`Particle Emmiter`を作成します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/e6edcdfd-412a-dc77-7a26-56badbc842b8.png" width=20%>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/0f0e5fa4-63c6-60b5-cc87-c3e23c9e804a.png" width=50%>

Particle Emmiter作成後インスペクタビューを見ると、上部に再生ボタンが表示されます。
これをクリックするとシーン内にパーティクルが再生され始めるので、
その様子を見ながらパラメータを操作していくことになります。
生成直後の初期状態だと上に向かってパーティクルが飛んでいく状態です。

今回は各コンポーネントを以下の通りに変更してみました。
`Transform/Position`を`0,25,0`
`ParticleEmmiter/Emitter/Speed`を`-0.1`
`ParticleEmmiter/Particle/Birth Rate`を`20`
`ParticleEmmiter/Particle/Life Span`を`3`
`ParticleEmmiter/Particle/Size`を`1`
`ParticleEmmiter/Particle/Start Color`,`ParticleEmmiter/Particle/End Color`を
それぞれいい感じに

サンプルプロジェクトでは以下のような挙動にしています。

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">Qiita用 <a href="https://t.co/xzfdJOpRi5">pic.twitter.com/xzfdJOpRi5</a></p>&mdash; ごんびぃーᯅ@XR開発者集会 (@GONBEEE_project) <a href="https://twitter.com/GONBEEE_project/status/1868727817485402484?ref_src=twsrc%5Etfw">December 16, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

パーティクルのパラメータはかなり色々自由にいじれるため、
時間があるときにじっくり向き合ってみるとよいでしょう。

### カリング用マテリアル
最後にShader Graphを使って、オクルージョンカリング用のマテリアルを作ります。
現状はUSDZデータをそのまま配置しているため、
このシーンをロードするとツリーの3Dモデルがそのまま表示されてしまいます。

せっかくのARコンテンツなので
**「現実の立体物の奥に存在する3Dモデルを隠す」オクルージョン**を実装してみます。
画面左側ヒエラルキービューの左下、プラスボタンから
`Shader Graph Material`を作成します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/e6edcdfd-412a-dc77-7a26-56badbc842b8.png" width=20%>

次に画面下部のプロジェクトビューがあるエリアから、
Shader Graphタブを選択し、Graphエディタに移動します。
デフォルトでは以下のような配置になっているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/54412077-fa45-d222-ee32-06b11a6a3777.png)

Graphエディタの右上から`New Node`ボタンをクリックし、
`Occlusion Surface(RealityKit)`ノードを検索します。
ノードが追加できると以下のように新規ノードが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/bdd5ebb8-6626-561e-7a3c-dc8fa16dcf59.png)

最後に`OcclusionSurface`ノードの`Out`ポートから、
`Outputs`ノードの`Custom Surface`ポートにドラッグ・アンド・ドロップで接続します。
以下のような接続ルートになったら完成です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/7df2bce1-dc0b-2aef-ca7a-22b905311409.png)

カリング用マテリアルが完成したら、
クリスマスツリーのモデルに対して適用します。
サンプルプロジェクトでは
`Root/ChristmasBlack/Geom/mesh`オブジェクトにマテリアルのバインド設定があるので、
今作ったカリング用マテリアルをバインドします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/cfad1eeb-ffce-77db-89a0-425de3f8c834.png" width=50%>

するとクリスマスツリーのモデルが透明になり、
加えてツリーの向こう側に存在するオブジェクトが見えなくなりました。
（黄色の輪郭は選択中のオブジェクトを示すアウトライン）
これで **「現実の立体物の奥に存在する3Dモデルを隠す」オクルージョン**が
実現できました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241210/e03984c0-32b0-2868-a15e-e646df368492.png" width=60%>

これにてAR演出側は完成です。
次ステップからコードの実装に進んでいきます。


## 2. ReferenceObjectのロード
Qiitaに乗せるには長すぎるので、GitHubへのリンクを貼っておきます。
コード全体
https://github.com/GONBEEEproject/ChristmARsTree/blob/main/ChristmARsTree/Main/ReferenceObjectLoader.swift

```ReferenceObjectLoader.swift
    func loadReferenceObjects() async {
        var referenceObjectFiles: [String] = []

        if let resourcesPath: String = Bundle.main.resourcePath {
            do {
                try referenceObjectFiles = FileManager.default.contentsOfDirectory(atPath: resourcesPath).filter { $0.hasSuffix(".referenceobject") }
            } catch {
                fatalError("Failed to load reference object files with error: \(error)")
            }
        }

        await withTaskGroup(of: Void.self) { [weak self] group in
            guard let self else { return }

            for file in referenceObjectFiles {
                let objectURL: URL = Bundle.main.bundleURL.appending(path: file)

                group.addTask {
                    await self.loadReferenceObject(objectURL)
                }
            }
        }
    }
```

重要なところとしては上記メソッドです。
プロジェクトデータから`.referenceobject`の拡張子をリストアップし、
中身をそれぞれロードします。
`.referenceobject`のファイルパスをURLとして保持しておき、
別メソッド`loadReferenceObject(objectURL)`にて`ReferenceObject`型でロードしています。

`ReferenceObject`型の中身には元モデルのUSDZファイルや、
個体識別のためのIDなどが含まれています。
それらをIDをKey、EntityをValueとするDictionaryに保持しておきます。

## 3. AR演出のロード・表示
コード全体
https://github.com/GONBEEEproject/ChristmARsTree/blob/main/ChristmARsTree/Main/ObjectAnchorVisualization.swift

### マーカーの分岐
ここでは検出したマーカーに合わせて何を表示するかを定義していきます。

```ObjectAnchorVisualization.swift
@MainActor
class ObjectAnchorVisualization {
    enum ObjectType: String {
        case VisionProCase
        case ChristmasBlack
    }
    
//～～～～～～～～～～中略～～～～～～～～～～～～～～～

        switch model.name {
        case ObjectType.VisionProCase.rawValue:
            self.type = .VisionProCase
        case ObjectType.ChristmasBlack.rawValue:
            self.type = .ChristmasBlack
        default:
            fatalError(
                "Attempted to create ObjectAnchorVisualization for unknown ObjectType"
            )
        }
```

`ObjectAnchorVisualization`クラスの中で検出したマーカーが何かを管理するための
enumを定義しています。
今回はデバッグ用の`VisionProCase`（ファイル名:[VisionProCase.referenceobject](https://github.com/GONBEEEproject/ChristmARsTree/blob/main/ChristmARsTree/ReferenceObjects/VisionProCase.referenceobject)）と
実際にツリーに使う`ChristmasBlack`（ファイル名:[ChristmasBlack.referenceobject](https://github.com/GONBEEEproject/ChristmARsTree/blob/main/ChristmARsTree/ReferenceObjects/ChristmasBlack.referenceobject)）を
それぞれリストアップしています。

もしここに自分で用意した立体物を追加したい場合は、
`enum ObjectType`に書き加えていきます。
**ファイル名をそのままデータとして扱っており、enum登録時もファイル名一致で**
追加するようにしてください。

### 表示するデータの定義

```ObjectAnchorVisualization.swift

        switch type {
        case .VisionProCase:
            entity.addChild(model)
        case .ChristmasBlack:

            guard
                let scene = try? await Entity(
                    named: "ChristmasTreeScene",
                    in: realityKitContentBundle)
            else {
                print("Unable to load CaseWithCarScene")
                self.entity = entity
                return
            }
            entity.addChild(scene)

        }

        entity.transform = Transform(matrix: anchor.originFromAnchorTransform)
        entity.isEnabled = anchor.isTracked

        self.entity = entity
    }
```

次に検出したマーカーに合わせて表示する3Dモデル・シーンデータを定義します。
`.referenceobject`に同梱されている元のUSDZデータをただ表示するだけなら
`case .VisionProCase:`のブロックにある通り、
`entity.addChild(model)`のみでとりあえず表示することができます。

RealityComposerProで制作した演出入りシーンを表示する場合は
`case .ChristmasBlack:`のブロックに書いてあるように
1. `realityKitContentBundle`からシーン名を指定し、非同期でロードする
1. 一応ロード失敗時用にエラーハンドリング
1. `entity.addChild(scene)`でロードしたシーンをentityに追加する

という手順でロード・表示します。

ロード完了後はentityの座標情報を制御したり、
トラッキング状態に合わせて有効無効を切り替えたりする行が入りますが、
下記に説明する`update`メソッドがあるため、ここではそこまで重要ではありません。
一応検出直後にその座標に表示する用ですが、
おまじないという事にして暗記したほうが早いと思います。

### 座標更新メソッド

```ObjectAnchorVisualization.swift
    func update(with anchor: ObjectAnchor) {
        entity.isEnabled = anchor.isTracked

        guard anchor.isTracked else { return }

        entity.transform = Transform(matrix: anchor.originFromAnchorTransform)

    }
```

こちらはマーカーのトラッキング状態が更新された場合に発火されるメソッドです。
4. にて説明するAppDataから発火されます。

`Transform(matrix: anchor.originFromAnchorTransform)`を使って
更新されたマーカー座標を3Dモデル・シーンデータに改めて適用します。
このメソッドによりマーカーが動いたり、体験者が移動しても
現実の立体物にオーバーレイされ続けるという挙動になります。



## 4. AppModel実装
コード全体
https://github.com/GONBEEEproject/ChristmARsTree/blob/main/ChristmARsTree/Main/AppModel.swift

```AppModel.swift

    private func processObjectUpdates(with rootEntity: Entity) async {
        guard let objectTrackingProvider else {
            print(
                "Error obtaining handTrackingProvider upon processHandUpdates")

            return
        }

        for await anchorUpdate in objectTrackingProvider.anchorUpdates {
            let anchor = anchorUpdate.anchor
            let id = anchor.id

            switch anchorUpdate.event {
            case .added:

                anchorReferences[id] = anchor
                
                let model: Entity? =
                    referenceObjectLoader.usdzsPerReferenceObjectID[anchor.referenceObject.id]

                let visualization = await ObjectAnchorVisualization(
                    for: anchor,
                    withModel: model,
                    appModel: self)

                objectVisualizations[id] = visualization
                rootEntity.addChild(visualization.entity)

            case .updated:

                anchorReferences[id] = anchor
                objectVisualizations[id]?.update(with: anchor)

            case .removed:

                anchorReferences.removeValue(forKey: id)
                objectVisualizations[id]?.entity.removeFromParent()
                objectVisualizations.removeValue(forKey: id)

            }
        }
    }
```

AppModel内で特に重要なのはマーカー検出時の更新処理です。
マーカー個別の更新時に`anchorUpdate.event`にてそれぞれ
`case .added`は新規に検出時（ロスト後、再検出を含む）に実行、
`case .updated`は検出済みのものが更新される時（1秒に1度更新）に実行、
`case .removed`はマーカーが隠れる、見失うなどロストした際に実行されます。

前項で説明した`update`メソッドは`case .updated`内で実行されています。
他プロジェクトでも使い回せる書き方なので、
こちらもおまじないとして覚えてしまったほうが早いかと思います。


## 5. 各種View実装
初期ウィンドウView
https://github.com/GONBEEEproject/ChristmARsTree/blob/main/ChristmARsTree/Views/ContentView.swift

モード切り替え用ボタン
https://github.com/GONBEEEproject/ChristmARsTree/blob/main/ChristmARsTree/Views/ToggleImmersiveSpaceButton.swift

トラッキングモード用View
https://github.com/GONBEEEproject/ChristmARsTree/blob/main/ChristmARsTree/Views/ObjectTrackingView.swift


各種UIウィンドウを制御するViewに関しては、
これといって特殊な事は行っていません。

```ObjectTrackingView.swift
    var body: some View {
        RealityView { content in
            content.add(rootEntity)
        }
        .task {
            await appModel.startTracking(with: rootEntity)
        }
        .onDisappear() {
            for (_, visualization) in appModel.objectVisualizations {
                rootEntity.removeChild(visualization.entity)
            }

            appModel.objectVisualizations.removeAll()
        }
    }
```

強いて言うなら`ObjectTrackingView.swift`にて、
Viewの起動時に`AppModel`に対してトラッキング開始の処理を送っていること、
Viewの終了時にトラッキングしていたものを全て破棄して終了していることくらいです。

#### Xcodeのプレビュー機能について
```ContentView.swift
#Preview(windowStyle: .automatic) {
    let appModel = AppModel()

    ContentView()
        .environment(appModel)
}
```
Xcodeでは上記のように`#Preview`ブロックを実装すると
そのViewのプレビューが表示できますが（visionOSの場合はエミュレーターが起動する）、
今回のアプリケーションでは**エミュレーターでは実行できないARKitの機能を使っており**、
プレビューを行うことができません。
面倒ですが、デバッグ時は実機に転送して本番環境でテストしましょう。

（これ本当にテスト方法ないのかな・・・デバッグすごい大変だけど・・・）


## 6. ChristmARsTreeApp実装
コード全体
https://github.com/GONBEEEproject/ChristmARsTree/blob/main/ChristmARsTree/Main/ChristmARsTreeApp.swift

```ChristmARsTreeApp.swift
@main
struct ChristmARsTreeApp: App {
    @State private var appModel = AppModel()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(appModel)
                .frame(width: 400,
                       height: 200)
                .task {
                    await appModel.referenceObjectLoader.loadReferenceObjects()
                }

        }
        .windowResizability(.contentSize)

        ImmersiveSpace(id: appModel.immersiveSpaceID) {
            ObjectTrackingView()
                .environment(appModel)
                .onAppear {
                    appModel.immersiveSpaceState = .open
                }
                .onDisappear {
                    appModel.immersiveSpaceState = .closed
                }
        }
        .immersionStyle(selection: .constant(.mixed),
                        in: .mixed)
     }
}
```

最後にエントリーポイントとなる`@main`を書きます。
初期ウィンドウとなる`ContentView`呼び出し時には追加タスクで、
`appModel.referenceObjectLoader.loadReferenceObjects()`を発火し、
2. で説明したReferenceObjectのデータのロードを開始しています。

ObjectTrackingを行う`ImmersiveSpace`の宣言では
`ObjectTrackingView`の呼び出しと、
`ImmersiveSpace`開始時・終了時にそれぞれ
`appModel`に対し`.open` `.close`のステートを送っています。


## 7. 実機にデプロイ
VisionProとXcodeの連携と初期セットアップはこちらの記事をご参照ください。

https://qiita.com/purple_jp/items/17995d1c5405fb0fcaec

一通りの実装が終わったらいよいよVisionPro実機にビルドを転送し、実行してみます。

アプリケーション起動後、現れるウィンドウから
Start Trackingのボタンを押すとマーカーの検出を始めます。
ウィンドウの表示がSearchingになったら目の前にクリスマスツリーをかざしましょう。
以下の動画のように3Dモデルやパーティクルが表示されたら成功です！

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">Qiita用、このあと公開されます<br><br>VisionProでクリスマスツリーを飾り付けるサンプルアプリ作った <a href="https://t.co/MNZL0yNKjV">pic.twitter.com/MNZL0yNKjV</a></p>&mdash; ごんびぃーᯅ@XR開発者集会 (@GONBEEE_project) <a href="https://twitter.com/GONBEEE_project/status/1868735947753628081?ref_src=twsrc%5Etfw">December 16, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


# 完成！&おわりに

かなり内容がミッチリ高密度な記事になってしまいましたが、いかがでしたでしょうか。
この記事をなぞったり、独自にカスタマイズを加えたりすれば
VisionProアプリの基礎的な実装方法や、
3Dスキャンを組み合わせた「現実空間に基づいたコンテンツ」制作、
RealityComposerProを使った3Dモデル配置～パーティクル演出などなど
かなり多方面の知識を身につけることが出来る（はず）です。

今回の内容から発展する形で
・独自の立体物をマーカーとして利用する
・複数の立体物に別々のAR演出を組み込み、同時表示する
・パーティクルや他の3Dモデルを追加し、より派手な演出を作る
などなど、様々な方向に掘り下げることができます。
この記事を足掛かりとして、Swiftを利用したVisionPro開発の道に
進んでいただければ幸いです。

最後まで読んでいただきありがとうございました。
「ここもっと詳細説明してほしい」や「ここバグってます」などご指摘ありましたら、
コメント欄か[ごんびぃー](https://x.com/GONBEEE_project)までご連絡いただければと思います。