Nico
Nico

Reputation: 194

SceneKit SCNSceneRendererDelegate - renderer function not called

I recently asked a question which had a pretty obvious answer. I'm still working on the same project and running into another problem. I need to implement per frame logic and the SCNSceneRendererDelegate protocol worked perfectly fine on iOS, but on OSX, the renderer function is not firing. I have created a little example project to illustrate my problem. It consists of a Scene Kit View in storyboard and following code in the ViewController class:

import Cocoa
import SceneKit

class ViewController: NSViewController, SCNSceneRendererDelegate {

    @IBOutlet weak var sceneView: SCNView!

    let cubeNode = SCNNode()

    override func viewDidLoad() {

        super.viewDidLoad()

        let scene = SCNScene()

        let sphere = SCNSphere(radius: 0.1)
        sphere.firstMaterial!.diffuse.contents = NSColor.yellowColor()
        let sphereNode = SCNNode(geometry: sphere)
        scene.rootNode.addChildNode(sphereNode)

        let cube = SCNBox(width: 0.2, height: 0.2, length: 0.2, chamferRadius: 0)
        cube.firstMaterial!.diffuse.contents = NSColor.greenColor()
        cubeNode.geometry = cube
        cubeNode.position = SCNVector3(1,0,0)
        scene.rootNode.addChildNode(cubeNode)

        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        cameraNode.position = SCNVector3(2, 1, 2)
        let constraint = SCNLookAtConstraint(target: cubeNode)
        cameraNode.constraints = [constraint]
        scene.rootNode.addChildNode(cameraNode)

        sceneView.scene = scene
        sceneView.backgroundColor = NSColor(red: 0.5, green: 0, blue: 0.3, alpha: 1)
        sceneView.allowsCameraControl = true

        sceneView.delegate = self
        sceneView.playing = true

    }

    func renderer(renderer: SCNSceneRenderer, updateAtTime time: NSTimeInterval) {
        cubeNode.position.x += 0.1
    }
}

All I want is to basically move the cube with every frame. But nothing happens. What is weird is that when I set sceneView.allowsCameraControl to true, the renderer function is called whenever I click or drag on the screen (which makes sense because it needs to update the view based on camera angles). But I would want it to be called every frame.

Is there an error I don't see or is this a bug in my Xcode?

Edit: I have tried following the instructions in the answer below and now have the following code for the ViewController:

import Cocoa
import SceneKit

class ViewController: NSViewController {

    @IBOutlet weak var sceneView: SCNView!
    let scene = MyScene(create: true)

    override func viewDidLoad() {

        super.viewDidLoad()

        sceneView.scene = scene
        sceneView.backgroundColor = NSColor(red: 0.5, green: 0, blue: 0.3, alpha: 1)
        sceneView.allowsCameraControl = true

        sceneView.delegate = scene
        sceneView.playing = true

    }

}

And a MyScene class:

import Foundation
import SceneKit

final class MyScene: SCNScene, SCNSceneRendererDelegate {

    let cubeNode = SCNNode()

    convenience init(create: Bool) {
        self.init()

        let sphere = SCNSphere(radius: 0.1)
        sphere.firstMaterial!.diffuse.contents = NSColor.yellowColor()
        let sphereNode = SCNNode(geometry: sphere)
        rootNode.addChildNode(sphereNode)

        let cube = SCNBox(width: 0.2, height: 0.2, length: 0.2, chamferRadius: 0)
        cube.firstMaterial!.diffuse.contents = NSColor.greenColor()
        cubeNode.geometry = cube
        cubeNode.position = SCNVector3(1,0,0)
        rootNode.addChildNode(cubeNode)

        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        cameraNode.position = SCNVector3(2, 1, 2)
        let constraint = SCNLookAtConstraint(target: cubeNode)
        cameraNode.constraints = [constraint]

        rootNode.addChildNode(cameraNode)
    }

    @objc func renderer(aRenderer: SCNSceneRenderer, updateAtTime time: NSTimeInterval) {
        cubeNode.position.x += 0.01
    }
}

However, it is still not working. What am I doing wrong?

Edit: setting sceneView.loops = true fixes the described problem

Upvotes: 10

Views: 8940

Answers (6)

Andrew Chinery
Andrew Chinery

Reputation: 231

I've been struggling with some similar bugs using SCNRenderer rather than SCNView, and this was the first hit on Google, so I just wanted to add a note with my solution in case it helps anyone.

I was using

.render(withViewport:commandBuffer:passDescriptor:)

but this does not call the delegate render method. Instead, use

.render(atTime:viewport:commandBuffer:passDescriptor:)

even if you are not using the time interval parameter. Then the delegate method renderer(_:updateAtTime:) will be called properly.

Upvotes: 0

Allen King
Allen King

Reputation: 313

In addition to several helpful hints in this chain, the final one for me to get delegate called was the following: If you use the pre Swift 4 methods for the SCNSceneRendererDelegate class, it compiles fine with no errors or warnings, but the delegate is never called.

Thus the obsolete pre-Swift 4 definition:

func renderer(aRenderer:SCNSceneRenderer, updateAtTime time:TimeInterval) {...}

(which I got from a snippet on the web) compiled just fine and was never called, while the correct definition

func renderer(_ renderer:SCNSceneRenderer, updateAtTimet time:TimeInterval) {...}

compiles and gets called!

Since SCNSceneRendererDelegate is a protocol, the normal Swift protections afforded by override are not appropriate. Since SCNSceneRendererDelegate defines its methods as optional (which I like), it is not caught that way either.

Upvotes: 3

Hal Mueller
Hal Mueller

Reputation: 7646

I don't understand what's causing the problem, but I was able to replicate it. I got it to work, though, by adding a meaningless SCNAction:

let dummyAction = SCNAction.scaleBy(1.0, duration: 1.0)
let repeatAction = SCNAction.repeatActionForever(dummyAction)
cubeNode.runAction(repeatAction)

The render loop fires only if the scene is "playing" (see SKScene becomes unresponsive while being idle). I expect that setting

    sceneView.isPlaying = true

(as you're already doing) would be enough to trigger the render callbacks.

The code I have above is not a solution. It's a nasty hack to work around your problem and allow you to get on with life.

Upvotes: 10

Entitize
Entitize

Reputation: 4969

For anyone still having problems, setting the delegate and playing variables will work.

sceneView.delegate = self
sceneView.isPlaying = true

Upvotes: 8

Integer Poet
Integer Poet

Reputation: 747

I suspect the answer hinges on what Querent means by "every frame". Querent should probably clarify this, but I'll try to answer anyway because I'm like that.

The simplest interpretation is probably "every frame that would render anyway", but that seems unlikely to be what is desired unless the cube is intended as a kind of activity monitor for the renderer, which doesn't seem likely either; there are much better approaches to that.

What Querent may want is to render repeatedly while the view's playing property is YES. If that's the case, then perhaps the answer is as simple as setting the view's loops property to YES. This recently solved a problem for me in which I wanted rendering to occur while a keyboard key was held down. (I had noticed that setting playing to YES would induce a single call to my delegate.)

Upvotes: 5

StackUnderflow
StackUnderflow

Reputation: 2463

Try this…put your code in a scene class instead – keep the view controller clean.

final class MySCNScene:SCNScene, SCNSceneRendererDelegate
{
    @objc func renderer(aRenderer:SCNSceneRenderer, updateAtTime time:NSTimeInterval)
    {
    }
}

Also set the view's delegate to your scene:

mySCNView!.delegate = mySCNScene

Upvotes: -2

Related Questions