【问题标题】:How to add SCNNodes without blocking main thread?如何在不阻塞主线程的情况下添加 SCNNodes?
【发布时间】:2017-05-02 00:50:23
【问题描述】:

我正在创建大量 SCNNode 并将其添加到 SceneKit 场景中,这会导致应用冻结一两秒。

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

我看到this answer 并在添加它们之前将节点通过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 可以接受的任何速度添加新节点。相反,应用程序完全冻结了一两秒钟,然后所有新节点立即出现。

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

【问题讨论】:

  • 如何将每个 SCNNode 添加作为自己的任务添加到队列中?然后让系统决定何时执行每个任务。现在,您创建一个添加 5000 个节点的大任务,而不是创建每个添加一个节点的 5000 个任务。在这种情况下不可能有并发。
  • 他要求的并发是减少/消除主(UI)线程上的冻结。在一个后台线程上完成所有节点创建很好。冻结发生在prepare()addChildNode() 调用中。
  • 您是否尝试将所有新节点添加到后台线程中的单个父节点,然后通过一次调用 addChildNode() 将该节点添加到根节点?
  • @ScottAhten 我尝试了你的建议,但不幸的是它没有消除或减少阻塞。
  • @KarlSigiscar 我尝试了你的建议。当任务被异步添加到队列中时,它在崩溃之前产生了 74 个线程。同步添加时,阻塞并没有减少。

标签: ios swift multithreading swift3 scenekit


【解决方案1】:

我认为使用 DispatchQueue 无法解决此问题。如果我替换一些其他任务而不是创建SCNNodes,它会按预期工作,所以我认为问题与 SceneKit 有关。

this question 的答案表明 SceneKit 有自己的私有后台线程,它将所有更改批处理。所以不管我用什么线程来创建我的SCNNodes,它们最终都会在渲染循环所在的线程中的同一个队列中结束。

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

【讨论】:

  • 不错的技巧!关于每帧添加几何或资源限制的任何提示?
  • @HalMueller 我仍在使用球体。我选择了每帧 10 个,在 6s 上轻松获得 60fps。添加所有 5000 个节点需要 8 秒多一点,但这对我的应用程序来说很好。我首先添加最大的节点,所以小节点仍然出现的情况不太明显。
【解决方案2】:

我在这个问题上四处寻找并没有解决冻结问题(我确实减少了一点)。

我预计prepare() 会加剧冻结,而不是减少冻结,因为它将立即将所有资源加载到 GPU 中,而不是让它们被延迟加载。我认为您不需要从后台线程调用prepare(),因为文档说它已经使用了后台线程。但是在后台线程上创建节点是一个不错的举措。

通过将geometry 移到循环外并使用临时父节点(然后将其克隆),我确实看到了相当不错的性能改进,因此只需一次调用即可将新子节点添加到场景的根节点.我还将球体的段数减少到 10(默认为 48)。

我从旋转的宇宙飞船示例项目开始,并通过点击手势触发了球体的添加。在我进行更改之前,我看到了 11 fps、每帧 7410 个绘制调用、818 万个三角形。在将几何体移出循环并将球体树展平后,我达到了 60 fps,每帧只有 3 个绘制调用和 167 万个三角形(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")
        }
    }
}

【讨论】:

  • 感谢您的有用建议!不幸的是,在我的实际用例中,geometry 对于每个球体都不同,因此必须保留在循环中。临时父节点可以为我工作,所以我会尝试。我喜欢你关于使用存档资产的建议,尽管我觉得这应该是可行的。
  • 绝对可以现场直播。这是一个关于冻结多少以及您对它的容忍程度的问题。由于您已经在后台线程上创建,因此使用存档场景不会(我预测)对过渡时间有太大影响。您正在添加 8M 三角形,无论它们是如何制作的,这都需要时间! present(...) 方法可以帮助您,愚弄用户(使用淡入淡出或翻转,而不是冻结)。 SCNLevelOfDetail 可能无济于事,因为您仍在一次添加一堆几何图形。你可以减少段数,或者使用金字塔代替吗?将绘制调用从 7400 减少到 3 是巨大的。
【解决方案3】:

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

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

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

我还使用了一个共享的细节层次节点和一个不均匀的边块作为它的几何图形,这样场景就可以一次绘制这些节点。

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

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

显示 10000 的整个字段仍然会稍微影响 FPS 10 到 20 FPS,但此时容器节点自己的 LOD 开始生效,显示单个环。

【讨论】:

    【解决方案4】:

    在应用程序启动时添加所有这些,但将它们放置在相机看不到的地方。当你需要他们时,改变他们应该在的位置。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-04-13
      • 2014-07-26
      相关资源
      最近更新 更多