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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
iOS Metal Tutorial with Swift Part 5: Switching to MetalKit
20 mins
- Getting Started
- Getting Started with MetalKit
- Switching to SIMD
- Deleting the Objective-C Wrapper
- Replacing Matrix4 With a SIMD Data Type
- Fixing the Remaining Errors
- Fixing Issues in BufferProvider.swift
- Fixing Issues in Node.swift
- Exploring float4x4+Extensions.swift
- MetalKit Texture Loading
- Fixing Issues in Cube.swift
- Fixing Issues in MetalViewController.swift
- Fixing Issues in MySceneViewController.swift
- Switching to MTKView
- Removing Redundant Code From MetalViewController.swift
- Adding the MTKViewDelegate Protocol
- Where to Go From Here?
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!
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.
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)
:
- First, you load the image from a file.
- Next, you extract the pixel data from that image into raw bytes.
- Then, you ask the
MTLDevice
to create an empty texture. - 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 usednewTexture(_:options:)
on thetextureLoader
to directly load the image into aMTLTexture
.
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.
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:
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)