WWDC Quick Look 💓 By SwiftGGTeam
Discover Metal for immersive apps

Discover Metal for immersive apps

元の動画を見る

ハイライト

Apple は CompositorServices API で開発者が Metal を使い visionOS のコンポジターに直接レンダリングできるようにし、ARKit と組み合わせて完全没入型体験を実現します。中核アーキテクチャは SwiftUI で ImmersiveSpace を作成し、CompositorLayer でレンダリングセッションを設定し、エンジン本体は C/C++ で記述できます。

主要内容

完全没入型体験のレンダリング選択

00:10)visionOS の完全没入型体験には 2 つのレンダリングパスがあります。RealityKit はほとんどのシーンに適し、内部で CoreAnimation と MaterialX を使用します。レンダリングパイプラインを直接制御したい場合は Metal + ARKit が別の道です。RecRoom は CompositorServices + Metal + ARKit を使用する例です。

SwiftUI シーンアーキテクチャ

01:47)visionOS には 3 つの主要なシーンタイプがあります:

  • Window:macOS と同様の 2D 体験
  • Volume:Shared Space で他のアプリと共存し、コンテンツは境界内でレンダリング
  • ImmersiveSpace:任意の位置でコンテンツをレンダリングし、完全没入型体験に使用

Metal レンダリングは ImmersiveSpace タイプを選択します。コンテナとして機能し、コンテンツは ImmersiveSpaceContent プロトコルで提供されます。Metal 使用時、このコンテンツタイプは CompositorLayer と呼ばれます。

CompositorServices API

03:42)CompositorServices は visionOS の新 API で、Metal レンダリングインターフェースを提供し、アプリがコンポジターサーバーに直接レンダリングできます。低 IPC オーバーヘッド、低レイテンシ、C と Swift API の両方をサポートします。

CompositorLayer の作成には 2 つのパラメータが必要です:

  • CompositorLayerConfiguration プロトコル:レンダリングセッションの動作と機能を定義
  • LayerRenderer:フレームのスケジューリングとレンダリング用のレンダリングセッションインターフェース

アプリケーションエントリコード

04:45

@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()
            }
        }
    }
}

キーポイント:

  • @main でアプリケーションエントリポイントをマーク
  • ImmersiveSpace がシーンコンテナとして機能
  • CompositorLayerlayerRenderer インスタンスを受け取る
  • クロージャ内でエンジンインスタンスとレンダースレッドを作成
  • レンダーループは別スレッドで実行し、メインスレッドのブロックを回避

デフォルトシーン設定

05:20)SwiftUI は最初のシーンが ImmersiveSpace でもデフォルトで Window シーンを作成します。これを変更するには、Info.plist に UIApplicationPreferredDefaultSceneSessionRole キーを追加し、値を CPSceneSessionRoleImmersiveSpaceApplication に設定します。

CompositorLayer 設定

10:32

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
   }
}

キーポイント:

  • supportsFoveation でデバイスがフォベーテッドレンダリングをサポートするか確認(シミュレーターは非対応)
  • supportedLayouts で利用可能なレイアウトオプションをクエリ
  • layout でテクスチャマッピングを選択:layered(単一テクスチャ、デュアルスライス)、dedicated(デュアルテクスチャ、各単一スライス)、shared(単一テクスチャ、単一スライス、デュアルビューポート)
  • isFoveationEnabled でフォベーテッドレンダリングを有効化
  • colorFormat.rgba16Float に設定して HDR レンダリングをサポート

フォベーテッドレンダリング

06:51)フォベーテッドレンダリングの中核目標:テクスチャサイズを増やさずにピクセル/度の密度を向上させること。visionOS はサンプリングレートマップで実現—視線領域は高サンプリング、周辺領域は低サンプリング。レンダリング消費電力を削減しながら視覚的忠実度を維持します。

コンポジターはフレームごとに MTLRasterizationRateMap を提供します。Xcode Metal Debugger でターゲットテクスチャとラスタライゼーションレートマップを検査し、領域ごとの圧縮度を観察できます。

LayerRenderer レイアウトの詳細

08:28)レイアウトは各目のコンテンツが Metal テクスチャにどうマッピングされるかを定義します:

レイアウトテクスチャ数スライス数ビューポート数特徴
layered122シングルパスステレオレンダリング、フォベーション対応
dedicated211各目独立テクスチャ、移植が容易
shared112単一テクスチャデュアルビューポート、フォベーションなしシーンに適している

Layered が最適な選択で、シングルパスレンダリングをサポートしながらフォベーション機能を保持します。

カラー管理

09:57)コンポジターは拡張線形 Display P3 色空間でレンダリングされたコンテンツを期待します。visionOS は 2.0 の EDR ダイナミックレンジ(SDR の 2 倍)をサポートします。デフォルトのピクセルフォーマットは HDR 非対応のため、明示的に rgba16Float に設定する必要があります。

詳細

レンダーループ

12:20

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);
}

