VisionPro开发系列首页:漫游Apple Vision Pro
系列项目全部代码:github.com/xuchi16/vis...
本文主要包含以下内容:
- 视频材质(Video Material)
- 控制视频材质的播放和暂停
- 获取 USDZ 模型的子实体
最终成果:一台支持播放/暂停功能的复古小电视
基本概念
在 RealityKit 中,材质(Material)是一种用于定义 3D 物体表面渲染效果的对象。视频材质是一种支持动画纹理的材质,可以把视频映射到物体表面。视频材质是一种无光照(unlit)类型,意味着场景中的光照不会对它造成任何影响。同时,如果源视频文件的格式支持透明度,视频材质也支持透明度调整。
视频材质使用AVPlayer
实例来控制视频播放,因此我们可以使用任何AVPlayer
支持的视频格式。还可以利用其play()
和pause()
方法等控制视频播放和暂停。
主要步骤
- 创建视频播放控制和状态管理的模型(Model)
- 导入实体,并获取需要展示为视频材质的子实体
- 创建视频材质,并添加到子实体上
- 创建控制视频播放的视图
- 将播放试图通过附件(attachment) 附加到实体上
基本实现
视频播放和状态管理
首先,我们需要创建一个管理视频播放状态的模型,其中包含播放器和播放内容。当应用启动时,初始化该 Model,并且传递给不同的 View。
swift
class PlayModel: ObservableObject {
var player = AVPlayer()
var item: AVPlayerItem?
func load(_ url: URL) {
item = AVPlayerItem(url: url)
player.replaceCurrentItem(with: item)
}
}
实体导入及子实体获取
将电视的 USDZ 文件添加到 Packages 中,并在 ImmersiveView 中加载该模型。这一模型包含两个部分:电视的主体和屏幕。可以通过 Entity 类型的findEntity(named:)
方法找到屏幕对应的子实体。
swift
RealityView { content, attachments in
let tv = try! await Entity(named: "tv_retro")
if let screen = tv.findEntity(named: "tv_retro_2_RetroTVScreen") as? ModelEntity {
// 获取到了屏幕实体
}
content.add(tv)
}
创建视频材质
在 ImmersiveView 中从 Model 中获取到播放器,添加到视频材质上,并绑定到屏幕实体上。并且当实体加载完成后播放视频。
swift
var playModel: PlayModel
RealityView { content in
let tv = try! await Entity(named: "tv_retro")
if let screen = tv.findEntity(named: "tv_retro_2_RetroTVScreen") as? ModelEntity {
let player = playModel.player
let material = VideoMaterial(avPlayer: player)
screen.model?.materials = [material]
player.play()
}
content.add(tv)
}
.onAppear() {
if let url = Bundle.main.url(forResource: "GetReadyRemixRB", withExtension: "mp4") {
playModel.load(url)
}
}
创建视频播放控制视图
播放控制试图中只包含一个按钮:
- 当视频为暂停状态时,为播放按钮,点击后即可播放
- 当视频为播放状态时,为暂停按钮,点击后暂停视频
因此需要对PlayModel
进行增强,增加对应视频播放状态判断以及播放控制的功能。其中,togglePlayPause()
用于在不同状态下对视频进行播放和暂停操作。observePlayer()
用于监听视频的播放状态。
swift
class PlayModel: ObservableObject {
var player = AVPlayer()
var item: AVPlayerItem?
private var subscriptions = Set<AnyCancellable>()
@Published var isPlaying: Bool = false
init() {
observePlayer()
}
// ...
func togglePlayPause() {
if player.rate == 0 {
player.play()
} else {
player.pause()
}
}
private func observePlayer() {
player.publisher(for: .timeControlStatus)
.receive(on: RunLoop.main)
.sink(receiveValue: { [weak self] status in
switch status {
case .playing:
self?.isPlaying = true
default:
self?.isPlaying = false
}
})
.store(in: &subscriptions)
}
}
在此基础上,新增一个 ControllerView,用于展示播放控制按钮。
- 图标根据视频状态不同,分别用
pause.fill
和play.fill
表示 - 按钮行为通过调用 Model 中的
togglePlayPause()
函数实现控制
swift
struct ControllerView: View {
@ObservedObject var playModel = PlayModel()
var body: some View {
Button(action: {
playModel.togglePlayPause()
}) {
Image(systemName: playModel.isPlaying ? "pause.fill" : "play.fill")
.font(.system(size: 40))
.accessibilityLabel(playModel.isPlaying ? "Pause" : "Play")
.padding(.all, 40)
}
.padding(12)
}
}
为实体添加附件
回到 ImmversiveView 中,在电视实体下方添加上述新建的 ControllerView。我们为新建一个 entity 用于容纳 tv 实体及其附件,并且将它们都添加到 entity 中即可。
swift
@State var entity = Entity()
RealityView { content, attachments in
// load tv
entity.addChild(tv)
if let attachment = attachments.entity(for: "controller") {
attachment.position = [0, 0.98, -1.2]
entity.addChild(attachment)
}
content.add(entity)
} attachments: {
Attachment(id: "controller") {
ControllerView(playModel: playModel)
}
}
最终效果
关注我
欢迎在掘金上关注我和我的专栏VisionOS Workshop,以及各种收藏/围观/评论/反馈/批评/Star/点歌