如何在不阻塞主线程的情况下添加 SCNNodes?
How to add SCNNodes without blocking main thread?
我正在创建大量 SCNNode 并将其添加到 SceneKit 场景,这导致应用程序冻结一两秒。
我想我可以通过使用 DispatchQueue.global(qos: .background).async()
将所有操作放在后台线程中来解决这个问题,但没有骰子。它的行为完全相同。
我看到 and put the nodes through 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 满意的任何速度添加新节点。相反,应用程序会完全冻结一两秒钟,然后所有新节点都会立即出现。
为什么会这样,如何在不阻塞主线程的情况下添加大量节点?
我查了一下这个并没有解决冻结问题(我确实减少了一点)。
我预计 prepare()
会加剧冻结,而不是减少冻结,因为它会立即将所有资源加载到 GPU 中,而不是让它们延迟加载。我认为您不需要从后台线程调用 prepare()
,因为文档说它已经使用了后台线程。但是在后台线程上创建节点是一个很好的举措。
我确实看到通过将 geometry
移到循环之外并使用临时父节点(然后被克隆),性能得到了相当好的改进,因此只有一次调用来添加一个新的子节点场景的根节点。我还将球体的分段数减少到 10(从默认值 48)。
我从旋转的宇宙飞船示例项目开始,并通过点击手势触发了球体的添加。在我改变之前,我看到了 11 fps,每帧 7410 次绘制调用,8.18M 三角形。将几何体移出循环并展平球体树后,我达到了 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")
}
}
}
我认为使用 DispatchQueue 无法解决此问题。如果我替换其他任务而不是创建 SCNNode
s 它会按预期工作,所以我认为问题与 SceneKit 有关。
的答案表明 SceneKit 有自己的私有后台线程,它可以将所有更改批处理到该线程。因此,无论我使用哪个线程来创建我的 SCNNodes
,它们最终都在与渲染循环相同的线程中的相同队列中。
我使用的丑陋的解决方法是在 SceneKit 的委托 renderer(_:updateAtTime:)
方法中一次添加几个节点,直到它们全部完成。
我有一个包含 10000 个节点的小行星模拟,我自己 运行 对此问题进行了研究。对我有用的是创建容器节点,然后将其传递给后台进程以用子节点填充它。
该后台进程在该容器节点上使用 SCNAction 将每个生成的小行星添加到容器节点。
let action = runBlock {
Container in
// generate nodes
/// then For each node in generatedNodes
Container.addChildNode(node)
}
我还使用了一个共享的细节层次节点,它的几何形状是不均匀的边块,这样场景就可以一次性绘制这些节点。
我还预先生成了 50 个小行星形状,这些形状在背景生成过程中应用了 运行dom t运行sformations。该过程只需要在 运行dom 处抓取一个 pregen 块应用一个 运行dom simd t运行sformation 然后存储以供以后添加场景。
我正在考虑为 LOD 使用金字塔,但 5 x 10 x 15 块适合我的目的。此外,通过创建多个动作并将其传递给节点,可以轻松地限制此方法一次仅添加一定数量的块。最初我将每个节点作为一个动作传递,但这种方式也有效。
显示整个 10000 字段仍然会稍微影响 FPS 10 到 20 FPS,但此时容器节点自己的 LOD 开始生效,显示单个环。
在应用程序启动时添加所有这些,但将它们放在相机看不到的地方。当你需要它们时,改变它们应该在的位置。
我正在创建大量 SCNNode 并将其添加到 SceneKit 场景,这导致应用程序冻结一两秒。
我想我可以通过使用 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 满意的任何速度添加新节点。相反,应用程序会完全冻结一两秒钟,然后所有新节点都会立即出现。
为什么会这样,如何在不阻塞主线程的情况下添加大量节点?
我查了一下这个并没有解决冻结问题(我确实减少了一点)。
我预计 prepare()
会加剧冻结,而不是减少冻结,因为它会立即将所有资源加载到 GPU 中,而不是让它们延迟加载。我认为您不需要从后台线程调用 prepare()
,因为文档说它已经使用了后台线程。但是在后台线程上创建节点是一个很好的举措。
我确实看到通过将 geometry
移到循环之外并使用临时父节点(然后被克隆),性能得到了相当好的改进,因此只有一次调用来添加一个新的子节点场景的根节点。我还将球体的分段数减少到 10(从默认值 48)。
我从旋转的宇宙飞船示例项目开始,并通过点击手势触发了球体的添加。在我改变之前,我看到了 11 fps,每帧 7410 次绘制调用,8.18M 三角形。将几何体移出循环并展平球体树后,我达到了 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")
}
}
}
我认为使用 DispatchQueue 无法解决此问题。如果我替换其他任务而不是创建 SCNNode
s 它会按预期工作,所以我认为问题与 SceneKit 有关。
SCNNodes
,它们最终都在与渲染循环相同的线程中的相同队列中。
我使用的丑陋的解决方法是在 SceneKit 的委托 renderer(_:updateAtTime:)
方法中一次添加几个节点,直到它们全部完成。
我有一个包含 10000 个节点的小行星模拟,我自己 运行 对此问题进行了研究。对我有用的是创建容器节点,然后将其传递给后台进程以用子节点填充它。
该后台进程在该容器节点上使用 SCNAction 将每个生成的小行星添加到容器节点。
let action = runBlock {
Container in
// generate nodes
/// then For each node in generatedNodes
Container.addChildNode(node)
}
我还使用了一个共享的细节层次节点,它的几何形状是不均匀的边块,这样场景就可以一次性绘制这些节点。
我还预先生成了 50 个小行星形状,这些形状在背景生成过程中应用了 运行dom t运行sformations。该过程只需要在 运行dom 处抓取一个 pregen 块应用一个 运行dom simd t运行sformation 然后存储以供以后添加场景。
我正在考虑为 LOD 使用金字塔,但 5 x 10 x 15 块适合我的目的。此外,通过创建多个动作并将其传递给节点,可以轻松地限制此方法一次仅添加一定数量的块。最初我将每个节点作为一个动作传递,但这种方式也有效。
显示整个 10000 字段仍然会稍微影响 FPS 10 到 20 FPS,但此时容器节点自己的 LOD 开始生效,显示单个环。
在应用程序启动时添加所有这些,但将它们放在相机看不到的地方。当你需要它们时,改变它们应该在的位置。