公式ドキュメントなど
-
Apple Vision Pro用のApp Storeへのアプリの提出
-
空間コンピューティング:アプリは空間コンピューティングアプリとして言及してください。アプリ体験を、拡張現実(AR)、仮想現実(VR)、クロスリアリティ(XR)、複合現実(MR)などと表現しないでください。
とのこと。
-
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
Material
物体の表面の質感などを指す。
-
PhysicallyBasedMaterial
- RealityKitで3Dオブジェクトを生成した時自動で適用されるマテリアル。現実世界の光の反射などを再現した質感となっている。
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
VisionOS Shader Graph は、Unity の Shader Graph や Unreal Engine の Material Editor に似たビジュアルプログラミング環境を提供し、シェーダーの作成を直感的に行えるツールです。ただし、現時点(2023年)では VisionOS が Apple 社の新しいプラットフォームであるため、その具体的な機能やノードの詳細については公式ドキュメントや開発者向けリソースを確認する必要があります。
以下は、一般的な Shader Graph のノードカテゴリとその役割を解説します。VisionOS の Shader Graph でも同様のカテゴリが存在すると仮定して説明を進めます。
1. Input ノード
入力値を設定するためのノードで、シェーダーの基礎となるパラメータを提供します。
- Color: 色を指定します。
- Vector1/2/3/4: スカラー値やベクトル値を入力します。
- Texture: テクスチャ画像を読み込みます。
- Time: 時間に基づくアニメーション用の値を提供します。
- Screen Position: 画面座標を取得します。
- UV: UV 座標を操作します。
2. Math ノード
数学的な演算を行うノードです。シェーダーの計算処理において非常に重要です。
- Add/Subtract/Multiply/Divide: 基本的な四則演算を行います。
- Lerp: 線形補間を行います。
- Clamp: 値を特定の範囲内に制限します。
- Power: 指数演算を行います。
- Dot/Cross Product: ベクトルの内積や外積を計算します。
- Normalize: ベクトルを正規化します。
3. Utility ノード
便利な機能を提供する汎用的なノードです。
- Split/Combine: ベクトルやカラーを分割・結合します。
- Remap: 値の範囲を再マッピングします。
- One Minus: 入力値から 1 を引きます(1 - x)。
- Absolute: 値の絶対値を取得します。
4. Texture ノード
テクスチャに関連する操作を行うノードです。
- Sample Texture 2D: 2D テクスチャをサンプリングします。
- Normal Map: 法線マップを適用します。
- Triplanar Mapping: トリプランナー投影によるテクスチャマッピングを行います。
5. Color ノード
色の操作や変換を行うノードです。
- RGB to HSV: RGB 形式から HSV 形式に変換します。
- HSV to RGB: HSV 形式から RGB 形式に変換します。
- Blend: 色をブレンドします。
6. Lighting ノード
ライティングに関連する計算を行うノードです。
- Diffuse Lighting: 拡散反射光を計算します。
- Specular Lighting: 鏡面反射光を計算します。
- Fresnel Effect: フレネル効果をシミュレートします。
7. Procedural ノード
プロシージャル生成に関連するノードです。
- Noise: ノイズパターンを生成します(Perlin Noise など)。
- Gradient: グラデーションを作成します。
- Voronoi: ボロノイパターンを生成します。
8. Custom Function ノード
独自の HLSL/GLSL コードを実装するためのノードです。高度なカスタマイズが必要な場合に使用します。
9. Master ノード
最終的な出力を決定するノードです。
- PBR Master: 物理ベースレンダリング(PBR)用のシェーダーを構築します。
- Unlit Master: ライティングを無視した単純なシェーダーを構築します。
- Post Process Master: ポストプロセスエフェクト用のシェーダーを構築します。
Timeline Editor
- Compose interactive 3d content in reality composer pro
- Composing interactive 3D content with RealityKit and Reality Composer Pro
Normal Node
- Normal: 3Dオブジェクトの法線(オブジェクトの表面に対して垂直方向のベクトル)を指す。
エラー対応など
組み込んだはずの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
クォータニアン
クォータニオンについて :
クォータニオンは、オイラー角(ピッチ、ヨー、ロール)よりも回転の表現が効率的で、ジンバルロック(回転の自由度が失われる現象)を回避できるため、3Dグラフィックスで広く使用されます。
クォータニオンの性質
クォータニオン(四元数)は、3D空間での回転を効率的に表現するための数学的ツールです。クォータニオンは次のように表されます:
q=w+xi+yj+zk
ここで:
w はスカラー成分(実数部分)。
x,y,z はベクトル成分(虚数部分)。
クォータニオンを使用すると、回転を単一の数値として扱うことができ、オイラー角や行列よりも計算が効率的でジンバルロックを回避できます。
- 回転の合成:掛け算の役割
クォータニオン同士の掛け算 は、回転の合成(複数の回転を組み合わせること)を意味します。例えば、以下のような場合を考えます:
q1: 最初の回転(例:Y軸周りの回転)。
q2: 次の回転(例:X軸周りの回転)。
このとき、q1と q2を掛け算することで、両方の回転を適用した結果の新しいクォータニオンが得られます:
q result=q2⋅q1
この順序は重要で、一般的に非可換(q1q2 ≠ q2q1)。
線形補間(Slerp)
2つのクォータニオンの間を滑らかに補間したい場合、球面線形補間(Spherical Linear Interpolation, Slerp)を使用します。
これにより、2つの異なる回転状態を自然に遷移させることができます。例えば、アニメーションやカメラの動きに利用されます。
let interpolatedQuaternion = simd_slerp(q1, q2, t)
t は0から1の間の値で、補間の進行度を制御します。
SIMD
行列を表すことができるクラスであって、空間上の位置計算や移動計算などに用いられる。
protocol - SIMD
Accelarate - API Collection - simd
simd_float4x4(4かける4の行列、典型的には空間座標上のアフィン変換を表すため使われる)から位置や回転を表す部分を抽出するための便利な
extensionの例。
-
simd_act(::)
ベクトルにクオータにアンをかけて、回転させる。 - simd_normalize(_:) クオータにあんの向いている方向は同じまま、長さを1にして返す。
線形補間 Lerp
線形補間 (Lerp) とは
線形補間(Linear Interpolation、略して Lerp) は、2つの値の間を直線的に補間するための数学的な手法です。この操作は、グラフィックスプログラミングやシェーダー作成において非常に一般的であり、色、座標、スカラー値など、さまざまなデータに対して使用されます。
基本的な概念
数式
線形補間の計算は以下の数式で表されます:
$$
\text{Result} = A + t \cdot (B - A)
$$
- A: 始点の値。
- B: 終点の値。
-
t: 補間係数(0 から 1 の範囲の値)。
- $ t = 0 $ のとき、結果は $ A $。
- $ t = 1 $ のとき、結果は $ B $。
- $ 0 < t < 1 $ のとき、$ A $ と $ B $ の間の値。
グラフ的イメージ
線形補間は、2点 $ A $ と $ B $ を結ぶ直線上の任意の点を求めることに相当します。以下のようなグラフで表せます:
A -------------------X------------------- B
- $ X $ は補間された値で、$ t $ の値によって位置が決まります。
具体的な例
1. スカラー値の場合
例えば、$ A = 10 $、$ B = 20 $、$ t = 0.5 $ の場合:
$$
\text{Result} = 10 + 0.5 \cdot (20 - 10) = 10 + 5 = 15
$$
結果として、$ A $ と $ B $ の中間値 $ 15 $ が得られます。
2. ベクトルの場合
ベクトルでも同様に計算できます。例えば、2次元ベクトル $ A = (1, 2) $、$ B = (4, 6) $、$ t = 0.5 $ の場合:
$$
\text{Result}_x = 1 + 0.5 \cdot (4 - 1) = 2.5
$$
$$
\text{Result}_y = 2 + 0.5 \cdot (6 - 2) = 4
$$
結果として、ベクトル $ (2.5, 4) $ が得られます。
3. カラーの場合
色(RGB)の補間も可能です。例えば、$ A = (1, 0, 0) $(赤)、$ B = (0, 0, 1) $(青)、$ t = 0.5 $ の場合:
$$
\text{Result}_R = 1 + 0.5 \cdot (0 - 1) = 0.5
$$
$$
\text{Result}_G = 0 + 0.5 \cdot (0 - 0) = 0
$$
$$
\text{Result}_B = 0 + 0.5 \cdot (1 - 0) = 0.5
$$
結果として、紫(マゼンタ)に近い色 $ (0.5, 0, 0.5) $ が得られます。
Shader Graph での Lerp ノード
Shader Graph では、Lerp ノードを使用して簡単に線形補間を実現できます。このノードは以下の入力を持ちます:
- A: 始点の値。
- B: 終点の値。
- T: 補間係数。
これらの入力を接続することで、自動的に補間計算が行われます。
Lerp の応用例
1. アニメーション
時間 $ t $ を 0 から 1 に変化させることで、オブジェクトの位置や色を滑らかにアニメーションさせることができます。
2. グラデーション
2つの色の間を補間して、グラデーションを作成できます。
3. 法線マップのブレンド
異なる法線マップをブレンドして、複雑な表面効果をシミュレートできます。
4. フレネル効果
視線方向に基づいて、表面の反射率や色を変化させるために使用されます。
法線マップ
法線マップ (Normal Map) とは
法線マップは、3D グラフィックスにおいてポリゴンモデルの表面に細かい凹凸を表現するためのテクスチャ技法です。この技術により、低ポリゴンのモデルであっても高品質なディテールを効率的にシミュレートできます。
基本的な概念
法線とは?
- 法線とは、3D 空間におけるポリゴン面に対して垂直な方向を示すベクトルです。
- ライティング計算では、光源からの光がどの程度反射されるかを決定するために使用されます。
法線マップの役割
- 法線マップは、実際には存在しない「仮想的な凹凸」を表現するために使われます。
- 元のモデルの形状(ジオメトリ)を変更することなく、視覚的に細かいディテールを追加できます。
法線マップの仕組み
法線マップは、RGB の各チャンネルを使って表面の法線情報を格納します。
-
RGB の意味
- R (赤): X 軸方向の法線成分(左右方向)。
- G (緑): Y 軸方向の法線成分(上下方向)。
- B (青): Z 軸方向の法線成分(奥行き方向)。
通常、法線マップは青っぽい色で表示されますが、これは Z 成分が正の方向(カメラ側)を向いているためです。
-
テクスチャ空間での法線情報
- 法線マップは、モデルの UV 座標に基づいて適用されます。
- 各ピクセルの法線情報がテクスチャにエンコードされ、ライティング計算に使用されます。
法線マップの作成方法
-
高ポリゴンモデルから生成
- 高解像度のディテールを持つ高ポリゴンモデルを作成し、それを低ポリゴンモデルに投影して法線マップを生成します。
- 代表的なツール:
- Blender
- Substance Painter
- ZBrush
-
手動またはプロシージャル生成
- 手動で法線マップを描画したり、ノイズやパターンを用いて自動生成することも可能です。
法線マップの利点
-
パフォーマンスの向上
- 実際にジオメトリを分割せず、シェーダー上で視覚的な凹凸を再現できるため、レンダリングコストを抑えられます。
-
リアルなディテール
- 凹凸や細かなディテールをリアルに表現できるため、ゲームや映像制作で広く利用されています。
-
柔軟性
- 法線マップを切り替えることで、同じモデルに異なるディテールを与えることが可能です。
法線マップの種類
-
オブジェクト空間法線マップ (Object Space Normal Map)
- 法線情報がモデル全体の座標系に基づいて格納されます。
- 特定のモデルに固定されるため、他のモデルに再利用できません。
-
接空間法線マップ (Tangent Space Normal Map)
- 法線情報が各ポリゴンの接空間(Tangent Space)に基づいて格納されます。
- 最も一般的に使用される形式で、アニメーションや変形に対応可能です。
Shader Graph での法線マップの使用
Shader Graph では、以下のような手順で法線マップを適用できます。
-
法線マップの読み込み
- Texture ノードを使用して、法線マップを読み込みます。
-
法線マップの変換
- 法線マップの RGB 値を法線ベクトルに変換するために、「Normal Map」ノードを使用します。
-
ライティング計算への反映
- 変換された法線情報を Master ノード(例: PBR Master)に接続して、ライティング計算に反映させます。
VisionOS での法線マップの可能性
VisionOS は AR/VR 向けのプラットフォームであるため、以下の特徴を持つ法線マップの活用が期待されます:
-
リアルタイム環境光との統合
- AR/VR 環境でのリアルタイムな光源や環境光と組み合わせることで、より自然な反射や影を表現できます。
-
視線追跡との連携
- ユーザーの視線方向に基づいて、法線マップの影響をダイナミックに調整することが可能です。
-
空間認識データとの融合
- 現実世界の物体や地形データと仮想的な法線マップを組み合わせることで、没入感のある体験を提供できます。
カリングとは
カリングとは?
カリング(Culling)は、コンピュータグラフィックスやゲーム開発において、描画する必要のないオブジェクトやポリゴンを効率的に排除する技術です。これにより、レンダリングの負荷を軽減し、パフォーマンスを向上させることができます。
具体的には、視点から見えないオブジェクトやポリゴン(例えば、カメラの背後にあるものや他のオブジェクトに完全に隠れているもの)を計算や描画から除外することで、処理時間を短縮します。この技術は特にリアルタイムレンダリングが必要な環境(例えばゲームやAR/VRアプリケーション)で重要です。
Vision OS におけるカリング
Vision OSは、Appleが提供するAR/VR向けのプラットフォームであり、3D空間でのインタラクションを前提としています。このような環境では、多数の3Dオブジェクトを同時に処理する必要があるため、カリングは特に重要な役割を果たします。
Vision OSで利用される主なカリング技術
-
Frustum Culling(視錐台カリング)
- カメラの視野(視錐台)に入らないオブジェクトを排除します。
- 視錐台の範囲外にあるオブジェクトは描画されません。
-
Occlusion Culling(遮蔽カリング)
- 他のオブジェクトによって完全に隠れているオブジェクトを排除します。
- 例えば、壁の向こう側にあるオブジェクトは描画されません。
-
Distance-Based Culling(距離ベースのカリング)
- 視点からの距離が一定以上離れたオブジェクトを排除します。
- 遠くのオブジェクトは詳細を省略したり、完全に非表示にしたりできます。
-
Level of Detail (LOD)
- 距離に応じてオブジェクトの詳細度を調整します。
- 遠いオブジェクトには低解像度のモデルを使用し、近づいたときに高解像度のモデルに切り替えます。
Vision OSでは、これらのカリング技術がシステムレベルで最適化されており、開発者が意識せずとも効率的なレンダリングが行われるよう設計されています。ただし、カスタムの3Dコンテンツを開発する場合、Unityなどのツールを使って独自のカリング設定を行うことが可能です。
Unity におけるカリング
Unityは、3Dおよび2Dゲーム開発に広く使用されるゲームエンジンであり、カリングの機能も豊富に提供されています。以下はUnityで利用可能な主要なカリング技術です。
1. Frustum Culling
- Unityのカメラはデフォルトで視錐台カリングを実行します。
- プロジェクトの「Camera」コンポーネントが自動的にこの機能を適用します。
2. Occlusion Culling
- Unityでは、特定のシーンに対して「Occlusion Culling」を設定できます。
- 「Occlusion Area」と「Occlusion Portal」を使用して、遮蔽される領域を定義します。
- 遮蔽カリングは事前にベイク(Bake)する必要があります。
3. Distance-Based Culling
- オブジェクトの「Camera Distance」に基づいてカリングを設定できます。
- 例えば、特定の距離を超えたオブジェクトを非アクティブ化するスクリプトを記述できます。
4. LOD Groups
- Unityには「LOD Group」コンポーネントがあり、距離に応じて異なる詳細度のモデルを切り替えることができます。
- 遠くのオブジェクトには低解像度のモデルを使用し、近づいたときに高解像度のモデルに切り替えます。
5. Custom Culling with Scripts
- 必要に応じて、カスタムスクリプトでカリングを実装することも可能です。
- 例えば、特定の条件を満たすオブジェクトを手動で非表示にすることができます。
Vision OS と Unity の連携におけるカリングの重要性
Vision OSはAR/VR特化型のプラットフォームであるため、3D空間での描画負荷を最小限に抑えることが不可欠です。Unityを使用してVision OS向けのアプリケーションを開発する場合、以下の点に注意してください。
-
パフォーマンスの最適化
- Vision OSはモバイルデバイス上で動作するため、CPU/GPUのリソースは限られています。
- カリングを適切に設定することで、不要な描画処理を削減し、フレームレートを維持できます。
-
遮蔽カリングの活用
- AR/VR環境では、複雑なシーン構造が一般的です。
- 遮蔽カリングを活用して、背景や奥に隠れたオブジェクトの描画を回避しましょう。
-
LODの導入
- 遠景のオブジェクトには低解像度のモデルを使用することで、全体の描画負荷を軽減できます。
-
Occlusion Cullingのベイク
- UnityのOcclusion Culling機能を活用して、事前に遮蔽情報を生成しましょう。
- ただし、ベイクには時間がかかるため、開発プロセスの中で計画的に実施することが重要です。
結論
カリングは、Vision OSやUnityで3Dアプリケーションを開発する際に非常に重要な技術です。これにより、描画負荷を効率的に管理し、スムーズなユーザーエクスペリエンスを提供できます。Vision OS特有のAR/VR環境では、特に遮蔽カリングやLODの活用が効果的です。Unityの豊富なツールとVision OSの最適化機能を組み合わせることで、高品質かつパフォーマンスに優れたアプリケーションを実現できます。
L-System
植物の成長など、自然物の構造を記述するためのアルゴリズム。
L-Systems (Lindenmayer Systems) について
L-Systems(リンデンマイヤーシステム)は、1968年にハンガリーの生物学者アリスター・リンデンマイヤー(Aristid Lindenmayer)によって考案された形式言語の一種です。このシステムは、植物の成長や分岐構造をモデル化するために開発されましたが、その後、フラクタル図形や自然現象のシミュレーション、さらにはコンピュータグラフィックスやアルゴリズム芸術など幅広い分野で応用されています。
基本的な考え方
L-System は「書き換え規則」に基づく形式文法であり、初期状態(Axiom)といくつかの生成規則(Production Rules)から成り立っています。これらの規則を繰り返し適用することで、文字列が段階的に変化していきます。
主な要素
-
Axiom(公理/初期状態)
- システムの出発点となる文字列。
- 例:
F
-
Alphabet(アルファベット)
- 使用される文字(記号)の集合。
- 例:
{F, +, -}
-
Production Rules(生成規則)
- 各文字に対してどのような置換を行うかを定義する規則。
- 例:
F → F+F-F
(F を F+F-F に置き換える)
-
Interpretation(解釈)
- 生成された文字列をどのように描画するかを定義する。
- 例:
F
は「前進」、+
は「右に回転」、-
は「左に回転」。
システムの動作
L-System は以下のように動作します:
- 初期状態(Axiom)を準備する。
- 生成規則を適用して、文字列を書き換える。
- 必要な回数だけステップ2を繰り返す。
- 最終的に得られた文字列を解釈して描画する。
例: コッホ曲線
コッホ曲線(Koch Curve)は、L-System を使って簡単に生成できるフラクタル図形の一つです。
規則
-
Axiom:
F
-
Rules:
F → F+F--F+F
-
Interpretation:
-
F
: 前進して線を描く -
+
: 右に60度回転 -
-
: 左に60度回転
-
ステップごとの展開
-
Step 0:
F
-
Step 1:
F+F--F+F
-
Step 2:
(F+F--F+F) + (F+F--F+F) -- (F+F--F+F) + (F+F--F+F)
(以降、再帰的に展開)
最終的に、この文字列を解釈するとコッホ曲線が描画されます。
応用例
-
植物の成長モデル
- L-System は植物の分岐構造や葉の配置を模倣するために特に有用です。
- 例えば、「茎が伸びる」「枝分かれする」「葉が生える」といった過程をルールとして記述できます。
-
フラクタル図形の生成
- コッホ曲線、シェルピンスキーの三角形、ドラゴン曲線などのフラクタル図形を生成できます。
-
コンピュータグラフィックス
- アルゴリズム芸術やゲーム開発において、自然環境や地形の生成に利用されます。
-
生物学的シミュレーション
- 細胞分裂や組織の成長をモデル化するために使用されます。
注意点と限界
-
計算量
- 段階を増やすほど文字列が指数関数的に増加するため、計算コストが高くなることがあります。
-
制約
- 現実世界の複雑な現象を完全に再現するのは難しい場合があります。
まとめ
L-Systems は、単純なルールに基づいて複雑なパターンや構造を生成できる強力なツールです。特に植物の成長モデルやフラクタル図形の生成において優れたパフォーマンスを発揮します。その柔軟性と視覚的な魅力から、数学、生物学、コンピュータグラフィックスなど多くの分野で活用されています。