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

Fixing Issues in Node.swift

Now, open Node.swift and find the following code under render(_ commandQueue: MTLCommandQueue, pipelineState: MTLRenderPipelineState, drawable: CAMetalDrawable, parentModelViewMatrix: float4x4, projectionMatrix: float4x4, clearColor: MTLClearColor?):

let nodeModelMatrix = self.modelMatrix()

Replace that code with:

var nodeModelMatrix = self.modelMatrix()

Under modelMatrix(), find:

let matrix = float4x4()

And replace that code with:

var matrix = float4x4()

Also, remove the question marks and the exclamation mark right below it.

The various helper methods from the float4x4 extension are modifying the struct, therefore the variables must be declared as var instead of let.

Your project should now be error free. Time for another build and run. The result should look exactly the same as before, which is to be expected!

IMG_5924

The main difference is that you’ve now removed all the Objective-C code, and you’re now using the new SIMD data type float4x4 instead of that old Matrix4.

Exploring float4x4+Extensions.swift

Open float4x4+Extensions.swift and take a look at the methods. As you can see, this file still calls math functions from GLKMath under the hood in order to use well-written and well-tested code instead of reinventing the wheel.

68950697

This change might not seem worth it, but it’s important to use SIMD’s float4x4 because it’s a standardized solution for 3D graphics and it will allow easier integration with third-party code.

At the end of the day, it doesn’t really matter how the matrix math is done. You can use GLKit, a 3rd party extension or perhaps Apple will release their own solution down the road someday. The important thing is to have your matrices represented in the same format as the rest of them, out there in the wild! :]

MetalKit Texture Loading

Before you take a look at the functionality that MetalKit offers, open MetalTexture.swift and review how it currently loads the texture in loadTexture(_ device: MTLDevice, commandQ: MTLCommandQueue, flip: Bool):

  1. First, you load the image from a file.
  2. Next, you extract the pixel data from that image into raw bytes.
  3. Then, you ask the MTLDevice to create an empty texture.
  4. Finally, you copy the bytes data into that empty texture.

Lucky for you, MetalKit provides a great API that helps you with loading textures. Your main interaction with it will be through the MTKTextureLoader class.

You might be asking, “How much code can this API save me from writing?” The answer is pretty much everything in MetalTexture!

To switch texture loading to MetalKit, delete MetalTexture.swift from your project. Again, this will cause some errors; you’ll fix these shortly.

Fixing Issues in Cube.swift

First, open Cube.swift and find the following at the top of the file:

import Metal

Then, replace it with this:

import MetalKit

Next, add a parameter to the initializer. Find this line of code:

init(device: MTLDevice, commandQ: MTLCommandQueue) {

Then, replace it with the following:

init(device: MTLDevice, commandQ: MTLCommandQueue, textureLoader :MTKTextureLoader) {

Scroll down to the end of this initializer and find the following code:

let texture = MetalTexture(resourceName: "cube", ext: "png", mipmaped: true)
texture.loadTexture(device, commandQ: commandQ, flip: true)
    
super.init(name: "Cube", vertices: verticesArray, device: device, texture: texture.texture)

Now, replace it with the following:

let path = Bundle.main.path(forResource: "cube", ofType: "png")!
let data = NSData(contentsOfFile: path) as! Data
let texture = try! textureLoader.newTexture(with: data, options: [MTKTextureLoaderOptionSRGB : (false as NSNumber)])
    
super.init(name: "Cube", vertices: verticesArray, device: device, texture: texture)

Here’s a recap of what you’ve just done:

  • You added a MTKTextureLoader parameter to the cube’s initializer.
  • Then, after converting the image into Data, you used newTexture(_:options:) on the textureLoader to directly load the image into a MTLTexture.

Fixing Issues in MetalViewController.swift

Now you need to pass a texture loader to the cube when you create it.

Open MetalViewController.swift and find the following at the top of the file:

import Metal

Replace it with this:

import MetalKit

Next, add the following new property to MetalViewController:

var textureLoader: MTKTextureLoader! = nil

Finally, initialize this property by adding the line below, right after the point where you create the default device in viewDidLoad():

textureLoader = MTKTextureLoader(device: device)

Fixing Issues in MySceneViewController.swift

Now that you’ve got a default instance of a texture loader, you need to update MySceneViewController.swift to pass it to the cube.

Go to viewDidLoad() in MySceneViewController.swift and find this code:

objectToDraw = Cube(device: device, commandQ:commandQueue)

Now, replace the call to Cube() with this:

objectToDraw = Cube(device: device, commandQ: commandQueue, textureLoader: textureLoader)

Build and run the app, and you should have the exact same result as before. Again, this is the expected result.

IMG_5923

Although the result didn’t change, you’re making positive changes to your app, under the hood. You’re now using MTKTextureLoader from MetalKit to load a texture. Compared to before, where you had to write a whole bunch of code yourself to achieve the same result.

Switching to MTKView

The idea behind MTKView is simple. In iOS, it’s a subclass of UIView, and it allows you to quickly connect a view to the output of a render pass. A MTKView will help you do the following:

  • Configure the CAMetalLayer of the view.
  • Control the timing of the draw calls.
  • Quickly manage a MTLRenderPassDescriptor.
  • Handle view resizes easily.

To use a MTKView, you can either implement a delegate for it or you can subclass it to provide the draw updates for the view. For this tutorial, you’ll go with the first option.

First, you need to change the main view’s class to be a MTKView.

Open Main.storyboard, select the view controller view, then change the class to MTKView in the Identity Inspector:

Screen Shot 2016-06-19 at 10.13.51 PM

An instance of MTKView, by default, will ask for redraws periodically. So you can remove all the code that sets up a CADisplayLink.

Removing Redundant Code From MetalViewController.swift

Open MetalViewController.swift, scroll to the end of viewDidLoad() and remove the following:

timer = CADisplayLink(target: self, selector: #selector(MetalViewController.newFrame(_:)))
timer.add(to: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode)

After that, you can also remove both newFrame(_:) and gameloop(_:) functions.

Now you need to remove the code that sets up the Metal layer since the MTKView will handle that for you.

Again, in viewDidLoad(), remove the following:

metalLayer = CAMetalLayer()
metalLayer.device = device
metalLayer.pixelFormat = .bgra8Unorm
metalLayer.framebufferOnly = true
view.layer.addSublayer(metalLayer)