iOS Metal Tutorial with Swift Part 5: Switching to MetalKit

Learn how to use MetalKit in this 5th part of our Metal tutorial series. By Andrew Kharchyshyn.

Leave a rating/review
Save for later
Share

Update 9/30/16: This tutorial has been updated for Xcode 8 and Swift 3.

Welcome the 5th part of our iOS Metal tutorial series!

In the 1st part, you learned how to get started with Metal and render a simple 2D triangle.

In the 2nd part, you learned how to setup a series of transformations to move from a triangle to a full 3D cube.

In the 3rd part, you learned how to add a texture to the cube.

In the 4th part, you learned how to add light to the scene.

In this 5th part, you’ll learn how to update your app to take advantage of the MetalKit framework. In addition, you’ll also be updating the app to use the SIMD (pronounced “sim-dee”) framework for 3D-related math.

To get the most out of this tutorial, you should have a basic understanding of 3D graphics with either Metal or OpenGL. If this is the first time you’re learning about Metal, you should go back and complete the previous parts of the series.

Without further ado, let’s get into it!

Note: The iOS Simulator can’t access your computer’s GPU, so you’ll need to test your Metal apps on a real 64-bit device. Additionally, the sample code for this tutorial is written in Swift 3.0 using Xcode 8.

Getting Started

Start by downloading the starter project for this tutorial. This tutorial starts essentially where you ended with the previous tutorial. Also, make sure to download the additional resources required to complete this tutorial.

Do a quick build and run, just to make sure the starter project works. The result should resemble something like this:

IMG_5921

Getting Started with MetalKit

metal_icon_small

Apple presented MetalKit at WWDC 2015 as a gateway to Metal. The framework gives you utilities that reduce the amount of boilerplate code you have to write in order to get an app running on Metal.

MetalKit provides three major pieces of functionality:

  • Texture loading: Allows you to easily load image assets into Metal textures using a MTKTextureLoader.
  • View management: Reduces the amount of code you need to get Metal to render something on-screen via MTKView.
  • Model I/O integration: Allows you to efficiently load model assets into Metal buffers and manage mesh data using built-in containers.

In this tutorial, you’ll be focusing on texture loading and view management. Model I/O integration will be the subject of a future part in the series.

Switching to SIMD

The SIMD framework provides many common data types and functions that help when dealing with vector and matrix math. When this tutorial series first started, there were issues in Swift that prevented you from using C data types like the ones found in the SIMD and GLKit frameworks.

To overcome those issues, you ended up having to write an Objective-C wrapper to represent a 4×4 matrix and perform various GLKMath operations. It served its purpose, but it didn’t look that nice, because you needed to use a bridging header to bring it to Swift.

163fmh

Thankfully, the problem was fixed in Swift 2 and you can now take full advantage of the SIMD framework, which means you can now remove all the remaining Objective-C code from the project!

Deleting the Objective-C Wrapper

Open the project and take a look at the Matrix4 class. You’re currently using this class to store your matrix data; it also provides helper methods for a couple of matrix math operations.

To get started, select Matrix.h, Matrix.m and HelloMetal-Bridging-Header.h and delete them from your project. This will understandably cause a lot of errors to show up in your project. Fear not, because you’ll be working on getting the app back to a runnable state.

Even though you deleted the bridging header file, you still need to unlink it from your project settings. Go to Build Settings and search for bridging to find the Objective-C Bridging Header setting. Highlight it and press the Delete key to clear the setting:

Screen Shot 2016-06-19 at 7.13.09 PM

At this point, Objective-C is no longer part of this project. Hooray!

Replacing Matrix4 With a SIMD Data Type

Next, you’re going to replace all of your Matrix4 instances with float4x4 instances in your app. A float4x4 is a SIMD data type representing a 4×4 matrix of floats.

Since you’ll replace code throughout your entire project, you can perform a rare case of blind search and replace. Open the find navigator, click the Find text and select Replace from the dropdown. Put in Matrix4 in the search field and float4x4 in the replace field. To enable the Replace All button, you actually have to perform the search first, so press the enter-key with the search field selected:

Screen Shot 2016-06-19 at 7.43.25 PM

Click the Replace All button to replace all occurrences of Matrix4 with float4x4.

Now, since float4x4 is part of the SIMD library, you’ll need to import it everywhere you use float4x4.

At the top of these four files:

  • BufferProvider.swift
  • MetalViewController.swift
  • MySceneViewController.swift
  • Node.swift

Add the following line:

import simd

There are still lots of errors, because float4x4 doesn’t have some of the methods that Matrix4 had; specifically, methods for applying transformations, creating a projection matrix and returning the number of elements in matrix.

Fixing the Remaining Errors

To fix the remaining errors, find the float4x4+Extensions.swift file under the additional resources you downloaded and add it to your project. This file contains an extension to float4x4, which adds a Swift version of those helper methods.

You’ll notice there are a few errors left; don’t panic, you’ll take care of them next.

Fixing Issues in BufferProvider.swift

Open BufferProvider.swift and under nextUniformsBuffer(_:modelViewMatrix:light:) find the following code:

memcpy(bufferPointer, modelViewMatrix.raw(), MemoryLayout<Float>.size*float4x4.numberOfElements())
    memcpy(bufferPointer + MemoryLayout<Float>.size*float4x4.numberOfElements(), projectionMatrix.raw(), MemoryLayout<Float>.size*float4x4.numberOfElements())
    memcpy(bufferPointer + 2*MemoryLayout<Float>.size*float4x4.numberOfElements(), light.raw(), Light.size())

Replace that code with:

// 1
var projectionMatrix = projectionMatrix
var modelViewMatrix = modelViewMatrix
    
// 2
memcpy(bufferPointer, &modelViewMatrix, MemoryLayout<Float>.size*float4x4.numberOfElements())
memcpy(bufferPointer + MemoryLayout<Float>.size*float4x4.numberOfElements(), &projectionMatrix, MemoryLayout<Float>.size*float4x4.numberOfElements())
memcpy(bufferPointer + 2*MemoryLayout<Float>.size*float4x4.numberOfElements(), light.raw(), Light.size())

The two main differences between these blocks of code are:

  1. Matrices are now Swift structs and you need to mark them as mutable when you pass them by reference.
  2. To pass matrix data to memcpy, you simply need to get a pointer to it.