DarkString
暗弦科技
Astra YVR 600C visionOS 与 RealityKit 开发教程概念封面
← 文章
English version ↗DarkString / Field Notes
visionOS 27 / Projective Textures API / RealityKit tutorial

visionOS 27 Projective Textures:让 3D 模型播放并投影视频

以 Astra 为例,把一条视频时间线接到曲面屏和 RealityKit 聚光灯,理解 visionOS 27 Projective Textures API 的实际用法。

GunnerApple Vision Pro2026.09.07

Astra 是一个很具体的空间应用样本:YVR 600C USDZ 模型被放进 Apple Vision Pro,用户可以移动、缩放、旋转、展开零件,并按下模型上的实体按键切换视频预设。曲面屏显示视频,投影器则尝试把同一时间线的画面投向空间表面。

文章只保留一条主线:VideoMaterial 负责模型屏幕,SpotLightComponent.ProjectiveTexture 负责环境投影。两者可以共享 AVPlayerItem,但屏幕 UV、视频帧提取和纹理生命周期要分开处理。

Video / Astra YVR 600C在 B 站打开 ↗
01

先把空间应用拆成清楚的边界

控制窗口负责状态和动作,ImmersiveSpace 负责模型与空间追踪,自己的根节点负责整体 Transform;屏幕、按钮和投影各自拥有独立模块。加载 USDZ 时先包一层容器,读取 visual bounds,归一化最长边,再设置初始位置,避免资源实体直接变成应用根节点。

AstraApp.swift / WindowGroup + ImmersiveSpace
@main
struct AstraApp: App {
    @State private var appModel = AppModel()

    init() {
        KeyboardActionComponent.registerComponent()
    }

    var body: some Scene {
        WindowGroup { ContentView().environment(appModel) }
        ImmersiveSpace(id: appModel.immersiveSpaceID) {
            ImmersiveView().environment(appModel)
        }
        .immersionStyle(selection: .constant(.mixed), in: .mixed)
    }
}

这个结构让失败更容易定位:模型没出现看 placement,按钮没命中看 input tree,屏幕比例不对看 UV,墙面没有投影则看系统版本、GPU 和 World Sensing 条件。

02

在 3D 模型的屏幕上播放视频

不要给整个 USDZ 换材质,只替换命名屏幕实体的 ModelComponent。先缓存原始网格和材质,等待 AVPlayerItem 的 presentationSize,根据屏幕尺寸和视频宽高比重算 UV,使用 cover 策略铺满曲面,再创建 VideoMaterial。停止或更换视频时暂停播放器、移除 current item,并恢复原始组件。

ScreenVideoPlayer.swift / VideoMaterial
func installVideo(on screen: Entity, item: AVPlayerItem) throws {
    guard let original else { throw VideoError.missingGeometry }
    let mesh = try makeVideoMesh(
        from: original.mesh,
        videoAspectRatio: Float(item.presentationSize.width / item.presentationSize.height)
    )

    player.replaceCurrentItem(with: item)
    screen.components.set(
        ModelComponent(mesh: mesh, materials: [VideoMaterial(avPlayer: player)])
    )
    player.play()
}

func restoreScreen() {
    player.pause()
    player.replaceCurrentItem(with: nil)
    if let original { screen?.components.set(original) }
}

关键点是“可恢复”:曲面形状和法线仍来自作者制作的网格,视频只是屏幕实体的临时材质。这样切换内容不会污染模型,画面也不会因为直接拉伸 UV 而变形。

03

让 3D 模型按钮响应用户输入

可见的模型按键本身不会自动响应。Astra 为每个按键创建不可见的命中区域,挂上 InputTargetComponent、CollisionComponent 和自定义 KeyboardActionComponent,再用 targetedToAnyEntity() 接收命中。命中子网格后沿 parent 向上查找责任区域,最后由动作枚举统一调用播放、投影或预设方法。

