【SceneKit】3Dアニメーションの切り替え（その2）

# はじめに
以前、[ARKitでの3Dアニメーション切り替え](https://qiita.com/c_geru/items/d176039425aa76b30b4e)について書いたけど、別のやり方が見つかったので、そのメモ。今回はSceneKitベースだけど、基本的には同じです。

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

# やりたいこと
- ボタンタップでアニメーションを切り替える。

# 3Dアニメーションの準備
[MagicaVoxel](https://ephtracy.github.io)で作ったモデルを [Mixamo](https://www.mixamo.com) にアップロードして、いくつかのアニメーションを設定して個別にCollada（.dae）形式で書き出しておきます。
![cap02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106624/ee120f3c-a84d-fccb-4676-c05eb2684709.png)
※このとき「In Place」のチェックボックスがあるものは、チェックしておくと同じ位置でアニメーションしてくれるので、切り替え時に位置がズレなくていいかもしれません。

![cap01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106624/ef95da68-3030-2768-8b9e-54fdc4d722d1.png)
※また、ベースとして使う .scn ファイルにはrootNode を作って名前を付けておきます。＋ボタンでノードを追加して名前（今回は「idle」にしました）を付け、その下に「unamed」「mixamorig_Hips」を移動します。

# 3Dアニメーションの切り替え（差し替え）
以前の方法では、アニメーションを含む3Dのノード自体を差し替えていましたが、[「3Dモデル（アニメーションつき）の再生速度を変更する」](https://qiita.com/c_geru/items/c9ce0836527c9d44fbd3)という記事を書いたときに、node.addAnimationPlayer で node のアニメーションを再設定できるということに気がついたので、その方法で切り替えてみました。

1.ベースにするDaeファイルを.scnに変換
2.上記1.の.scn から 3Dモデルのnodeを取り出す。
3.daeファイルから SCNAnimationPlayerr.loadAnimation でアニメーションを取り出す。
4.上記3.で取りだしたアニメーションを 上記2.のnodeに node.addAnimationPlayer で再設定する。

で、上記2.で取り出すアニメーションを事前に配列化しておいて、SegmentedControl が押されたときに、selectedIndexで配列から、アニメーションを取りだして再設定するようにします。

```Swift:アニメーションの初期化
        let scene = SCNScene(named: "art.scnassets/idle.scn")!
        
        //アニメーション用配列；初期化
        danceAnimationArray.append(SCNAnimationPlayer.loadAnimation(fromSceneNamed: "art.scnassets/Box Idle.dae"))
        danceAnimationArray.append(SCNAnimationPlayer.loadAnimation(fromSceneNamed: "art.scnassets/Cross Jumps Rotation.dae"))
        danceAnimationArray.append(SCNAnimationPlayer.loadAnimation(fromSceneNamed: "art.scnassets/Hip Hop Dancing.dae"))
        danceAnimationArray.append(SCNAnimationPlayer.loadAnimation(fromSceneNamed: "art.scnassets/Jazz Dancing.dae"))
        danceAnimationArray.append(SCNAnimationPlayer.loadAnimation(fromSceneNamed: "art.scnassets/Northern Soul Spin.dae"))

        danceControl.selectedSegmentIndex = 0
        
        // 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: "idle", recursively: true)!
        
        //アニメーションを配列から取りだして再設定する
        danceAnimation = danceAnimationArray[danceControl.selectedSegmentIndex]
        
        //speed：1
        danceAnimation.speed = 1

        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
```
※初回は、scene を .scn ファイルから取り出し、SCNView（self.mySceneView） へ追加する。

```Swift:アニメーションの切り替え
    //アニメーションの変更
    @IBAction func actionSegmentedControl(_ sender: UISegmentedControl) {
        print("actionSegmentedControl",sender.selectedSegmentIndex)
        
        // ノード作成
        let node = self.mySceneView.scene!.rootNode.childNode(withName: "idle", recursively: true)!
        
        //アニメーションを配列から取りだして再設定する
        danceAnimation = danceAnimationArray[sender.selectedSegmentIndex]
        danceAnimation.speed = CGFloat(speedSlider.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")
        if (isPlay){
            danceAnimation.play()
        }
    }
```
※アニメーション切り替え時は、node を SCNView の scene（self.mySceneView.scene）から取り出し、その node へ アニメーションを追加する。

# 完成
SegmentedControlをタップすることで、割り当てられたアニメーションに切り替わります。

また前回の[「【SceneKit】3Dモデル（アニメーションつき）の再生速度を変更する」](https://qiita.com/c_geru/items/c9ce0836527c9d44fbd3)をベースにしているため、再生ボタンでのアニメーションを再生／停止。スライダーでアニメーションの再生速度変更にも対応しています。

![IMG_6181.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106624/79a2416a-9c4a-888e-5b41-b0c5527972d8.png)

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

```ViewContoller.swift
//
//  ViewController.swift
//  CahngeVariousDances
//
//  Created by c-geru on 2020/04/17.
//  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!
    
    @IBOutlet weak var danceControl: UISegmentedControl!
    
    var danceAnimation: SCNAnimationPlayer!
    var isPlay: Bool = false
    
    var danceAnimationArray:[SCNAnimationPlayer?] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        let scene = SCNScene(named: "art.scnassets/idle.scn")!
        
        //アニメーション用配列；初期化
        danceAnimationArray.append(SCNAnimationPlayer.loadAnimation(fromSceneNamed: "art.scnassets/Box Idle.dae"))
        danceAnimationArray.append(SCNAnimationPlayer.loadAnimation(fromSceneNamed: "art.scnassets/Cross Jumps Rotation.dae"))
        danceAnimationArray.append(SCNAnimationPlayer.loadAnimation(fromSceneNamed: "art.scnassets/Hip Hop Dancing.dae"))
        danceAnimationArray.append(SCNAnimationPlayer.loadAnimation(fromSceneNamed: "art.scnassets/Jazz Dancing.dae"))
        danceAnimationArray.append(SCNAnimationPlayer.loadAnimation(fromSceneNamed: "art.scnassets/Northern Soul Spin.dae"))

        danceControl.selectedSegmentIndex = 0
        
        // 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: "idle", recursively: true)!
        
        //アニメーションを配列から取りだして再設定する
        danceAnimation = danceAnimationArray[danceControl.selectedSegmentIndex]
        
        //speed：1
        danceAnimation.speed = 1

        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: "idle", recursively: true)!
        
        //speed：2
        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()
    }
    
    //アニメーションの変更
    @IBAction func actionSegmentedControl(_ sender: UISegmentedControl) {
        print("actionSegmentedControl",sender.selectedSegmentIndex)
        
        // ノード作成
        let node = self.mySceneView.scene!.rootNode.childNode(withName: "idle", recursively: true)!
        
        //アニメーションを配列から取りだして再設定する
        danceAnimation = danceAnimationArray[sender.selectedSegmentIndex]
        danceAnimation.speed = CGFloat(speedSlider.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")
        if (isPlay){
            danceAnimation.play()
        }
    }
    
    //開始ボタン：表示切替
    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
    }
}
```

# まとめ
これでdaeからアニメーションを取得して、切り替えることができるようになりました。
もっといい方法があるよ！という方はご指摘頂けるとありがたいです。