简体   繁体   English

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

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

How do I create a material in SceneKit that plays a looping video?如何在 SceneKit 中创建播放循环视频的材质?

It's possible to achieve this in SceneKit using a SpriteKit scene as the geometry's material . 使用SpriteKit场景作为几何体的材质,可以在SceneKit中实现此目的。

The following example will create a SpriteKit scene, add a video node to it with a video player, make the video player loop, create a SceneKit scene, add a SceneKit plane, and finally add the SpriteKit scene as the plane's diffuse material. 以下示例将创建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)
        }
    }

    ...
}

Year 2019 solution: 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()

Code for method in selector:选择器中的方法代码:

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

Note: for ten years now you do NOT remove notifications that run a selector.注意:十年来,您不会删除运行选择器的通知。 (You only need to do so in the obscure case you're using a block.) (你只需要在你使用块的模糊情况下这样做。)


If you have time-travelled to before 2015: Don't forget to remove your notification observer when the object is deallocated!如果你穿越到 2015 年之前:当 object 被释放时,不要忘记删除你的通知观察者! Something like NotificationCenter.default .removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)NotificationCenter.default .removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)

2022, it's now trivial to do this in Scene Kit. 2022 年,现在在 Scene Kit 中执行此操作很简单。 See Apple docs.请参阅苹果文档。

Note that the apple doco clearly states you can now just put video on an SCNNode.请注意,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

Note that you can put anything you want on the mesh.请注意,您可以在网格上放置任何您想要的东西。 ("geometry" is the mesh.) It's easy. (“几何”是网格。)这很容易。 For example, if you just want a plain color:例如,如果您只想要一种纯色:

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

Note that the question asks about looping the video.请注意,该问题询问有关循环播放视频的问题。 This is trivial and unrelated to using SceneKit.这是微不足道的,与使用 SceneKit 无关。 You can see a million QA about looping video, it's this easy:你可以看到一百万个关于循环视频的 QA,就这么简单:

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

and then接着

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

It is possible to use an AVPlayer as the content of the scene's background. 可以使用AVPlayer作为场景背景的内容。 However, it was not working for me until I sent .play(nil) to the sceneView. 但是,在我将.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 Swift 5.7

1. SceneKit + SpriteKit's Looping Video Material 1. SceneKit + SpriteKit的循环视频素材

...works fine – video is looping seamlessly... ...工作正常 - 视频正在无缝循环...

I tested this app on Xcode 14.1 Simulator (iOS 16.1) on macOS Ventura 13.0.1.我在 macOS Ventura 13.0.1 上的 Xcode 14.1 模拟器 (iOS 16.1) 上测试了这个应用程序。 For video texture I used QuickTime 1600x900 .mov file with H.264 codec.对于视频纹理,我使用了带有 H.264 编解码器的 QuickTime 1600x900 .mov文件。 3D model's in .scn format. 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 scene is capable of playing back a .mov video file: 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)
}

The SceneKit's material is used as a medium for the SpriteKit's video: 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()
}

And, at last, the method used for loading the video contains an AVPlayerLooper object:最后,用于加载视频的方法包含一个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's Looping Video Material 2. Pure SceneKit的循环视频素材

...works incorrectly – video is looping with delay... ...工作不正常 – 视频循环播放有延迟...

You can take a shorter route to solve this task, but the problem is, an approach does not work as it should - the video loop plays with a delay equal to the duration of the whole video.您可以采取更短的路线来解决此任务,但问题是,一种方法无法正常工作 - 视频循环播放的延迟等于整个视频的持续时间。

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() { ... }
}

Here we are assigning an AVQueuePlayer object to the content of the material:在这里,我们将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