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 3 of 3 of this article. Click here to view the first page.

Matrices

In order to manipulate the scene, you need to pass the projection and model-view matrices to the GPU. The projection matrix allows you to manipulate the perception of the scene to make closer objects appear bigger than farther ones. The model view matrix allows you to manipulate the position, rotation, and scale of an object or the whole scene.

In order to use those matrices, you’ll create a new struct. Open Vertex.swift. At the top of the file, add:

struct SceneMatrices {
    var projectionMatrix: GLKMatrix4 = GLKMatrix4Identity
    var modelviewMatrix: GLKMatrix4 = GLKMatrix4Identity
}

Note that you’ll still use GLKMatrix4. This part of GLKit is not deprecated, so you can use it for matrices in Metal.

Now, open ViewController.swift, and add two new properties:

private var sceneMatrices = SceneMatrices()
private var uniformBuffer: MTLBuffer!

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

renderEncoder.setRenderPipelineState(pipelineState)

Add:

// 1
let modelViewMatrix = GLKMatrix4MakeTranslation(0.0, 0.0, -6.0)
sceneMatrices.modelviewMatrix = modelViewMatrix

// 2
let uniformBufferSize = MemoryLayout.size(ofValue: sceneMatrices)
uniformBuffer = metalDevice.makeBuffer(
  bytes: &sceneMatrices, 
  length: uniformBufferSize, 
  options: .storageModeShared)

// 3
renderEncoder.setVertexBuffer(uniformBuffer, offset: 0, index: 1)

Here’s what the code above does:

  1. Creates a matrix to shift the object backwards by 6 units, to make it look smaller.
  2. Creates a uniform buffer like you did with vertex buffer before, but with matrices data.
  3. Hooks the uniform buffer to the pipeline and sets its identifier to 1.

Projection Matrix

While you’re still in ViewController.swift, inside mtkView(_:drawableSizeWillChange:), add the following:

let aspect = fabsf(Float(size.width) / Float(size.height))  
let projectionMatrix = GLKMatrix4MakePerspective(
  GLKMathDegreesToRadians(65.0), 
  aspect, 
  4.0, 
  10.0)
sceneMatrices.projectionMatrix = projectionMatrix 

This code creates a projection matrix based on the aspect ratio of the view. Then, it assigns it to the scene’s projection matrix.

With this in place, your square will now look square and not stretched out to the whole screen. :]

Matrices in Shaders

You’re almost there! Next, you’ll need to receive matrices data in shaders. Open Shaders.metal. At the very top, add a new struct:

struct SceneMatrices {
    float4x4 projectionMatrix;
    float4x4 viewModelMatrix;
};

Now, replace the basic_vertex function with the following:

vertex VertexOut basic_vertex(
  const device VertexIn* vertex_array [[ buffer(0) ]],
  const device SceneMatrices& scene_matrices [[ buffer(1) ]], // 1
  unsigned int vid [[ vertex_id ]]) {
    // 2
    float4x4 viewModelMatrix = scene_matrices.viewModelMatrix;
    float4x4 projectionMatrix = scene_matrices.projectionMatrix;
    
    VertexIn v = vertex_array[vid];

    // 3
    VertexOut outVertex = VertexOut();
    outVertex
      .computedPosition = projectionMatrix * viewModelMatrix * float4(v.position, 1.0);
    outVertex.color = v.color;
    return outVertex;
}

Here’s what has changed:

  1. Receives matrices as a parameter inside the vertex shader.
  2. Extracts the view model and projection matrices.
  3. Multiplies the position by the projection and view model matrices.

Build and run the app. You should see this:

Gradient Static Cube

W00t! A square!

Making it Spin

In the OpenGL implementation, GLViewController provided lastUpdateDate which would tell you when the last render was performed. In Metal, you’ll have to create this yourself.

First, in ViewController, add a new property:

private var lastUpdateDate = Date()

Then, go to draw(in: ), and just before:

commandBuffer.present(drawable)

Add the following code:

commandBuffer.addCompletedHandler { _ in
  self.lastUpdateDate = Date()
}

With this in place, when a frame drawing completes, it updates lastUpdateDate to the current date and time.

Now, it’s time to spin! In draw(in:), replace:

let modelViewMatrix = GLKMatrix4Translate(GLKMatrix4Identity, 0, 0, -6.0)

With:

var modelViewMatrix = GLKMatrix4MakeTranslation(0.0, 0.0, -6.0)
let timeSinceLastUpdate = lastUpdateDate.timeIntervalSince(Date()) 

// 1
rotation += 90 * Float(timeSinceLastUpdate)

// 2
modelViewMatrix = GLKMatrix4Rotate(
  modelViewMatrix, 
  GLKMathDegreesToRadians(rotation), 0, 0, 1)

This increments the rotation property by an amount proportional to the time between the last render and this render. Then, it applies a rotation around the Z-axis to the model-view matrix.

Build and run the app. You’ll see the cube spinning. Success!

Metal Spinning Cube

Where to Go From Here?

Download the final project for this tutorial using the Download Materials button at the top or bottom of the tutorial.

Congrats! You’ve learned a ton about the Metal API! Now you understand some of the most important concepts in Metal, such as shaders, devices, command buffers, and pipelines, and you have some useful insights into the differences between OpenGL and Metal.

For more, be sure to check out these great resources from Apple:

You also might enjoy this Beginning Metal course, which covers these concepts in detail with video tutorials.

If reading is more your style, check out the Metal Getting Started tutorial. For an entire book on the subject, see Metal by Tutorials.

I hope you’ve enjoyed this tutorial! If you have any comments or questions, please join the forum discussion below!