-
ボリュームとイマーシブな空間の詳細
visionOSでボリュームとイマーシブな空間をカスタマイズするための、新しいパワフルな方法をご紹介します。ボリュームのサイズ変更の方法を微調整し、ボリュームを周囲のユーザーの移動に反応させる方法を習得できます。座標変換を活用することで、ボリュームやイマーシブな空間とのインタラクションが可能になります。ユーザーがDigital Crownでイマーシブな体験を調整した際にアプリを反応させる方法と、サラウンドエフェクトを使用して、イマーシブな空間体験でのパススルーの色合いを動的にカスタマイズする方法をご紹介します。
関連する章
- 0:00 - Introduction
- 2:04 - Volumes
- 2:06 - Volumes: Baseplate
- 4:08 - Volumes: Size
- 6:59 - Volumes: Toolbars
- 8:48 - Volumes: Ornaments
- 11:36 - Volumes: Viewpoints
- 15:34 - Volumes: World alignment
- 16:52 - Volumes: Dynamic scale
- 18:26 - Intermezzo
- 18:42 - Immersive spaces
- 19:38 - Immersive spaces: Coordinate conversions
- 22:40 - Immersive spaces: Immersion styles
- 26:08 - Immersive spaces: Anchored UI interactions
- 29:03 - Immersive spaces: Surroundings effects
- 31:21 - Next steps
リソース
関連ビデオ
WWDC24
WWDC23
-
ダウンロード
こんにちはSwiftUIエンジニアの Owenです そしてTroyです 同じくSwiftUIエンジニアです 本ビデオでは visionOSのボリュームと イマーシブ空間における3Dコンテンツの 操作について説明します
visionOSには3つのシーンタイプがあります ウインドウ、ボリューム、イマーシブ空間です この3つを組み合わせることで 独自の魅力的な体験を創出できます 本ビデオでは ボリュームと イマーシブ空間を取り上げます これらのシーンタイプはvisionOS独自のもので 豊かなイマーシブ3Dコンテンツを実現します これらのシーンタイプを使用した ボリュメトリックアプリは特に魅力的で Apple Vision Proならではの機能の 一つと言えます アプリやゲームに 新たな次元の可能性をもたらし アプリの体験を 実世界の体験のように ユーザーに感じさせることができます
visionOSではすでに 楽しい空間体験が多数提供されています これらのアプリは3次元を活かしたもので スマートに情報を表示し 遊び心のある楽しいインタラクションを 実現しています 実世界を模したものも 新たな別世界を現出するものもあります
これらのアプリでは visionOS用の Spatial APIが使用されています そしてvisionOS 2では 多数の新機能が追加され これまで以上に生き生きした ボリュメトリックアプリを実現できます 本ビデオでは BOT-anistという 新しいボリュメトリックアプリを構築する 手順をお見せします まず ボリュームの追加方法を解説し アプリを構築する中で 新しいAPIのメリットをご紹介します
Troyはそのアプリをさらに拡張し イマーシブ空間により部屋全体に表示します
では早速ボリュームを使ってみましょう ボリュメトリックアプリを 新規作成する場合や visionOS 2 SDKに対して 既存のアプリをリンクする場合 最初に注意すべきことの一つが 新しいベースプレートです
目を向けると自動的に現れ ボリュームの底面がハイライトされます
これが 新規作成したボリュームの 実際のベースプレートです ボリュームの底面が自然にわかるので 対象とする空間が どの程度あるかがすぐにわかります
ベースプレートが適しているのは コンテンツがボリューム内にあり 境界一杯まで占めていない場合です コンテンツがボリューム内の空間の 一部しか占めていない場合でも そのボリュームの端が どこまでかがわかります ただし アプリのコンテンツが ボリュームの境界を はみ出している場合や すでに独自のサーフェスを 描画している場合は ベースプレートを無効にすることを おすすめします それにより アプリとの 衝突を避けられます コンテンツ自体でユーザーに 端がわかるようにしましょう
visionOS 2ではデフォルトで ベースプレートが有効になっており volumeBaseplateVisibility モディファイアで制御できます visionOS 2によるこの自動的な挙動は ベースプレートに視線が向くと実行されます モディファイアを記述してvisibleを 指定した場合と同様の動作になります ベースプレートを明示的に無効にするには hiddenを設定します
ベースプレートを使用することで ほかのコンテンツがなくても ボリュームの境界がすぐにわかります
BOT-anistアプリに 円形の水準器を追加した場合に ボリュームの端や隅を見つけるのに ベースプレートが役立ちます ここにウインドウコントロールがあります visionOS 2では これが特に重要です ボリュームの新しい サイズ変更ハンドルは隅にあるためです ウインドウと同様です
ベースプレートによって 隅の サイズ変更ハンドルがわかりやすくなります しかし シーンのサイズを変更しようとしても すぐに元のサイズに戻ります 何が起きているのでしょうか ボリュームは デフォルトでは コンテンツのサイズから 最小サイズと最大サイズを継承します SwiftUIでこの挙動を操作するには windowResizabilityモディファイアの contentSizeの挙動を使います ボリュームの自動的な挙動は モディファイアの記述と同様に機能します この場合 ボリュームの最小サイズと 最大サイズはいずれも ビューのサイズによって決まります
ExplorationViewでframeを記述し width、height、depthを指定しました これにより ボリュームのサイズは ビューのフレームに合わせて固定されます
ここに変更を加えて フレームの最小値を指定すると ビューのサイズを拡大できるようになります ボリュームは コンテンツのサイズを継承するため ボリュームのサイズも変更可能になります
また 最大サイズを指定すると ボリュームのサイズの上限を指定できます
サイズ変更ハンドルによる スムーズな ボリュームサイズ変更が可能になりました
この挙動を使うと ボリュームの サイズをコードで変更することもできます アプリでコンテンツが更新され ビューのフレームに変更があった場合に そのサイズがボリュームにも通知されます これにより コンテンツの変化に 簡単に対応でき シーンの境界で切り取られてしまう 心配もありません
サイズ変更ハンドルは コンテンツに近い 隅の部分に表示されるため コントロールが遠く離れた場所に 行ってしまうこともありません
ボリュームのサイズを プログラムで変更するには スケールに対応する 新しい状態変数を追加します
このスケール変数の値を変更すると ビューのフレームの値が更新され その新しいサイズに合うように ボリュームのサイズが自動変更されます
RealityKitエンティティのスケールについても 円形のexplorationLevelを量で指定します ここでスケールを変更する コントロールが必要です
サイズの切り替えボタンを追加して ここではオーバーレイに配置します
これで ボタンを押すと 小さいサイズから大きいサイズへと レベルが切り替わり それに応じて ボリュームの 境界が調整されるようになりました ただし ボタン自体は少し場違いに見えます ツールバーに入れるのがよいでしょう
ボリュームではツールバーがサポートされており 下部のオーナメントに表示されます ツールバーは アプリでよく使う コントロールをまとめて 表示するのに適しています
ツールバーはボリュームの動きに応じて 自動的に大きさが変わるので ボリュームの配置に関わらず コンテンツにアクセスできます このツールバーは アプリでよく使う コントロールのグループ化に便利なので このアプリに追加しましょう
ツールバーにコントロールを配置するには toolbarモディファイアを アプリのビューに記述します
toolbar内にToolbarItemと ToolbarItemGroupを作成します
各アイテムの配置をplacementで .bottomOrnamentと指定します これはvisionOS 1との 互換性確保のために必要です ただし visionOS 2では配置を自動にしても 下部のオーナメントに配置されます visionOS 2のみを対象とするアプリでは この引数を省略できます
ToolbarItem内にボタンを追加し ゲームで 様々なアクションを実行可能にします
visionOS 2の新機能により ツールバーは ウインドウコントロールと共に プレイヤーが立つ場所のボリュームの横に 自動的に移動します これによりプレイヤーは 新しい角度からコンテンツを見ることができ その際 ツールもすべて 必要な場所に表示されます ツールバーだけでなく 付加的な オーナメントもボリュームに追加できます オーナメントは 付加的なコントロールの表示や 現在のコンテンツの 詳細情報の提供に適しています
オーナメントを使用して アプリのウインドウの上部や周辺に 補足情報を表示させることで メインウインドウの見た目がすっきりします ウインドウでは どこにでもオーナメントを追加でき ツールバーとしてだけでなく ウインドウ周辺の 任意の場所に配置できます ボリュームでも同じことが可能です ボリューム内部にもオーナメントを 配置でき 深さも制御可能です オーナメントのサイズも動的に変更され ボリュームが遠く離れた場合でも 適切なサイズが維持されます
ボリュームの周囲に 多数のオーナメントを配置すると そのアプリで本当に目立たせたい コンテンツが目立たなくなります 1つのオーナメントにコントロールや情報を まとめて格納すると見やすくなります また システムで用意されている コントロールの中には ツールバーやタブビューなど オーナメントになっているものがあります カスタムオーナメントが それらと 競合しないように注意してください
アプリ内で 栽培の目標達成に向けた進捗を表示する このようなビューを追加しました 現在このビューは ボリュームのメインビュー内にあります
そのため ボリュームの周りを歩いても 更新されません ボリュームを遠ざけると ビューが小さくなり 読みにくくなります このビューを取り出して オーナメントに配置すれば 自動的な挙動を使用できます ここではビューが ボリュームのボディ内にあります
ビューをオーナメント内に配置するために ornamentモディファイア内に移動します
UnitPoint3Dを使用して sceneアンカーを指定します これにより オーナメントを配置する際に ボリュームの幅、高さ、奥行きが 考慮されます ここでtopBackの配置を指定すると メインレベルの背後の 上部中央にオーナメントが表示されます
このように メインレベルの背後に浮かんでいます ツールバーと同じように オーナメントも 視点に追従してボリュームの周囲を移動し どの方向からでも常にアクセスできます シーン内での位置は プレイヤーがどの方向から ボリュームを見ているかに応じて決まります
ボリュームの各側面が ビューポイントになります プレイヤーがボリュームの周りを動くと ウインドウコントロールとオーナメントが プレイヤーに最も近い ビューポイントに自動的に移動します
現在のビューポイントに応じて オーナメントの位置が 自動的に更新されます ここで 小さなロボットを追加すると ロボットは正面を向いており プレイヤーがどこにいるかわかっていません プレイヤーの動きに応じて ロボットが プレイヤーの方向を向いてくれたら アプリが生き生きします
まず ビューポイントの更新情報を取得する ために 新しいモディファイアを追加します onVolumeViewpointChangeです これは アクティブなビューポイントが 更新されるたびに呼び出されます これを使い アクティブなビューポイントを 追跡する appStateの変数を設定します ロボットは 更新時にこの値を使用することで 現在のビューポイントの方向を向きます
ビューポイントの squareAzimuthの値を使用します この型では ボリュームに対する位置が 4つの値のいずれかに正規化されます 4つの値は ボリュームの4つの側面に対応します
SquareAzimuthの4つの側面は front、left、right、backの セマンティックな値を取ります これらのセマンティックな値には 特殊なRotation3Dも含まれます これは ビューやエンティティに対し 回転として直接適用できます
ロボットの動きを処理するコードで ロボットがアイドルモードのときに プレイヤーの方を向くように コードを少し追加しました さらに 手を振るアニメーションを実行します
手を振ってくれるようになりました これでアプリに動きが出て 楽しい雰囲気になってきました
ただし すべてのビューポイントへの対応は すべてのアプリで必須とは限りません この例では 正面と左右だけに ビューポイントを限定しています
サポートされる ビューポイントを指定するには 別の新しいViewモディファイアを使用します supportedVolumeViewpointsです デフォルトでは すべてのビューポイントがサポートされます
ここでは ボリュームの 正面と左右のみをサポートし 背面は除外します
そのために front、left、rightの値を オプションセットとして渡します
オーナメントとウインドウコントロールは プレイヤーがボリュームの背面に行くと 移動してきません ロボットも停止しています 今までは プレイヤーの動きに応じて ロボットが反応していたので プレイヤーが適切でない方向にいることを 知らせてほしいところです
サポートされるビューポイントに 新しい値が含まれていない場合でも VolumeViewpointChangeの ブロックが呼び出されるように ビューポイントの新しい引数である updateStrategyを追加します .allを指定すると すべての ビューポイントの更新情報を取得できます サポートされていないビューポイントも 含まれます サポートされる値に 新しい値が 含まれているかどうかをチェックし 含まれていない場合は ロボットの 新しいアニメーションをトリガーして サポートされるビューポイントの いずれかに戻るように プレイヤーに伝えます
これで ボリュームの背面に向かうと サポートされている側面のところで オーナメントの動きが止まり
ロボットは怒ったような動きをして 元の場所に戻るように伝えます
新しいオプションもいくつかあります この空間内で ボリュームをどのように 表示するかを制御するオプションです 1つ目のオプションは アプリの世界に応じた配置です .gravityAlignedでは ボリュームは鉛直方向に配置され 底面が床面と平行になります .adaptiveでは ボリュームを持ち上げると傾きます 通常は Adaptiveを使用して傾かせる方が 見やすくなります visionOS 2ではこちらがデフォルトです ボリュームは最初は地面と平行ですが 水平よりも高くなると 傾き始めます
これにより ボリュームの コンテンツを利用しやすくなります 仰向けの状態でも利用できます インタラクティブなコンテンツでは この方が快適に利用できます
ただし ボリュメトリックアプリでも 必要なインタラクションが少ない場合や 主に環境コンテンツを 提供するアプリの場合には
volumeWorldAlignmentモディファイアでは .adaptiveの配置をオーバーライドして ボリュームの向きを 床と水平に維持できます
このたび ボリュームで動的なスケールを 使用できるようになりました
visionOSのウインドウのスケールが 空間内での移動に応じて変化します ウインドウを遠ざけると 視界での大きさを維持するために 拡大されます ウインドウには テキストやコントロールが 含まれていることが多く 距離が離れると使いにくくなるので この機能が役立ちます
これは ボリュームのツールバーや オーナメントと同様の動作です
これに対して ボリューム自体は デフォルトではスケールが固定されています その方が 空間内での実在感が強くなるためです
遠く離れても サイズが固定されたままなので 距離が離れると コンテンツは小さく見えます
ボリュメトリックアプリでは多くの場合 これが適しています 室内の仮想コンテンツが あたかもそこに実在するかのように 見えるからです
ただし ボリュメトリックアプリで使用する コンテンツの密度が高く 種類の異なる 多数の インタラクティブな領域がある場合は 動的なスケールが適します
ボリュームに動的なスケールを適用するには defaultWorldScalingBehaviorという 新しいシーンモディファイアを使用します BOT-anistはインタラクティブなゲームなので 動的なスケールの方が適しています そこで .dynamicオプションを指定して この挙動を有効にします
順調なスタートですボリュームを活用した 楽しいアプリができそうです ボリュームについて たくさん説明してくれましたね 次はなんでしょう このロボットを イマーシブ空間により 現実の世界に連れ出したいと思います 魔法のような話ですね
多くのデベロッパの方々が イマーシブ空間により Apple Vision Proで 魅力的な体験を創出しています Owenはここまで BOT-anistアプリを使って 共有スペースにおける ボリュームについて説明してくれました ここからは ウインドウからさらに話を進め 素晴らしいイマーシブ体験を構築して 部屋全体を温室にしてみましょう
最初のタスクは イマーシブ空間の作成です Xcodeの新規プロジェクト作成ダイアログに 構成のためのオプションがありますが ここでは自分で追加します
イマーシブ空間を開いたら ボリュームからイマーシブ空間に RealityKitのコンテンツを すべて移植します このプロセスは非常にシームレスで 驚くほど快適です これで BOT-anistで現実の世界を 探索できるようになります
ここで利用できる 名前付きの座標空間があります visionOS 1.1で導入されたもので immersiveSpaceと呼ばれます
座標空間は 特定の座標系に対する相対位置を 正確に指定するための手段です
新しいイマーシブな座標空間は SwiftUIの 既存のローカルおよび グローバルの座標空間に適合します
.localは 現在のビューの座標空間であり 起点はビューの左上です
.globalはウインドウの座標空間であり 起点はウインドウの左上です
.immersiveSpaceは .globalを基盤としており イマーシブ空間が開かれている間 起点は プレイヤーが立つ地面の位置になります
ここでは RealityViewの座標空間を使用して イマーシブ空間への移行を実現します RealityViewには 多数の変換関数が用意されています それらを使ってRealityKitとSwiftUIの間の 座標空間の変換を行います まず ロボットのtransformの変換を処理し ボリュームのRealityKitのシーン空間から SwiftUIのイマーシブ空間へ変換します このRealityViewのupdateクロージャで 最初の変換関数を呼び出します
ここで ロボットのtransformを ローカルの ボリュームのRealityKitシーン空間から SwiftUIのイマーシブ空間に変換します 次に 変換されたtransformを 後で使用できるようappModelに保存します
さらに ロボットの親をボリュームから イマーシブ空間の ルートエンティティに変更します ロボットの親が変更され ボリュームからロボットを取り出す transformが計算されたら ボリュームビューからの変換を 完了としてマークします ここからは イマーシブ空間ビューで変換を続けます
次に SwiftUIのイマーシブ空間から RealityKitシーン空間に移行するための transformの計算を行います そして この2つのtransformを合成します この合成では 今の計算結果と 先ほどappModelに 保存しておいた結果を乗算します その結果 ボリュームのローカル座標空間から イマーシブ空間の座標空間への 変換が行われます ロボットのtransformを更新すると ロボットはイマーシブ空間で ボリュームで表示されていた時と 同じ位置に 配置されるようになります
ロボットのtransformが変換されたら ジャンプを開始します
移行の準備は整っています 座標変換のAPIを活用することで ロボットはボリュームから飛び出して イマーシブ体験の世界へ 移行できるようになります 着地が決まりました
次に BOT-anistアプリの イマーシブ体験のスタイルを選択します
デフォルトのイマーシブ体験のスタイルは Mixedです プレイヤーの周囲を背景として アプリが表示されます Progressiveスタイルは パススルーとフルイマーシブの中間です 曲面型のポータルにアプリが表示され アプリの周囲はパススルーが適用されます Fullスタイルでは 周囲の環境が完全に イマーシブアプリに置き換えられます ここではProgressiveスタイルを選択します
BOT-anistアプリに Progressiveスタイルを適用すると デフォルトでは ポータルの初期サイズは プレイヤーの視界の半分を占める大きさです また サポートされるイマーシブ度の範囲も システムにより定義されます この定義では イマーシブ度の範囲の 最小値と最大値を指定します
BOT-anistアプリのイマーシブ感を さらに高めたいと思います visionOS 2の新機能として イマーシブ度のカスタム範囲を指定できます イマーシブ度を増減させることができ ロボットが ボリュームから イマーシブ空間に飛び出したという感覚を 魅力的に演出できます この新しいAPIを詳しく見てみましょう
まず 新しいイニシャライザを使って イマーシブ度のカスタム範囲と 初期のイマーシブ度の値を考慮して Progressiveの イマーシブ体験スタイルを作成します イマーシブ空間に Progressiveスタイルを適用すると システムは 指定した値を使用して シーンに適用されるProgressive効果の 最小値、最大値、初期値を定義します
BOT-anistアプリのイマーシブ体験を よりイマーシブ感の高いものにするために イマーシブ度の初期値を80%にします BOT-anistアプリのイマーシブ度の カスタム範囲については 最小値を20% 最大値を100%に設定します 100%はFullスタイルに相当します
イマーシブ度のカスタム範囲の効果を 実際に確認してみましょう イマーシブ度を高めることは ロボットがボリュームから 飛び出した感じを演出するうえで有効です 範囲を指定したことで 素晴らしい表示になりました イマーシブ体験の調整に Digital Crownを使うこともできます
次に サポートされるイマーシブ度の調整に Digital Crownを使う場合に ロボットが反応するようにします
onImmersionChangeモディファイアを使って イマーシブ度の変更に反応させます これにより 新しいイマーシブ度を持つ context値が得られます
イマーシブ度が変更されたら 得られたcontextから値を読み込みます BOT-anistでは この値を保存して 変更前後の値を比較できるようにします
onChangeを使用して 保存されている イマーシブ度に対する変更を処理します クロージャから新旧の値を取得して渡します
イマーシブ度の変化に ロボットが反応するようにするには イマーシブ度が高くなると ロボットが外に出てくる処理をトリガーする 関数を呼び出します
また イマーシブ度が低くなると ロボットが中に入る処理をトリガーする 関数も呼び出します 確かめてみましょう イマーシブ度が変わると ロボットが反応するようになりました イマーシブ度を高くすると こちらに向かってきます 低くすると向こうへ遠ざかります
もう一度イマーシブ度を高くしてみましょう イマーシブ度が高くなるとロボットは反応し 外に出てきて広い空間を探索しますが 現時点では 環境に隙間が多いようです
対策として 床をタップして 植物を置けるようにします 環境の床に対して植物を置き ロボットが探索できるようにします 床の特定の場所に植物を配置するには 床のアンカーに対して 植物を置く必要があり そのためには そのアンカーの3D位置情報が必要です
アプリでアンカーの3D位置情報に アクセスできるようにするには RealityKitの 新しい SpatialTrackingSession APIを使用して トラッキングの必要なアンカー機能を プレイヤーが許可できるようにします
このAPIを使用するには まず 空間トラッキングセッションを作成します
タスクを作成し イマーシブ空間を開けたら 空間トラッキングセッションを実行する 関数を呼び出します
セッションを実行するには まず平面アンカーの トラッキングの設定を行います
その設定を使用してセッションを実行し 平面アンカーの変換の許可について プロンプトを表示します
平面のトラッキングを登録できたので トラッキングするアンカーを 追加する必要があります
ターゲットの平面について 水平方向の配置を指定し .floorのclassificationで トラッキングする フロアアンカーエンティティを作成します
次に フロアアンカーを イマーシブ空間の RealityViewコンテンツに追加します 最後に 新しいアンカーの3D位置情報を 使用して 室内に植物を配置します
イマーシブ空間のターゲット エンティティに対するタップの検出に 使用できる SpatialTapGestureを追加します
ジェスチャーが終了したら そのジェスチャーの値を タップを処理する関数に渡します
タップを処理するには まずジェスチャーの値に対して convert関数を使用して floorAnchorに対する ジェスチャーの位置を取得します この処理では アプリで floorAnchorのtransformを 使用できる必要があります
最後に 植物を配置するために floorAnchorの子として植物を追加し 変換済みの位置情報を使用して 植物エンティティの位置を設定します
リストから植物を選びます 床の上のホバーエフェクトは 植物を配置できる場所を示しています タップすると植物が配置されます 室内に植物を置くと BOT-anistアプリに精彩が加わります
SpatialTrackingSession APIの 詳細を知りたい方に 視聴をおすすめするセッションは 「Build a spatial drawing app with RealityKit」です
ロボットが近づくと 成長を喜ぶ直物の アニメーションが再生されます
嬉しい気分をさらに高めましょう 現時点では この環境の植物はそれぞれ 関連する色が付いた プランターに配置されています プランターの色合いに合わせて パススルーに色を付けると さらに楽しそうな雰囲気が加わるでしょう
preferredSurroundingsEffect APIを 使うと 周囲のパススルーに 色を付けられます BOT-anistアプリのイマーシブ体験を アップデートして プランターの色使いに合わせて パススルーに色を付け 植物を育てる喜びを 表現できるようにします
カスタムのPlantComponentに tintColorプロパティを追加します switchで植物の種類ごとに 条件分岐を作り 色合いを選択します 例えば コーヒーベリーはライトブルーです
色を付けるエフェクトを実行するには ロボットがプランターに近付いたら 検出できるようにする必要があります これを実現するには RealityKitによる衝突検出を使用します
ロボットの経時的な動作を処理するには collisionクロージャを使用して 衝突したエンティティを処理します そしてcollisionの値を ヘルパー関数に渡します
RealityKitを使用した衝突検出の 詳細については 「Develop your first immersive app」を ご覧ください
このヘルパー関数では まず ロボットが 植物と衝突したかどうかをチェックして していなければreturnを返し
最後に イマーシブ空間にいる場合は appModelのactiveTintColorを 後で使用できるように保存します
イマーシブビューに戻り 保存されているアクティブなtintColorをもとに SurroundingsEffectの colorMultiplyを作成します そのSurroundingsEffectを使用して パススルーに色を付けます
では 確認してみましょう ロボットが植物に近づくと 更新された 配色で パススルーに色が付くでしょうか
本セッションでは アプリで ボリュームやイマーシブ空間を 構築するための様々な新しい方法を ご紹介しました 新しいresizabilityの挙動で アプリの ボリュームサイズの変更を調整できます ビューポイントを使うと ボリュームの周囲を歩くプレイヤーに アプリを反応させることができます ボリュームから イマーシブ空間へと世界を移すには 座標変換が役立ちます イマーシブ空間でのイマーシブ度の変更に 反応させることができます 空間アプリは 従来想像もできなかった体験を実現できる まったく新しい世界です SwiftUIとRealityKitという パワフルで 表現力に優れたツールを利用すれば 可能性は無限です ほんの少し 斬新な感覚と想像力を働かせることで みなさんも驚異的な世界を創出できます ご視聴ありがとうございました
-
-
3:09 - Baseplate
// Baseplate WindowGroup(id: "RobotExploration") { ExplorationView() .volumeBaseplateVisibility(.visible) // Default! } .windowStyle(.volumetric)
-
4:29 - Enabling resizability
// Enabling resizability WindowGroup(id: "RobotExploration") { let initialSize = Size3D(width: 900, height: 500, depth: 900) ExplorationView() .frame(minWidth: initialSize.width, maxWidth: initialSize.width * 2, minHeight: initialSize.height, maxHeight: initialSize.height * 2) .frame(minDepth: initialSize.depth, maxDepth: initialSize.depth * 2) } .windowStyle(.volumetric) .windowResizability(.contentSize) // Default!
-
6:10 - Programmatic resize
// Programmatic resize struct ExplorationView: View { private var levelScale: Double = 1.0 var body: some View { RealityView { content in // Level code here } update: { content in appState.explorationLevel?.setScale( [levelScale, levelScale, levelScale], relativeTo: nil) } .frame(width: levelSize.value.width * levelScale, height: levelSize.value.height * levelScale) .frame(depth: levelSize.value.depth * levelScale) .overlay { Button("Change Size") { levelScale = levelScale == 1.0 ? 2.0 : 1.0 } } } }
-
7:39 - Toolbar ornament
// Toolbar ornament ExplorationView() .toolbar { ToolbarItem { Button("Next Size") { levelScale = levelScale == 1.0 ? 2.0 : 1.0 } } ToolbarItemGroup { Button("Replay") { resetExploration() } Button("Exit Game") { exitExploration() openWindow(id: "RobotCreation") } } }
-
10:41 - Ornaments
// Ornaments WindowGroup(id: "RobotExploration") { ExplorationView() .ornament(attachmentAnchor: .scene(.topBack)) { ProgressView() } } .windowStyle(.volumetric)
-
12:08 - Volume viewpoint
// Volume viewpoint struct ExplorationView: View { var body: some View { RealityView { content in // Some RealityKit code } .onVolumeViewpointChange { oldValue, newValue in appState.robot?.currentViewpoint = newValue.squareAzimuth } } }
-
13:06 - Using volume viewpoint
// Volume viewpoint class RobotCharacter { func handleMovement(deltaTime: Float) { if self.robotState == .idle { characterModel.performRotation(toFace: self.currentViewpoint, duration: 0.5) self.animationState.transition(to: .wave) } else { // Handle normal movement } } }
-
13:43 - Supported viewpoints
// Supported viewpoints struct ExplorationView: View { let supportedViewpoints: Viewpoint3D.SquareAzimuth.Set = [.front, .left, .right] var body: some View { RealityView { content in // Some RealityKit code } .supportedVolumeViewpoints(supportedViewpoints) .onVolumeViewpointChange { _, newValue in appState.robot?.currentViewpoint = newValue.squareAzimuth } } }
-
14:30 - Viewpoint update strategy
// Viewpoint update strategy struct ExplorationView: View { let supportedViewpoints: Viewpoint3D.SquareAzimuth.Set = [.front, .left, .right] var body: some View { RealityView { content in // Some RealityKit code } .supportedVolumeViewpoints(supportedViewpoints) .onVolumeViewpointChange(updateStrategy: .all) { _, newValue in appState.robot?.currentViewpoint = newValue.squareAzimuth if !supportedViewpoints.contains(newValue) { appState.robot?.animationState.transition(to: .annoyed) } } } }
-
16:42 - World alignment
// World alignment WindowGroup { ExplorationView() .volumeWorldAlignment(.gravityAligned) } .windowStyle(.volumetric)
-
18:05 - Dynamic scale
// Dynamic scale WindowGroup { ContentView() } .windowStyle(.volumetric) .defaultWorldScalingBehavior(.dynamic)
-
19:16 - Starting with an empty immersive space
struct BotanistApp: App { var body: some Scene { // Volume WindowGroup(id: "Exploration") { VolumeExplorationView() } .windowStyle(.volumetric) // Immersive Space ImmersiveSpace(id: "Immersive") { EmptyView() } } }
-
20:52 - Callout to convert function from volume view
// Coordinate conversions // Convert from RealityKit entity in volume to SwiftUI space struct VolumeExplorationView: View { (ImmersiveSpaceAppModel.self) var appModel var body: some View { RealityView { content in content.add(appModel.volumeRoot) // ... } update: { content in guard appModel.convertingRobotFromVolume else { return } // Convert the robot transform from RealityKit scene space for // the volume to SwiftUI immersive space convertRobotFromRealityKitToImmersiveSpace(content: content) } } }
-
21:08 - Convert robot's transform to SwiftUI immersive space
// Coordinate conversions // Convert from RealityKit entity in volume to SwiftUI space func convertRobotFromRealityKitToImmersiveSpace(content: RealityViewContent) { // Convert the robot transform from RealityKit scene space for // the volume to SwiftUI immersive space appModel.immersiveSpaceFromRobot = content.transform(from: appModel.robot, to: .immersiveSpace) // Reparent robot from volume to immersive space appModel.robot.setParent(appModel.immersiveSpaceRoot) // Handoff to immersive space view to continue conversions. appModel.convertingRobotFromVolume = false appModel.convertingRobotToImmersiveSpace = true }
-
21:42 - Callout to convert function from immersive space view
// Coordinate conversions // Convert from SwiftUI immersive space back to RealityKit local space struct ImmersiveExplorationView: View { (ImmersiveSpaceAppModel.self) var appModel var body: some View { RealityView { content in content.add(appModel.immersiveSpaceRoot) } update: { content in guard appModel.convertingRobotToImmersiveSpace else { return } // Convert the robot transform from SwiftUI space for the immersive // space to RealityKit scene space convertRobotFromSwiftUIToRealityKitSpace(content: content) } } }
-
21:48 - Compute transform to place robot in matching position in immersive space
// Coordinate conversions // Calculate transform from SwiftUI to RealityKit scene space func convertRobotFromSwiftUIToRealityKitSpace(content: RealityViewContent) { // Calculate transform from SwiftUI immersive space to RealityKit // scene space let realityKitSceneFromImmersiveSpace = content.transform(from: .immersiveSpace, to: .scene) // Multiply with the robot's transform in SwiftUI immersive space to build a // transformation which converts from the robot's local // coordinate space in the volume and ends with the robot's local // coordinate space in an immersive space. let realityKitSceneFromRobot = realityKitSceneFromImmersiveSpace * appModel.immersiveSpaceFromRobot // Place the robot in the immersive space to match where it // appeared in the volume appModel.robot.transform = Transform(realityKitSceneFromRobot) // Start the jump! appModel.startJump() }
-
23:54 - Customizing immersion
// Customizing immersion struct BotanistApp: App { // Custom immersion amounts private var immersionStyle: ImmersionStyle = .progressive(0.2...1.0, initialAmount: 0.8) var body: some Scene { // Immersive Space ImmersiveSpace(id: "ImmersiveSpace") { ImmersiveSpaceExplorationView() } .immersionStyle(selection: $immersionStyle, in: .mixed, .progressive, .full) } }
-
25:17 - Callout to function to handle immersion amount changed
// Reacting to immersion struct ImmersiveView: View { var immersionAmount: Double? var body: some View { ImmersiveSpaceExplorationView() .onImmersionChange { context in immersionAmount = context.amount } .onChange(of: immersionAmount) { oldValue, newValue in handleImmersionAmountChanged(newValue: newValue, oldValue: oldValue) } } }
-
25:39 - Handle function to make robot react to changed immersion amount
// Reacting to immersion func handleImmersionAmountChanged(newValue: Double?, oldValue: Double?) { guard let newValue, let oldValue else { return } if newValue > oldValue { // Move the robot outward to react to increasing immersion moveRobotOutward() } else if newValue < oldValue { // Move the robot inward to react to decreasing immersion moveRobotInward() } }
-
26:57 - Create spatial tracking session
// Create and run spatial tracking session struct ImmersiveExplorationView { var spatialTrackingSession: SpatialTrackingSession = SpatialTrackingSession() var body: some View { RealityView { content in // ... } .task { await runSpatialTrackingSession() } } }
-
27:11 - Run spatial tracking session
// Create and run the spatial tracking session func runSpatialTrackingSession() async { // Configure the session for plane anchor tracking let configuration = SpatialTrackingSession.Configuration(tracking: [.plane]) // Run the session to request plane anchor transforms let _ = await spatialTrackingSession.run(configuration) }
-
27:32 - Create a floor anchor to track
// Create a floor anchor to track struct ImmersiveExplorationView { var spatialTrackingSession: SpatialTrackingSession = SpatialTrackingSession() let floorAnchor = AnchorEntity( .plane(.horizontal, classification: .floor, minimumBounds: .init(x: 0.01, y: 0.01)) ) var body: some View { RealityView { content in content.add(floorAnchor) } .task { await runSpatialTrackingSession() } } }
-
27:54 - Detect taps on entities in immersive space
// Detect taps on entities in immersive space RealityView { content in // ... } .gesture( SpatialTapGesture( coordinateSpace: .immersiveSpace ) .targetedToAnyEntity() .onEnded { value in handleTapOnFloor(value: value) } )
-
28:09 - Handle tap event to place plant
// Handle tap event func handleTapOnFloor(value: EntityTargetValue<SpatialTapGesture.Value>) { let location = value.convert(value.location3D, from: .immersiveSpace, to: floorAnchor) plantEntity.position = location floorAnchor.addChild(plantEntity) }
-
29:47 - Add tint color to custom plant component
// Add tint color to custom plant component struct PlantComponent: Component { var tintColor: Color { switch plantType { case .coffeeBerry: // Light blue return Color(red: 0.3, green: 0.3, blue: 1.0) case .poppy: // Magenta return Color(red: 1.0, green: 0.0, blue: 1.0) case .yucca: // Light green return Color(red: 0.2, green: 1.0, blue: 0.2) } } }
-
30:09 - Handle collisions with robot
// Handle collisions with robot // // Handle movement of the robot between frames func handleMovement(deltaTime: Float) { // Move character in the collision world appModel.robot.moveCharacter(by: SIMD3<Float>(...), deltaTime: deltaTime, relativeTo: nil) { collision in handleCollision(collision) } }
-
30:29 - Set active tint color when colliding with plant
// Set active tint color when colliding with plant // // Handle collision between robot and hit entity func handleCollision(_ collision: CharacterControllerComponent.Collision) { guard let plantComponent = collision.hitEntity.components[PlantComponent.self] else { return } // Play the plant growth celebration animation playPlantGrowthAnimation(plantComponent: plantComponent) if inImmersiveSpace { appModel.tintColor = plantComponent.tintColor } }
-
30:48 - Apply effect to tint passthrough
// Apply effect to tint passthrough struct ImmersiveExplorationView: View { var body: some View { RealityView { content in // ... } .preferredSurroundingsEffect(surroundingsEffect) } // The resolved surroundings effect based on tint color var surroundingsEffect: SurroundingsEffect? { if let color = appModel.tintColor { return SurroundingsEffect.colorMultiply(color) } else { return nil } } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。