ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
SwiftUIでウインドウを超える
スペースを起動する準備をしましょう。- visionOSでのイマーシブな体験の創造をお手伝いする新しいSwiftUIシーンタイプを探求します。ImmersiveSpaceで新しいシーンを作成し、3Dコンテンツを配置、そしてRealityViewを統合する方法をご紹介します。immersionStyleシーン修飾子でアプリのイマージョンレベルを増加させる方法を探求し、スペースの管理、ARKitのvirtual handsの追加、SharePlayサポートの追加、そしてこの世のものとは思えない体験を構築するベストプラクティスを探求します。
関連する章
- 0:00 - Introduction
- 3:27 - Get Started
- 5:51 - Display content
- 12:00 - Managing your Space
- 19:12 - Customization
リソース
関連ビデオ
WWDC23
-
ダウンロード
♪ ♪ ♪ こんにちは 「Go beyond the window with SwiftUI」へ ようこそ エンジニアのRaffaです 後に同僚のMarkが加わります 今日はみなさんが既にご存知の ツールやフレームワークで イマーシブな体験を創造するために xrOSのフルパワーを活用するのが いかに簡単かをお見せします
iOS用のARアプリの開発で既に 拡張現実感には馴染みがあることでしょう この数年間で iPhoneやiPad用の リッチなARアプリの作成のため ARKitとRealityKitを含む 数々のツールやフレームワークを ご紹介しました これらのアプリはインタラクティブな ユーザーインターフェイスと 仮想オブジェクトで ユーザーの環境を増大させ 現実と想像を限りなく近付けてくれます 今年はxrOSの登場で ARが更にレベルアップされます まずはイマーシブな体験です これらの体験では ウインドウや3次元コンテンツなど アプリがあなたの周りにインターフェイスを 表示します 周りは見えたままで 実際 体験の一部となります アプリの要素を表面に据えつけて 仮想オブジェクトや効果で 現実の世界を増大させ 高めることができます そしてフルイマーシブな体験では 更に一歩先に進み環境全体を覆います アプリが視界を完全制御するのです これにより可能性が解き放たれます そして何よりもこれらは既にみなさんに 馴染みのあるツールやフレームワークで 実現可能になります このコアとなるのがSwiftUIの Immersive Spaceです では始めましょう 別のセッションで今年からSwiftUIに 三次元が追加されると学びました xrOSではウインドウとボリュームを用いて SwiftUIの簡単な宣言型パターンで 3次元ユーザーインターフェイス要素を 表示することができます ウインドウとボリュームは領域内に コンテンツを表示できます ではxrOSが供給する限りない空間を使い 真のイマーシブな体験を 創造したいとすればどうでしょう? アイテムをウインドウの枠外に配置し あなたの周りにあるものすべてのなかで 中心にいる状態にしたいでしょう。 スペースはこのためにデザインされました ウインドウとボリュームの 横に位置するスペースは xrOSのインターフェイスを提供する コンテナのようなものです ここではスペースに焦点を当て イマーシブな体験を どう創造できるか見てみます まずスペースを使用し始め コンテンツの表示の仕方を学びます そしてMarkがスペースの管理方法と 直接スペースに起動させる方法 そしてスペースで可能な カスタマイズについて説明します では早速始めてコードを見てみましょう スペース探検を楽しみにしています ほかのセッションで取り掛かっていた 「Wrold」アプリで 太陽系を探検できるスペースで アプリを一歩一歩拡張していきます スペースはImmersive Spaceという SwiftUIのシーンタイプです ほかのシーンタイプ同様に アプリでImmersive Spaceを定義し いつでも開いたり終了することができます スペースが一つだけの アプリも作成できますし ウインドウやボリュームの横に 一つ以上のスペースを足して 既存のアプリを拡張できます アプリは一度に一つのスペースを開け 別のスペースを開く前に 現在あるものを閉じます これもほかのシーンタイプ同様 シーンのボディにビュー階層を配置します ImmersiveSpaceにSolarSystemを配置すると 領域をクリッピングすることなく レンダリングされます 実に簡単かお分かりいただけたでしょうか この3行だけで solar system viewを リッチでイマーシブな体験にできたのです では詳細を見てみましょう スペースを開くことでこのシーンを ほかのシーンタイプから際立たせる 特別のビヘイビアを利用できます 幾つものアプリが並んで動作している時 すべてが同じスペースに表示されます ですのでこれを Shared Spaceと呼んでいます アプリがImmersive Spaceシーンを 表示すると アプリはフルスペースに入ります ユーザーに見えるのはそのアプリだけです ほかのアプリは消え去り そのコンテンツだけが 邪魔なく表示されます 後程 スペースを終了すると ほかのアプリが再び現れます Immersive Spaceはシーンなので 暗黙のうちにその座標システムを定義します つまりスペース内に配置するものは スペース自体の原点に相対して配置されます スペースが最初に開いた時 スペースの原点はユーザーの 足元の近くにあります 基本がわかったところで 次に スペースでどうコンテンツを 表示するかについてです ImmersiveSpaceはシーンタイプで その中にシーン階層を配置します ImmersiveSpaceは どのSwiftUIビューにも対応し クリッピング領域はないものの スペースはコンテンツを レイアウト領域内に配置します スペースに配置するものは みなさんが既に馴染みのある レイアウトシステムを使用します しかしスペースの原点が ユーザーの足の近くなので そこにはコンテンツを 置くべきではないでしょう RealityViewについてお話ししましょう SwiftUIとARKit 及びRealityKitを活用したいなら RealityViewのパワフルな機能と共に ImmersiveSpaceを 使用することをお勧めします ImmersiveSpaceとRealityViewは 相伴う関係で イマーシブな体験の創造に 必要な機能を提供するために 具体的に一緒にデザインされています 例えばRealityViewは アセットの非同期読み込みの 組み込み型サポートを含んでおり ご覧のようにここでは star fieldを読み込み表示しています しかし非同期読み込みに加え RealityViewを Immersive Spaceシーンに加えることで いろいろ可能になります ARKit anchorsで RealityView内に要素を配置したり スペースが開いてる間 アプリが手と頭部ポーズデータに アクセスできるため RealityView内でそのデータを使い エンティティを配置できます 後程Markがその例をお見せします 次は座標空間についてです RealityViewはRealityKitを使用し コンテンツを表示します ですのでRealityView内に エンティティを配置する時 座標空間の指向はSwiftUIの レイアウトシステムとは違います SwiftUIではY軸は下向きに Z軸はユーザーの方向に伸びます これはウインドウやボリューム Immersive Spacesに適応します しかしRealityKitではY軸は上向きです RealityViewにはまだまだ話題が豊富です 「Build spatial experiences with RealityKit」で 詳細をご覧ください ではコードを書いてみましょう ここでは「World」アプリを 簡素化したものを使用して イマーシブなsolar systemを 一つ一つ足していきます まずImmersiveSpaceの定義からです WindowGroupのように 識別子や値型もしくは両方を 割り当てられます この場合 solar識別子を割り当てます 後にスペースをこの識別子で開きます そしてスペースに SolarSystemビューを配置します またアプリの単純な基本ウインドウを定義し アプリ起動時に太陽系を見るための コントロールと共に現れるようにします 「World」アプリと似ています WindowGroupで 新しいLaunchWindowを定義し スペースを開く コントロールと共に情報を追加します そのコントロールはボタンです クリックすると タイトルが変わり スペースを開くようにします ウインドウのコントロールは SwiftUIはopenWindowと dismissWindow environment アクションを供給します Immersive Spaceには 新しくopenImmersiveSpaceと dismissImmersiveSpace アクションを追加します Environmentから 2つのアクションを得ました ボタンが押された時 これらのアクションを使用します スペースを開く時 先程定義した識別子をパスします スペースは一度に一つしか開かないので dismissImmersiveSpaceアクションには 引数は必要ありません システムは特定の時間で スペースの出入りを動画化します このenvironmentアクションは非同期で 動画の完成に反応することができます Immersive Spaceが開かないこともあり openImmersiveSpaceが 成功したか失敗したか その結果を通知します 適切なエラー処理を行いましょう 最初に定義したアプリに戻り ここにLaunchWindowを追加します 2つのシーンの順序を見てください LaunchWindowが最初にあるため アプリが起動するとSwiftUIは Launch Windowを表示します 起動時 Immersive Spaceは見えませんが ユーザーがボタンをクリックすると現れます では「Simulator」で試してみましょう アプリが起動するとLaunch Windowが見え ボタンをクリックすると リビングルームに太陽系が現れます
これで基本ウインドウと 太陽系を表示する スペースからなる マルチシーンアプリを定義しました 「World」アプリのモデルは ご覧になったでしょう イマーシブアプリでは スペース内に詳細のある3Dアセットを ぜひとも表示したいものです しかしアセットを読み込み アプリでレンダリングできるまで 時間がかかることを覚えておきましょう よりよいユーザー体験のため 3Dアセットを非同期的に読み込む Model3Dと RealityView APIsを 活用するようにしましょう このコードではテキストを モデルの読み込み中に表示し 失敗すればエラーが表示されます ではMarkがスペースの管理と 起動についてお話しします ありがとう Raffa ご覧のようにわずか数行のコードで Immersive Spacesを 「World」アプリに簡単に統合できました アプリをイマーシブな体験に 変換するにはscene phaseで システムと共にスペースを管理し スペースとシーンの収束を調整し 違うスタイルで表示せねばなりません ほかのSwiftUIシーンタイプのように Immersive Spaceはシステムが扱う 同じscene phaseをサポートします つまりスペースは常に SwiftUIのscene phaseの一つです スペースを開くとactive phaseに移行します またいつでもinactive phaseに 移行するかもしれません 例えばシステムが定義した領域を出たり システムアラートが出ると スペースとウインドウは一時隠れ inactive phaseに移行します ユーザーが再び体験に入ると スペースとウインドウは姿を表し scene phaseを再びactiveに更新します 「World」アプリでは数行のコードの追加で inactive scene phaseに対応できます Earth modelのサイズを半減させ スペースの状態が変わったことを示します またactive phaseに対応させ コンテンツを復元させます スペースはハードウェアや ソフトウェアでいつでも終了できます では「Simulator」でチェックです スペースを開いて アプリのinactive phaseへの 対応を試してみます 例えば アラートの表示で トリガされるかもしれません アラートが表示され 先程のコードの通り コンテンツのサイズが変わりました アラートを取り消すと active phaseに反応し スペースが元の大きさに戻ります SwiftUIでこれらの移行の対応と 動画化が簡単で便利にできます また別の管理方法で ほかのウインドウから コンテンツをスペースに統合できます 例えばメインウインドウの横に 地球のモデルを置き直したい場合 ウインドウの位置を Immersive Space座標システムで 知る必要がありますが 両オブジェクトとも 独自の座標システムを使用しています これを解決するためSwiftUIは Immersive Spaceという 新しい座標空間を供給します これでImmersive Spaceの 座標システムを示します アクセスするには 3Dコンテキストのgeometry reader内の ウインドウをカプセル化し transformのような 座標空間を受け入れる既存APIを使用し Immersive Space typeでパスし 新しい座標システムで proxy.transformを得ます このtransformで タップで地球の位置を更新します 「Simulator」で試しましょう スペースを再び開くと 地球と メインウインドウが見えます ウインドウの位置を 少し変えたので地球をその前に動かします 地球をタップし 予定通りの位置に動かします 座標転換で コンテンツの配置と スペースとウインドウ間の アセットの移動が簡単に行えます また座標転換は SharePlayのImmersive Spacesで コンテンツの位置を 個人のImmersive Spaceだけでなく グループのImmersive Spaceを通し 管理できます もしアプリがSharePlayとグループ Immersive Spacesをサポートするなら ほかの参加者が加わると システムはスペースの原点を 空間テンプレートに定義された 共有場所に移行させます 詳細に関しては 「Build spatial SharePlay experiences」を ご覧ください 「World」はscene phasesに対応し ほかのウインドウからのものを 結合できますが まだスペースの性能をフルに 活用していません 次はImmersion Stylesで 更に向上させましょう Immersion Stylesは スペースコンテンツが どう環境を受け継ぐか 違った表現を提供します コンテンツをmixed styleや 目の前にあるprogressive style または周囲を取り巻く full styleで表現できます これらを活用するため アプリを更新しましょう 再びアプリを開き Immersive Spaceを定義した箇所に戻ります 現在スペースはデフォルトの mixed immersion styleで solar systemを表示しています Styleの変更は簡単で 動的にすることもできます まずImmersionStyleタイプの 新しいstate variableを足し スペースのデフォルト値を割り当てます ここではmixed styleをキープします そしてimmersionStyleシーン Modifierを使い スペースにサポートさせたい style listを定義します 現在のstyleを参照させるため state variableをバインドとしてパスします バインドをSolar Systemにパスすると current styleを読み 制御してどのmap stylesにも移行できます ここではmagnification gestureで移行させ solar systemをスケールすると 違うstyleに行くようにします Immersive Spaceの持ち込みが どれだけ簡単か 「Simulator」でお見せしました しかしこれらのstyleが 環境とどう働くか知るために デバイスで試してみましょう 後にデバイスでの体験を 更に向上できる カスタマイズについてお話しします スペースのデフォルトである mixed immersion styleで開きます このスタイルもいいですが もう少しコンテンツに浸り 星を見るべきです そこでmagnification gestureを使い コンテンツが大きくなり やがてSpaceは progressive styleに移行します これはパススルーと フルイマーシブの橋渡しとなり Immersive Spaceコンテンツを 周りの環境と共に 目の前のポータルに見ることができます イマーシブですが 周りの環境にも意識できます 周りの人たちと会話し 落ち着く場所に座れ 環境ともインタラクトできます いざ落ち着けばDigital Crownを回し イマージョン度を増加することができます すごいでしょう? 銀河に浮かぶ宇宙飛行士のようです 周りの環境が見たくなれば Digital Crownを逆向きに回し イマージョンを減らします これでprogressive style内で イマージョン度を 簡単に素早くコントロールできます しかしずっとfull immersionに いたいでしょう 周りを囲まれる体験や 即座に別世界に行く体験には最適です これまでジェスチャーで 別のスタイルへの移行が どれだけ簡単か学びました Full immersionも同じで スタイルバインドを更新するため 再び地球をスケールアップして 体験できます 違うスタイル間の移行が 簡単でシームレスだったでしょう スペースがフルイマーシブになりました SwiftUIで数行のコードだけで済んだのです Digital Crownを押すと いつでも体験を去りたい時に パススルーに戻ることができます
Scene phaseへの反応や スタイルのコントロールでスペースを 違った形で管理できます ではスペースを更に レベルアップさせましょう デバイスの空間コンピューティング機能は スペース体験を簡単に向上し 更にエキサイティングに することができます ではスペースに直接 立ち上げたり 環境に効果を足したり バーチャルハンドのような オプションを見てみましょう このアプリはボタンのクリックで スペースを開きます フルイマーシブなゲームで アプリの起動と同時に イマーシブな体験を立ち上げたいとします Immersive Spaceに直接立ち上げるには アプリのscene manifestを 設定せねばなりません ImmersiveSpace application roleと immersion styleを設定します 通常通りスペースコンテンツを 設定すると 即座に開きます ユーザーがスペースを終了すると アプリをウインドウに戻すこともできます 次にsurrounding effects preferencesで パススルーを暗くしコンテンツに 焦点を当てることができます Progressive styleに移行する時 環境を暗くしたいものです そこで太陽系が現れた時 周囲の環境が自動的に暗くなるよう preferredSurroundingEffects modifierを darkにセットします upperLimbVisibility modifierは パススルーがなくフルイマーシブな スペースにいる間 両手を隠すことができます このアプリではスペースを開いた時 プリファレンスをfalseに設定します このように upperLimbVisibilityプリファレンスを 変更できます Full immersionで両手を隠し バーチャルハンドを使用できます 「World」でスペースグラブを お見せしましょう まずSpaceGlovesというViewを作ります 次にRealityViewを足し スペースでグラブを レンダリングできるようにします RealityViewでroot entityを作り entityをレンダーできるようにします アセットをentityに読み込み rootのchildとして足します Entityの正しい配置に ARKitとそのhand tracking APIを使用し hand tracking systemも開始します 次のステップは アセットを手に正しく据え付けることです Hand tracking anchor updatesを チェックします 次にhand chiralityをチェックします Anchorと同様に アセットが変形するよう確かめます ここではアセットがARKitが供給する joint namesと同じであることを確認ました これでanchor skeleton joint namesを 正しくマッピングし glove entityが 自動的に据え付けられます スペースの定義に戻り SpaceGloves viewを含みます バーチャルハンドに必要なのはそれだけです ARKitのカスタマイズの詳細は 「Evolve your ARKit app for spatial experiences」を ご覧ください ではデバイスでこれらの カスタマイズを試してみましょう 「World」体験が始まり スペースがデフォルトの immersion styleで開きます 地球にmagnify gestureを使い アプリはprogressive styleに移行します スペースが開くと コードにより環境が暗くなります Surrounding Effects APIで パススルーを暗くすることで よりイマーシブになりました 適応が簡単で 体験に集中できる最適の方法です 十分イマーシブですが 次のカスタマイズで 更に向上することができます 先程のコードのように full immersionに移行すると 両手が見えなくなり hand trackingを使用して virtual space glovesが現れます ARKitとRealityViewで hand trackingをオンにすると 仮想宇宙飛行士のように宇宙をさまよえ 素晴らしい気分です
僅かな向上とカスタマイズで 「World」アプリを Shared Spaceを超えた フルイマーシブ体験にできました このImmersive Space APIを使用して 体験を簡単に創造し 違うスタイルで引き立たせ カスタマイズで創造性を 広げることができます このパワフルで簡単なAPIで 環境を一変させる数々のツールを使い 新しくイマーシブな体験を創造できます ありがとうございました ♪
-
-
4:18 - Defining an ImmersiveSpace
@main struct WorldApp: App { var body: some Scene { ImmersiveSpace { SolarSystem() } } }
-
6:53 - RealityView in an ImmersiveSpace
ImmersiveSpace { RealityView { content in let starfield = await loadStarfield() content.add(starfield) } }
-
8:17 - ImmersiveSpace with a SolarSystem view
@main struct WorldApp: App { var body: some Scene { ImmersiveSpace(id: "solar") { SolarSystem() } } }
-
9:00 - LaunchWindow
struct LaunchWindow: Scene { var body: some Scene { WindowGroup { VStack { Text("The Solar System") .font(.largeTitle) Text("Every 365.25 days, the planet and its satellites [...]") SpaceControl() } } } }
-
9:11 - SpaceControl button using Environment actions for opening and dismissing an ImmersiveSpace scene
struct SpaceControl: View { @Environment(\.openImmersiveSpace) private var openImmersiveSpace @Environment(\.dismissImmersiveSpace) private var dismissImmersiveSpace @State private var isSpaceHidden: Bool = true var body: some View { Button(isSpaceHidden ? "View Outer Space" : "Exit the solar system") { Task { if isSpaceHidden { let result = await openImmersiveSpace(id: "solar") switch result { // Handle result } } else { await dismissImmersiveSpace() isSpaceHidden = true } } } } }
-
10:44 - WorldApp using LaunchWindow and ImmersiveSpace
@main struct WorldApp: App { var body: some Scene { LaunchWindow() ImmersiveSpace(id: "solar") { SolarSystem() } } }
-
11:32 - Model3D with phase handling
Model3D(named: "Earth") { phase in switch phase { case .empty: Text( "Waiting" ) case .failure(let error): Text("Error \(error.localizedDescription)") case .success(let model): model.resizable() } }
-
13:04 - Scene Phases
@main struct WorldApp: App { @EnvironmentObject private var model: ViewModel @Environment(\.scenePhase) private var scenePhase ImmersiveSpace(id: "solar") { SolarSystem() .onChange(of: scenePhase) { switch scenePhase { case .inactive, .background: model.solarEarth.scale = 0.5 case .active: model.solarEarth.scale = 1 } } } }
-
14:21 - Coordinate Conversions
var body: some View { GeometryReader3D { proxy in ZStack { Earth( earthConfiguration: model.solarEarth, satelliteConfiguration: [model.solarSatellite], moonConfiguration: model.solarMoon, showSun: true, sunAngle: model.solarSunAngle, animateUpdates: animateUpdates ) .onTapGesture { if let translation = proxy.transform(in: .immersiveSpace)?.translation { model.solarEarth.position = Point3D(translation) } } } } }
-
16:34 - Immersion Styles
@main struct WorldApp: App { @State private var currentStyle: ImmersionStyle = .mixed var body: some Scene { ImmersiveSpace(id: "solar") { SolarSystem() .simultaneousGesture(MagnifyGesture() .onChanged { value in let scale = value.magnification if scale > 5 { currentStyle = .progressive } else if scale > 10 { currentStyle = .full } else { currentStyle = .mixed } } ) } .immersionStyle(selection:$currentStyle, in: .mixed, .progressive, .full) } }
-
20:08 - Surrounding Effects
@main struct WorldApp: App { @State private var currentStyle: ImmersionStyle = .progressive var body: some Scene { ImmersiveSpace(id: "solar") { SolarSystem() .preferredSurroundingsEffect( .systemDark) } .immersionStyle(selection: $currentStyle, in: .progressive) } }
-
20:30 - Upper Limbs Visibility
@main struct WorldApp: App { @State private var currentStyle: ImmersionStyle = .full var body: some Scene { ImmersiveSpace(id: "solar") { SolarSystem() } .immersionStyle(selection: $currentStyle, in: .full) .upperLimbVisibility(.hidden) } }
-
20:52 - Hand Anchoring
struct SpaceGloves2: View { let arSession = ARKitSession() let handTracking = HandTrackingProvider() var body: some View { RealityView { content in let root = Entity() content.add(root) // Load Left glove let leftGlove = try! Entity.loadModel(named: "assets/gloves/LeftGlove_v001.usdz") root.addChild(leftGlove) // Load Right glove let rightGlove = try! Entity.loadModel(named: "assets/gloves/RightGlove_v001.usdz") root.addChild(rightGlove) // Start ARKit session and fetch anchorUpdates Task { do { try await arSession.run([handTracking]) } catch let error as ProviderError { print("Encountered an error while running providers: \(error.localizedDescription)") } catch let error { print("Encountered an unexpected error: \(error.localizedDescription)") } for await anchorUpdate in handTracking.anchorUpdates { let anchor = anchorUpdate.anchor switch anchor.chirality { case .left: if let leftGlove = Entity.leftHand { leftGlove.transform = Transform(matrix: anchor.transform) for (index, jointName) in anchor.skeleton.definition.jointNames.enumerated() { leftGlove.jointTransforms[index].rotation = simd_quatf(anchor.skeleton.joint(named: jointName).localTransform) } } case .right: if let rightGlove = Entity.rightHand { rightGlove.transform = Transform(matrix: anchor.transform) for (index, jointName) in anchor.skeleton.definition.jointNames.enumerated() { rightGlove.jointTransforms[index].rotation = simd_quatf(anchor.skeleton.joint(named: jointName).localTransform) } } } } } } } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。