ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
イマーシブアプリのためのMetal
イマーシブアプリのためのMetalMetalを使って、visionOSでフルイマーシブ体験をレンダリングする方法について確認しましょう。プラットフォーム上でのレンダリングセッションの設定方法や、基本的なレンダーループの作成方法、そして空間インプットを組み込むことでインタラクティブな体験を生み出す方法について紹介します。
関連する章
- 0:06 - Intro
- 1:50 - App Architecture
- 5:58 - Render configuration
- 11:29 - Render Loop
- 13:00 - Render a frame
- 17:28 - User input
- 20:22 - Wrap-Up
リソース
関連ビデオ
WWDC23
WWDC21
-
ダウンロード
♪心地よいヒップホップ音楽♪ ♪ こんにちは Pau Sastre Miguelです Appleのソフトウェアエンジニアです 今日は xrOSでMetalを使って どのようにイマーシブな体験を 作成するのかについて話します 今年は xrOSの発売によって Appleのエコシステムで お馴染みのテクノロジーを使って イマーシブな体験が作れるようになりました RealityKitを使って 自分のVRコンテンツを 現実世界と一体化する体験が作成できます その一方で みなさんのアプリでユーザーに フルイマーシブ体験を提供する場合は xrOSで現実世界のコンテンツを VRコンテンツに 完全に置き換えることも可能です フルイマーシブ体験を作る時は レンダリングメソッドに いくつかの選択肢があります 引き続き RealityKitも使えますし あるいは MetalとARKit APIを 選ぶことも可能です RecRoomがそのようなアプリの良い例で フルイマーシブ体験を生み出すのに CompositorServicesで レンダリングセッションを作成し Metal APIでフレームをレンダリングし ARKitで ワールドトラッキングと ハンドトラッキングをしています これらのテクノロジーすべてに サポートが行われているのは Unityのエディタのおかげです 自分自身のエンジンを書きたい場合 CompositorServices APIなら xrOSで Metalレンダリングへの アクセスが得られます ARKitと組み合わせれば ワールドトラッキングと ハンドトラッキングを追加して フルイマーシブ体験が作れるようになります xrOSでの作業にはCompositorServicesが エンジン設定のキーとなります また レンダーループの設定の仕方と 1フレームのレンダリングの 仕方を説明します 最後に ARKitを使ってどのように 体験をインタラクティブに できるかを説明します では xrOSアプリでの アーキテクチャから始めましょう 既にMetal APIや Metalレンダリングテクニックを 使ったことがある方には 非常に有益なセッションです Metalを使ったことがなければ developer.apple.com/Metalで コードサンプルや ドキュメントsを見てください MetalでxrOSにイマーシブな体験を作る時は アプリとレンダリングセッションを SwiftUIで作ることから始めます レンダリングセッションを作ったあとは CやC++などの馴染みのある言語に切り替えて エンジン内の部分を定義できます まずは SwiftUIアプリのプロトコルに 準拠するタイプを作成します このプロトコルに準拠するために アプリのシーンリストを定義します xrOSでは 主に3つの シーンタイプがあります ウインドウタイプはmacOSのような 2D プラットフォームと似た 体験を提供します ボリュームタイプはその範囲内の コンテンツをレンダリングし ほかのアプリと共有スペースで共存します ImmersiveSpaceはコンテンツを どこでもレンダリングできます Metalでフルイマーシブ体験を作成する時は 必ずImmersiveSpaceタイプを選びます ImmersiveSpaceはxrOSで利用可能な SwiftUIの新しいシーンタイプです フルイマーシブ体験のコンテナとしての 役割を果たします ImmersiveSpaceの使い方については 「Go beyond the window with SwiftUI」をご確認ください ImmersiveSpaceシーンを作成する時は ImmersiveSpaceContentプロトコルに 準拠するタイプを使って アプリがコンテンツを提供します ImmersiveSpaceシーンの コンテンツを作成する時は 多くの場合 アプリは RealityKitを使います 内部ではCoreAnimationや MaterialXが使われます ですが代わりに パワフルなMetalを使って アプリのコンテンツを レンダリングしたい場合には ほかのオプションがあります CompositorServices APIが MetalとARKitを使って アプリにイマーシブなレンダリング機能を 与えてくれます xrOSに導入された 新しいCompositorServices APIで Metalのレンダリングインターフェイスは ImmersiveSpaceのコンテンツを レンダリングできるようになりました CompositorServicesで アプリはコンポジターサーバに 直接レンダリングができます IPCオーバーヘッドが低いので レイテンシを最小限に抑えられ CとSwift APIを 両方サポートできるように 新規で構築されています CompositorServicesを使う時は ImmersiveSpaceContentは CompositorLayerと呼ばれます CompositorLayerの作成には 2つのパラメータを提供する必要があります 1つ目は CompositorLayerConfiguration プロトコルです このプロトコルはレンダリングセッションの 動作と機能を定義するものです 2つ目はLayerRendererです これはレイヤーレンダリングセッションの インターフェイスです アプリはこのオブジェクトを使って 新規フレームをスケジュールし レンダリングします Metalでのイマーシブな体験を書く時は アプリのタイプを定義することから始めます シーンタイプとしては ImmersiveSpaceを使います コンテンツタイプには CompositorLayerを使います CompositorLayerがコンテンツを レンダリングする準備ができたら システムはアプリを レンダリングセッションの インスタンスで呼び出します ここがカスタムのエンジンのインスタンスを 作成する絶好の場所です これでエンジンインスタンスができたので startを呼び出すことで レンダースレッドを作成し レンダーループを実行できます アプリのシーンリストを定義する時に 1つ考慮に入れるべきことは たとえアプリの最初のシーンが ImmersiveSpaceであっても SwiftUIはデフォルトで ウインドウシーンを作るということです そのデフォルトの動作を 変更するために アプリのinfo.plistを修正します アプリのシーンマニフェストに UIApplicationPreferred DefaultSceneSessionRoleの キーを追加することで アプリのデフォルトの シーンタイプが変更できます Compositor SpaceContentで spaceを使っている場合は 代わりに使うキーは CPSceneSessionRole ImmersiveSpaceApplicationです アプリを設定し終わったら レンダーループに着手する前に CompositorServicesにLayerRendererの 構成の仕方を伝えます CompositorLayerを構成するには CompositorLayer Configurationプロトコルに 準拠する新しいタイプを作成します このプロトコルで設定や レンダリングセッションの一部を 修正することができます CompositorLayerConfigurationが 付与するパラメータは2つあります 1つ目はレイヤー機能です これにより デバイスでどの機能が 利用可能かというクエリを実行できます その機能を使って有効な構成を作成します 2つ目のパラメータは LayerRenderer Configurationです このタイプはレンダリングセッションの タイプを定義するものです この構成で エンジンがコンテンツを どのようにレイヤーに マップするのか定義したり Foveated Renderingを有効にしたり パイプラインのカラーマネジメントを 定義したりできます これらのプロパティがそれぞれ エンジンに対して どう影響するのか説明します 最初は Foveated Renderingです この機能の主な目標は 大きなテクスチャサイズを用いずに より高い画素密度で コンテンツをレンダリング できるようにすることです 通常ディスプレイのパイプラインでは ピクセルはテクスチャに 直線的に配分されます xrOSは ディスプレイのどの領域が より低いサンプリングレートを使えるかを 定義するマップを作成して このワークフローを最適化します これによりフレームのレンダリングに 必要なパワーが削減でき ディスプレイの視覚的忠実性も保たれます 可能な時はいつでもフォビエーション を使うことが重要で それが より優れた視覚的体験に繋がります フォビエーションがどのように レンダリングパイプラインに影響するのか 分かりやすい例が Xcodeの Metal Debuggerの使用です Metal Debuggerを使えば レンダリングパイプラインで使われている ターゲットテクスチャや ラスタライズ率マップが検査できます このキャプチャが表示するのは ラスタライズ率マップ用に スケーリングしていない テクスチャのコンテンツです より圧縮されているテクスチャの領域に 焦点を当てると サンプルレートが 違うことに気づきます Metal Debuggerの添付ファイル ビューアオプションで 画像を拡大して ディスプレイが表示する 最終結果を見ることができます Compositorは各フレームに MTLRasterizationRateMapを使って フォビエーションマップを付与します フォビエーションがサポートされているか 常にチェックするのは良い習慣です これはプラットフォームにより変わります 例えば xrOSシミュレータでは フォビエーションは利用不可能です フォビエーションを有効化するには 設定で isFoveationEnabled をセットします 2つ目のプロパティは LayerRendererレイアウトです このプロパティはエンジンにとって 最も重要な設定の1つです ヘッドセットの各ディスプレイが レンダリングされたアプリのコンテンツに どうマップされるかを決めます Compositorが付与する Metalテクスチャに それぞれの目が最初にマップされます そしてCompositorは そのテクスチャ内で どのスライスを使うかの インデックスを付与します そして最後にCompositorは そのテクスチャスライス内で 使うべきviewportを付与します LayerRendererレイアウトでは テクスチャスライスとviewport間で 異なるマッピングが選べます レイヤードレイアウトでは Compositorは2つのスライスと 2つのviewportのある 1つのテクスチャを使います 専用レイアウトでは Compositorは1つのスライスと 1つのviewportをそれぞれ持つ テクスチャを2つ使います そして共有レイアウトでは Compositorは1つのスライスと その中に2つの異なるviewportがある テクスチャを1つ使います どのレイアウトを選ぶかは レンダリングパイプラインの設定次第です 例えば レイヤードや共有レイアウトでは レンダリングの実行が 単一のpassでできるので レンダリングパイプラインが 最適化できます 共有レイアウトでは Foveated Renderingが 選べない場合は 既存のコードベースをポートする方が 簡単かもれません レイヤードレイアウトが最適なレイアウトで シーンのレンダリングが 単一のpassで行える一方 Foveated Renderingも 維持されています 最後の構成プロパティは カラーマネジメントです Compositorはコンテンツのレンダリングに ExtendedLinearDisplay P3色空間を予測しています xrOSはHeadroom 2.0のEDRを サポートしています これはSDRのレンジの2倍です デフォルトでは Compositorは HDRでのレンダリングが可能な ピクセル形式を使いませんが HDRをサポートするアプリの場合は レイヤー設定でrgba16Floatを 特定できます EDRでHDRをレンダリングする 方法についての詳細は 「Explore HDR rendering with EDR」をご参照ください アプリでカスタムの構成を作成するには CompositorLayerConfiguration に準拠する 新しいタイプを定義することから始めます このプロトコルに準拠するには makeConfigurationメソッドを追加します このメソッドを使うと レイヤー機能と レイヤー設定が修正できます 前出の3つのプロパティを有効にするために まずは フォビエーションが サポートされているかをチェックします 次に このデバイスでどのレイアウトが サポートされるかをチェックします この情報を使って 構成内に 有効なレイアウトが設定できます シミュレータのようなデバイスでは Compositorは一つのビューしか レンダリングできないので レイヤードレイアウトは利用不可能です フォビエーションは デバイスが サポートするならtrueにします 最後に HDRコンテンツを レンダリングできるようにするために colorFormatをrgba16Floatにします Compositorレイヤーを作成した コードに戻って 今 作成した構成タイプを 追加しましょう これでレンダリングセッションが 構成されたので レンダーループが設定できます CompositorLayerの LayerRendererを使って始めます まず リソースをロードして フレームをレンダリングするのに エンジンが必要とする あらゆるオブジェクトを初期化します そしてレイヤーの状態をチェックします レイヤーが一時停止なら レイヤーが動作するまで待ちます レイヤーが待機から解放されたら 再びレイヤーの状態をチェックします レイヤーが動作中なら フレームをレンダリングできます フレームがレンダリングされたら そのレイヤーの状態をチェックしてから 次のフレームをレンダリングします レイヤーの状態が無効の場合は レンダーループに作成した リソースを解放します 今度はrender_loopの 主な機能の定義です これまではSwiftを使っていましたが それはImmersiveSpace APIが Swiftでしか利用できないからです ですがここからは Cに切り替えてレンダーループを書きます 繰り返しますが レンダーループの最初のステップは フレームのレンダリングに必要な すべてのオブジェクトの 割り当てと初期化です そのために カスタムエンジン内の 設定機能を呼び出します 次は ループの主要セクションです まずは layerRendererの状態を チェックします 状態が一時停止なら layerRendererが動作するまで スレッドはスリープになります レイヤーの状態が動作中なら エンジンは1フレームをレンダリングします そして レイヤーが無効なら レンダーループは終了です render_loop機能の最後のステップは 使用されたリソースをすべて クリアすることです これでアプリがレンダーループを 実行できるので 今度は1フレームの レンダリング方法を説明します xrOSでのコンテンツのレンダリングは常に デバイスの視点から行います ARKitを使ってデバイスの オリエンテーションや 変換が取得できます ARKitは既にiOSで利用可能で 今度はxrOSが真新しいAPIを導入し そこにはイマーシブな体験を作成しやすくなる 追加機能が含まれています ARKitではワールドトラッキングや ハンドトラッキングや そのほかのワールド感知機能を アプリに追加できます 新しいARKit APIは CやSwift APIをサポートするために ゼロから構築されているので 既存のレンダリングエンジンと 統合がしやすくなっています xrOSでのARKitについての詳細は 「Meet ARKit for spatial computing」をご確認ください レンダーループ内で 1フレームをレンダリングしましょう フレームをレンダリングする時は Compositorが2つの 主要セクションを定義します 1つ目はアップデートです 入力レイテンシが 重要でない作業はすべて ここで行います ここに含まれるのは シーンの アニメーションアップデートや キャラクターのアップデートや あるいは手の骨のポーズなどの システム内の入力収集などです フレームの2つ目のセクションは サブミッションです レイテンシが重要な作業は すべてここで行います また ヘッドセットポーズに依存する コンテンツもすべて ここでレンダリングします これらの各セクションへの タイミングを定義するために Compositorはタイミングオブジェクトを 付与します この図式は タイミングがどのように 異なるフレームセクションに 影響するかを定義しています CPUとGPUの追跡は アプリによって行われた 作業を表示しています そしてCompositorの追跡は フレームのディスプレイのために Compositorサーバが行った 作業を表示しています Compositor Servicesの タイミングタイプは 3つの主要な時間の値を意味します 最初は 最適な入力時間です これはレイテンシが重要な入力をクエリし フレームのレンダリングを開始する ベストの時間を意味します 2つ目は レンダリング終了時間です これは CPUやGPUが フレームのレンダリングを 終了しているべき時間です 3つ目は 提示時間です これは フレームがディスプレイに 提示される時間です フレームの2つのセクションのうち アップデートセクションは 最適な入力時間の前に起こるべきです アップデートのあとは フレームサブミッション開始の前に 最適な入力時間になるのを待ちます そしてフレームのサブミッションを行い レンダリングの結果が GPUにサブミットされます 重要なのは CPUとGPUの作業は レンダリング終了時間の前に 終わっていなければならず でなければCompositorサーバは このフレームを使えずに 代わりに前のフレームを 使うことになってしまいます 最後に レンダリング終了時になると Compositorサーバが システム内のほかのレイヤーと このフレームを複合します レンダーループコードに戻って render_new_frame機能を定義しましょう エンジンのrender_new_frame機能内で 最初にlayerRendererからの フレームをクエリします フレームオブジェクトで タイミング情報が取得できます そのタイミング情報を使って アップデート調べ インターバルをサブミットします 次に アップデートセクションを実行します フレームに開始と終了の アップデートを呼び出して このセクションを定義します その中で デバイスの入力を収集し フレーム内のコンテンツを アップデートします アップデートが終わったら 最適な入力時間を待ってから サブミッションを開始します 待ち終わったら 開始と終了の サブミッションを呼び出して サブミッションセクションを定義します このセクション内では最初に 描画可能なオブジェクトをクエリします CAMetalLayerと同様に 描画可能なオブジェクトは レンダリングパイプラインの設定に必要な ターゲットテクスチャと情報を含みます 描画可能なオブジェクトができたので Compositorがこのフレームの レンダリングに使う最終的な タイミング情報が取得できます 最終のタイミングを使って ar_poseがクエリできます 描画可能オプジェクトに ポーズを設定するのが重要なのは それをCompositorが使って フレームに再投影を行うからです ここではエンジンオブジェクトに get_ar_pose機能を呼び出して ポーズを取得しています ですが この機能のコンテンツは ARKitのワールドトラッキングAPIを 使っているものを実行する必要があります この機能の最後のステップは GPUの全作業をエンコードし フレームをサブミットすることです submit_frame内で 描画可能なオブジェクトを使って 通常通り フレームのコンテンツを レンダリングします これでレンダーループが フレームをレダリングしているので イマーシブな体験を インタラクティブなものにしましょう この動画で分かるように Unityを使ったRecRoomは 既にARKitとCompositorのAPIを利用して アプリにインタラクティブ性を 加えています このインタラクションを動かしているのは 2つの主要な入力ソースです ARKitのHandTrackingは バーチャルハンドをレンダリングするための ハンドスケルトンを提供しています そしてLayerRendererの ピンチイベントが ユーザーインタラクションを動かします 体験をインタラクティブにするためには まずはユーザー入力を収集して それをシーンのコンテンツに適用します この作業はすべて フレームの アップデートセクションで行われます 入力ソースには主に2つあり LayerRendererと ARKit HandTrackingプロバイダです LayerRendererではアプリが ピンチイベントを受け取るたびに アップデートが来ます これらのアップデートは 空間イベントの形式で公開されます これらのイベントには 3つの主要なプロパティがあります Phaseはイベントがアクティブなのか 終了したのか あるいは 中止になったのかを教えてくれます Selection rayでは イベントの開始時に注目された シーンのコンテンツが決められます 最後のイベントプロパティは Manipulator poseです これはピンチのポーズで イベント中 フレームごとに アップデートされます HandTracking APIから右手と左手両方に スケルトンが付与されます それでは入力サポートを コードで追加しましょう 入力を収集する前に アプリがバーチャルハンドを レンダリングするのか パススルーの手を使うのか決めます upperLimbVisibilityのシーン修飾子を ImmersiveSpaceに追加して パススルーの手を可視にしたり 隠したりします 空間イベントにアクセスするには CompositorLayerの レンダーハンドラを定義した所に戻ります ここでは layerRendererで ブロックを登録して 新しい空間イベントがあるたびに アップデートを受けるようにします エンジンコードをCで書いている場合は SwiftUIの空間イベントを C型にマップします Cコードの中で Cイベントのコレクションが 受けられるようになります 空間イベントのアップデートを 扱っている時に1つ覚えておきたいのは アップデートはメインスレッドで 配信されるということです つまり エンジンでの イベントの読み書きには 何らかの同期メカニズムを 使うということを意味します これでイベントはエンジンに保存されたので 今度は入力収集機能を実行しましょう 最初は このフレームの 現在の入力状態を保存する オブジェクトの作成です この入力状態が LayerRendererから受け取った イベントを保存します 必ず 安全な方法で 内部ストレージにアクセスしてください ハンドスケルトンに関しては ARKitのハンドトラッキング プロバイダAPIを使って 最新のハンドアンカーが取得できます これでアプリに入力サポートができたので 思いのままにxrOSで フルイマーシブ体験を作成する すべてのツールが揃いました 復習しますが アプリの定義は SwiftUIで行います CompositorServicesとMetalで レンダーループを設定し 3Dコンテンツを提示します そして ARKitを使えば 体験をインタラクティブにできます ご視聴ありがとうございました! ♪
-
-
4:45 - App architecture
@main struct MyApp: App { var body: some Scene { ImmersiveSpace { CompositorLayer { layerRenderer in let engine = my_engine_create(layerRenderer) let renderThread = Thread { my_engine_render_loop(engine) } renderThread.name = "Render Thread" renderThread.start() } } } }
-
10:32 - CompositorLayer Configuration
// CompositorLayer configuration struct MyConfiguration: CompositorLayerConfiguration { func makeConfiguration(capabilities: LayerRenderer.Capabilities, configuration: inout LayerRenderer.Configuration) { let supportsFoveation = capabilities.supportsFoveation let supportedLayouts = capabilities.supportedLayouts(options: supportsFoveation ? [.foveationEnabled] : []) configuration.layout = supportedLayouts.contains(.layered) ? .layered : .dedicated configuration.isFoveationEnabled = supportsFoveation // HDR support configuration.colorFormat = .rgba16Float } }
-
12:20 - Render loop
void my_engine_render_loop(my_engine *engine) { my_engine_setup_render_pipeline(engine); bool is_rendering = true; while (is_rendering) @autoreleasepool { switch (cp_layer_renderer_get_state(engine->layer_renderer)) { case cp_layer_renderer_state_paused: cp_layer_renderer_wait_until_running(engine->layer_renderer); break; case cp_layer_renderer_state_running: my_engine_render_new_frame(engine); break; case cp_layer_renderer_state_invalidated: is_rendering = false; break; } } my_engine_invalidate(engine); }
-
15:56 - Render new frame
void my_engine_render_new_frame(my_engine *engine) { cp_frame_t frame = cp_layer_renderer_query_next_frame(engine->layer_renderer); if (frame == nullptr) { return; } cp_frame_timing_t timing = cp_frame_predict_timing(frame); if (timing == nullptr) { return; } cp_frame_start_update(frame); my_input_state input_state = my_engine_gather_inputs(engine, timing); my_engine_update_frame(engine, timing, input_state); cp_frame_end_update(frame); // Wait until the optimal time for querying the input cp_time_wait_until(cp_frame_timing_get_optimal_input_time(timing)); cp_frame_start_submission(frame); cp_drawable_t drawable = cp_frame_query_drawable(frame); if (drawable == nullptr) { return; } cp_frame_timing_t final_timing = cp_drawable_get_frame_timing(drawable); ar_pose_t pose = my_engine_get_ar_pose(engine, final_timing); cp_drawable_set_ar_pose(drawable, pose); my_engine_draw_and_submit_frame(engine, frame, drawable); cp_frame_end_submission(frame); }
-
18:57 - App architecture + input support
@main struct MyApp: App { var body: some Scene { ImmersiveSpace { CompositorLayer(configuration: MyConfiguration()) { layerRenderer in let engine = my_engine_create(layerRenderer) let renderThread = Thread { my_engine_render_loop(engine) } renderThread.name = "Render Thread" renderThread.start() layerRenderer.onSpatialEvent = { eventCollection in var events = eventCollection.map { my_spatial_event($0) } my_engine_push_spatial_events(engine, &events, events.count) } } } .upperLimbVisibility(.hidden) } }
-
18:57 - Push spatial events
void my_engine_push_spatial_events(my_engine *engine, my_spatial_event *spatial_event_collection, size_t event_count) { os_unfair_lock_lock(&engine->input_event_lock); // Copy events into an internal queue os_unfair_lock_unlock(&engine->input_event_lock); }
-
19:57 - Gather inputs
my_input_state my_engine_gather_inputs(my_engine *engine, cp_frame_timing_t timing) { my_input_state input_state = my_input_state_create(); os_unfair_lock_lock(&engine->input_event_lock); input_state.current_pinch_collection = my_engine_pop_spatial_events(engine); os_unfair_lock_unlock(&engine->input_event_lock); ar_hand_tracking_provider_get_latest_anchors(engine->hand_tracking_provider, input_state.left_hand, input_state.right_hand); return input_state; }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。