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.

Leave a rating/review
Save for later
Share

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.

The most beautiful triangle I've ever seen!

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:

Screen Shot 2014-09-05 at 4.23.05 PM

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:

  1. Creating a Vertex Structure
  2. Creating a Node Class
  3. Creating a Triangle Subclass
  4. Refactoring your View Controller
  5. Refactoring your Shaders

Time to get started!

Note: This section is optional, since at the end of this section you’ll be right back where you started, albeit with a cleaner project and a colored triangle. If you’d like go straight to the 3D stuff, feel free to skip ahead to the “Creating a Cube” section. A starter project will be waiting there for you.

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.

Screen Shot 2014-09-05 at 5.28.41 PM

  1. Go through each vertex and form a single buffer with floats, which will look like this:
  2. Then, ask the device to create a vertex buffer with the float buffer you created above.
  3. 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!

4) Refactor your View Controller

Now that you have all of the pieces in place, let’s refactor your view controller to use your new Triangle class.

Open ViewController.swift and delete the following line:

var vertexBuffer: MTLBuffer! = nil

The node object holds vertexBuffer, so you won’t need it here.

Next, replace:

let vertexData:[Float] = [
    0.0, 1.0, 0.0,
    -1.0, -1.0, 0.0,
    1.0, -1.0, 0.0]

with the following:

var objectToDraw: Triangle!

And replace:

// 1
let dataSize = vertexData.count * sizeofValue(vertexData[0])
// 2
vertexBuffer = device.newBufferWithBytes(vertexData, length: dataSize, options: nil)

with:

objectToDraw = Triangle(device: device)

Now, when objectToDraw initializes and is ready to go, the only thing that’s missing a call to the draw method from objectToDraw in the ViewController render method.

Finally, replace the render() method with the following:

func render() {
  guard let drawable = metalLayer?.nextDrawable() else { return }
  objectToDraw.render(commandQueue: commandQueue, pipelineState: pipelineState, drawable: drawable, clearColor: nil)
}

Build and run, and… uh-oh, where’d the triangle go?

Do you have any ideas why that might be? Try to think of an answer, and then check to see if you are correct. Hint: take a look at the data inside the Vertex structure.

[spoiler]Vertex now contains color components, but in your vertex shader, you only expect three components for x, y and z.[/spoiler]

Andrew Kharchyshyn

Contributors

Andrew Kharchyshyn

Author

Over 300 content creators. Join our team.