I bet many of you missed the MetalKit
series, so today we are returning back to it, and we will learn how to draw 3D content in Metal
. Let’s continue working on our playground and pick up where we left off in part 8 of the series.
We will render a 3D cube by the end of this episode but first let’s draw a 2D square and then we can re-use the square logic for all the other faces of the cube. Let’s modify the vertex_data
array so that it holds 4 vertices instead of 3 we needed for a triangle:
let vertex_data = [
Vertex(pos: [-1.0, -1.0, 0.0, 1.0], col: [1, 0, 0, 1]),
Vertex(pos: [ 1.0, -1.0, 0.0, 1.0], col: [0, 1, 0, 1]),
Vertex(pos: [ 1.0, 1.0, 0.0, 1.0], col: [0, 0, 1, 1]),
Vertex(pos: [-1.0, 1.0, 0.0, 1.0], col: [1, 1, 1, 1])
]
Here comes the interesting part. Since squares and any other complex geometry is made from triangles, and since most vertices belong to 2 or more triangles, there is no need to create copies of these vertices because we have a way of reusing them via an index buffer
that keeps track of the order in which the vertices will be used by storing each vertex index from the vertex buffer
. So let’s create such a list of indexes:
let index_data: [UInt16] = [
0, 1, 2, 2, 3, 0
]
To understand how these indexes are stored, let’s look at this image below:

So for the front face (square) we use vertices stored at positions 0 through 3 in the vertex_buffer
. Later on we will add the other 4 vertices as well. The front face is made of two triangles. We first draw the triangle that uses vertices 0, 1 and 2 and then we draw the triangle that uses vertices 2, 3 and 0. Notice that two of the vertices are re-used, as expected. Also notice the drawing is done clockwise. This is the default front-facing winding order in Metal
but it can be changed to counterclockwise
as well.
Then, we need to create the index_buffer:
var index_buffer: MTLBuffer!
Next, we need to assign the index_data
to the index buffer
inside the createBuffers()
function:
index_buffer = device!.newBufferWithBytes(index_data, length: sizeof(UInt16) * index_data.count , options: [])
Last, inside the drawRect(:)
function we need to replace the drawPrimitives
call:
command_encoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
with a drawIndexedPrimitives call:
command_encoder.drawIndexedPrimitives(.Triangle, indexCount: index_buffer.length / sizeof(UInt16), indexType: MTLIndexType.UInt16, indexBuffer: index_buffer, indexBufferOffset: 0)
In the main playground page, see the generated new image:

Now that we know how to draw a square, let’s see how to draw more squares!
let vertex_data = [
Vertex(pos: [-1.0, -1.0, 1.0, 1.0], col: [1, 0, 0, 1]),
Vertex(pos: [ 1.0, -1.0, 1.0, 1.0], col: [0, 1, 0, 1]),
Vertex(pos: [ 1.0, 1.0, 1.0, 1.0], col: [0, 0, 1, 1]),
Vertex(pos: [-1.0, 1.0, 1.0, 1.0], col: [1, 1, 1, 1]),
Vertex(pos: [-1.0, -1.0, -1.0, 1.0], col: [0, 0, 1, 1]),
Vertex(pos: [ 1.0, -1.0, -1.0, 1.0], col: [1, 1, 1, 1]),
Vertex(pos: [ 1.0, 1.0, -1.0, 1.0], col: [1, 0, 0, 1]),
Vertex(pos: [-1.0, 1.0, -1.0, 1.0], col: [0, 1, 0, 1])
]
let index_data: [UInt16] = [
0, 1, 2, 2, 3, 0, // front
1, 5, 6, 6, 2, 1, // right
3, 2, 6, 6, 7, 3, // top
4, 5, 1, 1, 0, 4, // bottom
4, 0, 3, 3, 7, 4, // left
7, 6, 5, 5, 4, 7, // back
]
Now that we have the entire cube geometry ready for rendering, let’s go to MathUtils.swift
and in modelMatrix()
comment out the rotation
and the translation
calls, and only leave the scaling on for a factor of 0.5. You will most likely see an image like this:

