How to add SCNNodes without blocking main thread? How to add SCNNodes without blocking main thread? multithreading multithreading

How to add SCNNodes without blocking main thread?


I don't think this problem is solvable using the DispatchQueue. If I substitute some other task instead of creating SCNNodes it works as expected, so I think the problem is related to SceneKit.

The answers to this question suggest that SceneKit has its own private background thread that it batches all changes to. So regardless of what thread I use to create my SCNNodes, they all end up in the same queue in the same thread as the render loop.

The ugly workaround I'm using is to add the nodes a few at a time in SceneKit's delegated renderer(_:updateAtTime:) method until they're all done.


I poked around on this and didn't solve the freeze (I did reduce it a bit).

I expect that prepare() is going to exacerbate the freeze, not reduce it, because it's going to load all resources into the GPU immediately, instead of letting them be lazily loaded. I don't think you need to call prepare() from a background thread, because the doc says it already uses a background thread. But creating the nodes on a background thread is a good move.

I did see pretty good performance improvement by moving the geometry outside the loop, and by using a temporary parent node (which is then cloned), so that there's only one call to add a new child to the scene's root node. I also reduced the sphere's segment count to 10 (from the default of 48).

I started with the spinning spaceship sample project, and triggered the addition of the spheres from the tap gesture. Before my changes, I saw 11 fps, 7410 draw calls per frame, 8.18M triangles. After moving the geometry out of the loop and flattening the sphere tree, I hit 60 fps, with only 3 draw calls per frame and 1.67M triangles (iPhone 6s).

Do you need to build these objects at run time? You could build this scene once, archive it, and then embed it as an asset. Depending on the effect you want to achieve, you might also consider using SCNSceneRenderer's present(_:with:incomingPointOfView:transition:completionHandler) to replace the entire scene at once.

func spawnNodesInBackgroundClone() {    print(Date(), "starting")    DispatchQueue.global(qos: .background).async {        let tempParentNode = SCNNode()        tempParentNode.name = "spheres"        let geometry = SCNSphere(radius: 0.4)        geometry.segmentCount = 10        geometry.firstMaterial?.diffuse.contents = UIColor.green.cgColor        for x in -10...10 {            for y in -10...10 {                for z in 0...20 {                    let node = SCNNode()                    node.position = SCNVector3(x, y, -z)                    node.geometry = geometry                    tempParentNode.addChildNode(node)                }            }        }        print(Date(), "cloning")        let scnView = self.view as! SCNView        let cloneNode = tempParentNode.flattenedClone()        print(Date(), "adding")        DispatchQueue.main.async {            print(Date(), "main queue")            print(Date(), "prepare()")            scnView.prepare([cloneNode], completionHandler: { (Bool) in                scnView.scene?.rootNode.addChildNode(cloneNode)                print(Date(), "added")            })            // only do this once, on the simulator            // let sceneData = NSKeyedArchiver.archivedData(withRootObject: scnView.scene!)            // try! sceneData.write(to: URL(fileURLWithPath: "/Users/hal/scene.scn"))            print(Date(), "queued")        }    }}


I have an asteroid simulation with 10000 nodes and ran into this issue myself. What worked for me was creating the container node, then passing it to a background process to fill it with child nodes.

That background process uses an SCNAction on that container node to add each of the generated asteroids to the container node.

let action = runBlock {     Container in    // generate nodes    /// then For each node in generatedNodes    Container.addChildNode(node)}

I also used a shared level of detail node with an uneven sided block as its geometry so that the scene can draw those nodes in a single pass.

I also pre-generate 50 asteroid shapes that get random transformations applied during the background generation process. That process simply has to grab at random a pregen block apply a random simd transformation then stored for adding scene later.

I’m considering using a pyramid for the LOD but the 5 x 10 x 15 block works for my purpose. Also this method can be easily throttled to only add a set amount of blocks at a time by creating and passing multiple actions to the node. Initially I passed each node as an action but this way works too.

Showing the entire field of 10000 still affects the FPS slightly by 10 a 20 FPS but At that point the container nodes own LOD comes into effect showing a single ring.