Highlight
Apple 通过 CompositorServices API 让开发者用 Metal 直接渲染到 visionOS 的合成器,配合 ARKit 实现完全沉浸式体验。核心架构是 SwiftUI 创建 ImmersiveSpace + CompositorLayer 配置渲染会话,引擎主体可用 C/C++ 编写。
核心内容
完全沉浸式体验的渲染选择
(00:10)visionOS 上的完全沉浸式体验有两种渲染路径。RealityKit 适合大多数场景,底层使用 CoreAnimation 和 MaterialX。如果希望直接控制渲染管线,Metal + ARKit 是另一条路。RecRoom 就是使用 CompositorServices + Metal + ARKit 的实例。
SwiftUI 场景架构
(01:47)visionOS 有三种主要场景类型:
- 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 需要两个参数:
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 默认会创建 Window 场景,即使第一个场景是 ImmersiveSpace。要修改这个行为,在 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)注视点渲染(Foveated Rendering)的核心目标:在不增加纹理尺寸的前提下,提高每度像素密度。visionOS 通过创建采样率映射来实现——注视区域使用更高采样率,外围区域降低。这减少了渲染功耗,同时保持视觉保真度。
Compositor 为每帧提供 MTLRasterizationRateMap。在 Xcode Metal Debugger 中可以检查目标纹理和光栅化率映射,观察不同区域的压缩程度。
LayerRenderer Layout 详解
(08:28)Layout 定义每只眼睛的内容如何映射到 Metal 纹理:
| Layout | 纹理数 | 切片数 | 视口数 | 特点 |
|---|---|---|---|---|
| layered | 1 | 2 | 2 | 单通道渲染双眼,支持注视点渲染 |
| dedicated | 2 | 1 | 1 | 每眼独立纹理,易于移植 |
| shared | 1 | 1 | 2 | 单纹理双视口,适合无注视点场景 |
Layered 是最佳选择,支持单通道渲染且保留注视点渲染能力。
色彩管理
(09:57)Compositor 期望内容以扩展线性 Display P3 色彩空间渲染。visionOS 支持 2.0 的 EDR 动态范围(两倍于 SDR)。默认像素格式不支持 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状态时渲染一帧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 姿态,Compositor 用它做重投影my_engine_draw_and_submit_frame编码 GPU 工作并提交
帧时序图解
Compositor 的时序对象定义三个关键时间点:
- optimal input time:查询延迟敏感输入的最佳时间,也是开始渲染帧的最佳时间
- rendering deadline:CPU 和 GPU 渲染工作必须完成的时间
- presentation time:帧显示在屏幕上的时间
更新阶段应在 optimal input time 之前完成。等待 optimal input time 后开始提交阶段。CPU 和 GPU 工作必须在 rendering deadline 前完成,否则 Compositor 会使用前一帧。
添加输入支持
(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 layout + foveation,集成 ARKit 获取头部姿态
2. 开发手势交互的沉浸式艺术工具
- 做什么:让用户用手势在虚拟空间中创作 3D 雕塑或绘画
- 为什么值得做:ARKit 手部追踪 + Metal 直接渲染的组合,可以实现极低延迟的交互反馈。空间事件提供 pinch 的完整 3D 位置信息
- 怎么开始:在
gather_inputs中读取手部骨骼数据,用 pinch 事件作为”画笔”触发器,在 update 阶段更新虚拟画笔位置,在 submission 阶段渲染笔触
3. 移植现有 OpenGL/Metal 游戏到 visionOS
- 做什么:把用自研引擎开发的 3D 游戏带到 visionOS
- 为什么值得做:引擎主体可以用 C/C++,只需要用 SwiftUI 做一层薄包装。Single-Pass Instanced Rendering 减少 CPU 开销,Foveated Rendering 提升视觉质量
- 怎么开始:用 SwiftUI 创建 CompositorLayer,将现有渲染管线的最后一步改为写入 Compositor 提供的 drawable,添加深度缓冲写入,用 ARKit 替代原有的相机控制
4. 构建沉浸式数据可视化应用
- 做什么:把复杂数据集(如分子结构、气象模型)渲染为可交互的 3D 可视化
- 为什么值得做:HDR 支持(rgba16Float)让高动态范围数据展示更精确,用户可以用自然手势旋转、缩放数据模型
- 怎么开始:配置 HDR 色彩格式,在 update 阶段处理手势输入控制相机/模型变换,利用 Metal 的计算着色器做数据预处理
关联 Session
- 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