SceneKitにおけるアニメーションの実施方法と使い分けについて


はじめに

ARKitのためにSceneKitを勉強してアニメーションの実現方法がいくつかあることが分かり、
その実施方法と自分なりの使い分けをまとめる。

SCNTransactionによるアニメーション

AppleのSCNTransactionのリファレンスによると、SCNKitではrun loopが常時回っており、何か変更が加えられた場合にアトミックにその変更が1つのトランザクション内で実行されるとのこと。通常1トランザクションの長さは0であるため、例えばSCNNodeのpositionを変更した場合移動する過程は表示されず、瞬間移動の様に変更後の新しいポジションに切り替わる。SCNTransactionのanimationDurationを0以外にすることでその1トランザクションにかかる時間を指定でき、変更の過程がアニメーションとして表示される。

例)タップで人体モデルの背骨(脊髄)を回転させる

var state = false

func handleTap(_ gestureRecognize: UIGestureRecognizer) {
    SCNTransaction.animationDuration = 1.0

    // 既に人物モデルがsceneに読み込まれている状態で人物モデルの背骨部分のボーンを取得する
    guard let spine = scene.rootNode.childNode(withName: "spine", recursively: true) else {return}

    if !state {
        spine.rotation = SCNVector4(0, 0, 1, 0.3)
        state = true
    } else {
        spine.rotation = SCNVector4(0, 0, 1, -0.3)
        state = false
    }
}

一度に複数のSCNNodeの位置を変えることは手間がかかるので、複雑なアニメーションを表示したい場合はこの方法だとかなり難しい。
しかし、上記サンプルの様に一つのSCNNodeだけを動かすのであればSCNTransactionを使ったら方が楽である。

SCNActionによるアニメーション

移動や回転等の動作をAPIで定義されているmoveBy()やrotateBy()でそれそれSCNActionのインスタンスを作ることができる。それらを引数に対象のノードがrun()メソッドを呼ぶことでアニメーションが表示される。SCNTransactionと同じくmoveBy()やrotateBy()の引数にあるdurationに指定した時間がアニメーション時間となる。これだけだと、SCNTransactionとできることは変わらない様に思えるが、次の点が異なる。

  1. SCNActionのインスタンスメソッドreversed()を呼ぶことで逆再生アニメーションが容易に実現できる
  2. Actionを実行したSCNNodeインスタンスのhasActionsプロパティの値を参照することで現在アニメーション中かどうか判断できる
  3. run()の中のクロージャでアニメーションが完了したときの動作を指定できる

個人的には3番目ができることが大きいと思う。これをうまく使えば前進中にユーザー操作を受け入れて方向転換するということが実現できる。

例)前進中に方向転換する

func move() {
    // guyNode : 人体モデルのrootNode
    if let guyNode = scene.rootNode.childNode(withName: "guyNode", recursively: true) {
        // z軸方向に0.5進むベクトルをユーザ操作を受けて人体モデルが向いた方向に回転させる
        let vec = SCNVector4Make(0, 0, 0.5, 0)
        let mat = SCNMatrix4MakeRotation(guyNode.rotation.w, 0, 1, 0)
        let newVec = (mat * vec).to3()

        // 回転させたベクトル分人体モデルを動かす
        let move = SCNAction.moveBy(x: CGFloat(newVec.x), y: 0, z: CGFloat(newVec.z), duration: 1)

        // Actionが完了したら再帰的にmove()を呼び出す
        guyNode.runAction(move, forKey: "move", completionHandler: {
            self.move()
        })
    }
}

SCNIKConstraintによるアニメーション

人体モデルのボーンにIK(inverse kinematics)を設定することで、デッサン人形の様に手から腕、二の腕を連動して動かすことができる。詳しいやり方はこちらの記事で紹介されているが、SCNIKConstraintのtargetPositionに値を設定することで、個々のNodeのpositionを指定することなく連動して自動的に位置を変えてくれる。SCNTransactionを使うことで位置変更前から位置変更後までをアニメーション化することができるが、ユーザの入力をリアルタイムにtargetPositionに反映させることで操り人形の様なアニメーションが実現できる。

例)ユーザの画面ドラッグに合わせて右腕を動かす

func handlePan(_ gestureRecognize: UIPanGestureRecognizer) {
    if (gestureRecognize.state == UIGestureRecognizerState.changed) {
        // 既に人物モデルがsceneに読み込まれている状態で人物モデルの右手部分のボーンを取得する
        guard let controlNode = scene.rootNode.childNode(withName: "hand_R", recursively: true) else { return }
        // ユーザのドラッグ移動量を取得する
        let move = gestureRecognize.translation(in: view)

        // 設定済みIKのtargetPositionに移動量を加算する
        let targetPosition = SCNVector3(controlNode.position.x + Float(move.x * 0.1), controlNode.position.y + Float(move.y * 0.1), controlNode.position.z)
        ik?.targetPosition = targetPosition
    }
}

SCNAnimationによるアニメーション

3Dオーサリングツールで作ったアニメーションデータをロードしてアニメーションを表示(再生)させる方法。作り込み次第では滑らかなアニメーションを表現できる反面、事前に決められた動きしかできない。内部的には各ボーンを時間とともに決められた位置へpositionを変えているだけなので、アニメーション再生が完了すると、ロード前の状態に戻ってしまう。これについてはSCNTransactionを使ったり、各ボーンのアニメーション再生の最後の位置状態を記憶しておく事で、アニメーション再生の前後を滑らかに繋ぐことができるかもしれない。また、CAAnimationDelegateを利用することでアニメーションの再生開始と停止をフックすることができる。

例)ダブルタップに合わせてキックをする

func handleDoubleTap(_ gestureRecognize: UIGestureRecognizer) {
    // Collada形式のファイルをロードしてCAAnimationインスタンスを作る
    let animation = CAAnimation.animationWithSceneNamed(name: "art.scnassets/kick.dae")
    animation?.duration = 2
    animation?.speed = 1.5

    // CAAnimationを元にSCNAnimationインスタンスを作り、アニメーションを再生する
    let scnAnimation = SCNAnimation(caAnimation: animation!)
    scene.rootNode.addAnimation(scnAnimation, forKey: "kick")
}

それぞれの評価と使い分け

方法 評価 利用シーン
SCNTransaction 一番お手軽に実現できるがこれだけで人体の動きを表現するのはかなり難易度が高い、完了を検知できないのも難点 ボールなどのそれ自体に動きのないものを移動させるときに利用するのが良いかもしれない
SCNAction 実行中や完了を検知できるのは◯だが、SCNTransactionで済んでしまうことが多いかもしれない 動作を無限に実行させたい場合、動作中にユーザーの入力を反映させたい場合
SCNIKConstraint 個々のノードを意識せずに、動かせることは魅力的だが、targetPositionの指定次第ではありえない動きをするので制御が難しい ユーザーの入力を元に一度に複数のノードを変更したい場合
SCNAnimation 一番滑らかな動きを実現できるが、事前に動きを別ツールで定義しなければいけない点が面倒 ゲーム内のキャラクターの動きを表現するときには基本的にこれ一択と言う感じ