大多数浏览器和
Developer App 均支持流媒体播放。
-
在 visionOS 中使用透视功能渲染 Metal
准备好扩展适用于 visionOS 的 Metal 体验吧。了解利用透视功能将渲染的内容与用户所处的物理环境整合起来的推荐做法。了解如何放置渲染的内容以使其完美融入现实世界、使用可跟踪锚点预测来降低延迟,等等。
章节
- 0:00 - Introduction
- 1:49 - Mix rendered content with surroundings
- 9:20 - Position render content
- 14:49 - Trackable anchor prediction
- 19:22 - Wrap-up
资源
- Forum: Graphics & Games
- How to start designing assets in Display P3
- Improving Rendering Performance with Vertex Amplification
- Interacting with virtual content blended with passthrough
- Metal Developer Resources
- Rendering a Scene with Deferred Lighting in Swift
- Rendering at Different Rasterization Rates
相关视频
WWDC24
WWDC23
-
下载
大家好 欢迎各位 我叫 Pooya 是 Apple 的工程师 今天 我将展示如何 充分利用 visionOS 的强大功能 将 Metal 从完全沉浸样式 扩展到混合式沉浸 最棒的是 将 Metal 扩展到 混合式沉浸所用的工具和框架 很可能正是你已经非常熟悉的那些
去年 我们展示了如何使用 ARKit、 Metal 和 Compositor Services 通过你自己的渲染引擎 来提供完全沉浸式体验 而今年 我很高兴为你介绍 Metal 如何帮助你 以混合沉浸样式进行渲染 从而模糊现实世界 与想象力之间的界线
你可能已经在使用 Compositor Services 来实现 自己的渲染引擎 并将 Metal 和 ARKit 作为 RealityKit 中的替代渲染方法 你可以使用 Compositor Services 来创建渲染会话 使用 Metal API 来渲染精美图像帧 还可以使用 ARKit 来实现 现实场景跟踪和手部跟踪 在今天的讲座中 我将介绍如何使用 Metal、 ARKit 和 CompositorServices API 来创建能够提供 混合沉浸式体验的应用程序 第一步 是将渲染的 App 内容 与周围的物理环境无缝融合 接下来 我将展示如何改进渲染内容 相对于物理环境的定位 最后 我将讲解如何获取和使用 可跟踪锚点位姿的 正确预测时间 例如 当你跟踪某个人的手部 以及这双手与之交互的 虚拟物体时 接下来 我要介绍第一步 将渲染内容与周围环境进行混合 如果你之前有过 使用 Compositor Services API、 Metal API 和 Metal 渲染技术的经验 你就能充分利用 这次讲座介绍的内容 如果你以前从未使用过这些工具 可以观看我们之前发布的一些视频 还可以访问 developer.apple.com/cn/ 获取代码示例和文档 我们这就开始吧
在混合沉浸样式中 用户既能看到渲染内容 也能看到周围的物理环境 你可以完成以下几个步骤来确保 混合沉浸效果尽可能逼真 首先 将可绘制对象的纹理 清除至正确的值 这个值将会不同于 可能在完全沉浸式样式中使用过的值 第二步是确保你的渲染管道 能够生成正确的颜色值和景深值 比如预乘 alpha 以及 visionOS 应有的 P3 色域
然后 使用 ARKit 提供的 场景理解功能 将渲染内容锚定在现实世界中 并进行物理模拟 最后 指定适合你的 App 体验的 上肢可见性类型 要让 App 采用混合沉浸样式 这个过程的第一步 非常简单 只需在设置 immersionStyle 时 添加 mixed 作为选项即可 默认情况下 SwiftUI 会创建窗口场景 即使 App 中的第一个场景 是 ImmersiveSpace 也是如此 你可以将 PreferredDefaultSceneSessionRole 键 添加到应用程序场景清单中 来更改默认场景行为 如果你要使用包含 Compositor Space Content 的空间 则需要使用 CPSceneSessionRoleImmersiveApplication 你还可以根据应用程序场景 调整 InitialImmersionStyle 键 如果你打算启动进入混合沉浸 则需要使用 UIImmersionStyleMixed
在渲染循环开始时 应该清除可绘制对象的纹理 正确的颜色值 会根据渲染时采用的沉浸样式 而有所变化 以下是一个典型的渲染循环 App 获取了一个新的可绘制对象 通过载入和清除操作来配置管道状态 对它的 GPU 工作负载进行编码 呈现可绘制对象 最后提交
在编码阶段 首先需要 清除颜色和景深纹理 因为渲染可能不会 处理纹理中的每个像素 景深值始终应该清除至 0 值 但是 正确的颜色纹理值 具体取决于 所使用的沉浸样式 在完全沉浸样式中 应该 将颜色纹理清除至 (0,0,0,1) 在混合沉浸中 应该一律使用 0 值 以下是相关代码 在这个示例中 我创建了 renderPassDescriptor 并定义了它的颜色和景深纹理 然后 我调整了载入和储存操作 以确保在开始渲染之前清除纹理 我已将景深和颜色附加项 全部清除至 0 值 因为这个示例需要使用混合沉浸样式 所以将颜色纹理中的 Alpha 值设置为 0
接下来 确认 App 采用支持的惯例来渲染颜色值
你可能已经非常熟悉 alpha 融合了 这种方法用于以逼真的方式 将背景和前景中的纹理结合在一起 visionOS 上的颜色管道采用的是 预乘 alpha 颜色惯例 这意味着你会 先将着色器中的颜色通道 乘以 alpha 值 然后再 将它传递给 Compositor Services
visionOS 颜色管道 在 P3 显示屏色域中运行 为了提高 App 渲染内容 和透视之间的颜色一致性 素材应该处于显示屏 P3 色域内 如需了解更多信息 请参考 developer.apple.com/cn/ 上的文档
Compositor Services 同时使用 渲染器提供的 颜色和景深纹理 来进行合成操作 值得注意的是 Compositor Services 需要景深纹理 采用反向 Z 轴惯例 给定像素的渲染内容 如果具有大于 0 的 alpha 通道 和有效的透视景深值 就会显示在显示屏上 为了避免出现视差效果 渲染器需要提供 所有像素的正确景深值 此外 为了提升系统性能 对于 alpha 值为 0 的像素 渲染器应该传递 深度值 0 接下来 使用 ARKit 提供的数据 将场景理解与渲染内容整合 在混合沉浸样式中 用户可以同时看到渲染内容 和周围的物理环境 为了确保体验更逼真 需要将场景理解整合到 App 的渲染逻辑中 借助 ARKit 你可以将渲染内容 与现实世界中的物体和表面锚定 执行所需的任何物理模拟 以实现渲染内容 与环境中物理对象之间的 逼真交互 或者遮挡显示在物理对象后面的 任何渲染内容 要进一步了解 visionOS 上的 ARKit 请观看 “了解用于空间计算的 ARKit” 快要完成了! 最后一步是指定 visionOS 对用户双手和双臂 可见性的处理方式 称为“上肢可见性” 在混合沉浸样式中 上肢可见性有三种模式可供选择 第一种模式是 Visible 如果选择 Visible 模式 则无论对象的相对景深如何 手部会始终显示在上方
第二种模式是 Hidden 在这个模式下 手部会 始终被渲染内容遮挡 最后 还可以选择 Automatic 模式 在这个模式下 手部可见性 会根据渲染内容的景深 而发生变化 如果手部位于对象前方 就会显示出来 让用户看见 如果手部的景深增加 就会逐渐隐去 系统会为你代劳 我们来详细了解一下 这其中的工作方式 假设佩戴头显的用户 采用了混合沉浸样式 场景中有两个渲染对象 红色圆形和黄色立方体 用户的手部也位于视野范围内 渲染场景之后 可绘制对象的 景深和颜色纹理 就是这样的 此外 值得强调的是 在 Compositor Services API 中 渲染器提供的深度纹理 应该具有反向 Z 值 如果你为上肢可见性 指定了 Automatic 模式 Compositor Services 就会使用 你为纹理设置的景深值 框架会利用这些值 根据手部位于对象前方 来确定手部应该部分隐藏 如果手部位于对象后方或内部 则确定手部应该完全可见
以下是实现这项功能的代码 使用 upperLimbVisibility 可以让系统知道 你的 App 请求的是 哪一种上肢可见性模式 在这里 我请求的是 Automatic 模式 在视觉效果上将渲染内容 与周围物理环境进行混合后 务必要确定内容的位置 让内容看起来与环境浑然一体 为此 你需要首先将内容 从现实环境坐标空间 变换到归一化设备坐标空间 我将展示如何利用 具有场景感知能力的投影矩阵 来改进内容的定位 然后 我将介绍 Compositor Services 框架输入 支持哪个归一化设备坐标惯例 最后 我将提到一些 可在你的渲染引擎中使用的 替代中间惯例 在渲染对象时 就 Vision Pro 的 位置和方向而言 你需要将 3D 现实场景空间 中的内容变换到 2.5D 空间 也称为 归一化设备坐标空间 这个矩阵称为 ProjectionViewMatrix ProjectionViewMatrix 结合了 ProjectionMatrix 和 ViewMatrix ViewMatrix 可以 扩展为两种变换 第一种变换是从原始空间 到设备空间的变换 称为 deviceFromOrigin 第二种变换是从设备空间 到观察空间的变换 称为 viewFromDevice
为了与 visionOS API 相匹配 ProjectionViewMatrix 等于 ProjectionMatrix 乘以 originFromDevice 与 deviceFromView 相乘的逆矩阵 deviceFromView 变换可通过 在 Compositor Services API 中 调用 cp_view_get_transform 来获得 这个方法会返回 能够将渲染视图引入设备空间的矩阵 对于 originFromDevice 当你调用 ar_anchor_get_origin_from_anchor 时 ARKit API 会提供这一数据 今年 为了改进渲染内容 相对于现实场景对象的定位 你可以获取 具有场景感知能力的投影矩阵 这个矩阵可将两个相机内在函数 与实时逐帧场景理解因素相结合 通过改进渲染内容 相对于现实场景对象的定位 来优化混合沉浸样式的使用体验 如果应用程序通过 Compositor Services 请求混合沉浸样式 则必须使用这个新的 API 将内容从现实场景空间 变换到纹理坐标空间之后 App 的可绘制对象颜色和景深纹理 渲染后的效果就是这样 在纹理空间中 X 轴的方向为从左到右 Y 轴的方向为从上向下 请注意 深度值应该采用 反向 Z 轴惯例
鉴于 visionOS 使用的是 注视点颜色管道 我们来进一步了解一下 这就是可绘制对象纹理 在屏幕上和物理空间中的效果 这意味着物理空间的尺寸小于 屏幕空间纹理的尺寸 屏幕空间是用户感知这些值的位置 而物理空间是内存中 储存实际值的位置
要进一步了解注视点渲染的工作方式 请访问 developer.apple.com/cn/ 观看“以不同的光栅化速率进行渲染” 我们来更深入地了解一下 Compositor Services 需要的 可绘制对象归一化设备坐标空间 归一化设备坐标空间 有 3 个坐标轴 水平方向上的 X 轴 垂直方向上的 Y 轴以及正交绕序 所有这三个轴都会影响 渲染器的方向
Compositor Services 需要 使用归一化设备坐标空间 来渲染颜色和景深这两种纹理 以便让 X 轴方向为从左到右 让 Y 轴方向为从下到上 让绕序方向为从前到后 但是 你的渲染引擎可能会在 其他归一化设备坐标空间中 渲染中间纹理 Compositor Services 还可以 采用多种惯例 来提供场景感知投影矩阵 翻转 Y 轴或翻转绕序 从根本上说 渲染引擎需要负责 确保最终的可绘制对象纹理 采用 Compositor Services 需要的惯例 接下来 我要通过深入探讨 一个代码示例来详细说明 如何通过 visionOS API 构建投影视图矩阵 获得给定显示时间的 deviceAnchor 之后 应该将 deviceAnchor 传递给 Compositor Services API
对于给定的 deviceAnchor 现在可以遍历 可绘制对象上的视图 计算出 每个视图的 projectionViewMatrix 首先 获取相应的视图 然后使用 ARKit API 获取 originFromDevice 变换 然后使用 Compositor Services API 获取 deviceFromView 变换 然后构建由这两者组成的 viewMatrix 这个矩阵会提供原点处的视图 调用 computeProjection API 并采用 Compositor Services 需要的惯例 .rightUpBack space 来获取给定视图的 投影视图 最终通过将投影与视图矩阵变换相乘 构建出一个 projectionViewMatrix 还要注意一点 你的应用程序 应该在每一帧都获取一次这些变换 其中任一变换都不应重复使用 来自较早图像帧的变换 确定渲染内容的位置之后 这时候就需要思考用户与它交互时 会发生什么 锚点表示了现实世界中的位置和方向 可跟踪锚点是一种实体列表 系统可能会在会话过程中 获得或丢失这些实体的 跟踪状态 可以使用可跟踪锚点来定位渲染内容 例如一个人的手 是可跟踪的锚点实体 要设置可跟踪锚点 首先需要 授权与 ARKit 进行连接 你需要通过会话 请求你想要访问的 数据类型的授权 接下来 设置数据提供程序 通过数据提供程序 你可以 轮询或观察锚点变化等 数据更新 最终通过 ARKit API 获取可跟踪锚点 以每帧一次的频率 在渲染循环内 首先更新渲染器的逻辑 然后将数据提交到 Compositor Services 我们来详细了解一下渲染循环 它由两个阶段组成 第一个阶段是更新阶段 其中包括模拟端的逻辑 比如交互和模拟物理定律 这个阶段通常发生在 CPU 端 你需要获取可跟踪锚点 和设备位姿这两者 才能正确计算模拟逻辑 第二个阶段是提交阶段 会将最终结果渲染到纹理中 这个阶段发生在 GPU 端 随着图像帧显示时间 接近调用预测查询的时间 锚点位姿预测函数的准确性 也会有所提高 为了获得更准确的锚定结果 需要再次查询锚点和设备位姿 以确保结果尽可能准确 这个示意图界定了 时序对图像帧不同部分的影响 你可以从 Compositor Services 获取四种时序 图像帧时间线表示 应用程序正在完成的工作
最佳输入时间是指 App 完成交互和物理定律模拟等 非关键任务的时间 在更新阶段开始时 App 会查询可跟踪锚点 和设备锚点 最佳输入时间刚过的时候 是查询延迟关键型输入 并开始渲染图像帧的最佳时机 渲染截止时间是指 CPU 和 GPU 渲染一帧的工作应该完成的时间 可跟踪锚点时间是指 摄像头观察周围环境的时间 对于可跟踪锚点 应该利用这个时间来进行锚点预测 最后 显示时间是指 图像帧在显示屏上显示的时间 这个时间应该用于进行设备锚点预测 想象一下设备视野中 有这样一个场景 有一个红色球体被用户拿在手中 还有一个黄色立方体放置在环境中 进行渲染后 这个场景 在设备显示屏上的效果是这样的 现在 对于任何 没有附加到可跟踪锚点的渲染内容 比如这个黄色立方体 可以通过查询显示时间的设备位姿 来计算它相对于设备变换的变换 但是 对于任何 附加到可跟踪锚点的渲染内容 例如红色球体 则需要 同时使用可跟踪锚点位置 和设备位置 对于可跟踪锚点预测 请使用可跟踪锚点时间 接下来 我要深入探讨一个代码示例 在渲染循环中 在获取头部位姿 和可跟踪锚点位姿之前 应用程序应该完成它的 非关键工作负载 这有助于提高预测准确性 非关键工作完成之后 应用程序会在最佳时间过后 开始运行它的工作负载 依赖于锚点的工作负载开始运行时 你应该会首先从帧时序数据中 获取 presentationTime 和 trackableAnchorTime
然后将这些时间戳转换为秒
使用 presentationTime 查询 deviceAnchor 并使用 trackableAnchorTime 查询可跟踪锚点 然后 如果跟踪到可跟踪锚点 则执行相对于它的位置的逻辑 如需进一步了解 ARKit API 可以观看视频 “使用 ARKit 打造 更出色的空间计算体验” 现在 你已拥有所有必要的工具 能够在 visionOS 上打造 令人惊叹的混合沉浸体验 借助 Compositor Services 和 Metal 你可以设置渲染循环 并显示 3D 内容 最后 你可以使用 ARKit 来实现交互式体验 如需了解更多信息 请参考这些视频 感谢观看!
-
-
3:07 - Add mixed immersion
@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() } .immersionStyle(selection: $style, in: .mixed, .full) } } }
-
4:43 - Create a renderPassDescriptor
let renderPassDescriptor = MTLRenderPassDescriptor() renderPassDescriptor.colorAttachments[0].texture = drawable.colorTextures[0] renderPassDescriptor.colorAttachments[0].loadAction = .clear renderPassDescriptor.colorAttachments[0].storeAction = .store renderPassDescriptor.colorAttachments[0].clearColor = .init(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) renderPassDescriptor.depthAttachment.texture = drawable.depthTextures[0] renderPassDescriptor.depthAttachment.loadAction = .clear renderPassDescriptor.depthAttachment.storeAction = .store renderPassDescriptor.depthAttachment.clearDepth = 0.0
-
9:08 - Set Upper Limb Visibility
@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() } .immersionStyle(selection: $style, in: .mixed, .full) .upperLimbVisiblity(.automatic) } } }
-
13:37 - Compose a projection view matrix
func renderLoop { //... let deviceAnchor = worldTracking.queryDeviceAnchor(atTimestamp: presentationTime) drawable.deviceAnchor = deviceAnchor for viewIndex in 0...drawable.views.count { let view = drawable.views[viewIndex] let originFromDevice = deviceAnchor?.originFromAnchorTransform let deviceFromView = view.transform let viewMatrix = (originFromDevice * deviceFromView).inverse let projection = drawable.computeProjection(normalizedDeviceCoordinatesConvention: .rightUpBack, viewIndex: viewIndex) let projectionViewMatrix = projection * viewMatrix; //... } }
-
18:27 - Trackable anchor prediction
func renderFrame() { //... // Get the trackable anchor and presentation time. let presentationTime = drawable.frameTiming.presentationTime let trackableAnchorTime = drawable.frameTiming.trackableAnchorTime // Convert the timestamps into units of seconds let devicePredictionTime = LayerRenderer.Clock.Instant.epoch.duration(to: presentationTime).timeInterval let anchorPredictionTime = LayerRenderer.Clock.Instant.epoch.duration(to: trackableAnchorTime).timeInterval let deviceAnchor = worldTracking.queryDeviceAnchor(atTimestamp: devicePredictionTime) let leftAnchor = handTracking.handAnchors(at: anchorPredictionTime) if (leftAnchor.isTracked) { //... }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。