SceneKit在方块测试中的性能表现

22

在学习3D游戏图形编程时,我决定通过使用Scene Kit 3D API来开始简单的尝试。我的第一个游戏目标是构建一个非常简化版的Minecraft。只有方块的游戏-这有多难呢。

下面是我编写的循环,用于放置100 x 100个方块(10,000),但FPS性能非常糟糕(约20 FPS)。我的初始游戏目标对于Scene Kit 来说太过于艰难了,还是有更好的方法来解决这个问题吗?

我已经阅读了 StackExchange 上的其他主题,但觉得它们没有回答我的问题。将暴露表面块转换为单个网格不起作用,因为SCNGeometry是不可变的。

func createBoxArray(scene : SCNScene, lengthCount: Int, depthCount: Int) {
    let startX : CGFloat = -(CGFloat(lengthCount) * CUBE_SIZE) + (CGFloat(lengthCount) * CUBE_MARGIN) / 2.0
    let startY : CGFloat = 0.0
    let startZ : CGFloat = -(CGFloat(lengthCount) * CUBE_SIZE) + (CGFloat(lengthCount) * CUBE_MARGIN) / 2.0

    var currentZ : CGFloat = startZ

    for z in 0 ..< depthCount {
        currentZ += CUBE_SIZE + CUBE_MARGIN

        var currentX = startX
        for x in 0 ..< lengthCount {
            currentX += CUBE_SIZE + CUBE_MARGIN

            createBox(scene, x: currentX, y: startY, z: currentZ)
        }
    }
}


func createBox(scene : SCNScene, x: CGFloat, y: CGFloat, z: CGFloat) {
    var box = SCNBox(width: CUBE_SIZE, height: CUBE_SIZE, length: CUBE_SIZE, chamferRadius: 0.0)
    box.firstMaterial?.diffuse.contents = NSColor.purpleColor()

    var boxNode = SCNNode(geometry: box)
    boxNode.position = SCNVector3Make(x, y, z)
    scene.rootNode.addChildNode(boxNode)
}

更新时间:2014年12月30日 我修改了代码,现在SCNBoxNode只创建一次,然后通过数组中的每个附加盒子来创建100 x 100。

var newBoxNode = firstBoxNode.clone()
newBoxNode.position = SCNVector3Make(x, y, z)

这个更改似乎将FPS增加到了约30fps。其他统计数据如下(从SCNView显示的统计数据):

10K(我认为这是绘制调用?) 120K(我认为这是面数) 360K(假设这是顶点数)

大部分运行循环在渲染中(我估计有98%)。总循环时间为26.7毫秒(糟糕)。我正在Mac Pro Late 2013上运行(带有双D500 GPU的6核)。

考虑到像MineCraft风格的游戏会根据玩家的操作不断变化,我不知道如何在Scene Kit的限制内进行优化。这让我非常失望,因为我真的很喜欢这个框架。我很想听听别人对如何解决这个问题的想法-如果没有,我就不得不使用OpenGL。

更新12-30-2014 @ 2:00pm ET: 当使用flattenedClone()时,我看到了显着的性能提升。即使有更多的方块和两个绘图调用,FPS现在仍稳定在60fps。但是,适应动态环境(如MineCraft所支持的)仍然存在问题-请见下文。

由于数组会随时间而改变组成,因此我添加了一个keyDown处理程序来向现有的数组中添加一个更大的框数组,并计算添加框数组与添加flattenedClone之间的时间差异。这是我发现的:

在keyDown上,我添加另一个120 x 120方块(14,400个方块)的数组

// This took .0070333 milliseconds
scene?.rootNode.addChildNode(boxArrayNode)
// This took .02896785 milliseconds
scene?.rootNode.addChildNode(boxArrayNode.flattenedClone())

再次调用flattenedClone()的速度比添加数组慢4倍。

这导致两个绘图调用具有293K个面和878K个顶点。我仍在进行测试,如果发现任何新情况将进行更新。总之,通过我的额外测试,我仍然觉得Scene Kit的不可变几何约束意味着我无法利用该框架。


