将你的 iOS/iPadOS 游戏带到 visionOS
Bring your iOS or iPadOS game to visionOS
2024年6月10日
一句话判断
如果你有一个基于 Metal 渲染的 iOS/iPadOS 游戏,这场 Session 提供了一套从”兼容模式运行”到”原生立体渲染”的渐进式升级路径,不需要一步到位重写整个游戏。
这场 Session 讲了什么
visionOS 上的 3D 渲染有两条路:RealityKit 和 Metal。RealityKit 适合通过 Swift 或 Unity PolySpatial 来做体积窗口(Volume)和沉浸空间(ImmersiveSpace),Game Room、LEGO Builder’s Journey 就是这种模式。但如果你的游戏已经有成熟的 Metal 管线,直接用 Metal 渲染会更实际。
Metal 在 visionOS 上有两种运行模式。第一种是兼容应用(Compatible App),游戏跑在一个窗口里,体验跟 iPad 上差不多,好处是可以和其他应用并存于共享空间。第二种是用 CompositorServices 做完全沉浸式应用,玩家用头部控制相机。后者沉浸感强,但可能需要大规模重构。
这场 Session 的核心价值在于:它不让你做非此即彼的选择。而是从兼容应用出发,逐步叠加立体渲染(Stereoscopy)、头部追踪(Head Tracking)和可变刷新率(VRR),渐进式提升沉浸感。Wylde Flowers 就是典型案例——从 iPad 兼容版本出发,最终加上了 3D 窗口边框、蒲公英飘落的沉浸背景、以及立体深度渲染。
值得深挖的点
LowLevelTexture:Metal 与 RealityKit 的桥梁
把 iOS 游戏转成原生 visionOS 应用后,渲染输出有两种选择:CAMetalLayer 和 LowLevelTexture。CAMetalLayer 集成简单,跟 UIView 体系对接方便,但控制力有限。LowLevelTexture 是更推荐的方案——它创建一个底层纹理,再用 TextureResource 包装,就能在 RealityKit 场景的任意位置使用。
这意味着你可以用 ZStack 把 Metal 渲染的游戏画面和 RealityView 加载的 3D 模型叠加在一起。Cut The Rope 3 的动态边框就是这么做的:游戏画面渲染到纹理,RealityKit 负责周围的 3D 装饰。更妙的是,通过 SwiftUI 的 @State 变量,边框可以根据游戏关卡动态变化。
立体渲染和头部追踪的实现细节
立体渲染的本质是给左右眼分别渲染不同的图像,类似立体电影的原理。实现时需要创建两套相机(左右眼),设置合适的 interpupillary distance(瞳距),然后分别渲染。头部追踪则让游戏画面看起来像一扇通向另一个世界的窗户——当你移动头部时,画面会根据你的视角变化产生视差效果。这两个技术叠加在一起,就是从”平板游戏投射到 3D 空间”到”真正的空间体验”的关键跨越。
代码片段
用 CAMetalLayer 渲染游戏画面
// 声明 CAMetalLayer 作为 UIView 的 layerClass
class MetalGameView: UIView {
override class var layerClass: AnyClass {
CAMetalLayer.self
}
private var displayLink: CAMetalDisplayLink?
func setupRendering() {
// 创建 displayLink 获取每帧回调
let metalLayer = layer as! CAMetalLayer
displayLink = CAMetalDisplayLink(layer: metalLayer) { [weak self] _ in
self?.renderFrame()
}
displayLink?.add(to: .main, forMode: .common)
}
func renderFrame() {
// 在这里执行 Metal 渲染命令
// 每帧都会被调用
}
}
坑点:CAMetalDisplayLink 的回调在主线程,如果渲染逻辑较重,需要考虑用 CommandQueue 做异步提交。
用 LowLevelTexture 在 RealityKit 中渲染
// 创建底层纹理
let textureDescriptor = LowLevelTexture.Descriptor(
pixelFormat: .bgra8Unorm,
width: 1920,
height: 1080
)
let lowLevelTexture = try! LowLevelTexture(descriptor: textureDescriptor)
// 包装为 RealityKit 可用的纹理资源
let textureResource = try! TextureResource(from: lowLevelTexture)
// 在 RealityKit 场景中使用
let material = UnlitMaterial()
// 将 textureResource 应用到材质上...
// 用 CommandQueue 每帧绘制到纹理
func renderToTexture() {
let commandBuffer = commandQueue.makeCommandBuffer()!
// 获取 MTLTexture 进行渲染
if let mtlTexture = lowLevelTexture.replace(using: commandBuffer) {
// 执行 Metal 渲染命令到 mtlTexture
}
commandBuffer.commit()
}
添加沉浸空间背景
// 主游戏窗口
@main
struct GameApp: App {
@State private var gameState = GameState()
var body: some Scene {
WindowGroup {
MetalGameView()
.environment(gameState)
}
// 沉浸背景空间
ImmersiveSpace(id: "game-background") {
RealityView { content in
// 加载 3D 背景元素:粒子、环境光效等
let background = try await Entity(named: "GameBackground")
content.add(background)
}
}
.immersionStyle(selection: .constant(.mixed), in: .mixed)
}
}
坑点:ImmersiveSpace 需要在 Info.plist 中声明 UISupportsTrueScreenSize 权限,否则不会生效。
最佳实践
迁移建议分三步走。第一步,用 iOS SDK 编译为兼容应用跑在 visionOS 上,验证基本功能。触摸和手柄输入在兼容模式下开箱即用。第二步,在 Build Settings 中添加 Apple Vision 作为支持目标,用 visionOS SDK 编译。处理少量编译错误后,将渲染输出从 CAMetalLayer 迁移到 LowLevelTexture。第三步,按需添加 RealityKit 边框、ImmersiveSpace 背景、立体渲染和头部追踪。每一步都可以独立发布,不需要一次做完。
还有什么值得关注
- Session “Render Metal with passthrough in visionOS” 详细讲解了 CompositorServices 完全沉浸模式
- “Build a spatial drawing app with RealityKit” 深入介绍了 LowLevelTexture 的用法
- “Explore game input in visionOS” 覆盖了 visionOS 上的触摸、手柄和手势输入方案