はじめに
クリスマス!なのでクリスマスツリーを飾りたい。
せっかくなので最近勉強中のVisionPro + ObjectTrackingを使って、
ARでツリーを表示するアプリを作ってみました。
ObjectTrackingとは、現実空間の物体をAR用マーカーとし
VisionProでその物体を検出し、その物体に演出を重ね合わせる
ARコンテンツを開発できる機能です。
「スマートフォンでQRマーカーを読みAR表示を行う」という物と同じで、
マーカーが画像やQRではなく立体物にできるというのが特徴です。
現実の立体物をマーカーにできるため、
より没入感のある体験・ストーリーテリングを作れる可能性があります。
(ちなみに今回使ってるツリーは前後左右を識別できない
回転対称であることに加え、印刷カラーが黒のため
識別精度が低く、題材としてはあまりよろしくありません。
白い背景だと正常動作するのでご了承ください・・・)
TL;DR
サンプルプロジェクトはこちら↓
3Dプリントデータはこちら↓
PLAの黒、スケール80%で印刷するとそのまま使えるかもしれません。
参考元プロジェクト
今回のプロジェクトは下記リンクから派生する形で、
ロード用モジュールなどをお借りして実装しています。
元リンクの方が出来ることが多い(その代わり難しい)ので、
この記事を理解した後、下記のプロジェクトも試してみてください。
制作環境・バージョン
バージョン | |
---|---|
MacOS | Sequoia 15.2 Beta (24C5089c) |
Xcode | 16.1 (16B40) |
RealityComposerPro | 2.0 (448.0.16.0.3) |
物理ツリー側制作
0. ツリーを3Dプリント
まず初めにトラッキング用マーカーとなる立体物を作っていきます。
どのご家庭でもお試しいただけるように、
Bambu Lab A1miniで印刷できるサイズ・材質・データで制作します。
(真面目な話、3Dプリントさえあれば
データ的に検証環境を統一できるため大変オススメ)
以降の手順でマーカーの機械学習も行う場合は、
お好みの立体物で問題ありません。
今回はこちらのクリスマスツリーを使わせていただきます。
今回のプロジェクトではフィラメント節約もかねて
サイズを80%で印刷しています。
サンプルプロジェクトをそのまま利用する場合は80%設定にして
印刷するようにしてください。
出来上がったものがこちらです。
1. ツリーを3Dスキャン
次に用意した立体物を3Dスキャンし、
トラッキングマーカー用のデータを準備します。
次ステップにてUSDZ形式の3Dモデルデータが必要になりますので、
USDZ出力ができる3Dスキャンアプリを用意しましょう。
最近はスマホ向け3Dスキャンアプリが結構多く出てきていますが、
手軽さやUIのシンプルさを考えScaniverseがオススメです。
スキャン時はなるべく明るい環境下で実施し、
撮影者の影が対象物に被らないように立ち回ることを意識しましょう。
Scaniverseの結像は相当優秀なので、多少雑でも成功します。
結像に成功したら画面下部の編集ボタンからトリミングを行います。
スキャン時に映り込んだ背景などを切り取ります。
コツとしては横からの視点にし、床面を若干削り取ることです。
トリミングが終わったら保存して元の画面に戻ります。
背景をキレイに刈り取った後の様子がこちらです。
人間が見て「だいたいそれっぽい」という形状にできたら、
VisionProもだいたい検出してくれます。
編集が終わったら画面下部の共有ボタンから
モデルをエクスポートします。
この際USDZ形式のデータとしてエクスポートしてください。
出力後は何らかの方法(AirDropなど)でMac環境に転送しましょう。
CADデータをそのまま使えば?
それはそう。
次ステップで行う、立体物をマーカーとして利用できるようにする工程が
USDZ形式のデータしかロードできません。
そのため、一度印刷してそれをスキャンし直しUSDZで出力するという
回りくどいフローを採用しています。
CAD用のSTLデータなどをUSDZに変換してもよいのですが、
スケール統一や「カメラから見たオブジェクトの状態」として扱うなど、
いくつかの観点で意図的に3Dスキャンをし直しています。
2. スキャンデータを機械学習
Scaniverseから出力されたUSDZデータを機械学習にかけて
トラッキングマーカーとして利用できるように準備します。
機械学習を行うにはXcodeに同梱されている
CreateML
というアプリを使います。
上部メニュー/Xcode/Open Developer Tool/Create ML
からアクセスします。
CreateMLを立ち上げたらNew Documentから新規プロジェクトを作成し、
左メニューのSpatialからObject Trackingテンプレートを選びます。
プロジェクト名などを指定し保存先を選ぶと以下のような画面に遷移し、
機械学習のターゲットを選んだりオプションをいじる画面になります。
画面中央エリア左上からSelect an object
を選ぶと、
トレーニング対象のファイルを指定するダイアログが現れるので、
先ほどScaniverseから転送してきたスキャンデータを選択します。
(上記画像はトレーニング終了後なので厳密にはちょっと違いますが)
データを選択すると上のように中央に3Dモデルと、実寸スケールが表示されます。
ここで現実の立体物と概ね同じサイズになっていることを確認しましょう。
Scaniverseでの編集時に底面を若干削っているので、
高さ方向が少しズレているかもしれません。
View Anglesについて
画面下部エリアのView Angles
から
「そのオブジェクトをどのアングルから見る可能性があるか」を定義します。
All Angles
は文字通り立体物の全方位から見る可能性があるものです。
地球儀などの球体を利用する際はこちらのオプションを選びます。
Upright
は右上 上から見下ろすアングルで利用する場合に選びます。
今回はクリスマスツリーを見下ろす(見上げるアングルは無いという意味)形になるので
このオプションで進めます。
Front
は更に範囲を絞り、上からに加え見る角度も立体物の手前側のみという設定です。
机に固定されている物体をトラッキングマーカーとするケースや、
自動車のハンドルやメーターをマーカーとするケースに向いている・・・っぽいですが、
使ったことは今のところありません。
View Angles
を設定したら画面上部エリア左上にある
Train
ボタンから機械学習を開始します。
トレーニング開始後はTraining
タブから進行状況を確認することができます。
この工程がまァ〜〜〜〜〜〜〜〜それはそれは時間がかかります。
電力もアホほど食うので、コンセントに繋いで待つしかありません。
「寝る前に仕掛けて、朝起きて終わってたらラッキー」というくらい時間がかかります。
12時間待つのだぞ。
ちなみに学習中はCPUもメモリも食いつぶしてブン回るので、
基本的に他タスクは終了して、Mac自体もなるべく触らないようにしましょう。
なんなら低温やけどするレベルで熱くなります。触らないというか、触れない。
3. ReferenceObjectの出力
じっと我慢の子であった・・・。(ホンマに)
トレーニングが完了すると画面上部エリアのOutput
タブから学習結果の出力ができます。
Get
ボタンで確実に学習結果データを保存しましょう。
ここで出力されるデータはReferenceObject
という独自形式のデータです。
内部には学習元のUSDZデータやトラッキング用データが入ってるらしい・・・。
このReferenceObject
データは後ほどXcodeで利用するので、
わかりやすい場所に保存しておきます。
ちなみに今回利用する黒いクリスマスツリーのトレーニングは
約11時間かかっています。勘弁してヨ・・・。
(過去16時間かかった物体があったので、なんなら短い方)
アプリケーション側制作
0. Xcodeプロジェクトの立ち上げ
トラッキング用マーカーを用意したら、アプリ実装に進みます。
Xcodeから新規にプロジェクトを立ち上げます。
テンプレートはvisionOS/App
を選びます。
プロジェクトのオプションは以下のとおりです
Initial Scene | Volume |
Immersive Space Renderer | RealityKit |
Immersive Space | Mixed |
その他は特に指定はありません。
1. AR演出の制作
プロジェクトを立ち上げたらまず初めにAR表示用の演出シーンを制作します。
プロジェクトディレクトリ内にPackagesというフォルダがあるはずです。
プロジェクト名/Packages/RealityKitContent/Package.realitycomposerpro
というファイルを選択すると以下のような画面になるので、
右上のOpen in Reality Composer Pro
のボタンからRealityComposerProを起動します。
RealityComposerProが起動したら画面下部のプロジェクトビューで
command + n
キーで今回表示する用の新規シーンを作ります。
任意のファイル名で大丈夫です。
(サンプルコード上ではChristmasTreeScene
としています。)
次に.referenceobject
の学習元となったスキャンデータのUSDZファイルを
RealityComposerProに導入します。
USDZファイルをプロジェクトビューにドラッグ・アンド・ドロップするだけで
インポートできますので、適当に持ってきます。
USDZファイルを持ってきたら先程作成したシーン内にドラッグ・アンド・ドロップして
オブジェクトを設置します。
画面右のインスペクタビューからTransformコンポーネントを確認し、
Position,Rotationともに0,0,0になっていればOKです。
こんな感じ↓
飾り付けをする
ツリー本体が配置できたら、ツリー周辺に飾りを配置していきます。
今回はシンプルにRealityComposerProに同梱されているプリミティブデータのみで
飾り付けを行っていきます。
画面右上のプラスボタンからプロジェクト内に追加するデータを選択します。
例えばプリミティブデータの中からSphereをダブルクリックすると、
シーン内原点にSphereが配置されます。
追加されたオブジェクトを選択すると画面右側のインスペクタビューから
座標やスケールなどを操作することができます。
UnityやUnrealEngineなど、他のゲーム制作ツールを触ったことがある人なら
比較的スムーズに操作できると思います。
シーン内によしなに配置していきましょう。
オブジェクトの色を変更したい場合は、
画面左側ヒエラルキービューの左下、プラスボタンから
Physically Based Material
を作成します。
マテリアル作成後は画面右側インスペクタビューから
Diffuse Color
を始め各種パラメータを変更していきます。
マテリアルを作成したら先ほど配置したオブジェクトに適用します。
色を変えたいオブジェクトを選択し、インスペクタビューの
Material Bindings
からマテリアルを変更します。
今回はRoot/Material
を選択します。
以下のような形で色が変わったら成功です。
パーティクルを飛ばす
次にツリーの周辺に雪が降るようなパーティクルを飛ばします。
画面左側ヒエラルキービューの左下、プラスボタンから
Particle Emmiter
を作成します。
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
を
それぞれいい感じに
サンプルプロジェクトでは以下のような挙動にしています。
Qiita用 pic.twitter.com/xzfdJOpRi5
— ごんびぃーᯅ@XR開発者集会 (@GONBEEE_project) December 16, 2024
パーティクルのパラメータはかなり色々自由にいじれるため、
時間があるときにじっくり向き合ってみるとよいでしょう。
カリング用マテリアル
最後にShader Graphを使って、オクルージョンカリング用のマテリアルを作ります。
現状はUSDZデータをそのまま配置しているため、
このシーンをロードするとツリーの3Dモデルがそのまま表示されてしまいます。
せっかくのARコンテンツなので
「現実の立体物の奥に存在する3Dモデルを隠す」オクルージョンを実装してみます。
画面左側ヒエラルキービューの左下、プラスボタンから
Shader Graph Material
を作成します。
次に画面下部のプロジェクトビューがあるエリアから、
Shader Graphタブを選択し、Graphエディタに移動します。
デフォルトでは以下のような配置になっているはずです。
Graphエディタの右上からNew Node
ボタンをクリックし、
Occlusion Surface(RealityKit)
ノードを検索します。
ノードが追加できると以下のように新規ノードが表示されます。
最後にOcclusionSurface
ノードのOut
ポートから、
Outputs
ノードのCustom Surface
ポートにドラッグ・アンド・ドロップで接続します。
以下のような接続ルートになったら完成です。
カリング用マテリアルが完成したら、
クリスマスツリーのモデルに対して適用します。
サンプルプロジェクトでは
Root/ChristmasBlack/Geom/mesh
オブジェクトにマテリアルのバインド設定があるので、
今作ったカリング用マテリアルをバインドします。
するとクリスマスツリーのモデルが透明になり、
加えてツリーの向こう側に存在するオブジェクトが見えなくなりました。
(黄色の輪郭は選択中のオブジェクトを示すアウトライン)
これで 「現実の立体物の奥に存在する3Dモデルを隠す」オクルージョンが
実現できました。
これにてAR演出側は完成です。
次ステップからコードの実装に進んでいきます。
2. ReferenceObjectのロード
Qiitaに乗せるには長すぎるので、GitHubへのリンクを貼っておきます。
コード全体
https://github.com/GONBEEEproject/ChristmARsTree/blob/main/ChristmARsTree/Main/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演出のロード・表示
マーカーの分岐
ここでは検出したマーカーに合わせて何を表示するかを定義していきます。
@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)と
実際にツリーに使うChristmasBlack
(ファイル名:ChristmasBlack.referenceobject)を
それぞれリストアップしています。
もしここに自分で用意した立体物を追加したい場合は、
enum ObjectType
に書き加えていきます。
ファイル名をそのままデータとして扱っており、enum登録時もファイル名一致で
追加するようにしてください。
表示するデータの定義
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:
のブロックに書いてあるように
-
realityKitContentBundle
からシーン名を指定し、非同期でロードする - 一応ロード失敗時用にエラーハンドリング
-
entity.addChild(scene)
でロードしたシーンをentityに追加する
という手順でロード・表示します。
ロード完了後はentityの座標情報を制御したり、
トラッキング状態に合わせて有効無効を切り替えたりする行が入りますが、
下記に説明するupdate
メソッドがあるため、ここではそこまで重要ではありません。
一応検出直後にその座標に表示する用ですが、
おまじないという事にして暗記したほうが早いと思います。
座標更新メソッド
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
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
トラッキングモード用View
https://github.com/GONBEEEproject/ChristmARsTree/blob/main/ChristmARsTree/Views/ObjectTrackingView.swift
各種UIウィンドウを制御するViewに関しては、
これといって特殊な事は行っていません。
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のプレビュー機能について
#Preview(windowStyle: .automatic) {
let appModel = AppModel()
ContentView()
.environment(appModel)
}
Xcodeでは上記のように#Preview
ブロックを実装すると
そのViewのプレビューが表示できますが(visionOSの場合はエミュレーターが起動する)、
今回のアプリケーションではエミュレーターでは実行できないARKitの機能を使っており、
プレビューを行うことができません。
面倒ですが、デバッグ時は実機に転送して本番環境でテストしましょう。
(これ本当にテスト方法ないのかな・・・デバッグすごい大変だけど・・・)
6. ChristmARsTreeApp実装
@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の連携と初期セットアップはこちらの記事をご参照ください。
一通りの実装が終わったらいよいよVisionPro実機にビルドを転送し、実行してみます。
アプリケーション起動後、現れるウィンドウから
Start Trackingのボタンを押すとマーカーの検出を始めます。
ウィンドウの表示がSearchingになったら目の前にクリスマスツリーをかざしましょう。
以下の動画のように3Dモデルやパーティクルが表示されたら成功です!
Qiita用、このあと公開されます
— ごんびぃーᯅ@XR開発者集会 (@GONBEEE_project) December 16, 2024
VisionProでクリスマスツリーを飾り付けるサンプルアプリ作った pic.twitter.com/MNZL0yNKjV
完成!&おわりに
かなり内容がミッチリ高密度な記事になってしまいましたが、いかがでしたでしょうか。
この記事をなぞったり、独自にカスタマイズを加えたりすれば
VisionProアプリの基礎的な実装方法や、
3Dスキャンを組み合わせた「現実空間に基づいたコンテンツ」制作、
RealityComposerProを使った3Dモデル配置~パーティクル演出などなど
かなり多方面の知識を身につけることが出来る(はず)です。
今回の内容から発展する形で
・独自の立体物をマーカーとして利用する
・複数の立体物に別々のAR演出を組み込み、同時表示する
・パーティクルや他の3Dモデルを追加し、より派手な演出を作る
などなど、様々な方向に掘り下げることができます。
この記事を足掛かりとして、Swiftを利用したVisionPro開発の道に
進んでいただければ幸いです。
最後まで読んでいただきありがとうございました。
「ここもっと詳細説明してほしい」や「ここバグってます」などご指摘ありましたら、
コメント欄かごんびぃーまでご連絡いただければと思います。