Using Scenekit sceneTime to scrub through animations iOS

976 Views Asked by At

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

2

There are 2 best solutions below

0
On BEST ANSWER

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.

0
On

sceneTime is automatically set to 0.0 after actions and animations are run on every frame.

Use can use renderer(_:updateAtTime:) delegate method to set sceneTime to the needed value before SceneKit runs actions and animations.

Make GameViewController comply to SCNSceneRendererDelegate:

class GameViewController: UIViewController, SCNSceneRendererDelegate {
    // ...
}

Make sure you keep scnView.delegate = self inside viewDidLoad().

Now implement renderer(_:updateAtTime:) inside your GameViewController class:

// need to remember scene start time in order to calculate current scene time
var sceneStartTime: TimeInterval? = nil

func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    // if startTime is nil assign time to it
    sceneStartTime = sceneStartTime ?? time

    // make scene time equal to the current time
    let scnView = self.view as! SCNView
    scnView.sceneTime = time - sceneStartTime!
}