VisionPro开发 - 利用视频材质实现一台复古小电视


VisionPro开发系列首页:漫游Apple Vision Pro

系列项目全部代码:github.com/xuchi16/vis...


本文主要包含以下内容:

  • 视频材质(Video Material)
  • 控制视频材质的播放和暂停
  • 获取 USDZ 模型的子实体

最终成果:一台支持播放/暂停功能的复古小电视

基本概念

在 RealityKit 中,材质(Material)是一种用于定义 3D 物体表面渲染效果的对象。视频材质是一种支持动画纹理的材质,可以把视频映射到物体表面。视频材质是一种无光照(unlit)类型,意味着场景中的光照不会对它造成任何影响。同时,如果源视频文件的格式支持透明度,视频材质也支持透明度调整。

视频材质使用AVPlayer实例来控制视频播放,因此我们可以使用任何AVPlayer支持的视频格式。还可以利用其play()pause()方法等控制视频播放和暂停。

主要步骤

  1. 创建视频播放控制和状态管理的模型(Model)
  2. 导入实体,并获取需要展示为视频材质的子实体
  3. 创建视频材质,并添加到子实体上
  4. 创建控制视频播放的视图
  5. 将播放试图通过附件(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.fillplay.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/点歌

相关推荐
sweet丶40 分钟前
iOS开发必备的HTTP网络基础概览
网络协议·ios
dly_blog1 小时前
Vue 响应式陷阱与解决方案(第19节)
前端·javascript·vue.js
消失的旧时光-19431 小时前
401 自动刷新 Token 的完整架构设计(Dio 实战版)
开发语言·前端·javascript
console.log('npc')2 小时前
Table,vue3在父组件调用子组件columns列的方法展示弹窗文件预览效果
前端·javascript·vue.js
用户47949283569152 小时前
React Hooks 的“天条”:为啥绝对不能写在 if 语句里?
前端·react.js
我命由我123452 小时前
SVG - SVG 引入(SVG 概述、SVG 基本使用、SVG 使用 CSS、SVG 使用 JavaScript、SVG 实例实操)
开发语言·前端·javascript·css·学习·ecmascript·学习方法
用户47949283569153 小时前
给客户做私有化部署,我是如何优雅搞定 NPM 依赖管理的?
前端·后端·程序员
00后程序员张3 小时前
python 抓包在实际项目中的合理位置,结合代理抓包、设备侧抓包与数据流分析
android·ios·小程序·https·uni-app·iphone·webview
C_心欲无痕3 小时前
vue3 - markRaw标记为非响应式对象
前端·javascript·vue.js
qingyun9893 小时前
深度优先遍历:JavaScript递归查找树形数据结构中的节点标签
前端·javascript·数据结构