公式ドキュメントなど
WWDC
WWDC2023 | Principles of spatial design | Apple
- Vision OSにおいても、ボタンのサイズは最低44pt、各ボタン間の距離は最低16ptとすべきと述べている。
- 主要コンテンツはユーザーの視界の中央部に置くべきと述べている。でなければ、コンテンツ内容を把握するのが困難となる。極端に高い位置や低い位置、ユーザーの背後などは、没入型コンテンツでない限りはやめた方がいい。
- 逆に、ユーザーが頭の向きを変えているのに、視界の中央に居座り続けるコンテンツも邪魔なのでやめた方がいい。
Develop your first immersive app
Meet Reality Composer Pro
Explore materials in reality composer pro
Work with reality composer pro content in xcode
- 自分オリジナルのComponentを作成する方法を紹介する。コードから追加してもいいが、Reality Composer Proのコンポーネントを追加ボタンを押すと、該当するSwiftファイルが自動生成される。該当コンポーネントにコード上でプロパティを追加すれば、Reality Composer Proでも反映される。
Explore the usd ecosystem
Meet arkit for spatial computing
Evolve your arkit for spatial computing
Create enhanced spatial computing experiences with arkit
Build spatial experiences with realitykit
Get started with building apps for spatial computing
- Window, Volume, Spaceの3種の表示形式がある。Windowは伝統的なViewなどを表示できるが、
Model3D
などをつかって3D幅のあるコンテンツも表示可能。Volumeは奥行きがあり、RealityKitを使って3Dモデルを表示できる。複数のDepthは重なりうる。Spaceは画面全体を利用する方式であり、周囲の環境は見えたままのPassthroughという方式か、周囲の環境は見えずアプリの画面のみが見えるImmersiveの二方式がある。ソースコード上ではこの二つは.mixedと.full として表現される。なお中間の.progressiveというのもあり、ユーザーがDigital Crownでどの程度アプリがユーザーの視界を覆うかを変えられる。またSpaceではARキットのカスタムジェスチャーが利用できる。
- VolumeはWindowの一種として内部的に定義されており、使用時はWindowGroupを使う。Volumeには閉じるためのボタンや、ドラッグ移動するためのボタン、アプリ名を示すラベルが自動で表示されている。
var body: some View {
WindowGroup {
Model3D(named: "Satelite")
}
.windowStyle(.volumetric)
.defaultSize(width: 0.6, height: 0.6, depth: 0.4, in: .meters)
}
-
Model3D
でも良いが、RealityView
では複数の3Dモデルをシームレスに扱える。- 最初のクロージャは初期化時に一度呼ばれる。Entityを初期化し、追加すると良い。
- 二つ目のupdateクロージャは、SwiftUIのStateがアップデートされるたびに呼ばれるので、何度でも呼ばれる可能性がある。
- attachments クロージャで3Dモデルに追加部品を定義できるが、あくまでも定義しているだけである。そのため、make, updateクロージャの中で、attachmentを明示的に3Dモデルに追加・更新する処理を書く必要がある。
var body: some View {
RealityView { contents in
let initialEntity = Entity(named: "Initial")
contents.add(initialEntity)
if let attachment = attachments.entity(for: "id") {
content.add(attachment)
}
} udpate: { contents, attachments in
} attachments: {
LocationPin(id: "id")
.tag("pin")
}
}
- 今までのように画面を直接タップするなどのDirect Inputはもちろんのこと、目線を合わせてアイテムを選択し指同士をタップさせて選択するというIndirect Inputもサポートされる。これらジェスチャーはRealityKitで表示した3Dモデルに対してもそのまま働かせることができる。ARKitを使えばSkeletal Hand Tracking技術を利用し、さまざまな手の動きのカスタムジェスチャーを利用できる。Skeletal Hand Trackingを利用する際は、位置情報などと同じでユーザーの許可が必要。またトラックパッドやキーボード、コントローラと接続して使うこともできる。
- SharePlayを利用して3Dモデルの動きや、ユーザーのジェスチャーを全員に伝えることが可能。
- Xcode15以降、デバッグ機能としてVisualizationというのが追加された。3Dモデルの軸などを表示できる。
- デバッグツールのInstrumentsに
RealityKit Trace
というのが追加されており、3DモデルのCPU消費、ハングの有無などを分析できる。
-
Reality Composer Pro というツールで3Dモデルの編集ができる。デフォルトのものもたくさん用意されているほか、オリジナルの3Dモデルの追加も可能。3Dモデルへの音の追加もここでできる。
-
今までのジェスチャーに加えて、3D特有のジェスチャーに関するクラスなどが追加されている。
-
RotateGesture3D
: 元となるのはRotateGesture
。元と比べてx, y, z軸のどの回転に限定するかも指定可能となっている。 -
targetedToEntity(_:)
,targetedToAnyEntity()
: RealityKitで表示した3Dモデル(Entity)について、特定のEntityに触れたジェスチャーのみを扱いたい場合はこれらのメソッドを利用する。 -
SpatialTapGesture
: 元となったのはTapGesture
であるが、TapGestureと違ってタップ位置も取得できる。平面上の座標だけでなく空間上の座標も取得できる(location3D)。タップだけではなくドラッグなど他のジェスチャーでも、空間上の座標など3D関連情報を取得できるプロパティが追加されている:DragGesture.Value.location3D
-
-
Immersive Spaceについて、
.full
の場合はユーザーの視界を遮ることになるので、いきなり突入するのではなくユーザーがボタンを押したら開始するなどが良い。そのための基本的なコードは以下である。
@main
struct WorldApp: App {
@State priavte var selectedStyle: ImmersionStyle = .full
var body: some Scene {
// ...
ImmersiveSpace(id: "solar-system") {
SolarSystem()
}
.immersionStyle(
selection: $selectedStyle,
in: .full
)
}
}
// ...
@Environment(\.openImmersiveSpace)
private var openImmersiveSpace
Button("Show Outer Space") {
openImmersiveSpace(id: "solar-system")
}
Meet SwiftUI for spatial computing
Elevate your windowed app for spatial computing
- VisionOSではViewをかなり拡大縮小することがあり得るが、このときに綺麗に見れるようにするためには、ベクター画像が必要で、拡大縮小に弱いビットマップ画像ではいけない。
-
Color.white
などといった特定の色は、さまざまな明るさに対応しなければならないVisionOSにおいては不適切なことがある。このような特定の色(Solid Color)ではなく、.primary
,.secondary
といったSemantic Color を用いると、VisionOSにおいても適切に表示してくれる。SwiftUIのデフォルトの部品を使っている場合は自動的に適切な色となっている。またVisionOSではダークモードか否かの区別も無意味なので、やはりSemantic Colorの方が良い。 - ボタンなど、インタラクティブ可能なパーツに目線を合わせると、そこが浮き上がって見える(Hover Effect)ようにVisionOSではなっており、ユーザーがどこのボタンなどを選択するかわかりやすくしている。なお、MacOS, iPadOSでもマウスを使った場合はホバーエフェクトが出る。デフォルトのUI部品には全てHover Effectが自動でついている。タップジェスチャーなどのジェスチャーを自分で追加した場合は、.hoverEffectモディファイアを追加することで、Hover Effectを得られる。また、タップジェスチャーなどのタップ領域を広げるのによく用いられるcontentShape(::eoFill:)モディファイアに対し
.hoverEffect
を渡すと、Hover Effectで色が変わる領域を変更することができる:
VStack {
// something
}
.contentShape(.interaction, .rect)
.contentShape(.hoverEffect, .rect(cornerRadius: 16))
.onTapGesture {
action()
}
.hoverEffect()
- Hover Effectによりユーザーが視線を合わせていることをアプリ側が知る方法はない。プライバシー保護のため。
- iPadでは、(NavigationSplitViewを利用した)サイドバーの利用が推奨されていた。しかしVisionOSでは画面が十分広いことから、全要素がWindowの中に収まっている必要はなく、Windowの外にあっても問題ない。そのため、TabBar(TabView)の利用が推奨される。このようなWindowの外の追加的な要素をOrnamentsと呼んでいる。
- 以上のように、Vectorファイルを利用したり、Hover Effectを追加したり、Ornamentsを利用したりすることで、既存アプリがVisionOSで快適に利用できる。
Take SwiftUI to the next dimension
Windowとは異なり、深さ(Z軸)を持ち3Dモデルを表示できる「Depth」について紹介している。
WindowGroup {
GlobeView
}
.windowStyle(.volumetric)
- Model3D: USDZファイルの名前を渡すことで、ファイル内の3DコンテンツをDepth内に表示する。イニシャライザはいくつかあるが、読み込み中と読み込み完了後に表示物を分けられるようなイニシャライザもある。
Model3D(named: "Moon") { phase in
switch phase {
case .empty:
ProgressView()
case let .failure(error):
Text(error.localizedDecription)
caes let .success(model):
model
.resizable()
.scaledToFit()
}
}
- frame(depth:alignment:) : 奥行き(Z軸方向)の大きさを調整する。alignmentについて、該当のViewが、そのViewを包むフレームのどの位置に来るかを指定できる。このモディファイアを使わない場合は後ろに来るようだが、このモディファイアを使えば中心または前側も指定できる。
Model3D(url: URL(string: "https://example.com/robot.usdz")!)
.frame(depth: 300, alignment: .front)
-
.rotation3DEffect
とTimelineView
を組み合わせて、3Dモデルを少しずつ回転させる例が示されている。
TimeLineView(.animation) { context in
HStack {
ForEach(celestialObjects, id: \.self) { object in
CelestialObjectView(object.name)
.rotation3DEffect(
Angle2D(degrees: 5 * context.date.timeIntervalSinceReferenceDate),
axis: .y
)
}
}
}
- // TODO: その他、RealityViewに付加するattachmentと呼ばれるものについてもかなり説明しているが、そもそもRealityViewについて知識がすでにあることを前提としており理解が難しいので、のちに戻ってくる。
Go beyond the window with SwiftUI
- RealityKit、ImmersiveSpaceにおいてはY軸は上向きに伸びていくのだが、SwiftUIはUIKitと同じくY軸は下向きに伸びていくことに注意。
- ImmersiveSpaceにおいて、座標空間の原点はユーザーの足あたりにある。そのためImmersiveSpaceに直でUI部品などをおくと見えにくい位置に表示される。基本的には、ImmersiveSpaceの下にRealityViewを入れて使うことが推奨される。
- 便利な点として、RealityViewのクロージャ内ではasync を使ってエンティティを読み込んだりできる。
- RealityViewのmakeクロージャ内ではRealityViewContent構造体が渡される。これは様々な3Dコンテンツの追加、削除、イベントの購読のほか、RealityViewの座標系とSwiftUIの座標系の間で変換ができる。
ImmersiveSpace(id: "local-system") {
RealityView { content in
let planet = await loadPlanet()
content.add(planet)
}
}
- ImmersiveSpaceは、ユーザーの視界を奪うので、いきなり表示するのではなくユーザーがボタンを押したら表示するのが望ましい。ImmersiveSpaceを表示するopenImmersiveSpaceアクションは非同期である。このアクションはResult列挙型を返すので、エラーハンドリングが可能。
@main
struct WorldApp: App {
var body: some Scene {
// This window is displayed when the app is opened
WindowGroup {
VStack {
Text("The Local System")
Text("Show The Local System")
SystemControl()
}
}
ImmersiveSpace(id: "local-system") {
LocalSystem()
}
}
}
struct SystemControl: View {
@Environment(\.openImmersiveSpace)
private var openImmersiveSpace
@Environment(\.dismissImmersiveSPace)
private var dismissImmersiveSpace
private var isSpaceHidden: Bool = false
var body: some View {
Button(isSpaceHidden ? "Open Local System" : "Close Local System") {
Task {// We need Task because openning and dismissing spaces are asynclonous tasks
if isSpaceHidden {
let result = await openImmersiveSpace(id: "local-system")
switch result {
// Error Handling
}
} else {
await dismissImmersiveSpace()
}
isSpaceHidden.toggle()
}
}
}
}
- 3Dコンテンツの表示は時間がかかる処理である。Model3DまたはRealityViewを使って、読み込みフェーズに応じた表示を行える。
Model3D(name: "MilkeyWay") { phase in
switch phase {
case .empty:
Text("Wait...")
case .failure(let error):
Text("Error: \(error.localizedDescription)")
case .success(let model):
model
.resizable()
}
}
- 従来SwiftUIでactive, inactive, backgroundといった状態を検知するために使えたScenePhaseだが、ImmersiveSpaceでも引き続き利用可能。ScenePhase自体については以下を参照されたい。
struct UniverseApp: App {
@EnvironmentObject private var viewModel: ViewModel
@Environment(\.scenePhase) private var scenePhase
var body: some View {
ImmersiveSpace(id: "local-group") {
LocalGroup()
.onChange(of: scenePhase) {
if case .active = scenePhase {
model.scale = 1.0
} else {
model.scale = 0.5
}
}
}
}
}
- 3Dモデルの回転(Rotation)、並行移動(Translation)、拡大縮小(Scaling)などを表現する際一般にアフィン変換(Affine Transformation)という原理を利用しているので前提として理解しておく必要がある。アフィン変換は計算式をわかりやすく表現するため、次元数を一つ増やしているという特徴がある。例えば二次元座標のアフィン変換なら、余計な座標を1つ増やしてx, y, wという3つの要素を持つ行列で考えていく。三次元座標空間なら、余計な座標を1つ増やしてx, y, z, wの四つの要素を持つ行列で考えていく。回転、並行移動、拡大縮小などを行う際に、アフィン変換行列のどの部分の数値を変えればいいかが決まっているので、把握すると良い。
-
なお、3D空間では回転は三種類あることになる。すなわち、X軸を中心にした回転(Pitch), Y軸を中心にした回転(Yaw), Z軸を中心にした回転(Roll)である。複数の回転が重なる場合は、一つの式で一気に計算しようとすると複雑になるので、複数、すなわち最大3回転分の計算を順に3回行えば良い。なお座標平面上における回転の一般式は次のようになる。回転後の座標が(x, y)、回転前の座標が(x', y')である: x=x'*cosθ-y'*sinθ, y=x'*sinθ+y'*cosθ 。これは有名な三角関数の「加法定理」から導かれる式にすぎない。また、アフィン変換の回転を表す行列も、行列を解くとこのような回転の一般式が現れるように行列を構成しているだけにすぎない。空間上では4行✖️4行の行列となる。この行列からは並行移動、拡大、回転など空間上の移動一般を表すことができる。これとクォータニアンは相互に変換可能。
-
またアフィン変換は行列(の積)を利用しているので前提として理解しておく必要がある。行列AとBを掛け算する時、Aの「行」とBの「列」の要素を順に掛け算して合計していく。そのため、Aの行数とBの列数は一致する必要がある。
-
実際のアフィン変換のイメージは以下のサイトで試すことができる。以下は、並行移動、45度回転、拡大した画像である。回転がわかりにくいかもしれないが、これは三角関数を利用した回転の一般式を利用している。x=x'*cosθ-y'*sinθ, y=x'*sinθ+y'*cosθ である。今回は、θが45度(ラジアン表記でπ/2)となる。Affine transformation tool
Human Interface Guideline
Designing for visionOS
- Vision Pro は安全な環境で穏やかな動きをするときに用いることを想定しており、素早く大きく動くようなコンテンツはよろしくない。また車内や、バルコニーなど潜在的に危険な屋外などで使うことも想定していない。また13歳未満の子供は対象外となる。
- 装着者の視界の中のよく見える部分にコンテンツを配置するようにおすすめされている。目を左右に60度くらい動かした範囲内くらい。ただしだからと言って視界のど真ん中にコンテンツを常に留めておくと、装着車の視界を制限することになるのでやめておいたほうがいい。
-
Indirect Gesture
(指同士を重ね合わせるなど、タップやジェスチャーと違って腕を大きく動かさずにできるジェスチャー)をサポートすることを薦める。
Spatial layout
- 表示するViewなどの縮尺(Scale)について、Dynamic ScaleとFixed Scaleの2種がある。後者は現実世界の物体と同じように、離れると小さく見える。前者は現実世界とは異なり、離れると自動で大きくなり、常に同じ大きさを保つ。現実世界と全く同じ縮尺で物を表示するのが重要になってくる場合は、Dynamic Scaleを使うと良い。
- あまりに多くのViewなどを表示すると、装着者の視界を塞いでしまうので注意。
- ユーザーが向きを変えたとき、Viewなどは視界から外れる。ユーザーがDigital Crownを操作すると、自動でViewがユーザーの視界に戻ってくる。この動作はOS側が自動でやってくれるので、アプリ側が何かする必要はない。
- ボタンなどインタラクティブな要素をあまりに密集して配置すると視線での選択がやりにくくなる。最低でも、ボタン同士の中心が60pt以上離れるようにすること。
Getting Started with Apple’s Vision OS Development
visionOS developer
Submit your apps to the App Store for Apple Vision Pro
Bringing your existing apps to visionOS
位置情報はVisitなどを除いた通常の機能のみ使える。ただし「常に許可」はなく、「アプリ使用時に許可」のみだという。
CoreLocation. You can request someone’s location using the standard location service, but most other services are unavailable. Use availability checks to determine which services are present. The Always authorization level is unavailable and automatically becomes When in Use authorization.
例となる実装
公式実装例
World
太陽系などの様子を3Dモデルやイマーシブスペースで見せるアプリ。
DragRotationModifier
rotation3deffectをベースに、x軸とy軸を中心に回転させる。しかし一定以上は回転させず、かつ指を離したら元の位置に戻す。人工衛星や月などの3Dモデルを例として示す際にこのカスタムモディファイアを利用指定
3Dモデルの回転
以下のような流れで3Dモデルなどを回転させている。
1. ドラッグジェスチャーを感知するgesture(DragGesture)
を利用
2. gesture で取れる値を使って、指の移動量を計算
3. tan(タンジェント)を渡すと回転量(ラジアンの角度)を返すarctangent関数を利用し角度を算出 atan
なおarctangentとは以下のようなグラフになる。xとしてtangent(三角形の斜辺の傾きを指す)を渡すと、yとして三角形の斜辺と底辺の間の角度をラジアン表記で返す。最大値はπ1/2ラジアン(90度)、最小値は-π1/2ラジアン(-90度)です。ラジアンについて、1ラジアンは度数表記すると約57度。2πラジアンが360度、1πラジアンが180度、π*1/2ラジアンは90度となる。
画像はデスモスより引用
なお今回ここでAppleが利用しているやり方においては、sensitivity
という値を渡せるようになる。デフォルト値は10。例えば指の動きの変化量を10倍してからatan関数に渡している。こういった配慮がないとユーザーがものすごく大きく指を動かさないとなかなか回転しなくなる。sensitivity
があることで、ちょっとしたユーザーの動きでも3Dモデルが大きく回転してくれるようになるので、ユーザーのストレス軽減になる。
またyawLimit
, pitchLimit
という値も渡せるようになる。これは回転の限度を指定する。これを指定すると、arctangent関数で出てきた角度を割り算して減らすことができるので、20度、10度など一定以上の角度には絶対に回転しないような制御ができる。
sensitivity
、yawLimit
, pitchLimit
を全部合わせてグラフでもう一度表現してみる。上で一度書いたグラフがx軸方向、y軸方向にギュッと圧縮される。これは現実にどういう意味かというと、ほんのわずかなxの変化(指の動き)で、y(回転角度)がすぐにごく小さな最大角度に達するような表現になる。
画像はデスモスより引用
4. 回転量と回転軸と回転の起点を指定してviewを回転させるrotation3DEffect(_:axis
)
5. DragGestureの.onEnd
で、初期位置に回転を戻す(こともできるようにしている)
import SwiftUI
import RealityKit
extension View {
/// Enables people to drag an entity to rotate it, with optional limitations
/// on the rotation in yaw and pitch.
func dragRotation(
yawLimit: Angle? = nil,
pitchLimit: Angle? = nil,
sensitivity: Double = 10,
axRotateClockwise: Bool = false,
axRotateCounterClockwise: Bool = false
) -> some View {
self.modifier(
DragRotationModifier(
yawLimit: yawLimit,
pitchLimit: pitchLimit,
sensitivity: sensitivity,
axRotateClockwise: axRotateClockwise,
axRotateCounterClockwise: axRotateCounterClockwise
)
)
}
}
/// A modifier converts drag gestures into entity rotation.
private struct DragRotationModifier: ViewModifier {
var yawLimit: Angle?
var pitchLimit: Angle?
var sensitivity: Double
var axRotateClockwise: Bool
var axRotateCounterClockwise: Bool
@State private var baseYaw: Double = 0
@State private var yaw: Double = 0
@State private var basePitch: Double = 0
@State private var pitch: Double = 0
func body(content: Content) -> some View {
content
.rotation3DEffect(.radians(yaw == 0 ? 0.01 : yaw), axis: .y)
.rotation3DEffect(.radians(pitch == 0 ? 0.01 : pitch), axis: .x)
.gesture(DragGesture(minimumDistance: 0.0)
.targetedToAnyEntity()
.onChanged { value in
// Find the current linear displacement.
let location3D = value.convert(value.location3D, from: .local, to: .scene)
let startLocation3D = value.convert(value.startLocation3D, from: .local, to: .scene)
let delta = location3D - startLocation3D
// Use an interactive spring animation that becomes
// a spring animation when the gesture ends below.
withAnimation(.interactiveSpring) {
yaw = spin(displacement: Double(delta.x), base: baseYaw, limit: yawLimit)
pitch = spin(displacement: Double(delta.y), base: basePitch, limit: pitchLimit)
print("⭐️ DragRotationModifier: body: result yaw: \(yaw), pitch: \(pitch)")
}
}
.onEnded { value in
// Find the current and predicted final linear displacements.
let location3D = value.convert(value.location3D, from: .local, to: .scene)
let startLocation3D = value.convert(value.startLocation3D, from: .local, to: .scene)
let predictedEndLocation3D = value.convert(value.predictedEndLocation3D, from: .local, to: .scene)
let delta = location3D - startLocation3D
let predictedDelta = predictedEndLocation3D - location3D
// Set the final spin value using a spring animation.
withAnimation(.spring) {
yaw = finalSpin(
displacement: Double(delta.x),
predictedDisplacement: Double(predictedDelta.x),
base: baseYaw,
limit: yawLimit)
pitch = finalSpin(
displacement: Double(delta.y),
predictedDisplacement: Double(predictedDelta.y),
base: basePitch,
limit: pitchLimit)
}
// Store the last value for use by the next gesture.
baseYaw = yaw
basePitch = pitch
}
)
.onChange(of: axRotateClockwise) {
withAnimation(.spring) {
yaw -= (.pi / 6)
baseYaw = yaw
}
}
.onChange(of: axRotateCounterClockwise) {
withAnimation(.spring) {
yaw += (.pi / 6)
baseYaw = yaw
}
}
}
/// Finds the spin for the specified linear displacement, subject to an
/// optional limit.
private func spin(
displacement: Double,
base: Double,
limit: Angle?
) -> Double {
if let limit {
let atanResult = atan(displacement * sensitivity)
let limitDegrees = (limit.degrees / 90)
let spinResult = atanResult * limitDegrees
print("DragRotationModifier: private func spin: ⭐️ displacement: \(displacement), sensitivity: \(sensitivity), limit: \(limit), atanResult: \(atanResult), limitDegrees: \(limitDegrees), spinResult: \(spinResult)")
return atan(displacement * sensitivity) * (limit.degrees / 90)
} else {
return base + displacement * sensitivity
}
}
/// Finds the final spin given the current and predicted final linear
/// displacements, or zero when the spin is restricted.
private func finalSpin(
displacement: Double,
predictedDisplacement: Double,
base: Double,
limit: Angle?
) -> Double {
// If there is a spin limit, always return to zero spin at the end.
guard limit == nil else { return 0 }
// Find the projected final linear displacement, capped at 1 more revolution.
let cap = .pi * 2.0 / sensitivity
let delta = displacement + max(-cap, min(cap, predictedDisplacement))
// Find the final spin.
return base + delta * sensitivity
}
}
※AppleのHappyBeamより引用、ログ出しは筆者が追加
Happy Beam
手のジェスチャーを感知し、ハート型のジェスチャーをしているときはビームを発射して、雲に当てていくというゲームのアプリ。
基礎技術
vision OS 1.0 から使えるARKitの技術を用いている。
ARKitSessionクラスインスタンスを生成し、変数として保持しておく。このインスタンスのrun(_:)メソッドを呼ぶことで、ARセッションを開始できる。runメソッドにはDataProvider型のインスタンスを複数渡すことができ、どのようなデータを取得するかを指定できる。以下の5種が存在。
HandTrackingProvider
ImageTrackingProvider
PlaneDetectionProvider
SceneReconstructionProvider
WorldTrackingProvider
- anchorUpdates(AsyncSequence型)を購読することでWorldAnchorの情報
ジェスチャー感知部分
詳細に解説してくれているブログがある。
visionOSでのハンドジェスチャ実装に関する調査
コードを見る限り、ハート型とは言っても、左手と右手の人差し指の距離が十分短く(=くっついており)、左手と右手の親指も同等であれば、ハート型と判定しているようだ。結構雑な判定であるとは思った。
また、手の各関節の空間内での位置の計算は、まずHandAnchor.originFromAnchorTransformで空間座標の原点から手のアンカー、すなわち手首の座標を計算する。その後、HandSkeleton.Joint.anchorFromJointTransformを使い、手首から該当関節への距離を計算する。両者をmatrix_multiplyを使って行列の掛け算をして計算しているようだった。
非公式実装例
Awesome visionOS
VisionOSについての様々な追いかけるべき情報を集約してくれているレポジトリ。
jordibruin/visionOS-Examples
様々な実装を例として挙げてくれている。
satoshi0212/visionOS_30Days
三十種の様々な実相を挙げてくれている
satoshi0212/MySpatialTimer
空間上にタイマーを表示する実装例。
AppleVisionPro_app_book_2024
API、技術
SwiftUI
offset(z:)
Z軸方向に対象のViewを動かすことができる。+の値を渡せば、Viewを前に持ってくることができる。Vision Pro の空間内でユーザーの眼前に近づいた位置にくることになる。
Ornament
VisionOSでのみ利用可能なもの。Viewの外側に任意のサブ画面を表示できる。VisionOSでは空間内にViewなどが浮かんでおりその外側に飾りをつける空間的余裕があるからだろう。
ornament(visibility:attachmentAnchor:contentAlignment:ornament:)
attachmentAnchor
とは、親Viewと子Viewの接続点を親Viewのどこに持ってくるか。contentAlignment
は、親Viewと子Viewの接続点を子Viewのどこに持ってくるか。選べるのは、Top, Bottomなど。
ImmersiveSpace
ImmersiveSpaceとはVisionOSでのみ可能な表現形式で、ViewをVisionProの画面の多く、または全てに満ちるように表示し、没入的な体験を提供する。
Environment variableであるopenImmersiveSpaceを使って開く。複数のImmersiveSpaceがある場合は、パスによって検索できる。これはWindowGroupを開く手順に似ている。
immersionStyle(selection)モディファイアを使うことで、表現形式を変更できる。
glassBackgroundEffect(displayMode:)
周囲の環境の明るさ・暗さを考慮して、見やすくなるようなすりガラスのような3Dエフェクトを与えるモディファイア。glassBackgroundEffect
適切に動作させるためには、ZStackを使う場合はZStackに設定し、ZStackの中身のViewに設定すべきではない。.overlay
、.background
を使う場合も内部でZStackが使われているので、これらではなく明示的にZStackを用いるべきかもしれないとのこと。
全てのWindowにはデフォルトでこの効果が付与されている。そのためにVision OSにおいて、明るくても暗くても見えるようになっている。Elevate your windowed app for spatial computingより
targetedToAnyEntity()
ジェスチャーの作用する対象を、このモディファイアのついた3Dモデル上に限定する。使用例:
// 略
/// A modifier converts drag gestures into entity rotation.
private struct DragRotationModifier: ViewModifier {
var yawLimit: Angle?
var pitchLimit: Angle?
var sensitivity: Double
var axRotateClockwise: Bool
var axRotateCounterClockwise: Bool
@State private var baseYaw: Double = 0
@State private var yaw: Double = 0
@State private var basePitch: Double = 0
@State private var pitch: Double = 0
func body(content: Content) -> some View {
content
.rotation3DEffect(.radians(yaw == 0 ? 0.01 : yaw), axis: .y)
.rotation3DEffect(.radians(pitch == 0 ? 0.01 : pitch), axis: .x)
.gesture(DragGesture(minimumDistance: 0.0)
.targetedToAnyEntity()
// 略
RealityKit
USDZファイル
3Dモデルを表すファイル。https://sketchfab.com/feed で有償・無償でダウンロードできるほか、iPhoneを使って自分で作成することもできる。[Meet Object Capture for iOS](https://developer.apple.com/videos/play/wwdc2023/10191/)
Entity-Component-System (ECS)
RealityKit上に配置するオブジェクト(Entity), Entityの外観や物理特性などの特徴を指すコンポーネント(Component), Entityを検索するSystemからなるRealityKitの基本構造。
これらは自分でカスタマイズすることもできる。Component, Systemに加えCodableも継承させることで、オリジナルのComponent, SystemがRealityComposerProにも表示できるようにすることができる。
Understanding the modular architecture of RealityKit
Entity
Component
System
ghmagazine 氏による、オリジナルのSystem, Componentの作成例。
Scene
Sceneとは複数のEntityが収められ描画されているAR空間のオブジェクト。自ら初期化して使うことはまずない。Entity, Anchorたちを変数として持っているので検索して使うことができる。またAR空間内で発生する各種イベント(後述のEvents)の購読も、このSceneから行うこととなる。
Events
AR空間内で発生しうる各種イベントを指す。複数のEntityの衝突や、シーン内での時間経過など。リストは以下(Events より引用):
AccessibilityEvents.Activate
AccessibilityEvents.CustomAction
AccessibilityEvents.Decrement
AccessibilityEvents.Increment
AccessibilityEvents.RotorNavigation
AnimationEvents.PlaybackCompleted
AnimationEvents.PlaybackLooped
AnimationEvents.PlaybackStarted
AnimationEvents.PlaybackTerminated
AnimationEvents.SkeletalPoseUpdateComplete
AudioEvents.PlaybackCompleted
CollisionEvents.Began
CollisionEvents.Ended
CollisionEvents.Updated
ComponentEvents.DidActivate
ComponentEvents.DidAdd
ComponentEvents.DidChange
ComponentEvents.WillDeactivate
ComponentEvents.WillRemove
PhysicsSimulationEvents.DidSimulate
PhysicsSimulationEvents.WillSimulate
SceneEvents.AnchoredStateChanged
SceneEvents.DidActivateEntity
SceneEvents.DidAddEntity
SceneEvents.DidReparentEntity
SceneEvents.Update
SceneEvents.WillDeactivateEntity
SceneEvents.WillRemoveEntity
SynchronizationEvents.OwnershipChanged
SynchronizationEvents.OwnershipRequest
VideoPlayerEvents.ContentTypeDidChange
VideoPlayerEvents.ImmersiveViewingModeDidChange
VideoPlayerEvents.ImmersiveViewingModeDidTransition
VideoPlayerEvents.ImmersiveViewingModeWillTransition
VideoPlayerEvents.VideoSizeDidChange
VideoPlayerEvents.ViewingModeDidChange
EntityActions
Reality Composer Pro
USDZファイルを取り込んで大きさなどを調整したり、vision osに組み込むためのパッケージを作ってくれる。参照:
-
Apple Vision Pro (visionOS) Reality Composer Pro のプロジェクトをShared Space の Windowに表示するまでの手順
-
- 3DモデルをSwiftUI内に表示する。URLを渡すとRL先にあるリソースを使ってモデルを表示できる。
struct TestView: View {
var body: some View {
Model3D(named: "TestModel") { model in
model
.resizable()
.aspectRatio(contentMode: .fit)
} placeholder: {
ProgressView()
}
}
}
Shader Graph
Unityで3Dモデルを作るためのものであるが、vision os 用にも使える。vision os においてはMaterialXコンポーネントとして変換して利用される。
- 実装例: SGMEExamples
Timeline Editor
- Compose interactive 3d content in reality composer pro
- Composing interactive 3D content with RealityKit and Reality Composer Pro
エラー対応など
組み込んだはずの3Dモデルが、vision os上で表示されない。
3Dモデルがでかすぎてユーザーが3Dモデルの内部にめり込む形となっていると、3Dモデルがないように見える。Reality Composor Pro 上でscaleを小さくし、3Dモデルの大きさを小さくしてみるとうまく表示された。
ユーザーが動いてもユーザーの視界に存在し続けるように3Dモデルを動かしたい。
そのような動きはユーザーの視界を遮るため、あまり推奨はされていない。
どうしてもやりたい場合は、以下が参考になる。
How to know user's position in Surrounding Space in visionOS
この回答を参考に、筆者自身も作成した例が以下となる。
自分で作成した例の動作画像: https://youtu.be/779fpMMs9V8
RealityKitエンティティをタップしているのに、タップジェスチャーが反応しない。
RealityKitエンティティにCollisionComponent
とUserInteractionComponent
を設定する必要がある。コードから設定しても良いが、Reality Composer Proから設定することもできる。
Ornament(TabViewのTabBar)が表示されない。
ドキュメントには書いていなそうなのだが、WindowGroupでe.volumetricの場合表示されなくなる模様。
参考: SwiftUI .ornaments modifier for visionOS App on Vision Pro
Ambiguous use of 'init(make:update:placeholder:attachments:)
RealityView
を使う際、Attachmentにidを渡して初期化していないと、このようなエラーが出ることがある。
参考 RealityView Ambiguous use of... in Beta 8 of Xcode
シミュレーターではハンドトラッキングを試せず、困る。
AlohaYos/VisionGestureライブラリを使うと、ハンドトラッキングをシミュレーションすることができ、シミュレータからでも利用できる。
著者本人による説明: Vision Pro 空間ジェスチャーを作る
3Dエンティティを手に追従させたい。
Reality Composer Pro より、空のTransformを追加し、TransformにAnchoringコンポーネントを追加し、手に追従させる。
Reality Composer Proで、目的の3Dエンティティを上のTransformの子として設定する。これで、手の動きに追従するようになる。
3Dエンティティを別のエンティティの周囲に回転させたい。
別のエンティティを中心として回転させるのは直接はできないのでテクニックが必要。
例えば月を地球の周りで回転させたいとする。Reality Composer Proで実態のないTransformを設定すれば良い。Transformの位置を地球の位置と合わせる。月をTransformの子とする。月の位置をTransformの位置からずらす。
この状態でTransformを回転させれば、月は地球を中心に回転することとなる。
TransformにCollision Component, PhysicsBody Component, PhysicsMotion Componentを設定する。PhysicsBody ComponentでKineticモーションを設定する(ほかのエンティティからの衝突に影響されず、設定した動作を続ける)。PhysicsMotion ComponentでAngular Velocityにいくつか値を設定することで回転させる。これで完成となる。
物体同士の衝突をさせたい
EntityにCollisionComponent, PhysicsBodyComponentを追加すると良い。PhysicsBodyComponentのModeをDynamicに設定することで物理的な衝突をさせることができる。
[visionOS] Reality Composer ProのPhysics Bodyの各パラメータ解説
ハンドトラッキングをしたい。
さまざまなパターンが考えられる。
- ghmagazine氏による
- Happy Beam
- Incorporating real-world surroundings in an immersive experience
- Creating a spatial drawing app with RealityKit
3Dエンティティが表示されない。
Entityの初期化の際、in パラメータに何も渡さない場合は、アプリのメインバンドル内のみが捜索される:
Entity
そのため、Vision OSアプリで開始すると自動で追加されている別パッケージに3Dモデルを置いている場合は、そのパッケージがわかるように in に渡してあげる必要がある。
import Foundation
// ※自動で追加されているファイル。
/// Bundle for the RealityKitContent project
public let realityKitContentBundle = Bundle.module
entity = Entity(named: "MyEntity", in: realityKitContentBundle)
アプリを閉じたらImmersiveSpaceも閉じたい。
Appを継承したアプリのクラスで、@Environment
のScenePhase
を購読し、アプリがactive
で無くなったことを検知したら、開いているimmersive view をdismissするのがベストプラクティスと言える。
Volumeが開けない。
Info.plistの「Preferred Default Scene Session」がVolumeになっている必要がある。参考:
Volumeのサイズが変更できない。
defaultWorldScalingでdynamic
を指定することで、サイズを変更できる。
Volumeのサイズが変更するに応じて中のコンテンツも拡大縮小させたい。
GeometryReader3D
で親ビュー(Volume)のサイズが取得できる。これとデフォルトサイズとの割合を計算することで、拡大縮小させる割合を計算できるだろう。
WindowGroup {
GeomtryReader3D { geometry in
MyView()
.scaleEffect(geometry.size.height / 500)
}
}
.defaultSize(width: 500, height: 500, depth: 500)
.defaultWorldScaling(.dynamic)
.windowStyle(.volumetric)
Volumeについて、Windowと同様、移動してもデバイスの方を向き続けるようにしたい。
.volumeWorldAlignmentで.adaptiveを指定すればできr
ARKit
Anchor
ARKitの技術で、空間上のさまざまな情報をとることができる:
BarcodeAnchor
DeviceAnchor
EnvironmentProbeAnchor
HandAnchor
ImageAnchor
MeshAnchor
ObjectAnchor
PlaneAnchor
RoomAnchor
WorldAnchor: 空間上の特定の位置(例えば、テーブルの上など)を特定できる。オブジェクトを特定の場所に固定しておきたいときなどに使える。
- Tracking specific points in world space
CI/CD
Github Actions
Xcodeを用いた際の基本的なCI/CD設定の書き方は:
How to Set up GitHub Actions for CI with Xcode
GitHub ActionsでiOSアプリのCI環境を構築する方法
Vision OSアプリ向けに、コミットがpushされたらテストを実施するためのyamlファイルは例えば以下のようになった。
name: CI for Vision OS App
on:
push:
branches: [ main, develop, feature/* ]
jobs:
test:
runs-on: macOS-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Folder
run: pwd; ls -la
- name: List available Xcode versions
run: ls /Applications | grep Xcode
- name: Set up Xcode version
run: sudo xcode-select -s /Applications/Xcode_16.app/Contents/Developer
- name: Vision OS download
run: xcodebuild -downloadPlatform visionOS
- name: Build and Test
run: |
xcodebuild -project XXXXX.xcodeproj \
-scheme XXXXX \
-configuration Debug \
-destination 'platform=visionOS Simulator,name=Apple Vision Pro' \
test
エラー
Vision OSがダウンロードされてない
Github Actionsが動く最新のMac環境上にVision OSがダウンロードされていなかったので、Xcodeのバージョンを指定した後、自分でダウンロードする必要があった。
- name: Set up Xcode version
run: sudo xcode-select -s /Applications/Xcode_16.app/Contents/Developer
- name: Vision OS download
run: xcodebuild -downloadPlatform visionOS
参考: visionOS SDK not installed on macOS 13 runner when using Xcode 15
クォータニアン
SIMD
行列を表すことができるクラスであって、空間上の位置計算や移動計算などに用いられる。
protocol - SIMD
Accelarate - API Collection - simd
simd_float4x4(4かける4の行列、典型的には空間座標上のアフィン変換を表すため使われる)から位置や回転を表す部分を抽出するための便利な
extensionの例。
-
simd_act(::)
ベクトルにクオータにアンをかけて、回転させる。 - simd_normalize(_:) クオータにあんの向いている方向は同じまま、長さを1にして返す。