1
你正在哪个环境中进行测试?你的性能瓶颈在哪里?请参考WWDC 2014的“使用SceneKit构建游戏”演讲,了解如何追踪后者。 - rickster
2
我不熟悉SceneKit,但通常来说,“天真”的方法会比较慢。考虑到像Minecraft这样的游戏可能会确保不渲染完全被其他方块隐藏的方块,实现实例化(一次性绘制相同的方块)和其他一般和游戏特定的优化。SceneKit是一个通用的渲染器,因此您需要尝试并查看可以实现哪些优化以及什么对SceneKit最有效。如果您确定需要更多的低级控制,则可能需要回退到GLKit或原始OpenGL。 - CodeSmile
2
10K个绘制调用太多了。尝试将其降至100左右。通过对几何图形进行平坦化处理(flattenedClone()),您可以大大减少绘制调用的数量。如果一个盒子稍后应该由用户的操作分离,我会在那个动作时处理该盒子,而不是让整个场景处于分离状态,仅因为用户可能与它交互。 - David Rönnqvist
你决定了吗?你能使用SceneKit还是需要使用OpenGL? - Crashalot
我决定不使用SceneKit,虽然我喜欢这个想法,并认为苹果在设计框架方面做得很好,但它对于我的需求来说不够灵活。目前我正在学习Metal框架,这需要更高的学习曲线,但我一直喜欢接近底层的编程(打趣一下-汇编曾经是我最喜欢的语言)。 - Dead Pixel
我也在为绘制调用次数而苦恼。 我有一个简单的场景,大约有40个节点,2个灯光(环境和定向)。 结果是,我有不稳定的55 FPS和大约100多个绘制调用。 iOS 12 SceneKit。@DavidRönnqvist你们看到控制台中的这种错误吗:objc [88539]:__weak变量位于0x6000037d0f30,而不是0x6000032db180。 这可能是对objc_storeWeak()和objc_loadWeak()的不正确使用。 在objc_weak_error上断点进行调试。当使用flattenedClone()? 我正在面临这个问题。 没有展平,控制台中没有错误:( - piotr_ch
2个回答

1

为什么会出现丢帧?

2022年9月4日

自从你发出这个问题已经有8年多了,但是并没有太多的改变...


enter image description here


1. 多边形数量

在SceneKit或RealityKit场景中,多边形的数量不得超过100,000个三角形多边形。一个理想的SceneKit场景,能够更快地渲染所有模型,应该包含少于50,000个多边形。您的场景包含120,000个多边形。请不要忘记,SceneKit使用单线程渲染模型(与多线程的RealityKit渲染器不同)。


2. 着色器

在 Xcode 14.0+ 中,SceneKit 的默认 .lightingModel.physicallyBased 材质,适用于 Material Inspector(UI 版本)中的任何 3D 库的基本设置。这是最计算密集的着色器。对于任何 SCN 过程几何体的编程版本,.lightingModel 的着色模型是 .blinn。最不计算密集的着色器是 .constant(它不依赖于光照)。


3. 梯形体内有什么

如果所有的10,000个立方体都在SceneKit相机梯形体内,那么帧率将会是20-30 fps。但是如果你移动了立方体的矩阵,并且只看到了其中的九分之一,那么帧率将会是60 fps。因此,SceneKit不会渲染那些在梯形体边界之外的对象。


4. SCNScene 中的网格数量

每个模型网格都会产生一次绘制调用。为了实现 60 帧每秒,每个绘制调用应该在 16 毫秒或更短的时间内完成。为了获得最佳性能,苹果工程师建议将 .usdz 文件中的网格数量限制在大约 50 个。不幸的是,在官方文档中我没有找到 .scn 文件的值。


5. 灯光和阴影

灯光和阴影(尤其是阴影)是非常计算密集的任务。一般的建议是避免使用.forward阴影和带有虚假阴影的高分辨率纹理。

请查看this post以获取详细信息。


用于测试的SwiftUI代码

Xcode 14.0+,SwiftUI 4.0+,Swift 5.7

import SwiftUI
import SceneKit

struct ContentView: View {

    var scene = SCNScene()
    var options: SceneView.Options = [.allowsCameraControl]

    var body: some View {
        ZStack {
            ForEach(-50...49, id: \.self) { x in
                ForEach(-50...49, id: \.self) { z in
                    let _ = DispatchQueue.global().async {
                        scene.rootNode.addChildNode(createCube(x, 0, z))
                    }
                }
            }
            SceneView(scene: scene, options: options)
                .ignoresSafeArea()
            let _ = scene.background.contents = UIColor.black
        }
    }
    
    func createCube(_ posX: Int, _ posY: Int, _ posZ: Int) -> SCNNode {
        let geo = SCNBox(width: 0.5, height: 0.5, length: 0.5, 
                                           chamferRadius: 0.0)
        geo.firstMaterial?.lightingModel = .constant
        let boxNode = SCNNode(geometry: geo)
        boxNode.position = SCNVector3(posX, posY, posZ)
        return boxNode
    }
}


在这里,所有的立方体都在视锥体内,因此出现掉帧的明显原因。

enter image description here

这里,只有场景的一部分在视锥体内,因此不会出现丢帧。

enter image description here


0

你提到了Minecraft,我认为值得研究一下它的工作原理。

我没有技术细节或代码示例,但一切都应该非常简单:

你是否曾经在线玩过Minecraft,地形没有加载,让你可以透过看到里面?那是因为里面没有几何图形。

假设我有一个2x2x2的立方体数组。这使得2*2*2*6*2 = 96个三角形。

然而,如果你只测试和绘制从相机视点可见的多边形,也许通过测试法线(因为它是立方体),这个数字会降至48个三角形。

如果你找到一种方法来确定哪些面被其他面遮挡(这也不应该太难,因为你正在处理平面、正方形、基于网格的面),你只需要绘制这些面。这样,我们只需要绘制8到24个三角形。这是高达90%的优化。

如果你想深入研究,甚至可以将面组合起来,将可见的平面面组合成单个N-gon。如果你创建一种新的动态生成几何图形的方法,结合前两种方法,并测试同一平面上相邻的可见面,就可以做到这一点。

如果您成功了,我们将只需要渲染2到6个多边形,就能呈现出8个立方体。

请注意,上述方法仅适用于您的区块彼此相连的情况。

可能有很多类似Minecraft的渲染器论文,通过几次Google搜索,您就可以找到答案!


我同意你的评论Moustach,但这种方法的挑战在于SCNGeometry类。它支持你所说的一切,但是它是不可变的。因此,你需要为表示地形的SCNGeometry创建一个网格,一旦有任何变化发生,你就会丢弃该网格(我打赌会产生巨大的内存释放循环),然后重新创建一切来适应仅仅一个小的“块”变化。 - Dead Pixel
可以采用Minecraft的方法,使用“区块”。通过将几何元素分离,可以逐个测试它们,并查看是否需要根据当前POV进行更新。除非用户非常快速地移动,否则每个区块不应在几帧内更新一次,考虑到单个生成网格可以用于多个视点。还可以渲染完整对象,但仍然可以通过组合共面多边形和消除内部几何来进行优化。 - Moustach
如果你看到有人“玩”Minecraft时表现得很好,让我惊讶的是他们移动的速度有多快,以及Minecraft的反应速度有多么惊人。我认为它持久吸引人的部分原因是它的性能非常出色。在减速方面,用户体验很少会受到任何干扰。即使是物品菜单也以非凡的速度弹出和交换。 - Confused
考虑到它是用Java制作的,这仍然相当令人印象深刻...但再一次强调,我们只谈论最多几千个多边形,几乎没有深度测试,也没有真正的粒子、物理效果... - Moustach
同意。在图形和几何方面,它并没有太多的东西。但是其中的内容确实是一个引人入胜、吸引人的体验,部分(甚至大部分?)原因是因为其响应速度。我看到的孩子们在游戏卡顿时会发出明显的不满声音。他们认为性能是体验的一部分,不可或缺。 - Confused

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接