ハイライト
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がシーンコンテナとして機能CompositorLayerがlayerRendererインスタンスを受け取る- クロージャ内でエンジンインスタンスとレンダースレッドを作成
- レンダーループは別スレッドで実行し、メインスレッドのブロックを回避
デフォルトシーン設定
(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 テクスチャにどうマッピングされるかを定義します:
| レイアウト | テクスチャ数 | スライス数 | ビューポート数 | 特徴 |
|---|---|---|---|---|
| layered | 1 | 2 | 2 | シングルパスステレオレンダリング、フォベーション対応 |
| dedicated | 2 | 1 | 1 | 各目独立テクスチャ、移植が容易 |
| shared | 1 | 1 | 2 | 単一テクスチャデュアルビューポート、フォベーションなしシーンに適している |
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 つの重要な時間点を定義します:
- optimal input time:レイテンシ敏感な入力をクエリする最適な時間、フレームレンダリング開始の最適な時間でもある
- rendering deadline:CPU と GPU のレンダリング作業が完了しなければならない時間
- 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 コンピュートシェーダーでデータ前処理
関連セッション
- Bring your Unity VR app to a fully immersive space — 既存 Unity VR アプリを visionOS 完全没入空間に移行
- Build great games for spatial computing — 空間コンピューティングゲーム開発技術の概要
- Meet ARKit for spatial computing — 空間コンピューティング向け ARKit 新 API の詳細
コメント
GitHub Issues · utterances