【SceneKit】3Dモデル（アニメーションつき）の再生速度を変更する

# はじめに
アニメーションつき3Dモデルの再生速度をを可変にしたかったので、そのメモ。

# 実行環境
- Xcode 11.2.1
- [MagicaVoxel](https://ephtracy.github.io) 0.99.4
- [Mixamo](https://www.mixamo.com)
- iPhoneXR（iOS 12.1.4）

# やりたいこと
[Mixamo](https://www.mixamo.com)でアニメーションをつけた3Dモデルの、再生速度（テンポ）をゆっくりにしたり、速くしたり、任意に変更してみたかった。

# 再生速度をどこで変更できるか？
読み込んだ3Dモデル（Salsa Dancing.dae）から変換したSCNファイル（salsa.scn）を開いて、ボーンの設定（mixamorig_Hips）を選択すると、右下の「Animation Settings」に「speed」という項目があって、この数値を変更して画面下の再生ボタンを押すと再生速度が変わります（speed:1 が定速）。
![cap01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106624/ffb97a3b-417b-6385-ef9e-4d7bdf2eae53.png)

# 再生速度をプログラムで制御するには？

いろいろググってみると[SCNAnimatable](https://developer.apple.com/documentation/scenekit/scnanimatable) の [animation(forKey:)](https://developer.apple.com/documentation/scenekit/scnanimatable/1524020-animation) を使うらしいのですが、書いてみてもそもそもエラーが出て動かない。。。

更に調べてみると、こんな記事が。

[Alternative to animation(forKey:) (now deprecated)?
](https://stackoverflow.com/questions/46009032/alternative-to-animationforkey-now-deprecated)

ざっと（Google先生が）訳してみると「CAAnimationで動作するanimation（forKey :)およびその姉妹メソッドはiOS11で廃止になったので、新しく導入された [SCNAnimationPlayer] (https://developer.apple.com/documentation/scenekit/scnanimationplayer)を使え」ということらしい。SCNファイルで確認した「[speed](https://developer.apple.com/documentation/scenekit/scnanimationplayer/2866041-speed)」というプロパティもありますね。

# アニメーション速度の設定

具体的には、以下のような手順になるようです。

1.SCNScene から 3Dモデルのnodeを取り出す。
2.daeファイルから SCNAnimationPlayerr.loadAnimation でアニメーションを取り出す。
3.アニメーションの再生速度を設定して、上記1.のnodeに node.addAnimationPlayer でアニメーションを再設定する。
4. SCNScene に 3Dモデルのnodeを追加する（たぶん上書きになる？）

```Swift:アニメーション速度の設定
        let scene = SCNScene(named: "art.scnassets/salsa.scn")!

        // ノード作成
        let node = scene.rootNode.childNode(withName: "salsa", recursively: true)!
        
        //アニメーションをSCNAnimationPlayerで取りだして、設定変更してnode に再設定する 2020.3.14
        danceAnimation = SCNAnimationPlayer.loadAnimation(fromSceneNamed: "art.scnassets/Salsa Dancing.dae")
        
        //speed：2
        danceAnimation.speed = 2

        danceAnimation.stop()
        node.addAnimationPlayer(danceAnimation, forKey: "dance")
        
        // アイテムの配置
        node.position = SCNVector3(0, 0, 0)
        node.scale = SCNVector3(1, 1, 1)
        scene.rootNode.addChildNode(node)
```

# アニメーション速度を可変にしてみる
UISliderを使ってアニメーションの再生速度を可変にしてみました。
スライダーによるspeedの範囲は、0.1-3.0にしました。

先程との違いは、以下の点です。

- nodeは、配置済のSCNView内のシーン（self.mySceneView.scene）から取り出す。
- アニメーションは保存済の変数（danceAnimationを使う。
- アニメーションを追加する前に、設定済のアニメーションを削除する（node.removeAllActions()）。

```Swift:アニメーション速度の設定
    //スライダー：初期化
    func initSlider(val:Float){
        speedSlider.minimumValue = 0.1
        speedSlider.maximumValue = 3.0
        speedSlider.value = val
    }

    @IBAction func dragSlider(_ sender: UISlider) {
        
        print("+++value:",sender.value)
        
        // ノード作成
        let node = self.mySceneView.scene!.rootNode.childNode(withName: "salsa", recursively: true)!
        
        //speed：value
        danceAnimation.speed = CGFloat(sender.value)
        danceAnimation.stop() 

        node.removeAllActions()
        node.addAnimationPlayer(danceAnimation, forKey: "dance")
    }
```

# 完成
再生ボタンでアニメーションを再生／停止。スライダーでアニメーションの再生速度が変わります。
![スクリーンショット 2020-03-30 2.01.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106624/a8897cfd-a725-c877-cb93-0fc40ff146b9.png)

ViewContoller.swift全体のコードです。

```ViewContoller.swift
//
//  ViewController.swift
//  ChangeTempoDance
//
//  Created by c-geru on 2020/03/22.
//  Copyright © 2020 c-geru. All rights reserved.
//

import UIKit
import SceneKit

class ViewController: UIViewController {

    @IBOutlet weak var mySceneView: SCNView!
    
    @IBOutlet weak var playButton: UIBarButtonItem!
    
    @IBOutlet weak var speedSlider: UISlider!
    
    @IBOutlet weak var toolBar: UIToolbar!
    
    var danceAnimation: SCNAnimationPlayer!
    var isPlay: Bool = false
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        let scene = SCNScene(named: "art.scnassets/salsa.scn")!

        
        // create and add a camera to the scene
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        scene.rootNode.addChildNode(cameraNode)
        
        // place the camera
        cameraNode.position = SCNVector3(x: 0, y: 1, z: 5)
        
        // create and add a light to the scene
        let lightNode = SCNNode()
        lightNode.light = SCNLight()
        lightNode.light!.type = .omni
        lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
        scene.rootNode.addChildNode(lightNode)
        
        // create and add an ambient light to the scene
        let ambientLightNode = SCNNode()
        ambientLightNode.light = SCNLight()
        ambientLightNode.light!.type = .ambient
        ambientLightNode.light!.color = UIColor.darkGray
        scene.rootNode.addChildNode(ambientLightNode)
        
        // ノード作成
        let node = scene.rootNode.childNode(withName: "salsa", recursively: true)!
        
        //アニメーションをSCNAnimationPlayerで取りだして、設定変更してnode に再設定する 2020.3.14
        danceAnimation = SCNAnimationPlayer.loadAnimation(fromSceneNamed: "art.scnassets/Salsa Dancing.dae")
        
        //speed：2
        danceAnimation.speed = 2

        danceAnimation.stop() // stop it for now so that we can use it later when it's appropriate
        node.addAnimationPlayer(danceAnimation, forKey: "dance")
        
        initSlider(val: Float(danceAnimation!.speed))

        // アイテムの配置
        node.position = SCNVector3(0, 0, 0)
        node.scale = SCNVector3(1, 1, 1)
        scene.rootNode.addChildNode(node)

        // set the scene to the view
        self.mySceneView!.scene = scene

        // configure the view
        self.mySceneView!.backgroundColor = UIColor.clear
    }

    //スライダー：初期化
    func initSlider(val:Float){
        speedSlider.minimumValue = 0.1
        speedSlider.maximumValue = 3.0
        speedSlider.value = val
    }

    @IBAction func dragSlider(_ sender: UISlider) {
        
        print("+++value:",sender.value)
        
        // ノード作成
        let node = self.mySceneView.scene!.rootNode.childNode(withName: "salsa", recursively: true)!
        
        //speed：value
        danceAnimation.speed = CGFloat(sender.value)

        danceAnimation.stop() // stop it for now so that we can use it later when it's appropriate
        node.removeAllActions()
        node.addAnimationPlayer(danceAnimation, forKey: "dance")
    }
    
    @IBAction func touchUpSlider(_ sender: UISlider) {
        if (isPlay){
           danceAnimation.play()
        }
        
    }
    
    @IBAction func tapPlayButton(_ sender: UIBarButtonItem) {
        if (isPlay){
            danceAnimation.stop()
            isPlay = false
        } else {
            danceAnimation.play()
            isPlay = true
        }
        
        //開始ボタン：表示切替
        changePlayButton()
    }
    
    //開始ボタン：表示切替
    func changePlayButton() {
        var items = toolBar.items!
        var btnItem: UIBarButtonItem!

        if (isPlay) {
            print("----pause")
            btnItem = UIBarButtonItem(barButtonSystemItem: .pause, target: self, action: #selector(self.tapPlayButton(_:)))
        } else {
            print("----play")
            btnItem = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(self.tapPlayButton(_:)))
        }

        items[0] = btnItem
        toolBar.setItems(items, animated: false)
    }

}

extension SCNAnimationPlayer {
    class func loadAnimation(fromSceneNamed sceneName: String) -> SCNAnimationPlayer {
        let scene = SCNScene( named: sceneName )!
        // find top level animation
        var animationPlayer: SCNAnimationPlayer! = nil
        scene.rootNode.enumerateChildNodes { (child, stop) in
            if !child.animationKeys.isEmpty {
                animationPlayer = child.animationPlayer(forKey: child.animationKeys[0])
                stop.pointee = true
            }
        }
        return animationPlayer
    }
}
```

# まとめ

こうやってアニメーションを差し替えられるのであれば、[以前書いた3Dアニメーションの切り替え](https://qiita.com/c_geru/items/d176039425aa76b30b4e)も、node ごと差し替えるんじゃなくて、アニメーションだけ再設定すればいけそうな気がする。それはまた追って試してみます。


もっといい方法があるよ！という方はご指摘頂けるとありがたいです。