Apple Engine

Apple, iPhone, iOS, その周辺のことについて

iOS で SceneKit を試す(Swift 3) その88 - SceneKit で SpriteKit の SKVideoNode を使用して動画のテクスチャを適応してみる

書き忘れていたが、SceneKit では SpriteKit の SKScene シーンを SCNMaterialProperty の各 contents に渡すことができる。

 

やり方としては作成した SKScene をジオメトリの firstMaterial.diffuse.contents を渡すだけ。 2D描画処理のコストはかかるが、複雑なテクスチャを作成できる。

ちなみに、SpriteKit 側も SK3DNode でイニシャライズして scnScene に設定した SCNScene を放り込むと、SceneKit の表示を 2D のテクスチャにしてくれる。

 

今回やること

AVFoundation の AVPlayer で動画を読み込み、それを SpriteKit でシーンを作り SKVideoNode を作成。 SpriteKit でシーンを SceneKit のテクスチャに割り当てる。

 

コードを書いてみる

いつも通り、Xcode の Game テンプレートで SceneKit を選択し作成する。

f:id:x67x6fx74x6f:20170904183836g:plain

 

動画をアップロードできないため、iOS で読み込める mov や m4v 動画ファイルをプロジェクトに自前で追加してほしい。

 

必要のないコードを変更する

GameViewController.swift を開き、宇宙船は今回使用しないので 19 行目を以下に変更。

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

 

アニメーションも必要ないので 44 行目と 47 行目の以下のコードを削除

let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!
ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1)))

 

インポート部分の変更

今回、SpriteKit と動画再生用の AVFoundation を使用するため、 GameViewController.swift の「import SceneKit」の下に以下のコードを追加

import SpriteKit
import AVFoundation

viewDidLoad を編集

以下のコードを追加。

流れとしては AVPlayerItem で動画ファイルを指定して、AVPlayer で先ほどの AVPlayerItem を設定し再生。 SKScene 作成後、SKVideoNode で作成した AVPlayer を設定し SKScene へ設定。 SCNBox の Diffuse の contents に設定した SKScene を適応するだけ。

let item = AVPlayerItem(url: URL(fileURLWithPath: Bundle.main.path(forResource: "ファイル名", ofType: "m4v")!))
let videoPlayer = AVPlayer(playerItem: item)
videoPlayer.play()

let skScene = SKScene()
skScene.backgroundColor = UIColor.black
skScene.size = CGSize(width: 1024, height: 1024)

let skVideoNode = SKVideoNode(avPlayer: videoPlayer)
skVideoNode.size = CGSize(width: 1024, height: 1024)
skVideoNode.position = CGPoint(x: 512, y: 512)

skScene.addChild(skVideoNode)

let boxNode = SCNNode(geometry: SCNBox(width: 4, height: 4, length: 4, chamferRadius: 0))
boxNode.geometry?.firstMaterial?.diffuse.contents = skScene
scene.rootNode.addChildNode(boxNode)

 

1回しか再生されないのでループしたい

このままの実装だと1回しか再生されないので、AVPlayer ループの処理をする。

終了時に何もしないようにして、AVPlayerItemDidPlayToEndTimeNotification で通知を飛ばす。 通知を受けたら再生位置を最初に戻し再生する。

 

AVPlayer の初期化の後に以下を追加。

videoPlayer.actionAtItemEnd = AVPlayerActionAtItemEnd.none;
NotificationCenter.default.addObserver(self,
                                        selector: #selector(self.stateEnd),
                                        name: NSNotification.Name("AVPlayerItemDidPlayToEndTimeNotification"),
                                        object: videoPlayer.currentItem)

 

GameViewController 内のどこかに以下の関数を追加

func stateEnd(notification: NSNotification) {
    let avPlayerItem = notification.object as? AVPlayerItem
    avPlayerItem?.seek(to: kCMTimeZero)
}

 

もっと簡単に書く

SKVideoNode は再生と一時停止ぐらいしか機能がないため、今回は AVPlayer を使用したが、 1回のみの再生で使用するのなら、AVPlayer の記述を消して init(fileNamed: String) や init(url: URL) で直接ファイルを参照できる。

 

パフォーマンスについて

それなりに 2D の描画処理が必要になる模様。

f:id:x67x6fx74x6f:20170904183815p:plain

 

GameViewController.swift の全コード

import UIKit
import QuartzCore
import SceneKit
import SpriteKit
import AVFoundation

class GameViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let scene = SCNScene()
        
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
        scene.rootNode.addChildNode(cameraNode)

        let lightNode = SCNNode()
        lightNode.light = SCNLight()
        lightNode.light!.type = .omni
        lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
        scene.rootNode.addChildNode(lightNode)
        
        let ambientLightNode = SCNNode()
        ambientLightNode.light = SCNLight()
        ambientLightNode.light!.type = .ambient
        ambientLightNode.light!.color = UIColor.darkGray
        scene.rootNode.addChildNode(ambientLightNode)

        // AVPlayer の設定
        let item = AVPlayerItem(url: URL(fileURLWithPath: Bundle.main.path(forResource: "ファイル名", ofType: "m4v")!))
        let videoPlayer = AVPlayer(playerItem: item)
        
        videoPlayer.actionAtItemEnd = AVPlayerActionAtItemEnd.none;
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(self.stateEnd),
                                               name: NSNotification.Name("AVPlayerItemDidPlayToEndTimeNotification"),
                                               object: videoPlayer.currentItem)
        
        videoPlayer.play()
        
        // SKScene の設定
        let skScene = SKScene()
        skScene.backgroundColor = UIColor.black
        skScene.size = CGSize(width: 1024, height: 1024)
        
        let skVideoNode = SKVideoNode(avPlayer: videoPlayer)
        skVideoNode.size = CGSize(width: 1024, height: 1024)
        skVideoNode.position = CGPoint(x: 512, y: 512)
        
        skScene.addChild(skVideoNode)
        
        // SCNBox の設定
        let boxNode = SCNNode(geometry: SCNBox(width: 4, height: 4, length: 4, chamferRadius: 0))
        boxNode.geometry?.firstMaterial?.diffuse.contents = skScene
        scene.rootNode.addChildNode(boxNode)
        
        let scnView = self.view as! SCNView
        scnView.scene = scene
        scnView.allowsCameraControl = true
        scnView.showsStatistics = true
        scnView.backgroundColor = UIColor.black
    }
    
    func stateEnd(notification: NSNotification) {
        let avPlayerItem = notification.object as? AVPlayerItem
        avPlayerItem?.seek(to: kCMTimeZero)
    }
    
    override var shouldAutorotate: Bool {
        return true
    }
    
    override var prefersStatusBarHidden: Bool {
        return true
    }
    
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        if UIDevice.current.userInterfaceIdiom == .phone {
            return .allButUpsideDown
        } else {
            return .all
        }
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Release any cached data, images, etc that aren't in use.
    }
}

 

今回はここまで。