在 visionOS 中用 Metal 渲染混合沉浸式内容
Render Metal with passthrough in visionOS
2024年6月10日
一句话判断
visionOS 2 把 Metal 渲染从 Full Immersion 扩展到了 Mixed Immersion——你的自定义渲染内容现在可以跟用户的真实环境无缝融合,关键是要搞清楚 clear color、pre-multiplied alpha 和 upper limb visibility 这三个跟 Full Immersion 完全不同的行为。
这场 Session 讲了什么
去年 visionOS 1 的 Metal 渲染只支持 Full Immersion 模式(完全遮挡真实环境),今年新增了 Mixed Immersion 模式——用户可以同时看到真实环境和你的虚拟内容。这场 Session 由 Pooya 主讲,聚焦在三个核心技术点。
第一,渲染混合。Mixed Immersion 模式下你的渲染内容需要跟真实环境无缝融合,这要求你正确处理 clear color(全零而非黑色)、颜色空间(Display P3)、alpha 混合(pre-multiplied alpha)和深度值(reverse Z)。
第二,空间定位。利用 ARKit 的 scene understanding 数据,把虚拟内容锚定到真实物体和表面上,执行物理模拟,处理遮挡关系。
第三,手部可见性。Mixed Immersion 下用户的手和你的虚拟内容会同时出现在视野中,你需要决定手的可见性策略——Visible(始终可见)、Hidden(始终被虚拟内容遮挡)还是 Automatic(根据深度自动切换)。
值得深挖的点
Mixed Immersion 的 clear color 为什么是全零而不是黑色
在 Full Immersion 模式下,你的 clear color 是 (0, 0, 0, 1)——黑色完全不透明,因为整个视野都是你的渲染内容,没有真实环境需要透出来。但在 Mixed Immersion 模式下,Compositor Services 会把你的渲染内容和 Passthrough 视频合成在一起。如果你把 alpha 清成 1,你的渲染内容就会完全遮挡真实环境,等于白做了 Mixed Immersion。
正确做法是把 clear color 设为 (0, 0, 0, 0)——全零。alpha 为 0 的像素表示”这里没有虚拟内容”,Compositor Services 会直接显示真实环境。alpha 大于 0 的像素则是虚拟内容,会根据 alpha 值和深度值与真实环境做混合。
这背后的 compositing 管线使用 pre-multiplied alpha:你的 shader 在输出颜色时需要先把 RGB 通道乘以 alpha 值。这意味着半透明的红色不是 (1, 0, 0, 0.5),而是 (0.5, 0, 0, 0.5)。如果你之前在 Full Immersion 模式下没严格遵守这个约定(反正背景是黑色,差异不明显),切到 Mixed Immersion 后颜色就会明显不对。
Upper Limb Visibility:三个模式的取舍
这是 Mixed Immersion 独有的新问题。当用户的手和虚拟物体在空间中重叠时,怎么处理谁在前面?
Visible 模式最简单:手永远在最上层。适合工具类应用,用户的手是你的主要输入方式,不应该被遮挡。
Hidden 模式相反:手永远被虚拟内容遮挡。适合沉浸式展示场景,用户的手不是主要交互方式。
Automatic 模式最有趣:Compositor Services 根据你提供的深度值自动判断。手在虚拟物体前面就显示,在后面就渐隐。实现方式是 Compositor Services 读取你的 depth texture(reverse Z),用深度值跟手的真实深度做比较。
选择 Automatic 的前提是你的深度值必须准确。如果你的虚拟物体有像素的深度值是错的(比如应该有深度但写成了 0),手的遮挡就会出错。Session 特别强调了:对于 alpha 为 0 的像素,depth 也必须设为 0,否则会产生视差伪影。
代码片段
配置 Mixed Immersion 的 clear color
let renderPassDescriptor = MTLRenderPassDescriptor()
// 配置颜色附件
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].storeAction = .store
// Mixed Immersion: clear color 必须是全零
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(
red: 0, green: 0, blue: 0, alpha: 0
)
// 配置深度附件
renderPassDescriptor.depthAttachment.texture = depthTexture
renderPassDescriptor.depthAttachment.loadAction = .clear
renderPassDescriptor.depthAttachment.storeAction = .store
// 深度值始终清零(reverse Z 约定)
renderPassDescriptor.depthAttachment.clearDepth = 0.0
一句话说明:Mixed Immersion 的 clear color 从 (0,0,0,1) 变成 (0,0,0,0) 是最关键的改动,搞错这个你的虚拟内容会变成一块不透明的黑色覆盖在真实环境上。
设置 Immersion Style
// SwiftUI 中设置沉浸式样式
ImmersiveSpace(id: "mixedScene") {
CompositorView()
}
.immersionStyle(selection: .constant(.mixed), in: .mixed)
// Info.plist 中设置默认启动为 Mixed Immersion
// PreferredDefaultSceneSessionRole: CPSceneSessionRoleImmersiveApplication
// InitialImmersionStyle: UIImmersionStyleMixed
一句话说明:默认情况下 SwiftUI 会创建 window scene 而非 ImmersiveSpace。如果你想让 App 启动就进入 Mixed Immersion,需要同时在代码和 Info.plist 里做配置。
在 Metal Shader 中遵守 Pre-multiplied Alpha
// visionOS 要求 pre-multiplied alpha 输出
fragment float4 fragmentShader(VertexOutput in [[stage_in]]) {
float4 color = in.color;
float alpha = color.a;
// 输出前将 RGB 乘以 alpha(pre-multiplied)
return float4(color.rgb * alpha, alpha);
}
一句话说明:如果你之前的 shader 没做 pre-multiply,在 Full Immersion 下问题不大(背景是黑色),但在 Mixed Immersion 下半透明物体的颜色会完全不对。所有 shader 都要过一遍这个改动。
最佳实践
新项目:从一开始就以 Mixed Immersion 为默认开发模式,Full Immersion 作为可选降级。Mixed Immersion 的技术约束更严格(pre-multiplied alpha、正确的 clear color、准确的 depth 值),满足这些约束后切到 Full Immersion 是零成本的反向操作。
已有项目:如果你的 App 已经用 Full Immersion 模式渲染 Metal,迁移到 Mixed Immersion 的 checklist 很清晰:1) clear color 改全零;2) 所有 shader 检查 pre-multiplied alpha;3) 深度值确保 reverse Z 约定且 alpha 为 0 的像素 depth 也为 0;4) 选择 upper limb visibility 策略。
还有什么值得关注
- ARKit 的 scene understanding 在 Mixed Immersion 下特别重要——你需要用它来做物理碰撞、遮挡和锚定,这些在 Full Immersion 下不需要。
- Display P3 色彩空间是 visionOS 的标准,你的素材和渲染管线都应该在 P3 下工作,否则虚拟内容和 Passthrough 之间会有色差。
- Mixed Immersion 模式下性能开销可能比 Full Immersion 更高,因为 Compositor Services 需要做额外的合成计算。