大多数浏览器和
Developer App 均支持流媒体播放。
-
探索 Metal 技术打造沉浸式 App
了解如何使用 Metal 为 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 的软件工程师 今天 我们一起来了解 如何使用 Metal 在 xrOS 上创建沉浸式体验 今年 随着 xrOS 的问世 你可以在 Apple 生态系统中 利用熟悉的技术打造沉浸式体验 你可以利用 RealityKit 创建虚拟内容 与现实世界融为一体的体验 另一方面 如果你的 App 能让用户 获得完全沉浸式的体验 xrOS 可以为你锦上添花 用你的虚拟内容 完全替代现实世界的内容 创建完全沉浸式体验的过程中 你有几种渲染方法可选择 你仍然可以使用 RealityKit 或者根据喜好 选择 Metal 和 ARKit API RecRoom 是一个很好的 App 示例 它使用 CompositorServices 创建渲染会话 使用 Metal API 渲染帧 使用 ARKit 获取 世界和手势跟踪 从而实现完全沉浸式体验 能够支持所有这些技术 得益于 Unity 编辑器 如果你想编写自己的引擎 可使用 CompositorServices API 实现 xrOS 上的 Metal 渲染 你可以将它与 ARKit 结合使用 后者增加了 世界跟踪和手势跟踪功能 从而创建完全沉浸式的体验 CompositorServices 是配置引擎 在 xrOS 上工作的关键 我将向你演示如何设置渲染循环 以及如何渲染一帧 最后 一起了解如何使用 ARKit 创建具有交互性的体验 首先 让我们从了解 xrOS App 的架构开始 如果你之前 有 Metal API 和 Metal 渲染技术的经验 今天的讲座会让你收获满满 如果你以前没有使用过 Metal 请查看 developer.apple.com/cn/metal/ 中的 代码示例和文档 使用 Metal 在 xrOS 上 创建沉浸式体验时 需要从 SwiftUI 开始创建 App 和渲染会话 创建渲染会话后 可切换到更熟悉的语言 如 C 或 C++ 定义引擎的内部 你首先要创建一个 符合 SwiftUI App 协议的类型 为了遵循此协议 需要在 App 中 对场景列表进行定义 xrOS 上主要有三种场景类型 Window 类型提供了类似 macOS 等的 2D 平台体验 Volume 类型在其边界内渲染内容 并与其他 App 共存于共享空间中 ImmersiveSpace 类型 支持在任何地方渲染内容 用 Metal 渲染完全沉浸式的体验时 可以选择 ImmersiveSpace 类型 ImmersiveSpace 是 xrOS 上 提供的一种 全新 SwiftUI Scene 类型 可作为完全沉浸式体验的容器 欲了解 如何使用 ImmersiveSpace 请观看 “使用 SwiftUI 超越窗口”讲座 创建 ImmersiveSpace 场景时 App 通过使用符合 ImmersiveSpaceContent 协议 的类型来提供内容 通常 App 会使用 RealityKit 为 ImmersiveSpace Scene 创建内容 它的底层使用了 CoreAnimation 和 MaterialX 当然 如果想使用 Metal 的功能 来渲染 App 的内容 还有另一种选择 CompositorServices API 使用 Metal 和 ARKit 为 App 提供沉浸式渲染功能 xrOS 中引入的新 CompositorServices API 提供了一个 Metal 渲染接口 可渲染 ImmersiveSpace 的内容 使用 CompositorServices App 可以 直接渲染到 Compositor 服务器中 它的进程间通信开销很低 可以最大限度地减少延迟 而且它从底层开始构建 可支持 C 语言和 Swift API 使用 CompositorServices 时 ImmersiveSpaceContent 称为 CompositorLayer 为创建 CompositorLayer 你需要提供两个参数 第一个 是 CompositorLayerConfiguration 协议 此协议定义 渲染会话的行为和功能 第二个是 LayerRenderer 这是层渲染会话的接口 App 将使用此对象 调度和渲染新帧 当使用 Metal 编写沉浸式体验时 首先要定义 App 类型 场景类型使用 ImmersiveSpace 内容类型使用 CompositorLayer CompositorLayer 准备好渲染内容后 系统将使用渲染会话的实例 调用 App 这样 我们就可以 创建自定义引擎实例了 有了引擎实例 就可以 调用 start 来创建渲染线程 并运行渲染循环 在 App 中定义场景列表时 需要考虑一件事 默认情况下 SwiftUI 会创建一个 Window Scene 即使 App 中的 第一个场景是 ImmersiveSpace 要更改默认行为 可以修改 App 的 info plist 可以将键值 UIApplicationPreferred DefaultSceneSessionRole 添加到 App 场景清单中 以更改 App 的默认场景类型 如果使用的空间具有 Compositor SpaceContent 则使用 CPSceneSessionRole ImmersiveSpaceApplication 在设置好 App 后 进入渲染循环之前 你要为 CompositorServices 配置 LayerRenderer 为了向 CompositorLayer 提供配置 你要创建一个 符合 CompositorLayerConfiguration 协议 的新类型 此协议下 你可以 修改渲染会话的设置和一些行为 CompositorLayerConfiguration 提供了两个参数 第一个是 LayerRenderer Capabilities 你可以借助它查询设备上的 可用功能 使用这些功能创建有效的配置 第二个是 LayerRenderer Configuration 此类型定义 渲染会话的配置 通过配置 你可以定义 引擎如何将其内容映射到层、 启用注视点渲染 并定义管线的颜色管理 现在 我们来探讨这些属性 如何影响引擎 第一个是注视点渲染 此功能的主要目标是 不使用更大纹理大小的情况下 以更高的像素密度渲染内容 在常规显示管线中 像素在纹理中线性分布 xrOS 通过创建一个地图 来优化这个工作流程 该地图定义了显示中的哪些区域 可以使用较低的采样率 这有助于降低渲染帧所需的功率 同时保持显示器的视觉保真度 请尽量使用注视点 这至关重要 因为它会带来更好的视觉体验 使用 Xcode 的 Metal Debugger 可以直观地显示 注视点如何影响渲染管线 使用 Metal Debugger 你可以检查 渲染管线中 使用的目标纹理和栅格化速率地图 这一捕捉显示纹理的内容 而不会缩放栅格化速率地图 通过聚焦在纹理中 压缩程度更高的区域 你可以注意到不同的采样率 使用 Metal Debugger 中的 附件查看器选项 你可以缩放图像使最终结果的 显示可视化 Compositor 使用 MTLRasterizationRateMap 为每一帧提供注视点图 最好时常检查 是否支持注视点 它会根据平台变化而改变 例如 在 xrOS 模拟器中 注视点不可用 要启用注视点 可以在配置中设置 isFoveationEnabled 第二个属性是 LayerRenderer 布局 此属性是引擎最重要的 配置之一 它定义了如何将 头显中的每个显示映射到 App 的渲染内容中 每只眼睛首先 映射到 Compositor 提供的 Metal 纹理中 随后 Compositor 提供在该纹理中使用的 切片索引 最后 Compositor 提供了在该纹理切片中 使用的视口 使用 LayerRenderer 布局 可以在纹理切片和视口之间 选择不同的映射 在分层的情况下 Compositor 会使用 一个具有两个切片 和两个视口的纹理 在专用的情况下 Compositor 会使用 两个具有一个切片 和一个视口的纹理 最后 通过共享 Compositor 为该切片 使用一个具有一个切片 和两个不同视口的纹理 使用布局的选择取决于 渲染管线的设置方式 例如 通过分层和共享 你能够一次性执行渲染 因此可以优化渲染管线 使用共享布局 在无法选择注视点渲染的情况下 移植现有代码库可能会更容易 分层布局是最佳布局 因为你可一次性完成渲染场景 同时仍保持注视点渲染 最后讨论的配置属性是颜色管理 Compositor 希望使用扩展的 线性显示 P3 颜色空间渲染内容 xrOS 支持 2.0 EDR 动态余量 这是 SDR 范围的两倍 默认情况下 Compositor 不使用 HDR 渲染的像素格式 但如果 App 支持 HDR 则可以在层配置中 指定 rgba16Float 如果你想详细了解 如何使用 EDR 渲染 HDR 请观看 “探索使用 EDR 的 HDR 渲染”讲座 要在 App 中创建自定义配置 请首先定义一个 符合 CompositorLayerConfiguration 协议 的新类型 为了遵循此协议 请添加 makeConfiguration 方法 此方法提供了可以修改的 层功能和配置 要启用前面提到的三个属性 首先检查是否支持注视点 然后检查该设备支持哪些布局 有了这些信息 你就可以在配置中 设置有效的布局 在某些设备(如模拟器)中 Compositor 仅渲染一个视图 分层不可用 如果设备支持注视点 请将其设置为 true 最后 将 colorFormat 设置为 rgba16Float 以便能够渲染 HDR 内容 返回到创建 Compositor 层的代码 你现在可以添加刚才创建的 配置类型 配置了渲染会话 就可以设置渲染循环了 你将从使用 CompositorLayer 中的 LayerRenderer 对象开始 首先 加载资源并初始化引擎渲染帧 所需的全部对象 然后检查层的状态 如果层暂停 请等待直到层运行 一旦层从等待中解除暂停 请再次检查层状态 如果层正在运行 则可以渲染帧 渲染该帧后 在渲染下一帧前再次检查层状态 如果层状态无效 请释放为渲染循环创建的资源 现在可以定义 render_loop 的 主要函数了 直到现在 我一直在使用 Swift 因为 ImmersiveSpace API 只在 Swift 中可用 但从这里开始 我将切换到 C 语言来编写渲染循环 正如我提到的 渲染循环的第一步 是分配和初始化渲染帧所需的 全部对象 你可以通过调用自定义引擎中的 setup 函数来完成此操作 接下来 是循环的主要部分 第一步是检查 layerRenderer 状态 如果状态为暂停 线程将休眠 直到 layerRenderer 运行为止 如果层状态正在运行 引擎会渲染一帧 最后 如果层已经失效 渲染循环将结束 render_loop 函数的最后一步 是释放所有已使用的资源 现在 App 正在进行渲染循环 我会说明如何渲染一帧 在 xrOS 中渲染内容始终是 从设备的角度出发 你可使用 ARKit 获取设备方向 和平移 ARKit 已经可以在 iOS 上使用 xrOS 正在引入一个全新 API 它的新功能可以帮你创建 沉浸式体验 使用 ARKit 你可以 在 App 中添加世界跟踪、 手势跟踪 和其他世界感应功能 全新 ARKit API 也是 从底层开始构建 以支持 C 语言和 Swift API 这将使其更容易 与现有的渲染引擎整合 要进一步了解 xrOS 上 ARKit 的更多信息 请观看 “认识用于空间计算的 ARKit”讲座 现在可以在渲染循环中渲染一帧 渲染帧时 Compositor 定义了两个主要部分 第一是更新 在这里 你可以执行 任何对输入延迟不重要的工作 可以是更新场景中的动画、 更新角色或收集系统中的 输入(如手骨架姿势) 帧的第二个部分是提交 你可以在此处执行 任何延迟关键型工作 你还将在此处渲染任何 与头显姿势相关的内容 为了定义每个部分的计时 Compositor 提供一个计时对象 此图定义了计时如何影响 不同的帧部分 CPU 和 GPU 轨迹表示 App 正在完成的工作 Compositor 轨迹表示 Compositor 服务器 为显示帧所做的工作 Compositor Services 中的计时类型 定义了三个主要的时间值 首先是最佳输入时间 这是查询延迟关键型输入 并开始渲染帧的最佳时间 第二个是渲染截止时间 这是 CPU 和 GPU 应该完成渲染帧工作的时间 第三个是呈现时间 这是帧呈现在显示器上的时间 在帧的两个部分中 更新部分 应在最佳输入时间之前发生 更新后 你需等待最佳输入时间 然后再开始提交帧 然后 你要执行帧提交 将渲染工作提交到 GPU 需要注意的是 CPU 和 GPU 的工作 需要在渲染截止时间之前完成 否则 Compositor 服务器 无法使用此帧 转而使用先前的帧 最后 在渲染截止时 Compositor 服务器将这一帧 与系统中的其他层进行合成 回到渲染循环代码 现在可以定义 render_new_frame 函数了 在引擎的 render_new_frame 函数中 首先 从 layerRenderer 查询一个帧 使用帧对象 可预测计时信息 使用该计时信息确定更新和 提交间隔的范围 接下来 执行更新部分 通过调用帧上的 start 和 end 更新 来定义这个部分 在内部 你需要收集设备输入 并更新帧的内容 更新完成后 请等待最佳输入时间 然后再开始提交 等待之后 通过调用 start 和 end 提交 来定义提交部分 在本节中 首先查询可绘制对象 与 CAMetalLayer 类似 可绘制对象 包含目标纹理 和设置渲染管线所需的信息 你已经有了可绘制对象 可以获取 Compositor 用于渲染此帧的最终计时信息 你可以通过 最后的计时查询 ar_pose 在可绘制对象中设置姿势很重要 因为 Compositor 将使用它 在帧上执行重投影 在这里 我通过调用引擎对象中的 get_ar_pose 函数来获取姿势 但是你要使用 ARKit 世界跟踪 API 来执行此函数的内容 该函数的最后一步是 对所有 GPU 工作进行编码并提交帧 在 submit_frame 中 像往常一样 使用可绘制对象渲染帧的内容 现在渲染循环正在渲染帧 是时候将交互性 赋予你的沉浸式体验了 本视频展示了 使用 Unity 的 RecRoom 如何利用 ARKit 和 Compositor API 为 App 添加交互性 驱动此交互的主要输入源有两个 ARKit 的 HandTracking 提供了 用于渲染虚拟手的手部骨架 LayerRenderer 的手势响应事件 驱动着用户的交互 为了使体验具有交互性 你需要先收集用户输入 然后将其应用于场景内容 所有这些工作 都在帧的更新部分进行 这里有两个主要的输入源 LayerRenderer 和 ARKit HandTracking 提供程序 使用 LayerRenderer 每当 App 接收到手势响应事件时 你都将获取更新 这些更新以空间事件的形式公开 这些事件包含三个属性 阶段状态会告诉你 事件是否正在进行、 是否已完成或是否已取消 你可以通过选择光线 确定事件开始时引起关注的 场景内容 最后一个事件属性是操纵器姿势 它是手势响应姿势 在事件持续期间每帧都会更新一次 你可以从 HandTracking API 得到左手和右手的骨架 现在 你可以 在代码中添加输入支持 在收集输入之前 你将决定 App 是渲染虚拟手 还是使用穿透手 将 upperLimbVisibility 场景修饰符 添加到 ImmersiveSpace 显示或隐藏穿透手 要访问空间事件 请回到 定义 CompositorLayer 渲染处理器的位置 此处 在 layerRenderer 中注册一个 block 在每次出现 新的空间事件时获得更新 如果你用 C 语言编写引擎代码 则需要将 SwiftUI 空间事件映射为 C 语言类型 你现在可以在 C 语言代码中 接收 C 语言事件集合 在处理空间事件更新时 你需要牢记 更新在主线程中交付 这意味着你在引擎中 读取和写入事件时 需要使用一些同步机制 现在事件已存储在引擎中 是时候执行收集输入函数了 第一步是创建一个对象 来存储该帧的当前输入状态 此输入状态会存储 从 LayerRenderer 接收到的事件 请确保以安全的方式 访问内部存储 至于手部骨架 你可以使用 ARKit 中的 手势跟踪提供程序 API 来获取最新的手部锚点 现在 App 有了输入支持 你就获得了在 xrOS 上 创建完全沉浸式体验的所有工具 总结一下 你可以使用 SwiftUI 定义 App 使用 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; }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。