Custom SceneKit geometry

SceneKit offers a wide range of built in geometry like cubes, cylinders, pyramids, spheres, torus, etc., that can be used to for example create beautiful 3D bar charts. More advanced geometry (and even full scenes) like an environment for a game or an interior design application can be created by a designer in their favorite 3D modeling software to be modified and rendered by your application. In addition to these two options SceneKit can be used to generate geometry from a list of points. This comes in very handy when doing 3D graphs and visualizations.

A brief look at the documentation reveals that there is a method on SCNGeometry for creating a new geometry.

+ (id)geometryWithSources:(NSArray *)sources 
                 elements:(NSArray *)elements; 

It takes two arrays of SCNGeometrySource objects and SCNGeometryElement objects. Looking at the documentation for these two classes can be a scary experience. It certainly isn’t obvious how to proceed unless you already know one or two things about 3D computer graphics1. Let us instead take a closer look at 3D geometry in general and see where that gets us.

3D geometry

Geometry in 3D (and 2D as well for that matter) can be defined through a series of points. A cube, for example, has one point for each corner, 8 different points in total (four on top and for below).

The eight corners of a 3D cube.

These points in themselves do not make a cube. It is only through interpreting the points in some way that we get surfaces between some of the points which gives us the six faces of a solid cube. To create geometry the computer doesn’t only need the points. It also needs to know which points should go together to create surfaces. That is where triangles come in.

Triangles

Triangles are a central piece to 3D computer graphics since they are the smallest shape with a surface. A single point is just a point. Two points form a line. Three points form a surface. Four points (in a plane) form a bigger surface but that is really just two triangles.

The triangles that together form a quadrilateral.

On the other hand two triangles is much more than just a quadrilateral (a surface with four corners) plane. You can always create a triangle from three points (although the triangle may be at an angle) but four points need to be in the same plane to form a quadrilateral surface.

Applied to our cube we will have two triangles for each side of the cube for a total of 12 triangles. Let’s take our eight corners and make triangles out of them.

Indexed coordinates

We start by giving each point a number from 0 to 7 (we are programmers after all). The bottom surface has one triangle with the points 0, 1, 2 and another with the points 1, 2, 3. In the same way the back of our cube has one triangle with the points 2, 3, 6 and another with the points 3, 6, 7.

3 0 0, 1, 2 1, 2, 3 1 2 4 5 7 6
The corners of a cube numbered and two triangles forming a surface for the bottom side of the cube.

If we continue in the same way for the rest of the sides we get a list of all the indices for all the triangles in the cube. At this point we actually have enough data to draw our cube on screen.

Creating geometry

The two pieces of data that we have are the points for the corners and the indices that make the points into triangles. In SceneKit terminology the points are the source of our geometry since they are the actual data. The cube is our only element with the list of indices as its data2.

For a cube that extends half of its side in x, y and z (i.e. it is centered in origo (0, 0, 0)) our list of points are

SCNVector3 positions[] = {
    SCNVector3Make(-halfSide, -halfSide,  halfSide),
    SCNVector3Make( halfSide, -halfSide,  halfSide),
    SCNVector3Make(-halfSide, -halfSide, -halfSide),
    SCNVector3Make( halfSide, -halfSide, -halfSide),
    SCNVector3Make(-halfSide,  halfSide,  halfSide),
    SCNVector3Make( halfSide,  halfSide,  halfSide),
    SCNVector3Make(-halfSide,  halfSide, -halfSide),
    SCNVector3Make( halfSide,  halfSide, -halfSide)
};

And the indices for all the triangles that takes these points and makes them into a cube

int indices[] = {
    // bottom
    0, 2, 1,
    1, 2, 3,
    // back
    2, 6, 3,
    3, 6, 7,
    // left
    0, 4, 2,
    2, 4, 6,
    // right
    1, 3, 5,
    3, 7, 5,
    // front
    0, 1, 4,
    1, 5, 4,
    // top
    4, 5, 6,
    5, 7, 6
};

