繁体   English   中英

如何在 SceneKit 中为 iOS 应用创建循环视频素材?

[英]How do I create a looping video material in SceneKit for iOS app?

如何在 SceneKit 中创建播放循环视频的材质?

使用SpriteKit场景作为几何体的材质,可以在SceneKit中实现此目的。

以下示例将创建SpriteKit场景,使用视频播放器向其添加视频节点,制作视频播放器循环,创建SceneKit场景,添加SceneKit平面,最后将SpriteKit场景添加为平面的漫反射材质。

import UIKit
import SceneKit
import SpriteKit
import AVFoundation

class ViewController: UIViewController, SCNSceneRendererDelegate {

    @IBOutlet weak var sceneView: SCNView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // A SpriteKit scene to contain the SpriteKit video node
        let spriteKitScene = SKScene(size: CGSize(width: sceneView.frame.width, height: sceneView.frame.height))
        spriteKitScene.scaleMode = .aspectFit

        // Create a video player, which will be responsible for the playback of the video material
        let videoUrl = Bundle.main.url(forResource: "videos/video", withExtension: "mp4")!
        let videoPlayer = AVPlayer(url: videoUrl)

        // To make the video loop
        videoPlayer.actionAtItemEnd = .none
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(ViewController.playerItemDidReachEnd),
            name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
            object: videoPlayer.currentItem)

        // Create the SpriteKit video node, containing the video player
        let videoSpriteKitNode = SKVideoNode(avPlayer: videoPlayer)
        videoSpriteKitNode.position = CGPoint(x: spriteKitScene.size.width / 2.0, y: spriteKitScene.size.height / 2.0)
        videoSpriteKitNode.size = spriteKitScene.size
        videoSpriteKitNode.yScale = -1.0
        videoSpriteKitNode.play()
        spriteKitScene.addChild(videoSpriteKitNode)

        // Create the SceneKit scene
        let scene = SCNScene()
        sceneView.scene = scene
        sceneView.delegate = self
        sceneView.isPlaying = true

        // Create a SceneKit plane and add the SpriteKit scene as its material
        let background = SCNPlane(width: CGFloat(100), height: CGFloat(100))
        background.firstMaterial?.diffuse.contents = spriteKitScene
        let backgroundNode = SCNNode(geometry: background)
        scene.rootNode.addChildNode(backgroundNode)

        ...
    }

    // This callback will restart the video when it has reach its end
    func playerItemDidReachEnd(notification: NSNotification) {
        if let playerItem: AVPlayerItem = notification.object as? AVPlayerItem {
            playerItem.seek(to: kCMTimeZero)
        }
    }

    ...
}

2019年解决方案:

let mat = SCNMaterial()
let videoUrl = Bundle.main.url(forResource: "YourVideo", withExtension: "mp4")!
let player = AVPlayer(url: videoUrl)
mat.diffuse.contents = player
player.actionAtItemEnd = .none
NotificationCenter.default.addObserver(self,
                                       selector: #selector(playerItemDidReachEnd(notification:)),
                                       name: .AVPlayerItemDidPlayToEndTime,
                                       object: player.currentItem)
player.play()

选择器中的方法代码:

@objc private func playerItemDidReachEnd(notification: Notification) {
    if let playerItem = notification.object as? AVPlayerItem {
        playerItem.seek(to: .zero, completionHandler: nil)
    }
}

注意:十年来,您不会删除运行选择器的通知。 (你只需要在你使用块的模糊情况下这样做。)


如果你穿越到 2015 年之前:当 object 被释放时,不要忘记删除你的通知观察者! NotificationCenter.default .removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)

2022 年,现在在 Scene Kit 中执行此操作很简单。 请参阅苹果文档。

请注意,apple doco 明确指出您现在可以将视频放在 SCNNode 上。

// make some mesh. whatever size you want.
let mesh = SCNPlane()
mesh.width = 1.77
mesh.height = 1

// put the mesh on your node
yourNode.geometry = mesh

// add the video to the mesh
plr = AVPlayer(url: "https .. .m4v")
yourNode.geometry?.firstMaterial?.diffuse.contents = plr

请注意,您可以在网格上放置任何您想要的东西。 (“几何”是网格。)这很容易。 例如,如果您只想要一种纯色:

... firstMaterial?.diffuse.contents = UIColor.yellow

请注意,该问题询问有关循环播放视频的问题。 这是微不足道的,与使用 SceneKit 无关。 你可以看到一百万个关于循环视频的 QA,就这么简单:

NotificationCenter.default.addObserver(self,
  selector: #selector(loopy),
  name: .AVPlayerItemDidPlayToEndTime,
  object: plr.currentItem)

接着

@objc func loopy() { plr.seek(to: .zero) }

可以使用AVPlayer作为场景背景的内容。 但是,在我将.play(nil)发送到sceneView之前,它不适用于我。

override func viewDidLoad() {
    super.viewDidLoad()

    // Set the view's delegate
    sceneView.delegate = self

    // Show statistics such as fps and timing information
    sceneView.showsStatistics = true

    // Create a new scene
    let scene = SCNScene(named: "art.scnassets/ship.scn")!

    // create and add a camera to the scene
    let cameraNode = SCNNode()
    cameraNode.camera = SCNCamera()
    scene.rootNode.addChildNode(cameraNode)

    // Set the scene to the view
    sceneView.scene = scene

    let movieFileURL = Bundle.main.url(forResource: "example", withExtension: "mov")!
    let player = AVPlayer(url:movieFileURL)
    scene.background.contents = player
    sceneView.play(nil) //without this line the movie was not playing

    player.play()
}

