visionOSについて調べてみた

# はじめに

この記事は、[NTTテクノクロス Advent Calendar 2023](https://qiita.com/advent-calendar/2023/ntt-tx) 21日目です。

NTTテクノクロスの神長(ジンチョウ)です。社内では、モバイル向けアプリ開発・技術支援、技術ガイド執筆、[社内研修講師](https://www.ntt-tx.co.jp/products/soft_dojyo)を担当してます。

また、業務外では以下活動を行ってます。

- 学生向けプログラミング教育
  - 川崎市立上丸子小学校([社外ブログ『エンジニアが本気で⼩学⽣向けプログラミング教育をやってみた』を参照)](https://www.ntt-tx.co.jp/column/cross_field.html)
  - 都内高等学校
  - 他県大学
- 技術書典へ書籍の出展([最新刊『ゼロから始めるFlutterアプリ開発入門 2023.11版』](https://techbookfest.org/product/5925951072370688?productVariantID=4873627981840384))

[WWDC2023](https://developer.apple.com/jp/wwdc23/)で個人的に一番驚いた技術 **『visionOS』** について社内の勉強会等で発表した内容を記事化しました。

visionOSの概要からプロジェクト・簡単なアプリの作成方法迄をまとめたので、これからvisionOSを勉強しようとしている方の一助になればと思ってます。

# 参考情報

記事に掲載した情報、画像、3Dモデルの一部は以下サイトより引用してます。

- [https://developer.apple.com/visionos/](https://developer.apple.com/visionos/)
- [https://www.apple.com/apple-vision-pro/](https://www.apple.com/apple-vision-pro/)
- [https://developer.apple.com/visionos/learn/](https://developer.apple.com/visionos/learn/)
- [https://developer.apple.com/design/human-interface-guidelines/designing-for-visionos/](https://developer.apple.com/design/human-interface-guidelines/designing-for-visionos/)
- [https://developer.apple.com/documentation/visionos/creating-fully-immersive-experiences](https://developer.apple.com/documentation/visionos/creating-fully-immersive-experiences)
- [https://developer.apple.com/documentation/compositorservices/drawing_fully_immersive_content_using_metal](https://developer.apple.com/documentation/compositorservices/drawing_fully_immersive_content_using_metal)
- [https://sketchfab.com/3d-models/jellyfish-003-7ef761c3eb2143b6a7178ddfb97e9e88](https://sketchfab.com/3d-models/jellyfish-003-7ef761c3eb2143b6a7178ddfb97e9e88)

# visionOSとは

![visionPro.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59827/7c6deb6a-ccb3-2d2d-3019-4622d29319ed.png)

2023/6/6 WWDC2023で発表されたAR・MRヘッドセット **『VisionPro』** に搭載されるOSが **『visionOS』** です。
VisionProは、ユーザーに拡張現実(AR)や複合現実(MR)体験を与えるデバイスです。バイザー(目を覆う部分)を通し、ユーザーの周囲の空間に様々なコンテンツ(アプリ)を表示して、目と手と声で操作を行うことで没入感のある体験ができます。これを支えるのが **『visionOS』** です。

# visionOSを支える技術

![visionOS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59827/18b208d2-d052-d121-5f92-dd0a2af362d8.png)

- Window
- Volume
- Space
- SwiftUI
- RealityKit
- ARKit

## Window

![windows.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59827/182fe3bf-db21-12a6-c603-329f41380f23.png)

Window(以降、画面)とは、visionOSアプリの画面です。アプリの中に1つ以上のWindowを作成できます。
画面は **『SwiftUI』** で構築され、コントロール以外にも3Dコンテンツを追加することでよりUXに深みを加えることができます。

なお、iOSやiPadOS向けに実装されたアプリはvisionOSで動作します。
iOSとiPadOSに両対応したアプリの場合、レイアウトはiPadOS向けのものが優先されます。

## Space

![spaces.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59827/5a2a2c3f-b87a-3eea-b775-4f3de35e8d76.png)

Spaceとは、 **画面やコンテンツを表示する空間の概念です。** Spaceには、 **『Shared Space』** と **『Full Space』** の2種類があります。

Shared Spaceは、macOSのデスクトップに相当するものです。visionOSのアプリは、デフォルトではShared Space上で起動されます。

Full Spaceは、より没入感のある体験に利用されるもので、 **空間全体を1つのアプリで占有するもので、Apple社のサイトでは『別世界へのポータル(入口)を開いたり、人々を完全に没入させることもできる』と説明されてます。**

## SwiftUI

![swiftui.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59827/c0be68fb-c2a0-5e9c-f01f-015b0b953eab.png)

SwiftUIは、visionOSのUI(Window、Volume)を構築するフレームワークです。Appleも **『SwiftUIは、新しいvisionOSアプリを構築したり、既存のiPadOSまたはiOSアプリをプラットフォームにもたらすための最良の方法です。全く新しい3D機能と深度、ジェスチャー、エフェクト、没入型シーンタイプのサポートにより、SwiftUIはVision Pro用の美しく魅力的なアプリを構築するのに役立ちます。また、SwiftUIにRealityKitが深く統合され、シャープで応答性が高く、ボリューメトリック(3Dモデルを構築する技術)なインターフェースの構築を支援します。』** とと述べられています。

## RealityKit

![realitykit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59827/b44f2fde-f372-51fa-58f3-41e3a92852c6.png)

RealityKitは、 **3Dコンテンツ、アニメーション、ビジュアルエフェクトを行う場合に利用するフレームワーク** です。
物理的な照明条件を自動的に調整して影を落としたり、別世界へのポータル(入口)を開いたり、驚くようなビジュアルエフェクトを構築したりと様々なことができます。

また、RealityKitは、映画、ビジュアルエフェクト、エンターテイメント、ゲームの大手企業が使用するサーフェスおよびジオメトリシェーダを指定するためのオープンスタンダードである[MaterialX](https://materialx.org/)を採用しています。

## ARKit

![arkit6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59827/e722c2fa-28c6-d50e-e236-c07823058f64.png)

ARKitは、 **Shared Spaceに存在するアプリの為のコアシステム機能を提供するフレームワーク** です。アプリがフルスペースに移動して許可を求めると平面推定、シーン再構築、画像アンカリング、ワールド トラッキング、スケルタル ハンド トラッキングなどのAPIを提供します。これによって、『壁に水をかける』、『床からボールを跳ね返す』といった現実世界とコンテンツの融合を実現します。

# visionOSの設計思想

- **アクセシビリティを念頭に置いて設計** されており、目、声、その両方の組み合わせでデバイスを操作する人の為に設計されている。
- コンテンツをナビゲートを別の方法にしたい場合は、代替ポインターとして人差し指、手首、頭を選択できる。
- **他のAppleプラットフォームで使っている技術やツール(SwiftUI、Unity等)** を使い、アクセシブルなアプリケーションを作成し、全ての人が素晴らしい体験にする手助けをすることができる。

# visionOSアプリのBest Practices

- 優れたvisionOSアプリやゲームはユーザーにとって親しみ易いものであり、美しいコンテンツ、拡張された機能、魅力的な冒険で人々を包み込む特別な体験を提供します。

- Vision Proの空間、空間オーディオ、没入感を活用して、あなたの体験に生命を吹き込みましょう。パススルーや目や手からの空間入力は、デバイス上で違和感のない方法で統合できます。

- アプリの最も特徴的な瞬間を設計する際には、全領域の没入感を考慮してください。特徴的な瞬間の毎に最も適した没入感を見つけましょう。 **全ての瞬間を完全に没入させる必要があると決めつけないでください。**

- UI中心の体験にはWindowを使いましょう。標準的なタスクを実行し易くするために、空間内の平面として表示され、使い慣れたコントロールが含まれる標準的なWindowを提供しましょう。visionOSでは、Windowを好きな場所に移動させることができます。また、システムの動的なスケーリングによってWindowのコンテンツが近くにあっても遠くにあってもユーザーが内容を認識し易い状態が保たれます。

- アプリやゲームの操作は、快適で身体的にリラックスした状態を保てるよう以下を念頭に置いてください。

  - 視野内にコンテンツを表示し、頭と相対的な位置に配置する。コンテンツと対話するために、頭を回転させたり、位置を変えたりしなければならないような場所にコンテンツを配置することは避けましょう。

  - 圧倒されるような、耳障りな、速すぎる、または静止したフレームを見失うような動きの表示は避けましょう。

  - 膝の上や両脇に手を置いたままアプリを操作できるよう、間接的なジェスチャーをサポートする。

  - 直接ジェスチャーをサポートする場合、インタラクティブなコンテンツが遠すぎず、長時間インタラクティブに操作する必要がないように設計してください。

  - 完全没入型の体験をしている間は、あまり動きすぎないようにしましょう。

- アクティビティを他の人と共有できるようにする。[SharePlay](https://developer.apple.com/jp/shareplay/)を使って共有アクティビティをサポートすると、参加者は他の参加者の空間ペルソナを見ることができ、全員が同じ空間にいるように感じることができます。

# 開発環境

- Xcode 15.1 beta 3
- macOS Sonoma(macOS 14.1.1（23B81）)※

※ beta版の利用に際してDeveloperProgramは不要です。

# ツール

visionOSアプリを開発する際に活用することで開発効率が向上するツールを紹介します。

## Reality Composer Pro

![realitycomposerpro.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59827/a9a57721-80a3-9d49-7a3d-8954db602c9f.png)

Reality Composerは、iPhoneやiPadのAR体験を構築、テスト、チューニング、シュミレートするAR制作ツールです。
Reality Composerを基にvisionOSアプリ用の3Dコンテンツをプレビューできるように新たに設計されました。Reality Composer ProはXcodeで利用可能で、ビルドプロセスと統合されてvisionOSのデータをプレビュー、最適化できます。

また、3Dモデルやサウンドのインポート、整理にも活用できます。

## Unity

![unity.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59827/b5e677f0-a01c-7634-c400-7f57c61f23b8.png)

Unityは、ゲーム開発プラットフォームです。visionOSでは、Unityで開発したゲームの移植、新しいアプリやゲームの開発が可能になりました。 **RealityKitでのレンダリングを組み合わせることで、Unityで開発したコンテンツはvisionOSで動作します。** AR体験や周辺環境をそのまま表示する「パススルー機能」、ユーザーの視線をトラッキングして視野の中央部分のみを高解像度で描画する「Dynamically Foveated Rendering」との組み合わせも可能です。

# プロジェクト作成

visionOSアプリのプロジェクトは以下手順で作成できます。

- 新規プロジェクトの作成を選択する。
- visionOSのテンプレート選択する。

![Project_Temp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59827/c6b42b1b-1f44-2452-640e-069323bdb8ba.png)

- 『Initial Scene:』にWindow(2D)かVolume(3D)を選択する。(デフォルトはWindow)
- 『Immersive Space Renderer』にNone、Realitykit、Metalを選択する。(デフォルトはNone)
- 『Immersive Space』にNone(非対応)、Mixed(複合現実)、Progressive(複合現実(現実から徐々に仮想世界へ拡がる様な設定))、Full(全画面)を選択する。なお、ここで選択できる項目は、Immersive Space Rendererで選択した項目に依存する。
    - Immersive Space RendererでNoneを選択した場合は、Noneのみ選択可能。
    - Immersive Space RendererでRealitykitを選択した場合は、Mixed、Progressive、Fullが選択可能。
    - Immersive Space RendererでMetalを選択した場合は、Fullのみ選択可能。

![visionOS_CreateProject.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59827/d36a637b-1dd8-2030-22f6-cf15a5770228.png)

- プロジェクトの保存場所を選択し、Createボタンを押下する。
- 以下の様にプロジェクトが作成される。(WindowとNoneを選択してプロジェクトを作成した場合を例として掲載)

![Window_None_Select.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59827/337bd488-cc8b-ffa5-1fab-15774a52e329.png)

# 開発方法

## 2Dコンテンツ

開発のポイントは以下。

- SwiftUIを利用して画面を構築
- 画面の構築方法は、iOS等と同様

サンプルとして、リスト画面を開発する方法を掲載。

![windows.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59827/daa01a29-d3aa-dffa-57e7-5ede83a38569.gif)


``` swift
import SwiftUI

struct ContentView: View {
    
    // 中略
    
    var body: some View {
        // iOS等でSwiftUIを使った画面の構築と同様の手法をとる。
        NavigationStack{
            List(fishList) { fish in
                NavigationLink(destination: FishDetail(fishModel: fish)) {
                    // Listの一行に表示する内容を定義したコンポーネント。
                    FishRow(fishModel: fish)
                }
            }
            .navigationTitle("魚図鑑")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}

```

``` swift
import SwiftUI

struct FishRow: View {
    var fishModel: FishModel
    
    var body: some View {
        HStack {
            Image(fishModel.imageName)
                .resizable()
                .frame(width: 50, height: 50)
                .cornerRadius(10)
            Text(verbatim: fishModel.name)
                .font(.title)
        }
    }
}
```

``` swift
import SwiftUI

struct FishDetail: View {
    var fishModel: FishModel
    
    var body: some View {
        VStack {
            ZStack(alignment: .bottomTrailing) {
                Image(fishModel.imageName)
                    .resizable()
                    .frame(width:500, height: 300)
            }
            Form {
                Section(header: Text("説明")) {
                    Text(fishModel.details)
                }
            }
        }
        .navigationTitle(Text(fishModel.name))
        .navigationBarTitleDisplayMode(.inline)
    }
}
```

## 3Dコンテンツ

開発のポイントは以下。

- Reality Composer Proでプロジェクトを作成
- Reality Composer Proのプロジェクトをアプリのプロジェクトへインポート
- SwiftUIとRealityKitを利用して画面を構築

サンプルとして、3Dコンテンツを表示する画面を開発する方法を掲載。

![3D.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59827/734a53bc-8bd7-913f-8ec5-0794b3e794e0.gif)

#### Reality Composer Proでプロジェクトを作成

- Reality Composer Proを起動して『Create New Project』でプロジェクトを作成
- 3Dモデル(.usdz形式)をインポート(ドラッグ&ドロップ)
- 3DモデルをRoot配下に設定(ドラッグ&ドロップ)後、プロジェクトを保存(パッケージ化)

![RealityComposerPro_project.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59827/e0669163-1ecf-cf8d-f45d-77245f124450.png)

#### Reality Composer Proのプロジェクトをアプリのプロジェクトへインポート

- アプリのプロジェクトを開き、Reality Composer Proで作成したパッケージを追加
- プロジェクトのTARGETS＞Frameworks Libraries,and Embedded Contentにパッケージを追加

![AppProject_AddPackage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59827/21149f8e-ed4b-fe8b-0057-cda4215c7ab6.png)

#### SwiftUIとRealityKitを利用して画面を構築

パッケージを呼び出すファイルに以下実装を行う。

``` swift
import SwiftUI
import RealityKit
import RealityKitContent
import Jellyfish // Reality Composer Proで作成したパッケージをインポートする。

struct ContentView: View {
    @State var enlarge = false

    var body: some View {
        VStack {
            RealityView { content in
                // Reality Composer Proで作成したパッケージを画面に設定する。
                if let scene = try? await Entity(named: "Jellyfish", in: jellyfishBundle) {
                    content.add(scene)
                }
            } update: { content in
                if let scene = content.entities.first {
                    let uniformScale: Float = enlarge ? 0.001 : 0.001
                    scene.transform.scale = [uniformScale, uniformScale, uniformScale]
                }
            }
            .gesture(TapGesture().targetedToAnyEntity().onEnded { _ in
                enlarge.toggle()
            })

            VStack {
                Toggle("Enlarge RealityView Content", isOn: $enlarge)
                    .toggleStyle(.button)
                    .padding()
            }.padding().glassBackgroundEffect()
        }
    }
}

#Preview(windowStyle: .automatic) {
    ContentView()
}
```

## WindowGroup

開発のポイントは以下。

開発のポイントは以下。

- WindowGroupを利用
- openWindowを利用
- dismissWindowを利用

サンプルとして、3Dコンテンツを表示する画面から2Dの画面を表示する方法を掲載。

![windowgroup.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59827/77570d02-6206-682f-f09c-257c774e8928.gif)


``` swift
import SwiftUI

@main
struct Sample3DApp: App {
    private var fish: FishModel = FishModel(name: "クラゲ", imageName:  "jellyfish", details: "浮遊生活をする刺胞動物である。体はゼラチン質で柔らかく透明。体全体は多くのものでは傘のような形をしている。")

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        
        /* WindowGroupを利用して、3Dコンテンツを表示する画面から2Dの画面を表示する設定を行う。
           設定時に2Dの画面を表示する際に利用するidも定義する。
        */
        WindowGroup("Jellyfish", id:"JellyfishDetail") {
            FishDetailView(fishModel: fish)
        }
    }
}
```

``` swift
import SwiftUI
struct NewWindowButton: View {
    /*  @Enviroment(環境値を読み取るカスタム属性)で
       『supportsMultipleWindows(複数画面表示)』
       『openWindow(画面を開く)』
        を定義する。
    */
    @Environment(\.supportsMultipleWindows) private var supportsMultipleWindows
    @Environment(\.openWindow) private var openWindow
    
    var body: some View {
        Button("detail") {
            // WindowGroupで指定した『id:”KoiDetail”』を使い、『openWindow(画面を開く)』を実行する。
            openWindow(id: "JellyfishDetail")
        }
        .opacity(supportsMultipleWindows ? 1:0)
    }
}

#Preview {
    NewWindowButton()
}
```

``` swift
struct FishDetailView: View {
    var fishModel: FishModel
    
    var body: some View {
        VStack {
            ZStack(alignment: .bottomTrailing) {
                Image(fishModel.imageName)
                    .resizable()
                    .frame(width: 300, height: 300)
                
            }
            Form {
                Section(header: Text("説明")) {
                    Text(fishModel.details)
                }
            }.padding()
            
            CloseButton().padding(.bottom)
        }
        .navigationTitle(Text(fishModel.name))
        .navigationBarTitleDisplayMode(.inline)
    }
}

private struct CloseButton: View {
    // @Enviroment(環境値を読み取るカスタム属性)で『dismissWindow(画面を閉じる)』を定義する。
    @Environment(\.dismissWindow) private var dismissWindow
    
    var body: some View {
        Button("Close") {
            // WindowGroupで指定した『id:”KoiDetail”』を使い、『dismissWindow(画面を閉じる)』を実行する。
            dismissWindow(id:"JellyfishDetail")
        }
    }
}

#Preview {
    FishDetailView(fishModel: FishModel(name: "クラゲ", imageName:  "jellyfish", details: "浮遊生活をする刺胞動物である。体はゼラチン質で柔らかく透明。体全体は多くのものでは傘のような形をしている。"))
}
```

``` swift
import SwiftUI
import RealityKit
import RealityKitContent
import Jellyfish // Reality Composer Proで作成したパッケージをインポートする。

struct ContentView: View {
    @State var enlarge = false

    var body: some View {
        VStack {
            RealityView { content in
                // Reality Composer Proで作成したパッケージを画面に設定する。
                if let scene = try? await Entity(named: "Jellyfish", in: jellyfishBundle) {
                    content.add(scene)
                }
            } update: { content in
                if let scene = content.entities.first {
                    let uniformScale: Float = enlarge ? 0.001 : 0.001
                    scene.transform.scale = [uniformScale, uniformScale, uniformScale]
                }
            }
            .gesture(TapGesture().targetedToAnyEntity().onEnded { _ in
                enlarge.toggle()
            })

            VStack {
                NewWindowButton()
                    .padding(.bottom)

            }.padding().glassBackgroundEffect()
        }
    }
}

#Preview(windowStyle: .automatic) {
    ContentView()
}
```

## ImmersiveSpace

開発のポイントは以下。

Apple社のサイトの以下サイトをご確認ください。

- [Creating fully immersive experiences in your app](https://developer.apple.com/documentation/visionos/creating-fully-immersive-experiences)
- [Drawing fully immersive content using Metal](https://developer.apple.com/documentation/compositorservices/drawing_fully_immersive_content_using_metal)

# 最後に

最後まで読んで頂き、ありがとう御座います。
22日目となる明日の記事も楽しみにして頂ければと思います。