ModelKeyboardControls.swift / hit region + parent routing
let region = Entity()
region.name = "AstraVideoPreset_1"
region.components.set(InputTargetComponent(allowedInputTypes: .all))
region.components.set(
    CollisionComponent(shapes: [.generateBox(size: buttonSize)])
)
region.components.set(KeyboardActionComponent(action: .preset(1)))
button.addChild(region)

func handleTap(on hit: Entity) {
    var candidate: Entity? = hit
    while let entity = candidate {
        if regions.contains(where: { $0 === entity }) {
            activate(entity)
            return
        }
        candidate = entity.parent
    }
}

动作处理还需要两个状态守卫:模型爆炸、视频加载和场景切换时暂时禁用区域;同一按键设置约 0.25 秒去抖。整体模型抓取交给外壳代理和 ManipulationComponent.HitTarget,不要用一个覆盖全模型的碰撞体抢走按钮命中。

04

Projective Textures API:把视频帧送进聚光灯

Apple 在 WWDC26 Session 287 ↗ 中展示了给 spotlight 添加纹理的能力。Astra 把静态纹理换成视频帧:AVPlayerItemVideoOutput 取出新帧,Metal 写入 LowLevelTexture,再交给 SpotLightComponent.ProjectiveTexture;SurroundingsLight 让聚光灯参与支持的空间照明。

Apple WWDC26 Session 287 介绍 visionOS 27 Projective Textures API 的视频画面
截图来源:DarkString 公众号文章 ↗;技术背景见 Apple Developer / WWDC26 Session 287 ↗
VideoProjector.swift / Projective Textures
if #available(visionOS 27.0, *), device.supportsFamily(.apple6) {
    let texture = try makeProjectiveTexture(size: 1024)
    spotlight.components.set(
        SpotLightComponent.ProjectiveTexture(texture: texture)
    )
    spotlight.components.set(SpotLightComponent.SurroundingsLight())

    let output = AVPlayerItemVideoOutput(pixelBufferAttributes: [
        kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA
    ])
    item.add(output)
    guard output.hasNewPixelBuffer(forItemTime: item.currentTime()),
          let buffer = output.pixelBufferAndDisplayTime(forItemTime: item.currentTime()).pixelBuffer,
          let commandBuffer = commandQueue.makeCommandBuffer() else { return }
    let target = lowLevelTexture.replace(using: commandBuffer)
    render(buffer, to: target, commandBuffer: commandBuffer)
    commandBuffer.commit()
}

这条链路和屏幕 VideoMaterial 是两个消费者:屏幕解决“视频如何贴在曲面上”,投影解决“视频纹理如何离开模型并被聚光灯投向空间”。如果要验证真实墙面效果,必须在支持 visionOS 27、Apple GPU family 6 和 World Sensing 的 Apple Vision Pro 上测试。

05

按风险顺序验证,不夸大模拟器结果

教程里的验证顺序可以压缩成四步:先确认模型放置和恢复,再确认实体按钮在装配/爆炸状态下仍有明确命中;然后导入真实视频检查曲面屏铺满、播放暂停和原材质恢复;最后才在实机上验证投影方向、比例和遮罩。

VideoProjector.swift / availability guard
func makeProjectorIfSupported() -> ProjectorController? {
    guard #available(visionOS 27.0, *),
          let device = MTLCreateSystemDefaultDevice(),
          device.supportsFamily(.apple6) else { return nil }
    return ProjectorController()
}
截图区域 · 01
Apple Vision Pro 实机验证:按钮命中、曲面屏视频或墙面投影

后续可在这里替换为你的 Apple Vision Pro 实机演示截图。

结语

Astra 的可复用经验是边界划分:ModelPlacement 管放置,KeyboardActionComponent 管输入,ScreenVideoPlayer 管视频,VideoProjector 管纹理提交和能力检查。Projective Textures API 只有在这条链路里拥有清晰责任,才容易调试,也容易换成其他模型和视频来源。

Start a spatial brief

你的模型、空间交互或 visionOS 原型,需要一条从资产到体验的开发路径?

联系 DarkString →