Swift 5.7

1. SceneKit + SpriteKit的循环视频素材

...工作正常 - 视频正在无缝循环...

我在 macOS Ventura 13.0.1 上的 Xcode 14.1 模拟器 (iOS 16.1) 上测试了这个应用程序。 对于视频纹理,我使用了带有 H.264 编解码器的 QuickTime 1600x900 .mov文件。 3D 模型的.scn格式。

import SceneKit
import AVFoundation
import SpriteKit

class GameViewController: UIViewController {
    
    var sceneView: SCNView? = nil
    private var avPlayer: AVQueuePlayer? = nil
    private var looper: AVPlayerLooper? = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.sceneView = self.view as? SCNView
        guard let scene = SCNScene(named: "tv.scn") else { return }
        sceneView?.scene = scene
        sceneView?.allowsCameraControl = true
        
        self.spriteKitScene(self.sceneKitNode())
    }

    private func spriteKitScene(_ node: SCNNode) { ... }           // A

    internal func sceneKitNode() -> SCNNode { ... }                // B
    
    fileprivate func loadVideoMaterial() -> AVPlayer? { ... }      // C
}

SpriteKit 场景能够播放.mov视频文件:

private func spriteKitScene(_ node: SCNNode) {
    
    let screenGeo: SCNPlane = node.geometry as! SCNPlane

    let videoNode = SKVideoNode(avPlayer: self.loadVideoMaterial()!)

    let skScene = SKScene(size: CGSize(width: screenGeo.width * 1600,
                                      height: screenGeo.height * 900))
    
    videoNode.position = CGPoint(x: skScene.size.width / 2,
                                 y: skScene.size.height / 2)
    
    videoNode.size = skScene.size
    skScene.addChild(videoNode)
    
    let screenMaterial = screenGeo.materials.first
    screenMaterial?.diffuse.contents = skScene
    videoNode.play()
    sceneView?.scene?.rootNode.addChildNode(node)
}

SceneKit 的材质用作 SpriteKit 视频的媒介:

internal func sceneKitNode() -> SCNNode {
    
    if let screen = sceneView?.scene?.rootNode.childNode(withName: "screen",
                                                      recursively: false) {
        
        screen.geometry?.firstMaterial?.lightingModel = .constant
        screen.geometry?.firstMaterial?.diffuse.contents = UIColor.black
        return screen
    }
    return SCNNode()
}

最后,用于加载视频的方法包含一个AVPlayerLooper object:

fileprivate func loadVideoMaterial() -> AVPlayer? {
    
    guard let path = Bundle.main.path(forResource: "video", ofType: "mov")
    else { return nil }
    
    let videoURL = URL(fileURLWithPath: path)
    let asset = AVAsset(url: videoURL)
    let item = AVPlayerItem(asset: asset)
    self.avPlayer = AVQueuePlayer(playerItem: item)

    if let avPlayer {
        avPlayer.isMuted = true
        self.looper = AVPlayerLooper(player: avPlayer, templateItem: item)
        return avPlayer
    }
    return AVPlayer()
}

在此处输入图像描述

2. Pure SceneKit的循环视频素材

...工作不正常 – 视频循环播放有延迟...

您可以采取更短的路线来解决此任务,但问题是,一种方法无法正常工作 - 视频循环播放的延迟等于整个视频的持续时间。

import SceneKit
import AVFoundation

class GameViewController: UIViewController {
    
    var sceneView: SCNView? = nil
    private var avPlayer: AVQueuePlayer? = nil
    private var looper: AVPlayerLooper? = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.sceneView = self.view as? SCNView
        sceneView?.isPlaying = true               // if view is playing?
        guard let scene = SCNScene(named: "tv.scn") else { return }
        sceneView?.scene = scene
        sceneView?.allowsCameraControl = true
        
        self.loadModelWithVideoMaterial()
    }

    fileprivate func loadModelWithVideoMaterial() { ... }
}

在这里,我们将AVQueuePlayer object 分配给材料的内容:

fileprivate func loadModelWithVideoMaterial() {
    
    guard let path = Bundle.main.path(forResource: "video", ofType: "mov")
    else { return }
    
    let videoURL = URL(fileURLWithPath: path)
    let asset = AVAsset(url: videoURL)
    let item = AVPlayerItem(asset: asset)
    self.avPlayer = AVQueuePlayer(playerItem: item)

    if let avPlayer {
        avPlayer.isMuted = true

        guard let screen = sceneView?.scene?.rootNode.childNode(
                                                       withName: "screen",
                                                    recursively: true)
        else { return }
            
        screen.geometry?.firstMaterial?.lightingModel = .constant
        screen.geometry?.firstMaterial?.diffuse.contents = avPlayer
        sceneView?.scene?.rootNode.addChildNode(screen)
        
        self.looper = AVPlayerLooper(player: avPlayer, templateItem: item)
        avPlayer.playImmediately(atRate: 20)      // speed x20 for testing
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM