Categories
Uncategorized

Using MetalKit part 2

In the first part of this series we introduced the MetalKit framework. Let’s reuse the project from part 1 and pick up where we left off. If you look again at the render() function, it was looking like this:

 
func render() {
    let device = MTLCreateSystemDefaultDevice()!
    self.device = device
    let rpd = MTLRenderPassDescriptor()
    let bleen = MTLClearColor(red: 0, green: 0.5, blue: 0.5, alpha: 1)
    rpd.colorAttachments[0].texture = currentDrawable!.texture
    rpd.colorAttachments[0].clearColor = bleen
    rpd.colorAttachments[0].loadAction = .Clear
    let commandQueue = device.newCommandQueue()
    let commandBuffer = commandQueue.commandBuffer()
    let encoder = commandBuffer.renderCommandEncoderWithDescriptor(rpd)
    encoder.endEncoding()
    commandBuffer.presentDrawable(currentDrawable!)
    commandBuffer.commit()
}

Let’s improve this code a bit. First, since our class subclasses MTKView, it already has its own device so there is no need to declare another one. This lets us reduce the first two lines to just one:

 
device = MTLCreateSystemDefaultDevice()

Second, last week we said that we should make sure currentDrawable and currentRenderPassDescriptor are not nil or the app will crash. For the sake of simplicity, we have not done that in part one, so let’s do that now. This will also help us get rid of a couple more lines of code. The final version of the function will look like this:

 
func render() {
    device = MTLCreateSystemDefaultDevice()
    if let rpd = currentRenderPassDescriptor, drawable = currentDrawable {
        rpd.colorAttachments[0].clearColor = MTLClearColorMake(0, 0.5, 0.5, 1.0)
        let command_buffer = device!.newCommandQueue().commandBuffer()
        let command_encoder = command_buffer.renderCommandEncoderWithDescriptor(rpd)
        command_encoder.endEncoding()
        command_buffer.presentDrawable(drawable)
        command_buffer.commit()
    }
}

You might wonder what colorAttachments[0] means. To set the rendering pipeline state, the Metal framework provides 3 types of attachments that we can write to:

  • colorAttachments
  • depthAttachmentPixelFormat
  • stencilAttachmentPixelFormat

We are only interested in storing color data for now and colorAttachments is an array of textures that hold drawings results and display them on the screen. We currently only have one such texture – the first element (at index 0) of the array. Ok, now is a good time to run the app and make sure you are still seeing the same colored background we saw last time. So much better! With only 9 lines of code we can get safe Metal code running on our GPU. Not too shabby.

So far so good! Next, let’s dive into a new Metal topic – drawing geometry on the screen. All graphics tutorials such as those about OpenGL start with a Hello, Triangle type of program because a triangle is the simplest form of geometry that can be drawn on screen. It is a 2D graphics basic element and all the other objects in the world of graphics are composed of triangles, so this makes it a great place to start. Imagine the screen coordinate system having its axes running through the center of the screen which would have the coordinates (0, 0). The edges of the screen would have values of -1and 1 respectively. Let’s create an array of floats and a buffer to hold the vertex values for our triangle. Insert these lines right after initializing the device:

 
let vertex_data:[Float] = [-1.0, -1.0, 0.0, 1.0,
                            1.0, -1.0, 0.0, 1.0,
                            0.0,  1.0, 0.0, 1.0]
let data_size = vertex_data.count * sizeof(Float)
let vertex_buffer = device!.newBufferWithBytes(vertex_data, length: data_size, options: [])

The vertices above are located in order: bottom left, bottom right and top center. We notice that each vertex uses 4 floats for its coordinate. The first two are the x and y axes. The ones we do not use this time are: the third one is the depth (Z-axis) and the fourth one is the W coordinate making our coordinates homogeneous. We will talk about them in our next episode. Then we compute the size of this array to simply be the size of 12 floats and finally we create the buffer based on the array and its size. Now that we have our vertexes stored, we need a way to send them to the GPU so it can display them on the screen. Let’s look at the entire process (pipeline) that facilitates drawing graphics on the screen:

alt text

We have completed the first stage so far, storing the vertexes. You notice that the next stages require that we have a new construct named shader. A shader is where programmers are allowed to interfere in the graphics pipeline with their custom functions. Metal provides a few types of shaders, however, today we only look at two of them: the vertex shader which is responsible for the location of our point, and the fragment shader which is responsible for the color of our point.

The Metal framework provides a function that we can call on the device to create a Library of functions (shaders), so let’s create it:

 
let library = device!.newDefaultLibrary()!
let vertex_func = library.newFunctionWithName("vertex_func")
let frag_func = library.newFunctionWithName("fragment_func")

We create two new Functions and point them to their corresponding shaders (which we will create later). The next step is to create a Render Pipeline Descriptor which needs to know about our shaders:

 
let rpld = MTLRenderPipelineDescriptor()
rpld.vertexFunction = vertex_func
rpld.fragmentFunction = frag_func
rpld.colorAttachments[0].pixelFormat = .BGRA8Unorm

You might wonder what .BGRA8Unorm means. This setting configures the pixel format so that everything that goes through the render pipeline conforms to the same order (in this case BlueGreenRedAlpha) of color components as well as size (in this case an 8-bit color value goes from 0 to 255). The last step is to create a Render Pipeline State based on the above descriptor:

 
let rps = try! device!.newRenderPipelineStateWithDescriptor(rpld)

Finally, we only need to let the command encoder know about our triangle, so add the following lines right after creating the encoder:

 
command_encoder.setRenderPipelineState(rps)
command_encoder.setVertexBuffer(vertex_buffer, offset: 0, atIndex: 0)
command_encoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1) 

Now let’s get back to the two shaders we promised to create when we created the Library. For this, we need to create a new file in Xcode. Choose the Metal File type, name it Shaders.metal or something similar and click Create. You will immediately notice that the code does not resemble Swift much, and that is because the Metal shading language is based on C++. Let’s add the code below:

 
#include <metal_stdlib>

using namespace metal;

struct Vertex {
    float4 position [[position]];
};

vertex Vertex vertex_func(constant Vertex *vertices [[buffer(0)]], uint vid [[vertex_id]]) {
    return vertices[vid];
}

fragment float4 fragment_func(Vertex vert [[stage_in]]) {
    return float4(0.7, 1, 1, 1);
}

The code is pretty straightforward. We first create a struct named Vertex that has only one member – an array of position arrays. We notice that the array is float4 which in the shading language is the same as the vertexes we created earlier with 4 floats each. We leave the explanation for the [[…]]syntax for next time. Then we have the vertex_func shader which returns the location of the current vertex, and the fragment_func shader which returns the color of the current vertex. We hardcoded a particular color value, but we could have added a color struct member to Vertex and set the color separately for each vertex.

If you run the app, you should see a triangle like this:

alt text

In the next part we will learn more about the Metal shading language as well as how 3D graphics is rendered on the GPUs. 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