Moving from OpenGL to Metal

In this Metal tutorial, you’ll learn how to move your project from OpenGL to Apple’s 3D graphics API: Metal. 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.

Basic Drawing

You’re all set to draw! To keep things simple, you’ll draw a gray background first.

For each frame you want to draw in Metal, you must create a command buffer in which you specify what and how you want to draw. Then, this buffer is encoded on the CPU and sent to the GPU through the command queue.

Add the following code inside draw(in:):

// 1
guard let drawable = view.currentDrawable else {
  return
}
    
let renderPassDescriptor = MTLRenderPassDescriptor() // 2
renderPassDescriptor.colorAttachments[0].texture = drawable.texture // 3
renderPassDescriptor.colorAttachments[0].loadAction = .clear // 4
renderPassDescriptor.colorAttachments[0]
  .clearColor = MTLClearColor(red: 0.85, green: 0.85, blue: 0.85, alpha: 1.0) // 5
    
// 6
guard let commandBuffer = metalCommandQueue.makeCommandBuffer() else {
  return
}
    
// 7
guard let renderEncoder = commandBuffer
  .makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
    return
}
    
// Frame drawing goes here
    
renderEncoder.endEncoding() // 8

commandBuffer.present(drawable) // 9
commandBuffer.commit() // 10

This is a big one. Here’s what is going on in the code above:

  1. Ensure there’s a valid drawable to be used for the current frame.
  2. MTLRenderPassDescriptor contains a collection of attachments that are the rendering destination for pixels generated by a rendering pass.
  3. Set the texture from the view as a destination for drawing.
  4. Clear every pixel at the start of a rendering.
  5. Specify the color to use when the color attachment is cleared. In this case, it’s a lovely 85% gray.
  6. Ask the command queue to create a new command buffer.
  7. Create an encoder object that can encode graphics rendering commands into the command buffer. You’ll add your actual drawing code after this statement later.
  8. Declare that all command generations from this encoder are complete.
  9. Register a drawable presentation to occur as soon as possible.
  10. Commit this command buffer for execution in the command queue.

In summary, you create a command buffer and a command encoder. Then, you do your drawing in the command encoder and commit the command buffer to the GPU through the command queue. These steps are then repeated each time the frame is drawn.

Note: If you’re using the simulator, at this point you’ll get a build error. You need a compatible device to build and run the app.

Build and run the app.

Metal Gray View

What a gorgeous grey color!

Drawing Primitives

It will take some time to draw something more meaningful. So, take a deep breath and dive right in!

To draw something, you must pass data that represents the object to the GPU. You already have Vertex structure to represent the vertices data, but you need to make a small change to use it with Metal.

Open ViewController.swift and change the Indices property from this:

var Indices: [GLubyte]

To this:

var Indices: [UInt32]

You’ll see why this change is required when drawing primitives.

Data Buffers

To pass vertices data to the GPU, you need to create two buffers: one for vertices and one for indices. This is similar to OpenGL’s element buffer object (EBO) and vertex buffer object (VBO).

Add these properties to ViewController:

private var vertexBuffer: MTLBuffer!
private var indicesBuffer: MTLBuffer!

Now, inside setupMetal(), add the following at the bottom:

let vertexBufferSize = Vertices.size()
vertexBuffer = metalDevice
  .makeBuffer(bytes: &Vertices, length: vertexBufferSize, options: .storageModeShared)
    
let indicesBufferSize = Indices.size()
indicesBuffer = metalDevice
  .makeBuffer(bytes: &Indices, length: indicesBufferSize, options: .storageModeShared)

This asks metalDevice to create the vertices and indices buffers initialized with your data.

Now, go to draw(in:), and right before:

renderEncoder.endEncoding()

Add the following:

renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)  
renderEncoder.drawIndexedPrimitives(
  type: .triangle, 
  indexCount: Indices.count, 
  indexType: .uint32, 
  indexBuffer: indicesBuffer, 
  indexBufferOffset: 0) 

First this passes the vertex buffer to the GPU, setting it at index 0. Then, it draws triangles using indicesBuffer. Note that you need to specify the index type which is uint32. That’s why you changed the indices type earlier.

Build and run the app.

Crash! That’s not good. You passed data from the CPU to the GPU. It crashed because you didn’t specify how the GPU should use this data. You’ll need to add some shaders! Fortunately, that’s the next step.

Adding Shaders

Create a new file. Click File ▸ New ▸ File…, choose iOS ▸ Source ▸ Metal File. Click Next. Name it Shaders.metal and save it wherever you want.

Metal uses C++ to write shaders. In most cases, it’s similar to the GLSL used for OpenGL.

For more on shaders, check the references at the bottom of this tutorial.

Add this to the bottom of the file:

struct VertexIn {
    packed_float3 position;
    packed_float4 color;
};

struct VertexOut {
    float4 computedPosition [[position]];
    float4 color;
};

You’ll use these structures as input and output data for the vertex shader.

Writing a Vertex Shader

A vertex shader is a function that runs for each vertex that you draw.

Below the structs, add the following code:

vertex VertexOut basic_vertex( // 1
  const device VertexIn* vertex_array [[ buffer(0) ]], // 2
  unsigned int vid [[ vertex_id ]]) {                  // 3
    // 4
    VertexIn v = vertex_array[vid];

    // 5
    VertexOut outVertex = VertexOut();
    outVertex.computedPosition = float4(v.position, 1.0);
    outVertex.color = v.color;
    return outVertex;
}
  1. The vertex indicates this is a vertex shader function. The return type for this shader is VertexOut.
  2. Here, you get the vertex buffer that you passed to the command encoder.
  3. This parameter is a vertex id for which this shader was called.
  4. Grab the input vertex for the current vertex id.
  5. Here, you create a VertexOut and pass data from the current VertexIn. This is simply using the same position and color as the input.

At this point, the vertex shader’s job is done.

Writing a Fragment Shader

After the vertex shader finishes, the fragment shader runs for each potential pixel.

Below the vertex shader, add the following code:

fragment float4 basic_fragment(VertexOut interpolated [[stage_in]]) { 
  return float4(interpolated.color);              
}

The fragment shader receives the output from the vertex shader — the VertexOut. Then, the fragment shader returns the color for the current fragment.

Hooking up the Shaders to the Pipeline

The shaders are in place, but you haven’t hooked them to your pipeline yet. In order to do that, go back to ViewController.swift, and add this property to the class:

private var pipelineState: MTLRenderPipelineState!

This property will contain shaders data.

Now, find setupMetal(). Add the following at the bottom of the method:

// 1
let defaultLibrary = metalDevice.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! metalDevice
  .makeRenderPipelineState(descriptor: pipelineStateDescriptor)

This is what the code does:

  1. Finds the vertex and fragment shaders by their name in all .metal files.
  2. Creates a descriptor object with the vertex shader and the fragment shader. The pixel format is set to a standard BGRA (Blue Green Red Alpha) 8-bit unsigned.
  3. Asks the GPU to compile all that into the GPU-optimized object.

Now, the pipeline state is ready. It’s time to use it. For this, go to draw(in:), and right before:

renderEncoder.drawIndexedPrimitives(
  type: .triangle, 
  indexCount: Indices.count, 
  indexType: .uint32, 
  indexBuffer: indicesBuffer, 
  indexBufferOffset: 0)

Add:

renderEncoder.setRenderPipelineState(pipelineState)

Build and run. You should get this colorful screen.

Metal Gradient Background