キーポイント:

  • my_engine_setup_render_pipeline でレンダリングパイプラインに必要なリソースを初期化
  • @autoreleasepool で自動解放オブジェクトを管理
  • cp_layer_renderer_get_state でレイヤーレンダラーの状態をクエリ
  • paused 状態ではスレッドがスリープし、再開を待つ
  • running 状態では 1 フレームをレンダリング
  • invalidated 状態ではループを終了しリソースをクリーンアップ

単一フレームレンダリングフロー

15:56

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);
}

キーポイント:

  • cp_layer_renderer_query_next_frame で次のフレームオブジェクトを取得
  • cp_frame_predict_timing でフレームのタイミング情報を予測
  • cp_frame_start_update / cp_frame_end_update で更新フェーズをラップ(非レイテンシ敏感な作業:アニメーション、キャラクター更新、入力収集)
  • cp_time_wait_until で最適な入力クエリ時間まで待機
  • cp_frame_start_submission / cp_frame_end_submission で提出フェーズをラップ(レイテンシ敏感な作業:頭部姿勢依存のレンダリング)
  • cp_frame_query_drawable でドローアブル(ターゲットテクスチャを含む)を取得
  • cp_drawable_set_ar_pose で AR 姿勢を設定、コンポジターが再投影に使用
  • my_engine_draw_and_submit_frame で GPU 作業をエンコードして提出

フレームタイミング図解

コンポジターのタイミングオブジェクトは 3 つの重要な時間点を定義します:

  1. optimal input time:レイテンシ敏感な入力をクエリする最適な時間、フレームレンダリング開始の最適な時間でもある
  2. rendering deadline:CPU と GPU のレンダリング作業が完了しなければならない時間
  3. presentation time:フレームが画面に表示される時間

更新フェーズは optimal input time より前に完了すべきです。optimal input time を待ってから提出フェーズを開始します。CPU と GPU の作業は rendering deadline 前に完了する必要があり、そうでなければコンポジターは前のフレームを使用します。

入力サポートの追加

18:57

@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)
    }
}

キーポイント:

  • configuration パラメータでカスタム設定を渡す
  • onSpatialEvent で空間イベントコールバックを登録
  • Swift 空間イベントを C タイプイベントにマッピング
  • .upperLimbVisibility(.hidden) で実際の手を非表示にし、仮想の手を表示

イベントプッシュと入力収集

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);
}

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;
}

キーポイント:

  • 空間イベントはメインスレッドで配信されるため、同期にはロック機構が必要
  • os_unfair_lock でスレッドセーフな読み書き
  • ar_hand_tracking_provider_get_latest_anchors で左右の手のスケルトンデータを取得
  • 入力収集は更新フェーズで optimal input time より前に行われる

重要ポイント

1. Metal でカスタム VR レンダリングエンジンを構築

  • やること:visionOS 向けにゼロから軽量 VR レンダリングエンジンを作成
  • 価値:CompositorServices が低レベルアクセスを提供し、C/C++ エンジンコードはクロスプラットフォームで再利用可能。フォベーテッドレンダリングとシングルパスインスタンスレンダリングで大きなパフォーマンス最適化の余地
  • 始め方:SwiftUI で ImmersiveSpace と CompositorLayer を作成し、C でレンダーループを記述、layered レイアウト + フォベーションを設定、ARKit で頭部姿勢を統合

2. ジェスチャーインタラクティブな没入型アートツールを開発

  • やること:ユーザーがジェスチャーで仮想空間に 3D 彫刻や絵画を作成
  • 価値:ARKit 手部トラッキング + Metal 直接レンダリングで極めて低レイテンシのインタラクティブフィードバック。空間イベントがピンチの完全な 3D 位置を提供
  • 始め方gather_inputs で手のスケルトンデータを読み取り、ピンチイベントを「ブラシ」トリガーとして使用、更新フェーズで仮想ブラシ位置を更新、提出フェーズでストロークをレンダリング

3. 既存 OpenGL/Metal ゲームを visionOS に移植

  • やること:自社エンジンで開発した 3D ゲームを visionOS に持ち込む
  • 価値:エンジン本体は C/C++ で、SwiftUI の薄いラッパーのみ必要。シングルパスインスタンスレンダリングで CPU オーバーヘッドを削減、フォベーテッドレンダリングで視覚品質を向上
  • 始め方:SwiftUI で CompositorLayer を作成し、既存レンダリングパイプラインの最終ステップをコンポジター提供のドローアブルへの書き込みに変更、深度バッファ書き込みを追加、既存のカメラ制御を ARKit に置き換え

4. 没入型データ可視化アプリを構築

  • やること:複雑なデータセット(分子構造、気象モデルなど)をインタラクティブな 3D 可視化としてレンダリング
  • 価値:HDR サポート(rgba16Float)で高ダイナミックレンジデータのより正確な表示、ユーザーが自然なジェスチャーでデータモデルを回転・拡大縮小
  • 始め方:HDR カラーフォーマットを設定し、更新フェーズでジェスチャー入力を処理してカメラ/モデル変換を制御、Metal コンピュートシェーダーでデータ前処理

関連セッション

コメント

GitHub Issues · utterances