Metal Tutorial: Getting Started

In this Metal tutorial, you will learn how to get started with Apple’s 3D graphics API by rendering a simple triangle to the screen. By Andrew Kharchyshyn.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

3) Creating a Vertex Buffer

Everything in Metal is a triangle. In this app, you’re just going to draw one triangle, but even complex 3D shapes can be decomposed into a series of triangles.

In Metal, the default coordinate system is the normalized coordinate system, which means that by default you’re looking at a 2x2x1 cube centered at (0, 0, 0.5).

If you consider the Z=0 plane, then (-1, -1, 0) is the lower left, (0, 0, 0) is the center, and (1, 1, 0) is the upper right. In this tutorial, you want to draw a triangle with the following three points:

4_vertices metal

You’ll have to create a buffer for this. Add the following constant property to your class:

let vertexData: [Float] = [
   0.0,  1.0, 0.0,
  -1.0, -1.0, 0.0,
   1.0, -1.0, 0.0
]

This creates an array of floats on the CPU. You need to send this data to the GPU by moving it to something called a MTLBuffer.

Add another new property for this:

var vertexBuffer: MTLBuffer!

Then add this code to the end of viewDidLoad():

let dataSize = vertexData.count * MemoryLayout.size(ofValue: vertexData[0]) // 1
vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: []) // 2

Taking it comment by comment:

  1. You need to get the size of the vertex data in bytes. You do this by multiplying the size of the first element by the count of elements in the array.
  2. You call makeBuffer(bytes:length:options:) on the MTLDevice to create a new buffer on the GPU, passing in the data from the CPU. You pass an empty array for default configuration.

4) Creating a Vertex Shader

The vertices that you created in the previous section will become the input to a little program that you’ll write called a vertex shader.

A vertex shader is simply a tiny program that runs on the GPU, written in a C++-like language called the Metal Shading Language.

A vertex shader is called once per vertex, and its job is to take that vertex’s information, such as position — and possibly other information such as color or texture coordinate — and return a potentially modified position and possibly other data.

To keep things simple, your simple vertex shader will return the same position as the position passed in.

5_matrix metal

The easiest way to understand vertex shaders is to see it yourself. Go to File ▸ New ▸ File, choose iOS ▸ Source ▸ Metal File, and click Next. Enter Shaders.metal for the filename and click Create.

Note: In Metal, you can include multiple shaders in a single Metal file. You can also split your shaders across multiple Metal files if you would like, as Metal will load shaders from any Metal file included in your project.

Add the following code to the bottom of Shaders.metal:

vertex float4 basic_vertex(                           // 1
  const device packed_float3* vertex_array [[ buffer(0) ]], // 2
  unsigned int vid [[ vertex_id ]]) {                 // 3
  return float4(vertex_array[vid], 1.0);              // 4
}

Here’s what’s going on in the code above:

  1. All vertex shaders must begin with the keyword vertex. The function must return (at least) the final position of the vertex. You do this here by indicating float4 (a vector of four floats). You then give the name of the vertex shader; you’ll look up the shader later using this name.
  2. The first parameter is a pointer to an array of packed_float3 (a packed vector of three floats) – i.e., the position of each vertex.
    Use the [[ ... ]] syntax to declare attributes, which you can use to specify additional information such as resource locations, shader inputs and built-in variables. Here, you mark this parameter with [[ buffer(0) ]] to indicate that the first buffer of data that you send to your vertex shader from your Metal code will populate this parameter.
  3. The vertex shader also takes a special parameter with the vertex_id attribute, which means that the Metal will fill it in with the index of this particular vertex inside the vertex array.
  4. Here, you look up the position inside the vertex array based on the vertex id and return that. You also convert the vector to a float4, where the final value is 1.0 — long story short, this is required for 3D math.

5) Creating a Fragment Shader

After the vertex shader completes, Metal calls another shader for each fragment (think pixel) on the screen: the fragment shader.

The fragment shader gets its input values by interpolating the output values from the vertex shader. For example, consider the fragment between the bottom two vertices of the triangle:

6_points metal

The input value for this fragment will be a 50/50 blend of the output value of the bottom two vertices.

The job of a fragment shader is to return the final color for each fragment. To keep things simple, you’ll make each fragment white.

Add the following code to the bottom of Shaders.metal:

fragment half4 basic_fragment() { // 1
  return half4(1.0);              // 2
}

Reviewing line by line:

  1. All fragment shaders must begin with the keyword fragment. The function must return (at least) the final color of the fragment. You do so here by indicating half4 (a four-component color value RGBA). Note that half4 is more memory efficient than float4 because you’re writing to less GPU memory.
  2. Here, you return (1, 1, 1, 1) for the color, which is white.

6) Creating a Render Pipeline

Now that you’ve created a vertex and fragment shader, you need to combine them — along with some other configuration data — into a special object called the render pipeline.

One of the cool things about Metal is that the shaders are precompiled, and the render pipeline configuration is compiled after you first set it up. This makes everything extremely efficient.

First, add a new property to ViewController.swift:

var pipelineState: MTLRenderPipelineState!

This will keep track of the compiled render pipeline you’re about to create.

Next, add the following code to the end of viewDidLoad():

// 1
let defaultLibrary = device.makeDefaultLibrary()!
let fragmentProgram = defaultLibrary.makeFunction(name: "basic_fragment")
let vertexProgram = defaultLibrary.makeFunction(name: "basic_vertex")
    
// 2
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.vertexFunction = vertexProgram
pipelineStateDescriptor.fragmentFunction = fragmentProgram
pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
    
// 3
pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)

Taking it section by section:

  1. You can access any of the precompiled shaders included in your project through the MTLLibrary object you get by calling device.makeDefaultLibrary()!. Then, you can look up each shader by name.
  2. You set up your render pipeline configuration here. It contains the shaders that you want to use, as well as the pixel format for the color attachment — i.e., the output buffer that you’re rendering to, which is the CAMetalLayer itself.
  3. Finally, you compile the pipeline configuration into a pipeline state that is efficient to use here on out.