如何在不阻塞主线程的情况下添加SCNNode?

时间:2021-10-24 20:59:45

I'm creating and adding a large number of SCNNodes to a SceneKit scene, which causes the app to freeze for a second or two.

我正在创建并向SceneKit场景添加大量SCNNode,这会导致应用程序冻结一两秒钟。

I thought I could fix this by putting all the action in a background thread using DispatchQueue.global(qos: .background).async(), but no dice. It behaves exactly the same.

我想我可以通过使用DispatchQueue.global(qos:.background).async()将所有操作放在后台线程中来解决这个问题,但是没有骰子。它的行为完全相同。

I saw this answer and put the nodes through SCNView.prepare() before adding them, hoping it would slow down the background thread and prevent blocking. It didn't.

我看到了这个答案并在添加节点之前通过SCNView.prepare()放置节点,希望它会减慢后台线程的速度并阻止阻塞。它没有。

Here's a test function that reproduces the problem:

这是一个重现问题的测试函数:

func spawnNodesInBackground() {
    // put all the action in a background thread
    DispatchQueue.global(qos: .background).async {
        var nodes = [SCNNode]()
        for i in 0...5000 {
            // create a simple SCNNode
            let node = SCNNode()
            node.position = SCNVector3(i, i, i)
            let geometry = SCNSphere(radius: 1)
            geometry.firstMaterial?.diffuse.contents = UIColor.white.cgColor
            node.geometry = geometry
            nodes.append(node)
        }
        // run the nodes through prepare()
        self.mySCNView.prepare(nodes, completionHandler: { (Bool) in
            // nodes are prepared, add them to scene
            for node in nodes {
                self.myRootNode.addChildNode(node)
            }
        })
    }
}

When I call spawnNodesInBackground() I expect the scene to continue rendering normally (perhaps at a reduced frame rate) while new nodes are added at whatever pace the CPU is comfortable with. Instead the app freezes completely for a second or two, then all the new nodes appear at once.

当我调用spawnNodesInBackground()时,我希望场景能够继续正常渲染(可能以降低的帧速率),同时以CPU适应的任何速度添加新节点。相反,应用程序完全冻结一两秒,然后所有新节点立即出现。

Why is this happening, and how can I add a large number of nodes without blocking the main thread?

为什么会发生这种情况,如何在不阻塞主线程的情况下添加大量节点?

3 个解决方案

#1


5  

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.

我不认为使用DispatchQueue可以解决这个问题。如果我替换其他任务而不是创建SCNNode它按预期工作,所以我认为问题与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.

这个问题的答案表明SceneKit有自己的私有后台线程,它将所有更改批量化。因此,无论我使用什么线程来创建我的SCNNode,它们都会在与渲染循环相同的线程中的同一队列中结束。

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.

我正在使用的丑陋的解决方法是在SceneKit的委托渲染器(_:updateAtTime :)方法中一次添加几个节点,直到它们全部完成。

#2


1  

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.

我希望prepare()会加剧冻结,而不是减少它,因为它会立即将所有资源加载到GPU中,而不是让它们懒得加载。我不认为你需要从后台线程调用prepare(),因为doc说它已经使用了后台线程。但是在后台线程上创建节点是一个很好的举措。

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

我确实通过在循环外部移动几何体并使用临时父节点(然后克隆)来看到相当好的性能改进,因此只有一个调用将新子节点添加到场景的根节点。我还将球体的片段数量减少到10(默认值为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).

我开始使用旋转太空船示例项目,并从轻敲手势触发了球体的添加。在我改变之前,我看到每帧11帧fps,7410个绘图调用,8.18M三角形。将几何体移出循环并展平球体树后,我达到60 fps,每帧只有3个绘制调用和1.67M三角形(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.

您是否需要在运行时构建这些对象?您可以构建此场景一次,将其存档,然后将其作为资产嵌入。根据您想要实现的效果,您还可以考虑使用SCNSceneRenderer的当前(_:with:incomingPointOfView:transition:completionHandler)一次替换整个场景。

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")
        }
    }
}

#3


0  

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.

我有一个10000个节点的小行星模拟,我自己也遇到了这个问题。对我有用的是创建容器节点,然后将其传递给后台进程以使用子节点填充它。

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

该后台进程在该容器节点上使用SCNAction将每个生成的小行星添加到容器节点。

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.

我还预先生成50个小行星形状,在后台生成过程中应用随机变换。该过程只需随机抓取pregen块应用随机simd变换然后存储以便稍后添加场景。

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.

我正在考虑为LOD使用金字塔但是5 x 10 x 15块可以用于我的目的。此外,通过创建多个动作并将多个动作传递给节点,可以轻松地将此方法限制为仅一次添加一定量的块。最初我将每个节点作为一个动作传递,但这种方式也有效。

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.

显示10000的整个区域仍然会轻微影响FPS 10 a 20 FPS,但此时容器节点自己的LOD生效显示单个环。

#1


5  

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.

我不认为使用DispatchQueue可以解决这个问题。如果我替换其他任务而不是创建SCNNode它按预期工作,所以我认为问题与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.

这个问题的答案表明SceneKit有自己的私有后台线程,它将所有更改批量化。因此,无论我使用什么线程来创建我的SCNNode,它们都会在与渲染循环相同的线程中的同一队列中结束。

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.

我正在使用的丑陋的解决方法是在SceneKit的委托渲染器(_:updateAtTime :)方法中一次添加几个节点,直到它们全部完成。

#2


1  

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.

我希望prepare()会加剧冻结,而不是减少它,因为它会立即将所有资源加载到GPU中,而不是让它们懒得加载。我不认为你需要从后台线程调用prepare(),因为doc说它已经使用了后台线程。但是在后台线程上创建节点是一个很好的举措。

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

我确实通过在循环外部移动几何体并使用临时父节点(然后克隆)来看到相当好的性能改进,因此只有一个调用将新子节点添加到场景的根节点。我还将球体的片段数量减少到10(默认值为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).

我开始使用旋转太空船示例项目,并从轻敲手势触发了球体的添加。在我改变之前,我看到每帧11帧fps,7410个绘图调用,8.18M三角形。将几何体移出循环并展平球体树后,我达到60 fps,每帧只有3个绘制调用和1.67M三角形(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.

您是否需要在运行时构建这些对象?您可以构建此场景一次,将其存档,然后将其作为资产嵌入。根据您想要实现的效果,您还可以考虑使用SCNSceneRenderer的当前(_:with:incomingPointOfView:transition:completionHandler)一次替换整个场景。

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")
        }
    }
}

#3


0  

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.

我有一个10000个节点的小行星模拟,我自己也遇到了这个问题。对我有用的是创建容器节点,然后将其传递给后台进程以使用子节点填充它。

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

该后台进程在该容器节点上使用SCNAction将每个生成的小行星添加到容器节点。

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.

我还预先生成50个小行星形状,在后台生成过程中应用随机变换。该过程只需随机抓取pregen块应用随机simd变换然后存储以便稍后添加场景。

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.

我正在考虑为LOD使用金字塔但是5 x 10 x 15块可以用于我的目的。此外,通过创建多个动作并将多个动作传递给节点,可以轻松地将此方法限制为仅一次添加一定量的块。最初我将每个节点作为一个动作传递,但这种方式也有效。

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.

显示10000的整个区域仍然会轻微影响FPS 10 a 20 FPS,但此时容器节点自己的LOD生效显示单个环。