17
votes

In learning 3d graphics programming for games I decided to start off simple by using the Scene Kit 3D API. My first gaming goal was to build a very simplified mimic of MineCraft. A game of just cubes - how hard can it be.

Below is a loop I wrote to place a ride of 100 x 100 cubes (10,000) and the FPS performance was abysmal (~20 FPS). Is my initial gaming goal too much for Scene Kit or is there a better way to approach this?

I have read other topics on StackExchange but don't feel they answer my question. Converting the exposed surface blocks to a single mesh won't work as the SCNGeometry is immutable.

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

UPDATE 12-30-2014: I modified the code so the SCNBoxNode is created once and then each additional box in the array of 100 x 100 is created via:

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

This change appears to have increased FPS to ~30fps. The other statistics are as follows (from the statistics displayed in the SCNView):

10K (I assume this is draw calls?) 120K (I assume this is faces) 360K (Assuming this is the vertex count)

The bulk of the run loop is in Rendering (I'm guesstimating 98%). The total loop time is 26.7ms (ouch). I'm running on a Mac Pro Late 2013 (6-core w/Dual D500 GPU).

Given that a MineCraft style game has a landscape that constantly changes based on the players actions I don't see how I can optimize this within the confines of Scene Kit. A big disappointment as I really like the framework. I'd love to hear someone's ideas on how I can address this issue - without that, I'm forced to go with OpenGL.

UPDATE 12-30-2014 @ 2:00pm ET: I am seeing a significant performance improvement when using flattenedClone(). The FPS is now a solid 60fps even with more boxes and TWO drawing calls. However, accommodating a dynamic environment (as MineCraft supports) is still proving problematic - see below.

Since the array would change composition over time I added a keyDown handler to add an even larger box array to the existing and timed the difference between adding the array of boxes resulting in far more calls versus adding as a flattenedClone. Here's what I found:

On keyDown I add another array of 120 x 120 boxes (14,400 boxes)

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

Calling flattenedClone() again is 4x slower than adding the array.

This results in two drawing calls having 293K faces and 878K vertices. I'm still playing with this and will update if I find anything new. Bottom line, with my additional testing I still feel Scene Kit's immutable geometric constraints mean I can't leverage the framework.

1
What environment are you testing in? Where is your performance bottleneck? See the Building a Game with SceneKit talk from WWDC 2014 for tips on tracking down the latter. - rickster
i don't know scenekit but generally the "naive" approach will be rather slow. Consider that a game like minecraft probably ensures not to render any blocks that are completely hidden by others, that it implements instancing (drawing the same blocks in one go) and other general and game-specific optimizations. SceneKit is a general purpose renderer so you will have to try and see what kind of optimizations can be implemented, and what works best for SceneKit. If you determine you need more low level control you may need to revert to GLKit or raw OpenGL. - LearnCocos2D
10K draw calls is way too much. Try aiming for something closer to 100. You can greatly reduce the number of draw calls by flattening the geometry (flattenedClone()). If one box should later be separated by a user's action, I would deal with that box upon that action and not leave your entire scene in a separated state, just because the user might interact with it. - David Rönnqvist
What did you decide? Were you able to use SceneKit or did you need to use OpenGL? - Crashalot
I decided SceneKit wouldn't meet my needs. I love the idea and think Apple has done a great job designing the framework but it's not flexible enough for what I want. Learning the Metal framework is my current direction. A much higher learning curve to be sure but I've always enjoyed close to the metal programming (pun intended - Assembler used to be my favorite language). - Dead Pixel

1 Answers

0
votes

As you mentionned Minecraft, I think it's worth looking at how it works.

I have no technical details or code sample for you, but everything should be pretty straightfoward:

Have you ever played minecraft online, and the terrain is not loading allowing you to see through? That's because there is no geometry inside.

let's assume I have a 2x2x2 array of cubes. That makes 2*2*2*6*2 = 96 triangles.

However, if you test and draw only the polygons on the visible from the camera point of view, maybe by testing the normals (easy since it's cubes), this number goes down to 48 triangles.

If you find a way to see which faces are occluded by other ones (which shouldn't be too hard either considering you're working with flat, quared, grid based faces) you can only draw these. that way, we're drawing between 8 and 24 triangleS. That's up to 90% optimisation.

If you want to get really deep, you can even combine faces, to make a single N-gon out of the visible, flat faces. You can do that if you create a new way to generate the geometry on the fly that combines the two previous methods and test for adgacent visible faces on the same plane.

If you succeed, we're talking 2 to 6 polygons instead of 96, to render 8 cubes.

Note that the last method only works if your blocks are touching each other.

There is probably a ton of Minecraft-like renderer papers, a few googles will help you figure it out!