Categories
Uncategorized

Using MetalKit part 9

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:

alt text

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 01 and 2 and then we draw the triangle that uses vertices 23 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 counterclockwiseas 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:

alt text

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:

alt text

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 spaceto 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 Matrixstruct. 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:

alt text

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:

alt text

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:

alt text

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:

alt text

The source code is posted on Github as usual.

Until next time!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s