Apple Vision Pro開発の第一歩: XcodeとUnityで始める簡単開発ガイド
はじめに
MESONで実施中の MESON Apple Vision Proアドベントカレンダー #2 22日目の記事となります。
前日の記事はこちらになります。
この記事は、これからApple Vision Proで開発(visionOS向けの開発)を始める方向けに、開発の第一歩を踏み出す一助となるような内容をまとめたものです。
まず、開発に必要なものを紹介し、次に開発環境の準備を2つのパターンで説明します。
最後にプロジェクトの作成から動作確認までの手順を、MixedRealityの簡易的なアプリケーションを例に解説します。
開発に必要なもの
visionOS向けの開発には、Appleシリコンを搭載したMacが必要です。
シミュレーターを使って、テストを行うこともできますが、ハンドトラッキングや平面検知などの機能のテストには、実機デバイスが必要となります。
開発環境の準備
visionOS向けの開発環境を構築する方法として、Xcodeを使った開発とUnityを使った開発の2つのパターンを紹介します。
Xcodeを使った開発
Xcodeを使った開発では、Xcode 15が必要となります。
-
XcodeをApp Storeからダウンロードしてインストール
-
初回起動時に、開発するプラットフォームを選択する画面が表示されるので、visionOSを選択しインストール
開発に必要なSDKやシミュレーターがインストールされます。
※ β版時代の情報となりますが、この記事に掲載している内容に近しい画面が表示されます。
Unityを使った開発
Unityを使った開発でも、Xcode 15が必要となるため、Xcodeを使った開発と同様の設定が必要です。
また、visionOS向けの開発を行うためには、Unity Editorのバージョン 2022.3.19f1以上が必要です。
Unity Editorのインストールは、Unity Hubから行うことができます。
開発言語とフレームワーク・ツール、公式サンプルの紹介
ここでは、これから開発を始めるにあたり、使用する言語やフレームワーク、ツールキットについて簡単に紹介します。
また、公式サンプルを確認することで、どのようにプログラムを書くのか参考にすることができます。
Xcodeを使った開発
開発言語
Swift
フレームワーク
名称 | 概要 |
---|---|
SwiftUI | ユーザーインターフェイスを構築するための宣言型フレームワーク |
RealityKit | 高品質な拡張現実(AR)体験を提供するためのフレームワーク |
ARkit | 拡張現実(AR)フレームワーク |
公式サンプル
visionOS 2.0のベータリリースに伴い、公式のサンプルも2.0のものに更新されているものが多いですが、ここに情報が集まっています。
Unityを使った開発
開発言語
C#
ライブラリおよびツールキット
名称 | 概要 |
---|---|
AR Foundation | クロスプラットフォームでのAR開発をサポートするライブラリ |
XR Interaction Toolkit | ARおよびVRアプリケーションにおけるインタラクションシステムを構築するためのツールキット |
XR Hands | 手のトラッキングとジェスチャー認識をサポートするライブラリ |
PolySpatial | visionOS向けの開発をサポートするライブラリ |
公式サンプル
PolySpatialを導入した際に、Package Managerからインポートできるサンプルがあります。
また、以下のページにアプリケーション作成時のテンプレートとして利用できるサンプルが公開されています。
ハンズオン
ここでは、平面検知、ハンドトラッキングを利用した簡単なMixedRealityのアプリケーションを作成します。
Xcodeを使った開発とUnityを使った開発の2つのパターンで、ほぼ同等のアプリケーションを作成しています。
Xcodeを使った開発
1. プロジェクトの作成
Xcodeを起動し、visionOS向けの新規プロジェクトを作成します。
項目 | 設定内容 |
---|---|
Team |
Xcodeを使った開発で登録したアカウントを選択 |
Initial Scene |
Volumeを選択 |
Immersive Space Renderer |
RealityKitを選択 |
Immersive Space |
Mixedを選択 |
2. ビルドと実行
2.1. シミュレーターでの実行
Xcodeで Simulatorを選択し、実行します。
2.2. 実機デバイスでの実行
- Xcodeで Window > Devices and Simulatorsを選択し、ウインドウを開きます。
- 実機デバイスの 設定 > リモートデバイス メニューから開発しているMacを選択します。
- Xcodeのウインドウに実機デバイスが表示されるので、選択すると接続されます。
- Xcodeのウインドウに実機デバイスが表示されるので、選択すると接続されます。
- シミュレーターでの実行と同様に、実行ボタンを押すことで実機デバイスでのテストが可能です。
3. 平面検知の実装
平面検知とハンドトラッキングは、ARKitを使用して実装します。
ARKitのセッションとして同じものを利用するため、平面検知限定のプログラム名ではなく、ARInteractionManager.swift
としてコードを追加していきます。
3.1. 水平面のみ検知する
.horizontal
のみを指定することで、水平面に絞ります。
private let planeData = PlaneDetectionProvider(alignments: [.horizontal])
3.2. 地面のみ処理を行う
.floor
のみを処理するようにすることで、地面に限定できます。
func processPlaneDetectionUpdateHandler() async
{
for await update in planeData.anchorUpdates
{
// 処理を地面のみに限定する
guard update.anchor.classification == .floor else
{
continue
}
switch update.event
{
case .added, .updated:
await handlePlaneUpdate(update.anchor)
case .removed:
await handlePlaneRemoval(update.anchor)
}
}
}
完成した ARInteractionManager.swift のコード
import ARKit
import RealityKit
final class ARInteractionManager: ObservableObject
{
// ARInteractionManager全体を表すエンティティ
@Published var scene = Entity()
// ARKitセッションのインスタンス
private let session = ARKitSession()
// .horizontalのみを指定することで、水平面に絞る
private let planeData = PlaneDetectionProvider(alignments: [.horizontal])
@MainActor private var planeEntities: [UUID: Entity] = [:]
func runARSession() async
{
do
{
// 平面検知を開始
try await session.run([planeData])
}
catch
{
print("Error in runARSession: \(error)")
}
}
func processPlaneDetectionUpdateHandler() async
{
for await update in planeData.anchorUpdates
{
// 処理を地面のみに限定する
guard update.anchor.classification == .floor else
{
continue
}
switch update.event
{
case .added, .updated:
await handlePlaneUpdate(update.anchor)
case .removed:
await handlePlaneRemoval(update.anchor)
}
}
}
@MainActor
private func handlePlaneUpdate(_ anchor: PlaneAnchor)
{
if planeEntities[anchor.id] == nil
{
createPlaneEntity(anchor)
}
updatePlaneEntity(anchor)
}
@MainActor
private func createPlaneEntity(_ anchor: PlaneAnchor)
{
let entity = Entity()
// 現実世界の地面を表示するため、透明のマテリアルを作成
let material = UnlitMaterial(color: .white.withAlphaComponent(0))
// 検知した平面メッシュを持つエンティティを作成
let collisionEntity = ModelEntity(
mesh: .generatePlane(width: anchor.geometry.extent.width, height: anchor.geometry.extent.height),
materials: [material]
)
collisionEntity.transform = Transform(matrix: anchor.geometry.extent.anchorFromExtentTransform)
// メッシュをベースにCollisionコンポーネント設定
guard let mesh = collisionEntity.model?.mesh else
{
print("CollisionEntity has no mesh.")
return
}
let shape = ShapeResource.generateConvex(from: mesh)
collisionEntity.components.set(CollisionComponent(shapes:[shape]))
// 静的なPhysicsBosyコンポーネントを設定
let physicsBody = PhysicsBodyComponent(massProperties: .default, material: .default, mode: .static)
collisionEntity.components.set(physicsBody)
entity.addChild(collisionEntity)
planeEntities[anchor.id] = entity
scene.addChild(entity)
}
@MainActor
private func updatePlaneEntity(_ anchor: PlaneAnchor)
{
guard let entity = planeEntities[anchor.id] else
{
return
}
entity.transform = Transform(matrix: anchor.originFromAnchorTransform)
if let collisionEntity = entity.children.first as? ModelEntity
{
// 更新を検知した平面に合わせてメッシュを更新
collisionEntity.model?.mesh = .generatePlane(width: anchor.geometry.extent.width, height: anchor.geometry.extent.height)
collisionEntity.transform = Transform(matrix: anchor.geometry.extent.anchorFromExtentTransform)
// 更新したメッシュに合わせて、Collisionコンポーネントを更新
if var collisionComponent = collisionEntity.components[CollisionComponent.self]
{
guard let mesh = collisionEntity.model?.mesh else
{
print("CollisionEntity has no mesh.")
return
}
let shape = ShapeResource.generateConvex(from: mesh)
collisionComponent.shapes = [shape]
collisionEntity.components.set(collisionComponent)
}
}
}
@MainActor
private func handlePlaneRemoval(_ anchor: PlaneAnchor)
{
planeEntities[anchor.id]?.removeFromParent()
planeEntities.removeValue(forKey: anchor.id)
}
}
3.3. Immersive Spaceの処理に平面検知を追加
平面検知は、Immersive Spaceでのみ利用できます。
ImmersiveView.swift
がImmersive Spaceでの処理を行うプログラムです。
ARInteractionManagerのインスタンスを作成し、インスタンスが管理するシーンをコンテンツに追加するとともに、 ARkitのセッションの開始と平面検知の更新処理開始を行なっています。
import SwiftUI
import RealityKit
import RealityKitContent
struct ImmersiveView: View {
@StateObject private var arInteractionManager = ARInteractionManager()
var body: some View {
RealityView { content in
// ARInteractionManagerが管理するシーンをRealityViewのコンテンツに追加
// これにより、ARKitセッションの更新やインタラクションの結果が自動的にRealityViewに反映される
content.add(arInteractionManager.scene)
}
.task
{
// ARKitセッションの開始
await arInteractionManager.runARSession()
// 平面検知の更新処理
await arInteractionManager.processPlaneDetectionUpdateHandler()
}
}
}
3.4. アプリケーションがワールドセンシングデータにアクセスが必要な理由を追加
平面検知を利用する場合、Info.plist
に追加が必要となります。
Imformation Property List
に、NSWorldSensingUsageDescription Keyを追加、Typeを String に設定し、Valueに平面検知が必要な理由を記述します。
4. ハンドトラッキングの実装
ここでは、あらかじめ定義されているハンドジェスチャーによるオブジェクトの移動と、カスタムハンドジェスチャの足掛かりとなる手の関節の可視化を行います。
4.1. 手の関節を可視化: ハンドトラッキングの準備
ARInteractionManager.swift
に機能を追記して行きます。
private let handTracking = HandTrackingProvider()
func runARSession() async
{
do
{
// 平面検知とハンドトラッキングを開始
try await session.run([planeData, handTracking])
}
catch
{
print("Error in runARSession: \(error)")
}
}
4.2. 手の関節を可視化: 手の動きを検知
ハンドトラッキングの更新処理を行う。
func processHandTrackingUpdateHander() async
{
for await update in handTracking.anchorUpdates
{
switch update.event
{
case .updated:
await handleHandUpdate(update)
default:
break
}
}
}
4.3. 手の関節を可視化: 可視化処理
トラッキングしている手の関節をループで処理し、可視化するオブジェクトを配置しています。
private func handleSkeletonUpdate(_ handSkeleton: HandSkeleton, _ rootTransform: float4x4, _ prefix: String) async
{
// 手の全ての関節をループで処理
for joint in handSkeleton.allJoints
{
// 関節に名称を付与し、関節のモデル表示が行われているかを判定する
let name = "\(prefix)-\(joint.name)"
// 名称が存在した場合
if let entity = scene.findEntity(named: name)
{
// トラッキングされている場合
if joint.isTracked
{
// joint.anchorFromJointTransform: アンカーから関節への変換行列
// rootTransform * joint.anchorFromJointTransform により原点からのTransformが取得できる
entity.setTransformMatrix(rootTransform * joint.anchorFromJointTransform, relativeTo: nil)
entity.isEnabled = true
}
else
{
entity.isEnabled = false
}
}
else
{
guard joint.isTracked else
{
continue
}
do
{
// 関節のモデルをロードし、Entitiyを作成
let entity = try await Entity(named: "Joint")
entity.name = name
entity.setTransformMatrix(rootTransform * joint.anchorFromJointTransform, relativeTo: nil)
scene.addChild(entity)
}
catch
{
print("Failed to create entity: \(error)")
}
}
}
}
完成した ARInteractionManager.swift のコード
import ARKit
import RealityKit
final class ARInteractionManager: ObservableObject
{
// ARInteractionManager全体を表すエンティティ
@Published var scene = Entity()
// 最新の手のトラッキング情報
@Published var latestHandTracking: (leftHand: HandAnchor?, rightHand: HandAnchor?)
// ARKitセッションのインスタンス
private let session = ARKitSession()
// .horizontalのみを指定することで、水平面に絞る
private let planeData = PlaneDetectionProvider(alignments: [.horizontal])
@MainActor private var planeEntities: [UUID: Entity] = [:]
private let handTracking = HandTrackingProvider()
func runARSession() async
{
do
{
// 平面検知とハンドトラッキングを開始
try await session.run([planeData, handTracking])
}
catch
{
print("Error in runARSession: \(error)")
}
}
func processPlaneDetectionUpdateHandler() async
{
for await update in planeData.anchorUpdates
{
// 処理を地面のみに限定する
guard update.anchor.classification == .floor else
{
continue
}
switch update.event
{
case .added, .updated:
await handlePlaneUpdate(update.anchor)
case .removed:
await handlePlaneRemoval(update.anchor)
}
}
}
@MainActor
private func handlePlaneUpdate(_ anchor: PlaneAnchor)
{
if planeEntities[anchor.id] == nil
{
createPlaneEntity(anchor)
}
updatePlaneEntity(anchor)
}
@MainActor
private func createPlaneEntity(_ anchor: PlaneAnchor)
{
let entity = Entity()
// 現実世界の地面を表示するため、透明のマテリアルを作成
let material = UnlitMaterial(color: .white.withAlphaComponent(0))
// 検知した平面メッシュを持つエンティティを作成
let collisionEntity = ModelEntity(
mesh: .generatePlane(width: anchor.geometry.extent.width, height: anchor.geometry.extent.height),
materials: [material]
)
collisionEntity.transform = Transform(matrix: anchor.geometry.extent.anchorFromExtentTransform)
// メッシュをベースにCollisionコンポーネント設定
guard let mesh = collisionEntity.model?.mesh else
{
print("CollisionEntity has no mesh.")
return
}
let shape = ShapeResource.generateConvex(from: mesh)
collisionEntity.components.set(CollisionComponent(shapes:[shape]))
// 静的なPhysicsBosyコンポーネントを設定
let physicsBody = PhysicsBodyComponent(massProperties: .default, material: .default, mode: .static)
collisionEntity.components.set(physicsBody)
entity.addChild(collisionEntity)
planeEntities[anchor.id] = entity
scene.addChild(entity)
}
@MainActor
private func updatePlaneEntity(_ anchor: PlaneAnchor)
{
guard let entity = planeEntities[anchor.id] else
{
return
}
entity.transform = Transform(matrix: anchor.originFromAnchorTransform)
if let collisionEntity = entity.children.first as? ModelEntity
{
// 更新を検知した平面に合わせてメッシュを更新
collisionEntity.model?.mesh = .generatePlane(width: anchor.geometry.extent.width, height: anchor.geometry.extent.height)
collisionEntity.transform = Transform(matrix: anchor.geometry.extent.anchorFromExtentTransform)
// 更新したメッシュに合わせて、Collisionコンポーネントを更新
if var collisionComponent = collisionEntity.components[CollisionComponent.self]
{
guard let mesh = collisionEntity.model?.mesh else
{
print("CollisionEntity has no mesh.")
return
}
let shape = ShapeResource.generateConvex(from: mesh)
collisionComponent.shapes = [shape]
collisionEntity.components.set(collisionComponent)
}
}
}
@MainActor
private func handlePlaneRemoval(_ anchor: PlaneAnchor)
{
planeEntities[anchor.id]?.removeFromParent()
planeEntities.removeValue(forKey: anchor.id)
}
func processHandTrackingUpdateHander() async
{
for await update in handTracking.anchorUpdates
{
switch update.event
{
case .updated:
await handleHandUpdate(update)
default:
break
}
}
}
@MainActor
private func handleHandUpdate(_ update: AnchorUpdate<HandAnchor>) async
{
let anchor = update.anchor
// トラッキングが行われていて、手の関節の位置を取得できている場合は処理を続ける
guard anchor.isTracked, let handSkelton = anchor.handSkeleton else
{
return
}
// 最新のトラッキング情報を更新
latestHandTracking = handTracking.latestAnchors
// 原点からアンカーへの変換行列を取得
let rootTransform = anchor.originFromAnchorTransform
// 関節の更新処理を実行
await handleSkeletonUpdate(handSkelton, rootTransform, anchor.chirality.description)
}
@MainActor
private func handleSkeletonUpdate(_ handSkeleton: HandSkeleton, _ rootTransform: float4x4, _ prefix: String) async
{
// 手の全ての関節をループで処理
for joint in handSkeleton.allJoints
{
// 関節に名称を付与し、関節のモデル表示が行われているかを判定する
let name = "\(prefix)-\(joint.name)"
// 名称が存在した場合
if let entity = scene.findEntity(named: name)
{
// トラッキングされている場合
if joint.isTracked
{
// joint.anchorFromJointTransform: アンカーから関節への変換行列
// rootTransform * joint.anchorFromJointTransform により原点からのTransformが取得できる
entity.setTransformMatrix(rootTransform * joint.anchorFromJointTransform, relativeTo: nil)
entity.isEnabled = true
}
else
{
entity.isEnabled = false
}
}
else
{
guard joint.isTracked else
{
continue
}
do
{
// 関節のモデルをロードし、Entitiyを作成
let entity = try await Entity(named: "Joint")
entity.name = name
entity.setTransformMatrix(rootTransform * joint.anchorFromJointTransform, relativeTo: nil)
scene.addChild(entity)
}
catch
{
print("Failed to create entity: \(error)")
}
}
}
}
}
4.4. 手の関節の可視化: Immersive Spaceの処理にハンドトラッキングを追加
平面検知と同様にハンドトラッキングもImmersive Spaceのみで利用できるため、ImmersiveView.swift
にハンドトラッキング処理を追加します。
セッションの開始後、平面検知とハンドトラッキングの処理を並行して起動します。
.task
{
// ARKitセッションの開始
await arInteractionManager.runARSession()
// 平面検知とハンドトラッキングの非同期タスクを同時に開始
async let planeDetectionTask: () = arInteractionManager.processPlaneDetectionUpdateHandler()
async let handTrackingTask: () = arInteractionManager.processHandTrackingUpdateHander()
// タスク完了を待機
await planeDetectionTask
await handTrackingTask
}
4.5. 手の関節の可視化: アプリケーションがハンドトラッキングデータにアクセスが必要な理由を追加
ハンドトラッキングを利用する場合、Info.plist
に追加が必要となります。
Imformation Property List
に、NSHandsTrackingUsageDescription Keyを追加、Typeを String に設定し、Valueにハンドトラッキングが必要な理由を記述します。
4.6. ハンドジェスチャでオブジェクトを移動: シーンの作成
移動するオブジェクトは、Reality Composer Proでシーンを作成し、あらかじめ球体(Sphere)オブジェクトを配置しておきます。
- File > New > Scene... からシーンを作成
ここでは、HandGesture
という名前のシーンを作成します。 - プラスボタンの Primitive ShapeからSphereを追加
Sphereを追加し、Transformを以下のように調整します。-
Position
: (0, 1.5, -0.5) -
Scale
: (0.5, 0.5, 0.5)
-
- ハンドジェスチャでオブジェクトを移動するために、
Physics Body
、Collision
、Input Target
3つのコンポーネントを追加します。
コンポーネントの追加は、Add Componentボタンから行います。-
Physics Body
: Affected by Gravity のチェックを外します。 -
Collision
: Shapeを Sphere に設定します。
-
4.7. ハンドジェスチャでオブジェクトを移動: シーンの読み込み
ImmersiveView.swift
に、作成したシーンを読み込む処理を追加します。
if let handGestureScene = try? await Entity(named:"HandGesture", in: realityKitContentBundle)
{
guard let sphere = handGestureScene.findEntity(named: "Sphere") as? ModelEntity else
{
return
}
// ホバーエフェクトを追加
sphere.components.set(HoverEffectComponent())
sphereEntity = sphere
arInteractionManager.scene.addChild(sphereEntity)
}
4.8. ハンドジェスチャでオブジェクトを移動: オブジェクトの移動処理
ImmersiveView.swift
に、ドラッグジェスチャを追加し、オブジェクトの移動処理を行います。
.gesture(
DragGesture()
.targetedToEntity(sphereEntity)
.onChanged(
{ value in
// ドラッグの位置を3D空間に変換
let location3D = value.convert(value.location3D, from: .local, to: sphereEntity.parent!)
if dragOffset == nil
{
// 初回のドラック時のオフセットを計算
dragOffset = sphereEntity.position - location3D
}
if let offset = dragOffset
{
// 球体の位置を更新
sphereEntity.position = location3D + offset
}
}
)
.onEnded(
{ _ in
// ドラッグ終了時にオフセットをリセット
dragOffset = nil
if var physicsBodyComponent = sphereEntity.components[PhysicsBodyComponent.self]
{
// 重力の影響を有効にする
physicsBodyComponent.isAffectedByGravity = true
sphereEntity.components.set(physicsBodyComponent)
}
}
)
)
完成した ImmersiveView.swift のコード
import SwiftUI
import RealityKit
import RealityKitContent
struct ImmersiveView: View {
@StateObject private var arInteractionManager = ARInteractionManager()
// 球体のエンティティを保持
@State private var sphereEntity: Entity = Entity()
// ドラッグ操作時のオフセットを保持
@State private var dragOffset:SIMD3<Float>?
var body: some View {
RealityView { content in
// ARInteractionManagerが管理するシーンをRealityViewのコンテンツに追加
// これにより、ARKitセッションの更新やインタラクションの結果が自動的にRealityViewに反映される
content.add(arInteractionManager.scene)
if let handGestureScene = try? await Entity(named:"HandGesture", in: realityKitContentBundle)
{
guard let sphere = handGestureScene.findEntity(named: "Sphere") as? ModelEntity else
{
return
}
// ホバーエフェクトを追加
sphere.components.set(HoverEffectComponent())
sphereEntity = sphere
arInteractionManager.scene.addChild(sphereEntity)
}
}
.task
{
// ARKitセッションの開始
await arInteractionManager.runARSession()
// 平面検知とハンドトラッキングの非同期タスクを同時に開始
async let planeDetectionTask: () = arInteractionManager.processPlaneDetectionUpdateHandler()
async let handTrackingTask: () = arInteractionManager.processHandTrackingUpdateHander()
// タスク完了を待機
await planeDetectionTask
await handTrackingTask
}
.gesture(
DragGesture()
.targetedToEntity(sphereEntity)
.onChanged(
{ value in
// ドラッグの位置を3D空間に変換
let location3D = value.convert(value.location3D, from: .local, to: sphereEntity.parent!)
if dragOffset == nil
{
// 初回のドラック時のオフセットを計算
dragOffset = sphereEntity.position - location3D
}
if let offset = dragOffset
{
// 球体の位置を更新
sphereEntity.position = location3D + offset
}
}
)
.onEnded(
{ _ in
// ドラッグ終了時にオフセットをリセット
dragOffset = nil
if var physicsBodyComponent = sphereEntity.components[PhysicsBodyComponent.self]
{
// 重力の影響を有効にする
physicsBodyComponent.isAffectedByGravity = true
sphereEntity.components.set(physicsBodyComponent)
}
}
)
)
}
}
5. アプリ開始時に没入空間に遷移
平面検知、ハンドトラッキングともに、没入空間での処理を行うため、アプリケーションの起動時に没入空間に遷移するように設定します。
5.1. Application Scene Manifestの変更
Preferred Default Scene Session Role
を Immersive Space Application Session Role に変更します。
5.2. Appファイルの変更
プロジェクトを作成した際に、プロジェクト名
+ App
という名前のファイルが作成されています。
このファイルがアプリケーションのエントリーポイントとなります。
直接、ImmersiveViewを呼び出すように変更します。
import SwiftUI
@main
struct FirstStepAppApp: App {
var body: some Scene {
ImmersiveSpace(id: "ImmersiveSpace") {
ImmersiveView()
}
}
}
6. 完成動画
Unityを使った開発
1. プロジェクトの作成
Unity Hubを起動し、Universal 3Dを選択して新規プロジェクトを作成します。
2. プロジェクトの設定
2.1. ビルドターゲットを visionOS に設定
File > Build Settingsを開き、Platform
に visionOS を選択、Switch Platformをクリックします。
2.2. Company Name、Bundle Identifierの設定
Edit > Project Settings > Playerを開き、Company Name
、Bundle Identifier
を設定します。
2.3. XR Plugin Managementのインストール
Edit > Project Settings > XR Plugin Managementを開き、Install XR Plugin Managementをクリックします。
2.3.1. XR Plugin Managementの設定
visionOS settings
で、Apple visionOS を選択します。
2.3.2. Project Validationの修正
Project Validationで、Fix Allをクリックします。
2.3.3. Apple visionOSの設定
App Mode
を Mixed Reality - Volume or Immersive Space に設定します。
また、Hand Tracking Usage Description
にハンドトラッキングデータにアクセスが必要な理由、World Sensing Usage Description
にワールドセンシングデータにアクセスが必要な理由を記述します。
2.3.4. PolySpatialのサンプルをインポート
開発の参考になる他、実際の開発においても利用できるアセットが含まれているのでインポートしておきます。
2.3.5. XR Interaction Toolkitのインストールとサンプルのインポート
- XR Interaction Toolkitをインストール
- サンプルのインポート
- 開発の参考になる他、実際の開発においても利用できるアセットが含まれています。
- 開発の参考になる他、実際の開発においても利用できるアセットが含まれています。
3. ビルドと実行
3.1. シミュレーターでの実行
- Edit > Project Settings > Playerを開き、
Target SDK
に Simulator SDK を選択します。
- File > Build Settings を開き、Build をクリックします。
- ビルドが成功すると、Xcodeのプロジェクトが作成されていますので、
Unity-VisionOS.xcodeproj
をダブルクリックしてXcodeを起動、プログラムを実行します。
- ビルドが成功すると、Xcodeのプロジェクトが作成されていますので、
3.2. 実機デバイスでの実行
- Edit > Project Settings > Player を開き、
Target SDK
に Device SDK を選択します。
- File > Build Settings を開き、Build をクリックします。
- ビルドが成功すると、Xcodeのプロジェクトが作成されていますので、
Unity-VisionOS.xcodeproj
をダブルクリックしてXcodeを起動します。
- ビルドが成功すると、Xcodeのプロジェクトが作成されていますので、
- XcodeのSigning & Capabilitiesで、
Automatically manage signing
をチェック、Team
を選択します。
- Xcodeを使った開発の 2.2. 実機デバイスでの実行 の手順に従って実行します。
3.3. Play to Device
PolySpatialでは、Unity EditorからデバイスやvisionOSシミュレーターへの再生機能が用意されています。
わざわざ、Xcodeを介してビルドすることなく、Unity Editorからデバイスやシミュレーターへの再生が可能です。
3.3.1. シミュレーター、もしくは実機にPlay To Device Hostアプリケーションをインストール
- TestFlightアプリケーションをApp Storeからインストールします。
- このページのDevice TestFlight Linkから、Play To Device Hostアプリケーションをインストールします。
3.3.2. シミュレーター、もしくは実機でPlay To Device Hostアプリケーションを起動
接続URLが表示されます。
3.3.3. Unity EditorでPlay To Deviceの設定
Window > PolySpatial > Play To Deviceを選択します。
- 3.3.2で表示されたURLを、入力してAdd Deviceをクリックします。
- 登録したデバイスを選択して、接続を有効にします。
3.3.4. Unity Editorで実行
Play Modeを開始すると、Unity Editorからデバイスやシミュレーターへの再生が可能です。
4. ベースシーンの作成
公式で配布されているベーステンプレートから不要なものを削ったシーンを、新規シーンから作成してみます。
4.1. Volume Cameraコンポーネントの追加と設定
GameObject > XR > Set Volume Cameraを選択し、Volume Camera
コンポーネントを追加します。
-
Volume Window Configuration
に、Unbounded_VolumeCameraConfiguration を設定します。
4.2. AR Sessionコンポーネントの追加
GameObject > XR > AR Session を選択し、AR Session
コンポーネントを追加します。
4.3. XR Originコンポーネントの追加と設定
GameObject > XR > XR Origin (AR)を選択し、XR Origin
コンポーネントを追加します。
これにより、XR Origin (XR Rig)、XR Interaction Managerが追加されます。
Main Camera とXR Origin (XR Rig)に配置されている Left Controller、Right Controller は不要なので削除します。
4.4. XR Origin コンポーネント、Input Action Managerコンポーネントの設定
-
Camera Y Offset
を0に設定します。 -
Action Assets
に PolySpatialInputActions を設定します。
4.5. Tracked Pose Driverコンポーネントの設定
Position Inputの Action
、Rotation InputのAction
に、下記の内容でそれぞれBindingを追加します。
- <PolySpatialXRHMD>/centerEyePosition
-
<PolySpatialXRHMD>/centerEyeRotation
4.6. シーンの保存
ここまでで、ベースのシーンができましたので、保存します。
5. 平面検知の実装
地面を認識できるように平面検知の機能を設定していきます。
5.1. XR Origin (XR Rig)に AR Plane Managerコンポーネントを追加
Detection Mode
を Horizontal に設定します。
5.2. 平面用のプレハブを作成
5.2.1. 透明のマテリアルを作成
現実の地面が見えるよう透明のマテリアルを作成します。
Assets > Create > Materialを選択し、マテリアルのアセットを作成します。
-
Shader
: Universal Render Pipeline/Unlit -
Surface Type
: Transparent -
Base Map
: (0, 0, 0, 0)
5.2.2. 平面用のオブジェクトを作成
- GameObject > Create Emptyを選択し、空のオブジェクトAR Planeを作成します。
- 作成したAR Planeに下記のコンポーネントを追加します。
ARPlane
ARPlaneMeshVisualizer
MeshCollider
MeshFilter
MeshRenderer
MeshRendererのMaterials
に、作成した透明のマテリアルを設定します。
5.2.3. 平面用オブジェクトをプレハブ化
AR Planeをプレハブ化し、シーンから削除します。
5.3. AR Plane Managerコンポーネントの設定
6. ハンドトラッキングの実装
6.1. 手の関節の可視化
これは、PolySpatialのサンプルで既に実現されています。
- Main Cameraと同じ階層で、GameObject > Create Emptyを選択し、空のオブジェクトHand Managerを作成します。
- 作成したHand Managerに
HandVisualizer
コンポーネントを追加します。 -
HandVisualizer
コンポーネントのJoint Visual Prefab
に JointVisuals を設定します。
6.2. ハンドジェスチャでオブジェクトを移動: ハンドジェスチャなど空間ポインタの入力を利用
PolySpatialで、XRTouchSpeceInteractor
コンポーネントが提供されています。
これは、XR Interaction Toolkitを利用して空間ポインタ入力を受け付けるInteractorコンポーネントです。
今回は、これを利用してオブジェクトを移動する処理を実装します。
- GameObject > Create Emptyを選択し、空のオブジェクトXRTouchSpaceInteractorを作成します。
-
XRTouchSpaceInteractor
コンポーネントを追加します。-
Spatial Pointer
: Touch/Primary Touch
-
- 同様に、空のオブジェクトXRTouchSpaceInteractor Secondaryを作成し、
XRTouchSpaceInteractor
コンポーネントを追加します。-
Spatial Pointer
: Touch/Secondary World Touch
-
6.3. ハンドジェスチャでオブジェクトを移動: 移動するオブジェクトの作成
- GameObject > 3D Object > Sphereを選択し、Sphereを作成します。
-
Position
: (0, 1.5, 0.5) -
Scale
: (0.5, 0.5, 0.5)
-
- 以下のコンポーネントを追加します。
-
Rigidbody
-
Is Kinematic
: 有効
-
-
XRGrabInteractable
-
Interactable Events > Select Exitecd
:Rigidbody
のIs Kinematic
を無効
-
XRGeneralGrabTransfomer
-
VisionOSHoverEffect
-
7. 完成動画
まとめ
この記事では、XcodeとUnityを使ったApple Vision Pro開発の第一歩を紹介しました。
Xcodeを使った開発
SwiftUIやRealityKitを利用したコードの記述が必要です。公式サンプルや豊富なドキュメントを参考にしながら進めることで、visionOS向けのアプリケーションを効率的に開発できます。
Unityを使った開発
Unityを使った開発では、PolySpatial、AR Foundation、XR Interaction Toolkitなどのアセットを活用することで、コーディングを最小限に抑えた開発が可能です。ただし、Unity Proライセンスが必要になるため、個人開発者には敷居が高い場合があります。
どちらのアプローチにも利点がありますが、開発するアプリケーションの目的や開発者のスキルセットに応じて選択するのが良いでしょう。今後も、visionOSやApple Vision Proに関する最新情報をフォローし、開発を進めていきましょう。
書いた人
高野 剛
Unixシステムのインフラ構築・運用を経験後、ECサイトを中心としたWebアプリ開発、プロジェクトマネージメントに従事する。
ミニオン好きが高じて、USJに通い続ける中、XRアトラクションに魅了される。
自分が感動したことを他の人にも体験してもらいたいという思いから、転職を決意し、XRの学校での1年間の学びを経てMESONへ入社。
MESON Works
MESONの制作実績一覧もあります。ご興味ある方はぜひ見てみてください。
Discussion
実機がある場合、Unityのみでの開発は可能ですか?
ご確認ありがとうございます!
実機にアプリをビルドするためには、Xcodeが必要なため、Xcodeをインストール・設定しておくことは必要になります。
Unity上のビルドで作成されるのは、Xcode用のプロジェクトファイルとなります。
※ Play To Deviceは、あくまで実機再生が可能ということであり、実際にアプリケーションがインストールされるわけではありません。