10

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

Community
  • 1
  • 1
Nico
  • 194
  • 1
  • 12

6 Answers6

10

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.

johndpope
  • 5,035
  • 2
  • 41
  • 43
Hal Mueller
  • 7,019
  • 2
  • 24
  • 42
8

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

sceneView.delegate = self
sceneView.isPlaying = true
johndpope
  • 5,035
  • 2
  • 41
  • 43
Entitize
  • 4,553
  • 3
  • 20
  • 28
  • I had already done that, but as Integer Poet pointed out, sceneView.loops = true fixes the problem. – Nico Aug 15 '16 at 14:45
5

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

Integer Poet
  • 747
  • 5
  • 19
3

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.

Allen King
  • 303
  • 2
  • 8
0

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.

Andrew Chinery
  • 221
  • 1
  • 6
-2

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

StackUnderflow
  • 2,403
  • 2
  • 21
  • 28
  • Thanks for the suggestion, but I'm not really sure how I would connect this with the ViewController and Storyboard... Could you explain a bit more? – Nico Feb 14 '16 at 12:33
  • 1
    The storyboard and the view controller has no part in this actually, it is all about the SCNView and your SCNScene. The SCNView has a scene property, just set your SCNScene to it. – StackUnderflow Feb 14 '16 at 16:50
  • I have edited my question with what I think your solution looks like but it is still not working. Have I misunderstood you? – Nico Feb 14 '16 at 17:09
  • 1
    Subclassing `SCNScene` is not a good idea. You're setting yourself up for problems later when you try to serialize and deserialize with `NSKeyedArchiver`. http://stackoverflow.com/questions/34534743/trouble-subclassing-scnscene discusses this at depth, with several alternatives in the answers. – Hal Mueller Feb 14 '16 at 21:20
  • I don't agree at all with what you are saying regarding subclassing! What if he does not want to serialize or deserialize at all? I subclass all my scenes, it keeps things clean and where they should be instead of all over the place. – StackUnderflow Feb 14 '16 at 21:36
  • I edited my answer, you have to set the delegate on the view to your scene as well. – StackUnderflow Feb 14 '16 at 21:39
  • 1
    OP was already setting scene's delegate to the VC, in the original code sample. That didn't help. As for SCNScene subclassing, read @rickster's answer to the post I cited. I used to subclass SCNScene all the time myself. But the recurring archiving and initializer problems, Apple's sample code, and the way the Xcode Scene Editor interacts with SCNScene all argue against subclassing. Subclass SCNNode, or have a separate Game class to hold your game logic. – Hal Mueller Feb 14 '16 at 23:29
  • 1
    See this example project with working renderer & subclass. It is perfectly fine to subclass SCNScene, just load and add scene file contents to it. Simple & clean! https://dl.dropboxusercontent.com/u/6979623/ExampleSceneKitTester.zip – StackUnderflow Feb 15 '16 at 10:02
  • 1
    It's possible to subclass `SCNScene` for a SceneKit app in the same sense that it's possible to subclass `NSString` for a text editor. Neither causes immediate problems. However, Cocoa and its related frameworks encourage composition over inheritance — to such a degree that subclassing types that are intended as basic data-model objects can cause other problems for you later. – rickster Feb 15 '16 at 19:52
  • The lead dev for SceneKit is on record in this forum as saying SCNScene is not meant to be subclassed. Don't fight the frameworks. – Hal Mueller Feb 17 '16 at 10:42
  • Does that mean that he/they are doing it right? In my experience classes are ment to be subclasses unless they are explicitly set to not be subclassed... It is like creating a public variable but saying that you should not get/set it... – StackUnderflow Feb 17 '16 at 10:55
  • I've added my example project above, so why don't you add yours and explain why that solution is much better. I don't trust Apple devs, specially because of all the bugs and issues the have in their frameworks, and they change things around every year makes me wonder if they ever are gonna get things right! – StackUnderflow Feb 17 '16 at 10:58