
-
用于打造沉浸式 App 的 Metal 渲染的新功能
探索用于通过 Compositor Services 打造沉浸式 App 的 Metal 渲染的最新改进。了解如何通过添加悬停效果来突出展示 App 的交互式元素,以及如何通过动态渲染质量实现更高保真度的渲染。了解新的 progressive 沉浸样式。探索如何通过将 Mac 中的 Metal 内容直接渲染至 Vision Pro,来为 macOS App 提供沉浸式体验。 要充分利用好本次讲座,请先观看 WWDC23 讲座“探索 Metal 技术打造沉浸式 App”。
章节
- 0:00 - 简介
- 1:58 - 全新渲染循环 API
- 4:21 - 悬停效果
- 10:50 - 动态渲染质量
- 14:44 - Progressive 沉浸
- 18:32 - macOS 空间渲染
- 23:51 - 后续步骤
资源
- Analyzing the performance of your Metal app
- Optimizing GPU performance
- Rendering hover effects in Metal immersive apps
相关视频
WWDC25
WWDC24
WWDC23
-
搜索此视频…
大家好 我叫 Ricardo 是 Apple 的 软件工程师 今天 我将向大家展示在 Apple Vision Pro 上使用 Metal 渲染沉浸式内容时可以采用的新功能 去年我们展示了如何利用 Metal、 Compositor Services 和 ARKit 通过直接在 visionOS 上渲染内容来 打造沉浸式体验
你可以实现完全沉浸式体验 就像 Resolution Games 的 “Demeo”那样 或者 也可以采用混合沉浸式风格 让你的内容 与现实世界一起呈现
现在 由于像你一样的开发者给出了 宝贵反馈 visionOS 上的 Metal 渲染支持 令人兴奋的新功能
你能够将更丰富的细节和交互式效果 添加到 App 和游戏中 我将在这个视频中详细介绍 Compositor Services 的新功能
要充分利用这些功能 你应该熟悉 Compositor Services 框架 以及 Metal 渲染技术 如果你以前没有使用过这些技术 可以在之前的这些视频中 学习了解一下
要采用新功能 首先需要对 现有的渲染循环进行一些更改 我将介绍如何采用新的 API 使得管线更加灵活 更改之后 你将能够添加悬停效果来 突出显示 App 的交互式元素 你还可以动态调整渲染内容的分辨率 还有一种新的渐进式沉浸样式 可以让用户通过数码旋钮 调节沉浸程度 你可以使用 Mac 将沉浸式内容直接 渲染到 Vision Pro! 这一切的起点是新的渲染循环 API 我们来深入了解一下
Metal 沉浸式 App 从 SwiftUI 开始 你可以在其中创建一个沉浸式空间 这个空间包含一个合成器层
这个图层提供图层渲染器对象 以供在渲染循环中使用 你可以从图层渲染器查询帧 每一帧都可以得到可绘制对象 其中包含用于渲染内容的纹理 如果你已经创建了 Metal 沉浸式 App 那么你在查询的是每个渲染帧的 单个可绘制对象 今年 Compositor Services 新增了 一个 queryDrawables 函数 可以返回可绘制对象数组 根据系统上下文 这个数组包含 一、两个可绘制对象 多数情况下是一个可绘制对象 但每当你用 Reality Composer Pro 录制 高画质视频时 一个帧将返回两个可绘制对象 你可以通过新的 target 属性来 标识可绘制对象 Vision Pro 显示屏对象具有 .builtIn 值 录制对象具有 .capture 值 要了解如何拍摄高画质视频 请查看“开发者文档”
你可能对这个 renderFrame 函数 很熟悉 在查询下一帧并更新场景状态后 我将调用 queryDrawable 函数 然后等待最佳输入时间 之后将场景渲染到可绘制对象
现在 我将 queryDrawable 替换为 queryDrawables 我要确保数组不为空并将场景渲染到 所有可绘制对象
Xcode 包含一个方便的模板 可用来 展示如何创建 Metal 沉浸式 App 这是个很好的起点 要想试用这个模板 可以为 visionOS App 创建一个新的 Xcode 项目 然后 在“Immersive Space Renderer” 弹出式菜单上选取“Metal 4”
请注意 今年仍然完全支持 Metal 3 而且你可以选择“Metal”选项 以便在 Xcode 模板上使用它
你可以观看“探索 Metal 4” 进一步了解 如何采用最新版本的 Metal 采用新的 queryDrawables 函数后 你将能够使用今年的所有新功能 例如在场景的交互式对象上添加 悬停效果
借助这些效果 App 用户可以看到 哪些对象是交互式的 并能预测它们操作的目标 系统将动态突出显示观众正在 查看的对象 例如 拼图游戏可以突出那些 可由玩家选择的拼图块
假设某个 App 正在渲染一个包含 多个 3D 对象的场景 但只有其中一些可以交互 我想确保悬停效果仅用于场景中的 交互式对象 如果对象不是交互式的 就不会被追踪 也没有悬停效果 所以我就以正常方式渲染它 而如果对象是交互式的 我会给可绘制对象添加新的追踪区域 请注意 我需要为每个注册的 追踪区域分配唯一的 对象标识符
然后检查对象是否应该具有悬停效果 如果不应该 我就用 追踪区域渲染值来绘制 这对于检测对象上的捏合很有用 但如果对象具有悬停效果 在渲染前 我会在追踪区域上配置它
在代码中 我需要将图层渲染器 配置为使用悬停效果 我将追踪区域纹理设置为使用 8 位 像素格式 它最多支持 255 个并发交互式对象 我要确保图层功能支持这个格式 并在配置中设置好
在追踪区域注册码中 我要检查 对象是否是交互式的 如果是 就会用对象标识符在可绘制 对象中 注册新的追踪区域 请务必跟踪对象生命周期的 唯一标识符 然后检查对象有没有悬停效果 如果有 就会给它添加 .automatic 属性 这样 当观众注视对象时 系统将 自动添加悬停效果 最后进行渲染
借助 Compositor Services 可绘制对象提供多种纹理 可供 App 渲染内容
你可能熟悉颜色纹理 这是 App 的 观众所看到的内容 还有深度纹理 其中较深颜色表示 对象距离观众较远 观众在场景四处走动时 系统就是 通过这种纹理 更加准确地显示内容 今年我们新增了追踪区域纹理 用来 定义场景中不同的交互式区域 可绘制对象提供颜色和深度纹理 现在还可以查询新的追踪区域纹理 在其中可以绘制与交互式对象 相对应的不同区域 借助悬停效果 当有人注视场景中的 交互式对象时 系统会使用追踪区域纹理来查找 相应的区域 并在颜色纹理的匹配部分应用 悬停效果 这里又是我配置了追踪区域的 对象渲染函数 要渲染到追踪区域纹理 我需要 系统计算的渲染值 我声明了一个局部变量来存储它 我从相应的追踪区域获取它 如果对象不是交互式的 我可以使用 默认的 nil 渲染值 最后我将它传递给 draw 函数 在那里我将它发送到片段着色器
我们来看看着色器代码
片段着色器输出具有追踪区域渲染值 渲染值映射到索引 1 处的颜色附件 我已经在那里设置了追踪区域纹理 我已经将渲染值绑定到 uniforms 结构体 并在着色器输出中返回它和颜色值 现在 我的 App 就有了交互式 悬停效果 如果你使用多重采样抗锯齿 (MSAA) 技术 那么还需要记住一件事
这项技术的工作原理是渲染中等较高 分辨率的纹理 然后在采样窗口期对颜色值取平均值 为此 通常需要对“目标纹理存储” 操作采用 multisampleResolve 选项 解析追踪区域的方式不同于 解析颜色像素 这样做 会对渲染值取平均值 导致与任何追踪区域 都不对应的无效标量 如果对颜色使用多重采样解析 就必须为追踪区域纹理实现 自定图块解析程序 为此 可以使用具有自定 图块渲染管线的 dontCare 存储选项 一个好的策略是在 MSAA 源纹理上 选择采样窗口期间出现频率最高的 渲染值 如要深入了解悬停效果 包括如何与 MSAA 搭配使用 可以阅读“开发者文档”中的 “在 Metal 沉浸式 App 中 渲染悬停效果”
另外 追踪区域还让你的 App 能够 比以往更轻地 处理对象交互 空间事件现在包括可为空的追踪区域 标识符 我使用这个标识符来查看它是否 与任何场景对象匹配 如果找到目标对象 就可以对它 执行操作
我通过悬停效果改进了 App 的 交互性 App 用户能够清楚地看到 哪些对象是可操作的 以及哪些对象被捏合时会激活 这使得处理输入事件比以前更容易! 现在 你还能够以比之前更高的 保真度来绘制内容 通过动态渲染质量 你可以根据 场景的复杂程度 调整内容的分辨率
首先 我将归纳一下注视点渲染的 工作原理 在标准的非注视点纹理中 像素在 整个表面 均匀分布 通过注视点渲染 系统可以帮你绘制 中心具有较高像素密度的纹理 这样 App 就可以利用计算和 电源资源 在观众最有可能看到的任何地方 让内容看起来更美观 今年 你可以利用动态质量进行 注视点渲染 现在可以控制 App 所渲染帧的质量 首先需要指定适合 App 的 最大渲染质量 这将为 App 的渲染会话设置上限 然后可以根据所展示内容的类型 在选择范围内 调整运行时质量
随着渲染质量提升 纹理中高相关性 区域会扩大 导致整体纹理尺寸变大 提醒一下 提高质量 也意味着 App 将使用更多内存 和电力 如果你在渲染文本或用户界面元素 那么设置更高的渲染质量 会很有好处 但如果要显示复杂的 3D 场景 就可能会受到算力资源限制 为了确保 App 流畅运行 需要权衡 高质量视觉效果和 App 使用的电量
你可以使用 Instruments 来分析 App 的实时性能 并可以用 Metal 调试器深究 和优化 Metal 代码和着色器 请记住 用最复杂的场景来分析 App 非常重要 这样是为了确保 App 有足够的时间 以稳定的速度渲染帧
请查看“开发者文档” 了解有关优化 Metal 渲染 App 的 更多信息
在这个代码示例中 我对 App 进行了剖析 并确定我想以 .8 的质量渲染 App 菜单 这样 文本就看起来会更清晰 我想以 .6 的质量渲染世界 因为它是个复杂的场景 我还添加了一个计算属性 用于指定我将要使用的最大渲染质量 这是我的图层配置 动态渲染质量只能用于注视点 所以我检查了一下它是否已经启用 然后 我将最大渲染质量设置为 计算属性中的值 请记住将它设置为对内容来说合理的 最小值 否则 App 使用的内存将超过需要
加载新场景时 我调用了 adjustRenderQuality 函数 只有在启用注视点时才能调整 渲染质量 我切换了场景类型并相应地调整了 渲染质量
在质量值之间转换需要一些时间 而不是一蹴而就 系统会让切换过程平稳
动态渲染质量会让细节丰富的场景 大放异彩 渲染的场景分辨率越高 细节清晰度 就越高 但请记住 在非常复杂的场景中 可能需要降低质量 现在可以调整 App 的内容 渲染质量了!
今年的新功能是 Metal App 可以 在渐进沉浸式传送门中被渲染 借助渐进式沉浸样式 App 用户可以通过旋转数码旋钮 来控制沉浸程度 这种模式会将用户锚定到真实环境 并在用户查看复杂的动态场景时 有助于他们感到更自在 在渐进式沉浸模式下观看 Metal App 时 系统仅渲染当前沉浸级别内的内容
这是以完全沉浸模式渲染的游戏的 场景示例 这是观众调节数码旋钮后 在部分沉浸模式下 渲染的同一个场景 比较这两个场景 可以注意到 节省算力的方式是不渲染 传送门外的高亮显示区域 这部分看不到 所以没有必要渲染 新的 API 让你可以使用 系统计算的传送门模板来遮挡内容 这个白色椭圆显示了相应的 传送门模板 模板缓冲区用作渲染场景的遮罩 有了它 就只会渲染传送门内的内容 可以看到正在渲染的场景的边缘 还不太平滑 作为命令缓冲区的最后一步 系统将 采用淡入淡出效果 从而产生观众看到的场景
要使用模板来避免渲染不必要的内容 首先要配置合成器层 确保图层功能支持所需的模板格式 并在配置中进行设置 如要应用传送门模板遮罩 需要通过 命令缓冲区向可绘制对象添加 渲染上下文 在模板上绘制遮罩 将阻止渲染任何不可见的像素 你还必须通过渲染上下文结束编码 而不是直接结束命令编码器 这样 传送门效果将有效地应用到 内容上 在 App 中 我在 SwiftUI 中创建了 沉浸式空间并将 渐进式沉浸样式作为新选项 添加到列表中 App 用户可以在渐进和完整样式之间 切换 接下来配置图层 首先请注意渐进式沉浸样式 仅适用于分层布局 我将所需的模板格式指定为 每像素 8 位 我检查功能是否支持这个格式并 在配置中设置好 我还将样本计数设置为 1 因为没有使用 MSAA 如果使用这个技术 就设置为 MSAA 采样计数
在渲染器中 我为可绘制对象添加了 渲染上下文 并传递了将用于渲染命令的相同命令 缓冲区 然后在模板附件上绘制传送门遮罩 我选择了在任何其他模板操作中 不会使用的模板值 并在渲染编码器上设置了模板参考值 这样 渲染器就不会绘制当前 沉浸级别之外的区域 渲染场景后 请注意我是如何在可 绘制对象渲染上下文中结束编码的
要查看使用渐进式沉浸样式的 渲染器的工作示例 在 visionOS Metal App 模板中 选择“Progressive”选项 这将引导你开始构建传送门样式的 Metal App
最后我们来了解一下 macOS 空间渲染
目前为止 我一直在谈论在 Vision Pro 上构建原生沉浸式体验 今年 你可以利用 Mac 的强大功能 渲染沉浸式内容并 直接流式传输到 Vision Pro 这可用于为现有的 Mac App 增添 沉浸式体验
例如 3D 建模 App 可以直接在 Vision Pro 上预览场景 或者 你也可以从头开始构建沉浸式 macOS App 这样 你可以制作具有高计算需求的 复杂沉浸式体验 同时不受 Vision Pro 的用电限制 很容易从 Mac App 启动 远程沉浸式会话 在 macOS 中打开沉浸式空间时 系统会提示你在 Vision Pro 上 接受连接
这样做 你就会开始看到 Mac 渲染的 沉浸式内容
典型的 Mac App 是用 SwiftUI 或 AppKit 构建的 你可以使用这两个框架中的 任何一个来创建和显示 Windows 系统使用 Core Animation 渲染 窗口内容 你可以采用各种 macOS 框架来实现 App 的功能 系统随后会在 Mac 显示屏上显示 你的内容 要打造 Mac 支持的沉浸式体验 你将使用与创建沉浸式 visionOS App 相同的熟悉框架 首先将 SwiftUI 与新的远程沉浸式 空间场景类型搭配使用 然后采用 Compositor Services 框架 使用 ARKit 和 Metal 来放置和 渲染内容 系统可直接在 Vision Pro 上显示 沉浸式场景 macOS RemoteImmersiveSpace 托管了 Compositor Layer 和 ARKit 会话 就像本地 visionOS App 一样 它们可以无缝连接到 Vision Pro 显示器和传感器 为了将 ARKit 会话连接到 visionOS 有个新的 remoteDeviceIdentifier SwiftUI 环境对象 需要传递给会话构造器 这是 Mac 沉浸式 App 的结构
我定义了一个新的 RemoteImmersiveSpace 其中包含合成器内容 我稍后将展示它如何使用合成器层 在 Mac 上 仅支持渐进式和完全 沉浸样式 在 Mac App 的界面中 我使用新的 supportsRemoteScenes 环境变量 检查 Mac 是否具有这项功能 我可以自定 UI 以便在不支持远程 场景时显示一条信息 如果支持 而沉浸式空间还没有打开 那么可以启动 App 的最后一部分是合成器内容 它有合成层和 ARKit 会话 我创建和使用合成层的方法与在 visionOS 上的相同 我访问新 remoteDeviceIdentifier SwiftUI 环境对象 并将它传递给 ARKit 会话构造器 这样 Mac 的 ARKit 会话就会连接到 Vision Pro 最后启动渲染循环 就像在典型的 Metal 沉浸式 App 上一样
ARKit 和全球追踪服务提供程序现已 在 macOS 上推出 这样 你可以查询 Vision Pro 在空间中的位置 就像在原生沉浸式 App 中一样 你利用设备位姿 在渲染之前更新场景和可绘制对象 macOS 空间 App 支持连接到 Mac 的任何输入设备 你可以使用键盘和鼠标控制 或者你也可以连接游戏手柄 并使用 Game Controller 框架 处理输入 另外 还可以在沉浸式场景的交互式 元素上使用捏合事件 方法是在图层渲染器上使用 “onSpatialEvent”修饰符
新功能是 还可以利用现有的 AppKit 或 UIKit App 创建 SwiftUI 场景 这是将新沉浸式体验添加到现有的 Mac App 的绝佳方式 要进一步了解这方面的内容 请观看 “SwiftUI 的新功能”
渲染引擎通常用 C 或 C++ 实现 我解释过的所有 API 在 C 语言中 都有原生对应物 Compositor Services 框架的 C 类型以“cp”前缀开头 它们采用了与常见的 C 库 如 Core Foundation 类似的模式和 惯例 对于 ARKit 的 cDevice 属性 提供兼容 C 语言的远程设备标识符 你可以将它传入 C 框架中并使用 create_with_device 函数 初始化 ARKit 会话 现在 你已经拥有使用 Mac 的 所有东西 在 Vision Pro 上支持沉浸式内容
我很期待看到大家利用这些新功能 来增强你们的沉浸式 App 这些功能可提供更好的交互性、 更高的保真度 以及全新的渐进式沉浸样式 我迫不及待想看到大家利用新的 macOS 空间功能做些什么 要进一步了解如何将沉浸式 App 提升到新的水平 请观看 “借助 SwiftUI 在 visionOS 中设置场景” 如需大致了解其他平台改进内容 请观看“visionOS 的新功能” 感谢大家观看!
-
-
0:01 - Scene render loop
// Scene render loop extension Renderer { func renderFrame(with scene: MyScene) { guard let frame = layerRenderer.queryNextFrame() else { return } frame.startUpdate() scene.performFrameIndependentUpdates() frame.endUpdate() let drawables = frame.queryDrawables() guard !drawables.isEmpty else { return } guard let timing = frame.predictTiming() else { return } LayerRenderer.Clock().wait(until: timing.optimalInputTime) frame.startSubmission() scene.render(to: drawable) frame.endSubmission() } }
-
5:54 - Layer configuration
// Layer configuration struct MyConfiguration: CompositorLayerConfiguration { func makeConfiguration(capabilities: LayerRenderer.Capabilities, configuration: inout LayerRenderer.Configuration) { // Configure other aspects of LayerRenderer let trackingAreasFormat: MTLPixelFormat = .r8Uint if capabilities.supportedTrackingAreasFormats.contains(trackingAreasFormat) { configuration.trackingAreasFormat = trackingAreasFormat } } }
-
7:54 - Object render function
// Object render function extension MyObject { func render(drawable: Drawable, renderEncoder: MTLRenderCommandEncoder) { var renderValue: LayerRenderer.Drawable.TrackingArea.RenderValue? = nil if self.isInteractive { let trackingArea = drawable.addTrackingArea(identifier: self.identifier) if self.usesHoverEffect { trackingArea.addHoverEffect(.automatic) } renderValue = trackingArea.renderValue } self.draw(with: commandEncoder, trackingAreaRenderValue: renderValue) } }
-
8:26 - Metal fragment shader
// Metal fragment shader struct FragmentOut { float4 color [[color(0)]]; uint16_t trackingAreaRenderValue [[color(1)]]; }; fragment FragmentOut fragmentShader( /* ... */ ) { // ... return FragmentOut { float4(outColor, 1.0), uniforms.trackingAreaRenderValue }; }
-
10:09 - Event processing
// Event processing extension Renderer { func processEvent(_ event: SpatialEventCollection.Event) { let object = scene.objects.first { $0.identifier == event.trackingAreaIdentifier } if let object { object.performAction() } } }
-
13:08 - Quality constants
// Quality constants extension MyScene { struct Constants { static let menuRenderQuality: LayerRenderer.RenderQuality = .init(0.8) static let worldRenderQuality: LayerRenderer.RenderQuality = .init(0.6) static var maxRenderQuality: LayerRenderer.RenderQuality { menuRenderQuality } } }
-
13:32 - Layer configuration
// Layer configuration struct MyConfiguration: CompositorLayerConfiguration { func makeConfiguration(capabilities: LayerRenderer.Capabilities, configuration: inout LayerRenderer.Configuration) { // Configure other aspects of LayerRenderer if configuration.isFoveationEnabled { configuration.maxRenderQuality = MyScene.Constants.maxRenderQuality } }
-
13:57 - Set runtime render quality
// Set runtime render quality extension MyScene { var renderQuality: LayerRenderer.RenderQuality { switch type { case .world: Constants.worldRenderQuality case .menu: Constants.menuRenderQuality } } } extension Renderer { func adjustRenderQuality(for scene: MyScene) { guard layerRenderer.configuration.isFoveationEnabled else { return; } layerRenderer.renderQuality = scene.renderQuality } }
-
16:58 - SwiftUI immersion style
// SwiftUI immersion style @main struct MyApp: App { @State var immersionStyle: ImmersionStyle var body: some Scene { ImmersiveSpace(id: "MyImmersiveSpace") { CompositorLayer(configuration: MyConfiguration()) { @MainActor layerRenderer in Renderer.startRenderLoop(layerRenderer) } } .immersionStyle(selection: $immersionStyle, in: .progressive, .full) } }
-
17:12 - Layer configuration
// Layer configuration struct MyConfiguration: CompositorLayerConfiguration { func makeConfiguration(capabilities: LayerRenderer.Capabilities, configuration: inout LayerRenderer.Configuration) { // Configure other aspects of LayerRenderer if configuration.layout == .layered { let stencilFormat: MTLPixelFormat = .stencil8 if capabilities.drawableRenderContextSupportedStencilFormats.contains( stencilFormat ) { configuration.drawableRenderContextStencilFormat = stencilFormat } configuration.drawableRenderContextRasterSampleCount = 1 } } }
-
17:40 - Render loop
// Render loop struct Renderer { let portalStencilValue: UInt8 = 200 // Value not used in other stencil operations func renderFrame(with scene: MyScene, drawable: LayerRenderer.Drawable, commandBuffer: MTLCommandBuffer) { let drawableRenderContext = drawable.addRenderContext(commandBuffer: commandBuffer) let renderEncoder = configureRenderPass(commandBuffer: commandBuffer) drawableRenderContext.drawMaskOnStencilAttachment(commandEncoder: renderEncoder, value: portalStencilValue) renderEncoder.setStencilReferenceValue(UInt32(portalStencilValue)) scene.render(to: drawable, renderEncoder: renderEncoder) drawableRenderContext.endEncoding(commandEncoder: commandEncoder) drawable.encodePresent(commandBuffer: commandBuffer) } }
-
20:55 - App structure
// App structure @main struct MyImmersiveMacApp: App { @State var immersionStyle: ImmersionStyle = .full var body: some Scene { WindowGroup { MyAppContent() } RemoteImmersiveSpace(id: "MyRemoteImmersiveSpace") { MyCompositorContent() } .immersionStyle(selection: $immersionStyle, in: .full, .progressive) } }
-
21:14 - App UI
// App UI struct MyAppContent: View { @Environment(\.supportsRemoteScenes) private var supportsRemoteScenes @Environment(\.openImmersiveSpace) private var openImmersiveSpace @State private var spaceState: OpenImmersiveSpaceAction.Result? var body: some View { if !supportsRemoteScenes { Text("Remote SwiftUI scenes are not supported on this Mac.") } else if spaceState != nil { MySpaceStateView($spaceState) } else { Button("Open remote immersive space") { Task { spaceState = await openImmersiveSpace(id: "MyRemoteImmersiveSpace") } } } } }
-
21:35 - Compositor content and ARKit session
// Compositor content and ARKit session struct MyCompositorContent: CompositorContent { @Environment(\.remoteDeviceIdentifier) private var remoteDeviceIdentifier var body: some CompositorContent { CompositorLayer(configuration: MyConfiguration()) { @MainActor layerRenderer in guard let remoteDeviceIdentifier else { return } let arSession = ARKitSession(device: remoteDeviceIdentifier) Renderer.startRenderLoop(layerRenderer, arSession) } } }
-
23:17 - C interoperability
// Swift let remoteDevice: ar_device_t = remoteDeviceIdentifier.cDevice Renderer.start_rendering(layerRenderer, remoteDevice) // C void start_rendering(cp_layer_renderer_t layer_renderer, ar_device_t remoteDevice) { ar_session_t session = ar_session_create_with_device(remoteDevice); // ... }
-
-
- 0:00 - 简介
visionOS 中的 Metal 渲染与 Compositor Services 在今年带来了多项令人期待的新特性,包括:可交互对象的悬停效果、渲染内容的动态渲染质量、全新推出的渐进式沉浸样式,以及可直接从 macOS 向 Apple Vision Pro 渲染沉浸式内容的功能。
- 1:58 - 全新渲染循环 API
今年,visionOS 的渲染循环机制迎来了一项重要变更。queryDrawables 对象现在不会返回单个可绘制对象,而是返回一个包含一个或两个可绘制对象的数组。当你使用 Reality Composer Pro 录制高品质视频时,第二个可绘制对象就会出现。你可以在 Xcode 中找到相应的模板,帮助你快速上手。这个功能兼容 Metal 和 Metal 4。
- 4:21 - 悬停效果
在采用全新的渲染循环 API 后,就可以开始为可交互对象实现悬停效果。通过悬停效果,用户可以清楚地识别哪些对象是可交互的,并提前预判它们操作的目标。系统会动态高亮显示用户当前注视的对象。例如,拼图游戏可以突出那些可由玩家选择的拼图块。你可以使用全新的追踪区域纹理来实现这个功能,它用于定义场景中的不同交互区域。如果你使用了 MSAA (多重采样抗锯齿),还需留意一些额外的注意事项。
- 10:50 - 动态渲染质量
现在,你还能够以比之前更高的保真度来绘制内容。借助动态渲染质量,你可以根据场景的复杂程度来调整内容的分辨率。它建立在注视点渲染的基础上,注视点渲染优先考虑观看者注视处的像素密度。你可以设置一个最大渲染质量,然后在这个范围内调整运行时质量。较高的渲染质量有助于提升文字与用户界面的清晰度,但也会增加内存占用与电量消耗。因此,在质量与性能之间取得平衡至关重要。建议使用 Instruments 和 Metal 调试器等工具找到合适的平衡点。
- 14:44 - Progressive 沉浸
今年全新推出的功能之一是,你可以在渐进式沉浸传送门中渲染内容。通过这一模式,用户可以通过旋转数码旋钮来控制沉浸程度。这种方式让用户能够与现实环境建立联系,在观看包含动态元素的复杂场景时,能让用户感觉更加舒适自在。 要实现这个功能,需要请求系统提供一个模板缓冲区,用于遮罩传送门边界之外的内容。系统会在传送门边缘应用淡入淡出效果,使现实环境与渲染环境之间的过渡自然顺畅。同时,传送门视野之外的像素不会被渲染,从而节省计算资源。具体的实现细节将会进行介绍。
- 18:32 - macOS 空间渲染
macOS 空间渲染功能让你能够充分发挥 Mac 的强大功能,将沉浸式内容直接渲染和流式传输到 Apple Vision Pro。借助这一新功能,你可以为现有 Mac App 添加沉浸式体验,例如实时 3D 建模预览等。ARKit 和 worldTrackingProvider 现已在 macOS 上推出。这样,你就能够查询 Apple Vision Pro 在空间中的位置。 macOS 上的 RemoteImmersiveSpace 会托管 CompositorLayer 和 ARKitSession,就像原生 visionOS App 那样。你会使用新的 remoteDeviceIdentifier 将 Mac 的 ARKit 会话连接至 Apple Vision Pro。所有相关 API 在 C 语言中都有原生的对应版本。
- 23:51 - 后续步骤
visionOS 中 Metal 与 Compositor Services 的这些新功能,可以帮助你为 App 和游戏带来更出色的交互效果、更高的保真度,以及全新的渐进式沉浸样式。接下来,请观看讲座“借助 SwiftUI 在 visionOS 中设置场景”和“visionOS 26 的新功能”。