我该如何在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)
}
}
...
}
SKScene
和 SKVideoNode
。您可以直接将 AVPlayer
设置为 SCNMaterialProperty
实例的内容。 - mnuages2019年解决方案:
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年之前:
当对象被释放时,请勿忘记删除您的通知观察者!类似于 NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)
请注意,苹果文档明确说明,您现在可以将视频放置在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
NotificationCenter.default.addObserver(self,
selector: #selector(loopy),
name: .AVPlayerItemDidPlayToEndTime,
object: plr.currentItem)
然后
@objc func loopy() { plr.seek(to: .zero) }
可以将AVPlayer用作场景背景的内容。 但是,直到我向sceneView发送.play(nil)之前,它才能正常工作。
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
我在macOS Ventura 13.0.1上的Xcode 14.1模拟器(iOS 16.1)上测试了此应用程序。对于视频纹理,我使用了分辨率为1600x900的QuickTime .mov文件,并采用H.264编解码器。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
对象: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()
}
你可以采用一个更短的路线来解决这个任务,但问题是,这种方法的效果并不像应该的那样——视频循环会带有等于整个视频时长的延迟。
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
对象分配给素材的内容: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
}
}
yourNode.geometry?.firstMaterial?.diffuse.contents = AVPlayer(url: videoUrl)
您只需添加一个简单的平面作为网格即可。我会把它放在答案中。 - Fattie
.firstMaterial?.diffuse.contents
= .. 你的视频播放器)。谢谢! - Fattie