View in English

  • メニューを開く メニューを閉じる
  • Apple Developer
検索
検索を終了
  • Apple Developer
  • ニュース
  • 見つける
  • デザイン
  • 開発
  • 配信
  • サポート
  • アカウント
次の内容に検索結果を絞り込む

クイックリンク

5 クイックリンク

ビデオ

メニューを開く メニューを閉じる
  • コレクション
  • トピック
  • すべてのビデオ
  • 利用方法

その他のビデオ

ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。

  • 概要
  • トランスクリプト
  • コード
  • ボリュームとイマーシブな空間の詳細

    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

    リソース

    • BOT-anist
    • Forum: UI Frameworks
      • HDビデオ
      • SDビデオ

    関連ビデオ

    WWDC24

    • RealityKitによる空間描画アプリの構築

    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ではこちらがデフォルトです ボリュームは最初は地面と平行ですが 水平よりも高くなると 傾き始めます

    これにより ボリュームの コンテンツを利用しやすくなります 仰向けの状態でも利用できます インタラクティブなコンテンツでは この方が快適に利用できます

    ただし ボリュメトリックアプリでも 必要なインタラクションが少ない場合や 主に環境コンテンツを 提供するアプリの場合には

    Gravity alignedの動作が適しています

    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で世界を探索するのに 適したスタイルです

    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 {
          @State 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 {
          @Environment(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 {
          @Environment(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
          @State 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 {
          @State 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 {
          @State 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 {
          @State 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
              }
          }
      }

Developer Footer

  • ビデオ
  • WWDC24
  • ボリュームとイマーシブな空間の詳細
  • メニューを開く メニューを閉じる
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    Open Menu Close Menu
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    メニューを開く メニューを閉じる
    • アクセシビリティ
    • アクセサリ
    • App Extension
    • App Store
    • オーディオとビデオ(英語)
    • 拡張現実
    • デザイン
    • 配信
    • 教育
    • フォント(英語)
    • ゲーム
    • ヘルスケアとフィットネス
    • アプリ内課金
    • ローカリゼーション
    • マップと位置情報
    • 機械学習
    • オープンソース(英語)
    • セキュリティ
    • SafariとWeb(英語)
    メニューを開く メニューを閉じる
    • 英語ドキュメント(完全版)
    • 日本語ドキュメント(一部トピック)
    • チュートリアル
    • ダウンロード(英語)
    • フォーラム(英語)
    • ビデオ
    Open Menu Close Menu
    • サポートドキュメント
    • お問い合わせ
    • バグ報告
    • システム状況(英語)
    メニューを開く メニューを閉じる
    • Apple Developer
    • App Store Connect
    • Certificates, IDs, & Profiles(英語)
    • フィードバックアシスタント
    メニューを開く メニューを閉じる
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program(英語)
    • News Partner Program(英語)
    • Video Partner Program(英語)
    • セキュリティ報奨金プログラム(英語)
    • Security Research Device Program(英語)
    Open Menu Close Menu
    • Appleに相談
    • Apple Developer Center
    • App Store Awards(英語)
    • Apple Design Awards
    • Apple Developer Academy(英語)
    • WWDC
    Apple Developerアプリを入手する
    Copyright © 2025 Apple Inc. All rights reserved.
    利用規約 プライバシーポリシー 契約とガイドライン