首页 文章

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

提问于
浏览
10

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

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

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

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

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

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

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

3 回答

  • 6

    我不认为使用 DispatchQueue 可以解决这个问题。如果我替换其他任务而不是创建SCNNode s 它按预期工作,所以我认为问题与 SceneKit 有关。

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

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

  • 2

    我在这上面探讨并没有解决冻结(我确实减少了一点)。

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

    通过在循环外部移动geometry并使用临时父节点(然后克隆),我确实看到了相当不错的性能提升,因此只有一个调用将新子节点添加到场景的根节点。我还将球体的片段数量减少到 10(默认值为 48)。

    我开始使用旋转宇宙飞船示例项目,并从轻敲手势触发了球体的添加。在我改变之前,我看到每帧 11 帧 fps,7410 个绘制调用,8.18M 三角形。将几何体移出循环并展平球体树后,我达到 60 fps,每帧只有 3 次绘制调用和 1.67M 三角形(iPhone 6s)。

    您是否需要在运行时构建这些对象?您可以构建此场景一次,将其存档,然后将其作为资产嵌入。根据您想要实现的效果,您可能还会考虑使用 SCNSceneRenderer 的present(_: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")
            }
        }
    }
    
  • 0

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

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

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

    我还使用了一个共享级别的详细信息节点,其中不平坦的侧面块作为其几何体,以便场景可以在一次通过中绘制这些节点。

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

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

    显示 10000 的整个字段仍然会轻微影响 FPS 10 a 20 FPS 但是此时容器节点拥有 LOD 生效显示单个环。

相关问题