Metal Tutorial with Swift 3 Part 2: Moving to 3D
In this second part of our Metal tutorial series, learn how to create a rotating 3D cube using Apple’s built-in 3D graphics API. 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
Metal Tutorial with Swift 3 Part 2: Moving to 3D
40 mins
- Getting Started
- Refactoring to a Node Class
- 1) Creating a Vertex Structure
- 2) Create a Node Class
- 3) Create a Triangle Subclass
- 4) Refactor your View Controller
- 5) Refactor your Shaders
- Creating a Cube
- Introduction to Matrices
- Model Transformation
- Uniform Data
- Projection Transformation
- View Transformation
- A Rotating Cube
- Fixing the Transparency
- Where to Go From Here?
Update: This tutorial has been updated for Xcode 8.2 and Swift 3.
Update: This tutorial has been updated for Xcode 8.2 and Swift 3.
Welcome back to our iOS Metal tutorial series!
In the first part of the series, you learned how to get started with Metal and render a simple 2D triangle.
In this second part of the series, you’ll learn how to set up a series of matrix transformations to move to full 3D. In the process, you will learn:
- How to use model, view, and projection transformations
- How to use matrices to transform geometry
- How to pass uniform data to your shaders
- How to use backface culling to optimize your drawing
Get ready to rock – it’s time for more Metal!
Getting Started
First, download the starter project – this is in the same state where you left it off in the previous tutorial.
Build and run on your Metal-compatible device, and make sure you see the same triangle as before.
Next, download this Matrix4 helper class that I have created for you, and add it to your project. When prompted if you’d like to configure an Objective-C bridging header, click Yes.
You’ll learn more about matrices later in this tutorial, so for now just enjoy this short overview of Matrix4
.
There’s a built-in library on iOS called GLKit, which contains a library of handy 3D math routines named GLKMath
. This includes a class GLKMatrix4
to work with 3D matrices.
You’re going to do a lot of work with matrices in this tutorial, so it would be nice if you could use this class. However, GLKMatrix4
is a C struct, so you can’t use it directly from Swift — that’s why I created this class for you. It’s a simple Objective-C class wrapper around the C struct that lets you use GLKMatrix4
from Swift. Here’s an illustration of the setup:
Again, you’ll learn more about matrices later; this just gives you a quick overview of the class.
Refactoring to a Node Class
For now, the starter project has everything set up in ViewController.swift. Although this was the easiest way to get started, it won’t scale well as your app gets larger.
In this section, you’ll refactor your project through the following five steps:
- Creating a Vertex Structure
- Creating a Node Class
- Creating a Triangle Subclass
- Refactoring your View Controller
- Refactoring your Shaders
Time to get started!
1) Creating a Vertex Structure
Create a new class with the iOS\Source\Swift File template and name it Vertex.swift.
Open Vertex.swift and replace its contents with the following:
struct Vertex{
var x,y,z: Float // position data
var r,g,b,a: Float // color data
func floatBuffer() -> [Float] {
return [x,y,z,r,g,b,a]
}
}
This is a structure to store the position and color of each vertex. floatBuffer()
is a handy method that returns the vertex data as an array of Floats in strict order.
2) Create a Node Class
Create a new class with the iOS\Source\Swift File template and name it Node.swift.
Open Node.swift and replace the contents with the following:
import Foundation
import Metal
import QuartzCore
class Node {
let device: MTLDevice
let name: String
var vertexCount: Int
var vertexBuffer: MTLBuffer
init(name: String, vertices: Array<Vertex>, device: MTLDevice){
// 1
var vertexData = Array<Float>()
for vertex in vertices{
vertexData += vertex.floatBuffer()
}
// 2
let dataSize = vertexData.count * MemoryLayout.size(ofValue: vertexData[0])
vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: [])
// 3
self.name = name
self.device = device
vertexCount = vertices.count
}
}
Going over this code section by section:
Since Node
represents an object to draw, you need to provide it with the vertices it contains, a name for convenience, and a device to create buffers and render later on.
- Go through each vertex and form a single buffer with floats, which will look like this:
- Then, ask the device to create a vertex buffer with the float buffer you created above.
- Finally, you set the instance variables.
Nothing fancy, eh?
Next, you need to move some of the render code that is currently in ViewController
to Node
. Specifically, you want to move the code responsible for rendering a particular buffer of vertices.
To do this, add this method to Node.swift:
func render(commandQueue: MTLCommandQueue, pipelineState: MTLRenderPipelineState, drawable: CAMetalDrawable, clearColor: MTLClearColor?){
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].clearColor =
MTLClearColor(red: 0.0, green: 104.0/255.0, blue: 5.0/255.0, alpha: 1.0)
renderPassDescriptor.colorAttachments[0].storeAction = .store
let commandBuffer = commandQueue.makeCommandBuffer()
let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
renderEncoder.setRenderPipelineState(pipelineState)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, at: 0)
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount,
instanceCount: vertexCount/3)
renderEncoder.endEncoding()
commandBuffer.present(drawable)
commandBuffer.commit()
}
This code should be a review from the previous tutorial. You can see that it’s been copied from ViewController
‘s render()
method, but updated to use the vertex data for this node.
That’s it for the node class for now – time to create a subclass for the triangle!
3) Create a Triangle Subclass
Create a new class with the iOS\Source\Swift File template and name it Triangle.swift.
Open Triangle.swift and replace the contents with the following:
import Foundation
import Metal
class Triangle: Node {
init(device: MTLDevice){
let V0 = Vertex(x: 0.0, y: 1.0, z: 0.0, r: 1.0, g: 0.0, b: 0.0, a: 1.0)
let V1 = Vertex(x: -1.0, y: -1.0, z: 0.0, r: 0.0, g: 1.0, b: 0.0, a: 1.0)
let V2 = Vertex(x: 1.0, y: -1.0, z: 0.0, r: 0.0, g: 0.0, b: 1.0, a: 1.0)
let verticesArray = [V0,V1,V2]
super.init(name: "Triangle", vertices: verticesArray, device: device)
}
}
Here you subclass the Node
class you just wrote for the triangle. In the initializer, you define the three vertices that make up the triangle, and pass that data to the superclass’s initializer.
It’s looking nice and clean already!