2019年3月7日木曜日

ARKitのサンプルにアニメーションを追加しよう



ARKitのサンプルにアニメーションを追加しよう


今回扱うのはアニメーションです。

ただAR空間の物体に動きが入るとよりリアルさが出て楽しくなります。


アニメーションの設定方法


アニメーションの設定方法は大きく3つあります。

  • SCNAction
  • SCNTransaction
  • CoreAnimation


SCNAction


直進や回転、拡大縮小、フェードイン/フェードアウトをそれぞれ対応したActionを使ってAR上の物体にアニメーションを適用することができます。


SCNTransaction


AR空間の物体は60FPSのループで常に描画されていますが、その間隔を伸ばすことによりアニメーションを設定することができます。

イメージとしては物体を1m右に動かすことを1秒かけて行うことで、その間の動きが描画される感じです。



CoreAnimation


通常AR空間に表示する3DオブジェクトはBlenderなどの3Dモデリングツールを使って作られますが、3Dモデリングツールでは形を作るだけでなくオブジェクトにアニメーションを設定することができます。

そのような、外部ツールで設定されたアニメーションを呼び出すことでアニメーションを描画することができます。

キャラクターが歩くなどの複雑なアニメーションの場合は大抵CoreAnimationを使います。




修正方針


今回は次の2つのアニメーションをそれぞれ別の方法で実装してみます。

  1. 平面検知された時に表示される飛行機をフェードインさせる
  2. 飛行機をきりもみ回転させる


飛行機のフェードイン


SCNTransactionと、物体の透過を設定するopacityというプロパティを使って実装します。

まずARKitサンプルの飛行機を平面に置いてみようでshipの初期状態をHiddenにしましたが、これをopacityに変更しておきます。

ship.opacity = 0
このopacityは0〜1が設定でき、0が完全に透明な状態を示します。

次に、func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor)で飛行機を置いていますが、この処理の最後に次のSCNTransaction処理を追加します。
SCNTransaction.begin()
SCNTransaction.animationDuration = 1.5

ship.opacity = 1
SCNTransaction.commit()


飛行機のきりもみ回転


単純に飛行機を回転させる場合は次の2行で実装できます。

let rotationAction = SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 0, z: 1, duration: 1))
ship.runAction(rotationAction)

しかし、これを単純にshipに追加してしまうと、フェードインしている状態で回転をしてしまいます。

フェードインが完了してから回転をしたい場合は次のように実装をします。

let completion = {
    let rotationAction = SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 0, z: 1, duration: 1))
    ship.runAction(rotationAction)
}

SCNTransaction.begin()
SCNTransaction.animationDuration = 1.5
SCNTransaction.completionBlock = completion
ship.opacity = 1

SCNTransaction.commit()

実際に動作している様子




実際のコード

import UIKit
import SceneKit
import ARKit

class ViewController: UIViewController, ARSCNViewDelegate {

    @IBOutlet var sceneView: ARSCNView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Set the view's delegate
        sceneView.delegate = self
        
        // Show statistics such as fps and timing information
        sceneView.showsStatistics = true
        
        // Create a new scene
        let scene = SCNScene(named: "art.scnassets/ship.scn")!
        
        if let ship = scene.rootNode.childNode(withName: "ship", recursively: true) {
            ship.opacity = 0
        }
        
        // Setup Omni Light
        let light = SCNLight()
        light.type = .omni

        let LightNode = SCNNode()
        LightNode.light = light
        LightNode.name = "light"
        LightNode.position = SCNVector3(-1,2,-1)
        scene.rootNode.addChildNode(LightNode)
        
        // Set the scene to the view
        sceneView.scene = scene
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        // Create a session configuration
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = [.horizontal]
        
        // Run the view's session
        sceneView.session.run(configuration)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        // Pause the view's session
        sceneView.session.pause()
    }

    // MARK: - ARSCNViewDelegate
    
/*
    // Override to create and configure nodes for anchors added to the view's session.
    func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
        let node = SCNNode()
     
        return node
    }
*/
    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        
        if let ship = sceneView.scene.rootNode.childNode(withName: "ship", recursively: true) {
            ship.removeFromParentNode()
            node.addChildNode(ship)
            
            let configuration = ARWorldTrackingConfiguration()
            sceneView.session.pause()
            sceneView.session.run(configuration)
            
            let completion = {
                let rotationAction = SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 0, z: 1, duration: 1))
                ship.runAction(rotationAction)
            }
            
            SCNTransaction.begin()
            SCNTransaction.animationDuration = 1.5
            SCNTransaction.completionBlock = completion
            ship.opacity = 1
            SCNTransaction.commit()
        }
    }
    
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        guard let lightEstimate = self.sceneView.session.currentFrame?.lightEstimate else { return }
        let ambientIntensity = lightEstimate.ambientIntensity
        let ambientColorTemperature = lightEstimate.ambientColorTemperature
        
        if let lightNode = self.sceneView.scene.rootNode.childNode(withName: "light", recursively: true) {
            guard let light = lightNode.light else { return }
            light.intensity = ambientIntensity
            light.temperature = ambientColorTemperature
        }
    }
    
    func session(_ session: ARSession, didFailWithError error: Error) {
        // Present an error message to the user
        
    }
    
    func sessionWasInterrupted(_ session: ARSession) {
        // Inform the user that the session has been interrupted, for example, by presenting an overlay
        
    }
    
    func sessionInterruptionEnded(_ session: ARSession) {
        // Reset tracking and/or remove existing anchors if consistent tracking is required
        
    }
}


0 件のコメント:

コメントを投稿