Bring your iOS or iPadOS game to visionOS
Spatial Computing 进阶 20m

将你的 iOS/iPadOS 游戏带到 visionOS

Bring your iOS or iPadOS game to visionOS

2024年6月10日

在 Apple 官方观看视频

一句话判断

如果你有一个基于 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 上的触摸、手柄和手势输入方案
WWDC 2024