简体   繁体   中英

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?

It's possible to achieve this in SceneKit using a SpriteKit scene as the geometry's material .

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.

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:

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! Something like NotificationCenter.default .removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)

2022, it's now trivial to do this in Scene Kit. See Apple docs.

Note that the apple doco clearly states you can now just put video on an 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. You can see a million QA about looping video, it's this easy:

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. However, it was not working for me until I sent .play(nil) to the 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's Looping Video Material

...works fine – video is looping seamlessly...

I tested this app on Xcode 14.1 Simulator (iOS 16.1) on macOS Ventura 13.0.1. For video texture I used QuickTime 1600x900 .mov file with H.264 codec. 3D model's in .scn format.

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:

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:

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:

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

...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:

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
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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