iosswiftscenekitcaanimationscnscene

Using Scenekit sceneTime to scrub through animations iOS


I'm trying to modify Xcode's default game setup so that I can: program an animation into the geometry, scrub through that animation, and let the user playback the animation automatically.

I managed to get the scrubbing of the animation to work by setting the view's scene time based on the value of a scrubber. However, when I set the isPlaying boolean on the SCNSceneRenderer to true, it resets the time to 0 on every frame, and I can't get it to move off the first frame.

From the docs, I'm assuming this means it won't detect my animation and thinks the duration of all animations is 0.

Here's my viewDidLoad function in my GameViewController:

override func viewDidLoad() {
    super.viewDidLoad()

    // create a new scene
    let scene = SCNScene(named: "art.scnassets/ship.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: 0, z: 15)

    // 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)

    // retrieve the ship node
    let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!

    // define the animation
    //ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1)))
    let positionAnimation = CAKeyframeAnimation(keyPath: "position.y")
    positionAnimation.values = [0, 2, -2, 0]
    positionAnimation.keyTimes = [0, 1, 3, 4]
    positionAnimation.duration = 5
    positionAnimation.usesSceneTimeBase = true

    // retrieve the SCNView
    let scnView = self.view as! SCNView
    scnView.delegate = self

    // add the animation
    ship.addAnimation(positionAnimation, forKey: "position.y")

    // set the scene to the view
    scnView.scene = scene

    // allows the user to manipulate the camera
    scnView.allowsCameraControl = true

    // show statistics such as fps and timing information
    scnView.showsStatistics = true

    // configure the view
    scnView.backgroundColor = UIColor.black

    // add a tap gesture recognizer
    let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
    scnView.addGestureRecognizer(tapGesture)

    // play the scene
    scnView.isPlaying = true
    //scnView.loops = true
}

Any help is appreciated! :)

References:

sceneTime:

https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522680-scenetime

isPlaying:

https://developer.apple.com/documentation/scenekit/scnscenerenderer/1523401-isplaying

related question:

SceneKit SCNSceneRendererDelegate - renderer function not called


Solution

  • I couldn't get it to work in an elegant way, but I fixed it by adding this Timer call:

    Timer.scheduledTimer(timeInterval: timeIncrement, target: self,   selector: (#selector(updateTimer)), userInfo: nil, repeats: true)
    

    timeIncrement is a Double set to 0.01, and updateTimer is the following function:

    // helper function updateTimer
    @objc func updateTimer() {
        let scnView = self.view.subviews[0] as! SCNView
        scnView.sceneTime += Double(timeIncrement)
    }
    

    I'm sure there's a better solution, but this works.