
visionOS 27 Projective Textures:让 3D 模型播放并投影视频
以 Astra 为例,把一条视频时间线接到曲面屏和 RealityKit 聚光灯,理解 visionOS 27 Projective Textures API 的实际用法。
Astra 是一个很具体的空间应用样本:YVR 600C USDZ 模型被放进 Apple Vision Pro,用户可以移动、缩放、旋转、展开零件,并按下模型上的实体按键切换视频预设。曲面屏显示视频,投影器则尝试把同一时间线的画面投向空间表面。
文章只保留一条主线:VideoMaterial 负责模型屏幕,SpotLightComponent.ProjectiveTexture 负责环境投影。两者可以共享 AVPlayerItem,但屏幕 UV、视频帧提取和纹理生命周期要分开处理。
先把空间应用拆成清楚的边界
控制窗口负责状态和动作,ImmersiveSpace 负责模型与空间追踪,自己的根节点负责整体 Transform;屏幕、按钮和投影各自拥有独立模块。加载 USDZ 时先包一层容器,读取 visual bounds,归一化最长边,再设置初始位置,避免资源实体直接变成应用根节点。
@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 条件。
在 3D 模型的屏幕上播放视频
不要给整个 USDZ 换材质,只替换命名屏幕实体的 ModelComponent。先缓存原始网格和材质,等待 AVPlayerItem 的 presentationSize,根据屏幕尺寸和视频宽高比重算 UV,使用 cover 策略铺满曲面,再创建 VideoMaterial。停止或更换视频时暂停播放器、移除 current item,并恢复原始组件。
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 而变形。
可见的模型按键本身不会自动响应。Astra 为每个按键创建不可见的命中区域,挂上 InputTargetComponent、CollisionComponent 和自定义 KeyboardActionComponent,再用 targetedToAnyEntity() 接收命中。命中子网格后沿 parent 向上查找责任区域,最后由动作枚举统一调用播放、投影或预设方法。
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,不要用一个覆盖全模型的碰撞体抢走按钮命中。
Projective Textures API:把视频帧送进聚光灯
Apple 在 WWDC26 Session 287 ↗ 中展示了给 spotlight 添加纹理的能力。Astra 把静态纹理换成视频帧:AVPlayerItemVideoOutput 取出新帧,Metal 写入 LowLevelTexture,再交给 SpotLightComponent.ProjectiveTexture;SurroundingsLight 让聚光灯参与支持的空间照明。

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 上测试。
按风险顺序验证,不夸大模拟器结果
教程里的验证顺序可以压缩成四步:先确认模型放置和恢复,再确认实体按钮在装配/爆炸状态下仍有明确命中;然后导入真实视频检查曲面屏铺满、播放暂停和原材质恢复;最后才在实机上验证投影方向、比例和遮罩。
func makeProjectorIfSupported() -> ProjectorController? {
guard #available(visionOS 27.0, *),
let device = MTLCreateSystemDefaultDevice(),
device.supportsFamily(.apple6) else { return nil }
return ProjectorController()
}后续可在这里替换为你的 Apple Vision Pro 实机演示截图。
Astra 的可复用经验是边界划分:ModelPlacement 管放置,KeyboardActionComponent 管输入,ScreenVideoPlayer 管视频,VideoProjector 管纹理提交和能力检查。Projective Textures API 只有在这条链路里拥有清晰责任,才容易调试,也容易换成其他模型和视频来源。
你的模型、空间交互或 visionOS 原型,需要一条从资产到体验的开发路径?
联系 DarkString →