You may notice that the order of some of the indices may not be what you expect. For example the first triangle has the indices 0, 2, 1 instead of 0, 1, 2. This is because we want to control what is the front and back of the triangles.

Every surface has a front and a backside. The points on the front are specified in counter-clockwise order. It is a common optimization to never draw surfaces showing their back to the camera since they would be obscured by some other surface in any solid geometry. If you have issues with triangular holes or have transparent geometry you can configure the material to be double sided.

Creating the geometry object

Remember those classes that we couldn’t make sense of before. Let’s have a look at them now.

The geometry source is the easiest one. We want to create a source for our geometries vertices. “Vertex” is the name for a corner in a polygon and that is what our points are to the triangles. We know that we have 8 points so we can create a geometry source like this:

SCNGeometrySource *vertexSource =
  [SCNGeometrySource geometrySourceWithVertices:positions count:8];

The geometry element is slightly trickier but I’ll walk you through it. First we need our indices as an NSData object. The second parameter makes sure that the indices are read as a series of triangles. Finally pass the number of indices and their size.

NSData *indexData = [NSData dataWithBytes:indices
                                   length:sizeof(indices)];

SCNGeometryElement *element =
[SCNGeometryElement geometryElementWithData:indexData
                            primitiveType:SCNGeometryPrimitiveTypeTriangles
                             primitiveCount:12
                              bytesPerIndex:sizeof(int)];

After we have created both the source and the element with can finally create our custom geometry.

SCNGeometry *geometry = [SCNGeometry geometryWithSources:@[vertexSource]
                                                elements:@[element]];

If you add this to your scene you should first be glad that you managed to create your custom 3D object and secondly be surprised of how bad it renders. It’s all black!

The first rendering of the custom geometry. It's all black.

The problem with our geometry is that is has no normals so when light from the light sources hit our surface we can’t calculate the angle to determine how lit up it should become. One of the other kinds of geometry sources we could create was a source with normals. It is created just like the source with vertices but first we need some normals.

Normals

A normal, or surface normal, is a vector that points perpendicular to the surface. Wikipedia has some good illustrations of what it could look like.

Unfortunately for our cube, normals are specified per vertex and our cube reuses the same point three times. This means that we can’t specify three different normals for the three different usages. To give our cube proper normals for the lighting we need 24 vertices (3 × 8). I won’t bother including the code for the new vertices and normals since it’s just a long list of number but will instead refer you to the sample project on GitHub.

Just as we created an array of SCNVector3 for the vertices we create another array for the normals. Now that we have new vertices and normals we can create two new sources and a geometry object from them. Note that there are still only 12 indices even though some of them have changed. There are still only 12 triangles even though there are more vertices to choose from.

SCNGeometrySource *vertexSource =
  [SCNGeometrySource geometrySourceWithVertices:positions
                                          count:24];
SCNGeometrySource *normalSource =
  [SCNGeometrySource geometrySourceWithNormals:normals
                                         count:24];

SCNGeometry *geometry =
  [SCNGeometry geometryWithSources:@[vertexSource, normalSource]
                          elements:@[element]];

Now that our cube has proper normals we can give it proper lighting

The custom geometry with proper normals rendered in a scene with a red spotlight.
The custom geometry with proper normals rendered in a scene with a red spotlight.

Conclusion

It may feel like we did very much for so little but what we learnt is a very powerful tool that can be used to generate any geometry. Also, you may not have realized it but the entire discussion about triangles, front– and backside, surface normals and indices are very relevant when doing OpenGL.

One secondary lesson to take away from this is that you should either create your geometry using 3D modeling software or have the program generate the vertices and normals for you3. I have just started a small project on GitHub that does the latter for 3D graphs. Feel free to learn from it, use it to make graphs and give me feedback on how to improve it.

  1. If you have some familiarity with for example OpenGL I hope that you will see the strong similarities in the rest of this article. 

  2. You can have multiple SCNGeometryElements for a single geometry but that and its use-cases are far outside the scope of this article. 

  3. You didn’t have to figure out the cube indices by hand so maybe you never though of it as an issue.