はじめに
以前、ARKitでの3Dアニメーション切り替えについて書いたけど、別のやり方が見つかったので、そのメモ。今回はSceneKitベースだけど、基本的には同じです。
実行環境
- Xcode 11.2.1
- MagicaVoxel 0.99.4
- Mixamo
やりたいこと
- ボタンタップでアニメーションを切り替える。
3Dアニメーションの準備
MagicaVoxelで作ったモデルを Mixamo にアップロードして、いくつかのアニメーションを設定して個別にCollada(.dae)形式で書き出しておきます。
※このとき「In Place」のチェックボックスがあるものは、チェックしておくと同じ位置でアニメーションしてくれるので、切り替え時に位置がズレなくていいかもしれません。
※また、ベースとして使う .scn ファイルにはrootNode を作って名前を付けておきます。+ボタンでノードを追加して名前(今回は「idle」にしました)を付け、その下に「unamed」「mixamorig_Hips」を移動します。
3Dアニメーションの切り替え(差し替え)
以前の方法では、アニメーションを含む3Dのノード自体を差し替えていましたが、「3Dモデル(アニメーションつき)の再生速度を変更する」という記事を書いたときに、node.addAnimationPlayer で node のアニメーションを再設定できるということに気がついたので、その方法で切り替えてみました。
1.ベースにするDaeファイルを.scnに変換
2.上記1.の.scn から 3Dモデルのnodeを取り出す。
3.daeファイルから SCNAnimationPlayerr.loadAnimation でアニメーションを取り出す。
4.上記3.で取りだしたアニメーションを 上記2.のnodeに node.addAnimationPlayer で再設定する。
で、上記2.で取り出すアニメーションを事前に配列化しておいて、SegmentedControl が押されたときに、selectedIndexで配列から、アニメーションを取りだして再設定するようにします。
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) へ追加する。
//アニメーションの変更
@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モデル(アニメーションつき)の再生速度を変更する」をベースにしているため、再生ボタンでのアニメーションを再生/停止。スライダーでアニメーションの再生速度変更にも対応しています。
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からアニメーションを取得して、切り替えることができるようになりました。
もっといい方法があるよ!という方はご指摘頂けるとありがたいです。