Hmm, but it’s still a square! Yes, it is, because we still don’t have the notion of depth
and the cube looks just flat. It’s time to tweak some math logic now. We don’t need to use the Matrix
struct anymore because the simd framework offers us similar data structures and math functions we can readily use. We can easily rewrite our transform functions to work with matrix_float4x4 instead of the custom Matrix
struct we used.
But how do 3D objects end up on our 2D screens, you might ask. This process takes each pixel through a series of transformations. First the modelMatrix() transforms the pixel from object space
to world space
. This matrix is the one we already know, the one responsible for translations, rotations and scaling. With the newly rewritten functions above, the modelMatrix
could look like this:
func modelMatrix() -> matrix_float4x4 {
let scaled = scalingMatrix(0.5)
let rotatedY = rotationMatrix(Float(M_PI)/4, float3(0, 1, 0))
let rotatedX = rotationMatrix(Float(M_PI)/4, float3(1, 0, 0))
return matrix_multiply(matrix_multiply(rotatedX, rotatedY), scaled)
}
You notice the useful matrix_multiply
function which we could not use before for the Matrix
struct. Also, since all these pixels will undergo the same transformation, we want to store the matrix as a Uniform and pass it to the vertex shader
. For this. let’s create a new struct:
struct Uniforms {
var modelViewProjectionMatrix: matrix_float4x4
}
Back in the createBuffers()
function, let’s pass the Uniforms to the shader via the buffer pointer we already used to pass the modelMatrix
:
let modelViewProjectionMatrix = modelMatrix()
var uniforms = Uniforms(modelViewProjectionMatrix: modelViewProjectionMatrix)
memcpy(bufferPointer, &uniforms, sizeof(Uniforms))
In the main playground page, see the generated new image:

Hmm… the cube almost looks right, but something is still missing. The next transformation the pixels need to go through is from world space
to camera space
. Everything we see on the screen is viewed by a virtual camera through a frustum (pyramidal shape) that has a near and far planes to limit the view (camera) space:

Back in MathUtils.swift
let’s create the viewMatrix() as well:
func viewMatrix() -> matrix_float4x4 {
let cameraPosition = vector_float3(0, 0, -3)
return translationMatrix(cameraPosition)
}
The next transformation the pixels need to go through is from camera space
to clip space
. Here, all the vertices that are not inside the clip space
will determine whether the triangle will be culled
(all vertices outside the clip space) or clipped to bounds
(some vertices are outside but not all). The projectionMatrix() will help us compute the bounds and determine where the vertices are:
func projectionMatrix(near: Float, far: Float, aspect: Float, fovy: Float) -> matrix_float4x4 {
let scaleY = 1 / tan(fovy * 0.5)
let scaleX = scaleY / aspect
let scaleZ = -(far + near) / (far - near)
let scaleW = -2 * far * near / (far - near)
let X = vector_float4(scaleX, 0, 0, 0)
let Y = vector_float4(0, scaleY, 0, 0)
let Z = vector_float4(0, 0, scaleZ, -1)
let W = vector_float4(0, 0, scaleW, 0)
return matrix_float4x4(columns:(X, Y, Z, W))
}
The last two transformations are from clip space
to normalized device coordinates (NDC)
and from NDC
to screen space
. These two transformations are handled by the Metal framework for us.
Next, back in the createBuffers()
function, let’s modify the modelViewProjectionMatrix
we set before to just the modelMatrix
:
let aspect = Float(drawableSize.width / drawableSize.height)
let projMatrix = projectionMatrix(1, far: 100, aspect: aspect, fovy: 1.1)
let modelViewProjectionMatrix = matrix_multiply(projMatrix, matrix_multiply(viewMatrix(), modelMatrix()))
In drawRect(:)
we need to set rules for the culling mode and for front facing, in order to avoid weird artifacts such as cube transparency:
command_encoder.setFrontFacingWinding(.CounterClockwise)
command_encoder.setCullMode(.Back)
In the main playground page, see the generated new image:

This is finally the 3D cube we were all waiting to see! There is one more thing we can do to make it even more realistic and lively looking: give it a spin. First, let’s create a global variable named rotation which we want to update as time goes by:
var rotation: Float = 0
Next, grab all the matrices from inside the createBuffers()
function and let’s create a new one named update(). Here is where we update rotation
every frame to create a smooth rotation effect:
func update() {
let scaled = scalingMatrix(0.5)
rotation += 1 / 100 * Float(M_PI) / 4
let rotatedY = rotationMatrix(rotation, float3(0, 1, 0))
let rotatedX = rotationMatrix(Float(M_PI) / 4, float3(1, 0, 0))
let modelMatrix = matrix_multiply(matrix_multiply(rotatedX, rotatedY), scaled)
let cameraPosition = vector_float3(0, 0, -3)
let viewMatrix = translationMatrix(cameraPosition)
let aspect = Float(drawableSize.width / drawableSize.height)
let projMatrix = projectionMatrix(0, far: 10, aspect: aspect, fovy: 1)
let modelViewProjectionMatrix = matrix_multiply(projMatrix, matrix_multiply(viewMatrix, modelMatrix))
let bufferPointer = uniform_buffer.contents()
var uniforms = Uniforms(modelViewProjectionMatrix: modelViewProjectionMatrix)
memcpy(bufferPointer, &uniforms, sizeof(Uniforms))
}
In drawRect(:)
call the update
function:
update()
In the main playground page, you should see a similar image:

The source code is posted on Github as usual.
Until next time!