A custom geometry is constructed from a set of vertices and normals.
Vertices
In this context, a vertex is a point where two or more lines intersect. For a cube, the vertices are the corners shown in the following figure
We construct the geometry by building the cube's faces with a set of triangles, two triangles per face. Our first triangle is defined by vertices 0, 2, and 3 as shown in the below figure, and the second triangle is defined by vertices 0, 1, and 2. It is important to note that each triangle has a front and back side. The side of the triangle is determined by the order of the vertices, where the front side is specified in counter-clockwise order. For our cube, the front side will always be the outside of the cube.
If the cube's center is the origin, the six vertices that define one of the cube's face can be defined by
let vertices:[SCNVector3] = [
SCNVector3(x:-1, y:-1, z:1), // 0
SCNVector3(x:1, y:1, z:1), // 2
SCNVector3(x:-1, y:1, z:1) // 3
SCNVector3(x:-1, y:-1, z:1), // 0
SCNVector3(x:1, y:-1, z:1), // 1
SCNVector3(x:1, y:1, z:1) // 2
]
and we create the vertex source by
let vertexSource = SCNGeometrySource(vertices: vertices)
At this point, we have a vertex source that can be use to construct a face of the cube; however, SceneKit doesn't know how the triangle should react to light sources in the scene. To properly reflect light, we need to provide our geometry with a least one normal vector for each vertex.
Normals
A normal is a vector that specifies the orientation of a vertex that affects how light reflects off the corresponding triangle. In this case, the normal vectors for the six vertices of the triangle are the same; they all point in the positive z direction (i.e., x = 0, y = 0, and z = 1); see the red arrows in the below figure.
The normals are defined by
let normals:[SCNVector3] = [
SCNVector3(x:0, y:0, z:1), // 0
SCNVector3(x:0, y:0, z:1), // 2
SCNVector3(x:0, y:0, z:1), // 3
SCNVector3(x:0, y:0, z:1), // 0
SCNVector3(x:0, y:0, z:1), // 1
SCNVector3(x:0, y:0, z:1) // 2
]
and the source is defined by
let normalSource = SCNGeometrySource(normals: normals)
We now have the sources (vertices and normals) needed to construct a limited geometry, i.e., one cube face (two triangles). The final piece is to create an array of indices into the vertex and normal arrays. In this case, the indices are sequential because the vertices are in the order they are used.
let indices:[Int32] = [0, 1, 2, 3, 4, 5]
From the indices, we create an geometry element. The setup is a bit more involved because SCNGeometryElement
requires an NSData
as a parameter.
let pointer = UnsafeRawPointer(indices)
let indexData = NSData(bytes: pointer, length: MemoryLayout<Int32>.size * indices.count)
let element = SCNGeometryElement(data: indexData as Data, primitiveType: .triangles, primitiveCount: indices.count, bytesPerIndex: MemoryLayout<Int32>.size)
We can now create the custom geometry with
let geometry = SCNGeometry(sources: [vertexSource, normalSource], elements: [element])
and lastly create a node and assign the custom geometry to its geometry
property
let node = SCNNode()
node.geometry = geometry
scene.rootNode.addChildNode(node)
We now extend the vertices and normals to including all of the cube faces:
// The vertices
let v0 = SCNVector3(x:-1, y:-1, z:1)
let v1 = SCNVector3(x:1, y:-1, z:1)
let v2 = SCNVector3(x:1, y:1, z:1)
let v3 = SCNVector3(x:-1, y:1, z:1)
let v4 = SCNVector3(x:-1, y:-1, z:-1)
let v5 = SCNVector3(x:1, y:-1, z:-1)
let v6 = SCNVector3(x:-1, y:1, z:-1)
let v7 = SCNVector3(x:1, y:1, z:-1)
// All the cube faces
let vertices:[SCNVector3] = [
// Front face
v0, v2, v3,
v0, v1, v2,
// Right face
v1, v7, v2,
v1, v5, v7,
// Back
v5, v6, v7,
v5, v4, v6,
// Left
v4, v3, v6,
v4, v0, v3,
// Top
v3, v7, v6,
v3, v2, v7,
// Bottom
v1, v4, v5,
v1, v0, v4
]
let normalsPerFace = 6
let plusX = SCNVector3(x:1, y:0, z:0)
let minusX = SCNVector3(x:-1, y:0, z:0)
let plusZ = SCNVector3(x:0, y:0, z:1)
let minusZ = SCNVector3(x:0, y:0, z:-1)
let plusY = SCNVector3(x:0, y:1, z:0)
let minusY = SCNVector3(x:0, y:-1, z:0)
// Create an array with the direction of each vertex. Each array element is
// repeated 6 times with the map function. The resulting array or arrays
// is then flatten to an array
let normals:[SCNVector3] = [
plusZ,
plusX,
minusZ,
minusX,
plusY,
minusY
].map{[SCNVector3](repeating:$0,count:normalsPerFace)}.flatMap{$0}
// Create an array of indices [0, 1, 2, ..., N-1]
let indices = vertices.enumerated().map{Int32($0.0)}
let vertexSource = SCNGeometrySource(vertices: vertices)
let normalSource = SCNGeometrySource(normals: normals)
let pointer = UnsafeRawPointer(indices)
let indexData = NSData(bytes: pointer, length: MemoryLayout<Int32>.size * indices.count)
let element = SCNGeometryElement(data: indexData as Data, primitiveType: .triangles, primitiveCount: indices.count/3, bytesPerIndex: MemoryLayout<Int32>.size)
let geometry = SCNGeometry(sources: [vertexSource, normalSource], elements: [element])
// Create a node and assign our custom geometry
let node = SCNNode()
node.geometry = geometry
scene.rootNode.addChildNode(node)