iOS 8 Metal Tutorial with Swift Part 2: Moving to 3D

iOS 8 Metal Tutorial with Swift Part 2: Moving to 3D
Learn how to render a 3D cube with Metal!

Learn how to render a 3D cube with Metal!

Welcome back to our iOS 8 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 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, that contains a library of handy 3D math routines called GLKMath. This includes a class to work with 3D matrices called GLKMatrix4.

You’re going to need to work with matrices a lot in this tutorial, so it would be good to be able to 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 is a simple Objective-C class wrapper around the C struct, so you can 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; I just wanted to have an overview of what this class is for now.

Refactor 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 doesn’t scale well as your app gets larger.

In this section, you’ll refactor your project by following five steps:

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

Let’s get started!

Note: This section is optional, since at the end of this section you’ll be right where you started, just 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. We’ll have a starter project waiting for you there.

1) Create a Vertex Structure

Create a new class with the iOS\Source\Swift File template and name it Vertex.swift.

Open Vertex.swift and replace the 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 that will 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 name: String
  var vertexCount: Int
  var vertexBuffer: MTLBuffer
  var device: MTLDevice
 
  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 * sizeofValue(vertexData[0])
    vertexBuffer = device.newBufferWithBytes(vertexData, length: dataSize, options: nil)
 
    // 3
    self.name = name
    self.device = device
    vertexCount = vertices.count
  }
 
}

Let’s go over this section by section:

Since Node is 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.

  1. You go through each vertex and form a single buffer with floats, which will look like this:
  2. Screen Shot 2014-09-05 at 5.28.41 PM

  3. Then, you ask the device to create a vertex buffer with the float buffer you created above.
  4. 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 that is responsible for rendering a particular buffer of vertices.

To do this, add this new 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.commandBuffer()
 
  let renderEncoderOpt = commandBuffer.renderCommandEncoderWithDescriptor(renderPassDescriptor)
  if let renderEncoder = renderEncoderOpt {
    renderEncoder.setRenderPipelineState(pipelineState)
    renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, atIndex: 0)
    renderEncoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: vertexCount, instanceCount: vertexCount/3)
    renderEncoder.endEncoding()
  }
 
  commandBuffer.presentDrawable(drawable)
  commandBuffer.commit()
}

This code should be a review from the previous tutorial. You can see that it has 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)
 
    var 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 following line:

var vertexBuffer: MTLBuffer! = nil

The node object will hold vertexBuffer so you don’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:

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 use, the only thing missing is calling the draw method from objectToDraw in the ViewController render method.

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

func render() {
  var drawable = metalLayer.nextDrawable()
  objectToDraw.render(commandQueue, pipelineState: pipelineState, drawable: drawable, clearColor: nil)
}

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

bkvtq

The triangle has disappeared!

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!

Solution Inside SelectShow
Vertex now contains color components, but in your vertex shader, you only expect three components for x, y and z.

5) Refactor your Shaders

Open Shaders.metal and take a good look at the vertex shader. You’ll notice it returns a float4 value for the position of each vertex and takes an array of packed_float3, which is data from a vertex buffer.

Now you’ll create two structures to hold Vertex data that passes to vertex shader, and one for vertex shader to return. It’ll be more clear when you will see the code.

Add this to Shaders.metal below using namespace metal; :

struct VertexIn{
  packed_float3 position;
  packed_float4 color;
};
 
struct VertexOut{
  float4 position [[position]];  //1
  float4 color;
};

You want vertex shader function to return VertexOut struct instead of just float4.

Note that vertex shaders must always return a position. In VertexOut, you specify the position component with special qualifier [[position]].

Now, modify the vertex shader code, so it looks like this:

vertex VertexOut basic_vertex(                           // 1
  const device VertexIn* vertex_array [[ buffer(0) ]],   // 2
  unsigned int vid [[ vertex_id ]]) {
 
  VertexIn VertexIn = vertex_array[vid];                 // 3
 
  VertexOut VertexOut;
  VertexOut.position = float4(VertexIn.position,1);
  VertexOut.color = VertexIn.color;                       // 4
 
  return VertexOut;
}
  1. Mark the vertex shader as returning a VertexOut instead of a float4.
  2. Mark the vertex_array as containing VertexIn vertices instead of packed_float3. Note that the VertexIn structure maps to the Vertex structure you created earlier.
  3. Get the current vertex from the array.
  4. Create a VertexOut and pass data from VertexIn to VertexOut, which returns at the end.

You might ask, “Why not to use VertexIn as a return value, since the data is the same?”

Good question! At the moment that would work, but you’ll need to have different structures later after applying transformations to position value.

Build and run. Oh, look who’s back!

Guess who's back / back again / Triangle's back / tell a friend...

Triangle’s back, tell a friend…

But you haven’t used the vertex color component, which now passes to vertex shader.

You should fix that by modifying the fragment shader to look like this:

fragment half4 basic_fragment(VertexOut interpolated [[stage_in]]) {  //1
  return half4(interpolated.color[0], interpolated.color[1], interpolated.color[2], interpolated.color[3]); //2
}
  1. The vertex shader will pass you the VertexOut structure – however its values will be interpolated based on the position of the fragment you’re rendering. More on this later.
  2. Now you simply return color for the current fragment instead of the hardcoded white color.

Build and run. You should be blinded by colors:

IMG_2420

Now, you may be wondering how you got rainbow colors in the middle of the triangle, considering you only specified three color values in the triangle.

As hinted earlier, the color values are interpolated based on the fragment you’re drawing. For example, the fragment on the bottom of the triangle 50% between the green and blue vertices would be blended as 50% green, 50% blue. This is done for you automatically; for any kind of value that you pass from the vertex shader to the fragment shader.

Okay, very nicely done so far! Now that your project is nicely refactored, it will be much easier to change from a triangle to a cube.

Creating a Cube

Note: If you skipped the previous section, download this starter project with the newly refactored code. The only difference other than general cleanliness is the triangle is now colored. Feel free to take a look through and get comfortable with the changes.

Your next task is to create a cube instead of a triangle. For that, as with all object models, you’ll create subclass of Node class.

Create a new class with the iOS\Source\Swift File template and name it Cube.swift.

Open Cube.swift and replace the contents with the following:

import UIKit
import Metal
 
class Cube: Node {
 
  init(device: MTLDevice){
 
    let A = Vertex(x: -1.0, y:   1.0, z:   1.0, r:  1.0, g:  0.0, b:  0.0, a:  1.0)
    let B = Vertex(x: -1.0, y:  -1.0, z:   1.0, r:  0.0, g:  1.0, b:  0.0, a:  1.0)
    let C = Vertex(x:  1.0, y:  -1.0, z:   1.0, r:  0.0, g:  0.0, b:  1.0, a:  1.0)
    let D = Vertex(x:  1.0, y:   1.0, z:   1.0, r:  0.1, g:  0.6, b:  0.4, a:  1.0)
 
    let Q = Vertex(x: -1.0, y:   1.0, z:  -1.0, r:  1.0, g:  0.0, b:  0.0, a:  1.0)
    let R = Vertex(x:  1.0, y:   1.0, z:  -1.0, r:  0.0, g:  1.0, b:  0.0, a:  1.0)
    let S = Vertex(x: -1.0, y:  -1.0, z:  -1.0, r:  0.0, g:  0.0, b:  1.0, a:  1.0)
    let T = Vertex(x:  1.0, y:  -1.0, z:  -1.0, r:  0.1, g:  0.6, b:  0.4, a:  1.0)
 
    var verticesArray:Array<Vertex> = [
      A,B,C ,A,C,D,   //Front
      R,T,S ,Q,R,S,   //Back
 
      Q,S,B ,Q,B,A,   //Left
      D,C,T ,D,T,R,   //Right
 
      Q,A,D ,Q,D,R,   //Top
      B,S,T ,B,T,C    //Bot
    ]
 
    super.init(name: "Cube", vertices: verticesArray, device: device)
  }
}

That looks rather familiar, don’t you think? It’s almost a carbon copy of the Triangle implementation, it just has eight vertices instead of three.

Also, each side comprises two triangles. For a better understanding, it might help to sketch it out on paper.
Cube__PSF_

Next open ViewController.swift and change the objectToDraw property to a cube:

var objectToDraw: Cube!

Also, inside init(), change the line that initializes objectToDraw to make it a cube:

objectToDraw = Cube(device: device)

Build and run. It might not look like it, but believe it or not, you’ve made a cube!

IMG_2435

What you see now is just the cube’s front face — an up-close selfie, if you will. Also it stretches over the display aspect ratio.

Don’t believe me? Doubt your cube-making skills? Okay, for your own sanity you’ll modify Cube so it’s smaller.

Rework the lines with vertices data so they look like this:

let A = Vertex(x: -0.3, y:   0.3, z:   0.3, r:  1.0, g:  0.0, b:  0.0, a:  1.0)
let B = Vertex(x: -0.3, y:  -0.3, z:   0.3, r:  0.0, g:  1.0, b:  0.0, a:  1.0)
let C = Vertex(x:  0.3, y:  -0.3, z:   0.3, r:  0.0, g:  0.0, b:  1.0, a:  1.0)
let D = Vertex(x:  0.3, y:   0.3, z:   0.3, r:  0.1, g:  0.6, b:  0.4, a:  1.0)
 
let Q = Vertex(x: -0.3, y:   0.3, z:  -0.3, r:  1.0, g:  0.0, b:  0.0, a:  1.0)
let R = Vertex(x:  0.3, y:   0.3, z:  -0.3, r:  0.0, g:  1.0, b:  0.0, a:  1.0)
let S = Vertex(x: -0.3, y:  -0.3, z:  -0.3, r:  0.0, g:  0.0, b:  1.0, a:  1.0)
let T = Vertex(x:  0.3, y:  -0.3, z:  -0.3, r:  0.1, g:  0.6, b:  0.4, a:  1.0)

Build and run.

IMG_2436

Cube is smaller, but something just isn’t right here. Wait, is it that you have to painstakingly modify vertices every time you need to modify a node? There’s got to be a better way!

And indeed there is – that is where matrices come in.

Introduction to Matrices

First things first, what is a matrix?

A matrix is a rectangular array of numbers (Sorry, Neo!). In 3D gaming, you’ll often see a 4×4 sized matrix — with four columns and four rows.

Also note that you’re using the GLKit column-based GLKMatrix4, so elements are placed like this:

Screen Shot 2014-09-03 at 1.43.20 PM

With matrices, you can transform your object in three ways:

  1. Translation: Shift the object along x, y and z axis
  2. Rotation: Rotate the object around any axis
  3. Scale: Change object size along any axis. (Please note that in this tutorial, you’ll always scale proportionally along all axes.)

Screen Shot 2014-09-03 at 1.36.17 PM

How does that work? First, you create an instance of Matrix4, like this (you don’t need to add this or the rest of the code in this section, this is just to illustrate):

var modelTransformationMatrix = Matrix4()

Then you use methods to apply transformations, like this:

modelTransformationMatrix.translate(positionX, y: positionY, z: positionZ)
modelTransformationMatrix.rotateAroundX(rotationX, y: rotationY, z: rotationZ)
modelTransformationMatrix.scale(scale, y: scale, z: scale)

There’s some math behind this that you can learn about in a Linear Algebra course. It’s great if you understand it, but it’s not necessary to do so for this tutorial.

Before you continue, open HelloMetal-BridgingHeader.h and import your Matrix4 class – you’ll need that in the following sections.

#import "Matrix4.h"

Model Transformation

The first transformation you’ll need is the model transformation. This transformation converts your node’s coordinates from local coordinates to world coordinates. In other words, it allows you to move your model around your world.

Let’s see how this works. Open Node.swift and add the following new properties:

var positionX:Float = 0.0
var positionY:Float = 0.0
var positionZ:Float = 0.0
 
var rotationX:Float = 0.0
var rotationY:Float = 0.0
var rotationZ:Float = 0.0
var scale:Float     = 1.0

These are convenience properties that you will set in order to position, rotate, or scale the node within the world. You will construct a model matrix from these which you will use to apply the model transformation.

To do this, add the following method at the end of Node:

func modelMatrix() -> Matrix4 {
    var matrix = Matrix4()
    matrix.translate(positionX, y: positionY, z: positionZ)
    matrix.rotateAroundX(rotationX, y: rotationY, z: rotationZ)
    matrix.scale(scale, y: scale, z: scale)
    return matrix
}

In this method, you generate a matrix from those parameters.

Now you need is to pass this matrix to the shader so it can apply the model transformation. To do this, you need to understand the concept of uniform data.

Uniform Data

So far, you have passed data that is different for each vertex to shaders, by using vertex arrays. But the model matrix will be the same across an entire model, so it would be a waste of space to copy it for each vertex.

When you have some data that is the same across an entire model, you want to pass the data to the shader a different way: as uniform data.

The first step to do this is to put your data into a buffer object, which represents memory accessible to both the CPU and GPU.

In Node.swift, add this right below var vertexBuffer: MTLBuffer:

var uniformBuffer: MTLBuffer?

Add this right after renderEncoder.setVertexBuffer(self.vertexBuffer, offset: 0, atIndex: 0):

// 1
var nodeModelMatrix = self.modelMatrix()
// 2
uniformBuffer = device.newBufferWithLength(sizeof(Float) * Matrix4.numberOfElements(), options: nil)
// 3
var bufferPointer = uniformBuffer?.contents()
// 4
memcpy(bufferPointer!, nodeModelMatrix.raw(), UInt(sizeof(Float)*Matrix4.numberOfElements()))
// 5
renderEncoder.setVertexBuffer(self.uniformBuffer, offset: 0, atIndex: 1)

Here’s the section-by-section breakdown:

  1. Call the method you wrote earlier to convert the convenience properties (like position and rotation) into a model matrix.
  2. Ask the device to create a buffer with shared CPU/GPU memory.
  3. Get a raw pointer from buffer (similar to void * in Objective-C).
  4. Copy your matrix data into the buffer.
  5. Pass uniformBuffer (with data copied) to the vertex shader. This is similar to how you sent the buffer of vertex-specific data, except you use index 1 instead of 0.

There is one problem with this code; it’s in a render method that should be called, hopefully, 60 times per second. In this case, you’re creating a new buffer 60 times per second.

Continuously allocating memory each frame is expensive and not recommended in production apps; we’ll show a better way to do this in future tutorials (and you can also see it in the iOS Metal Game template). However, to keep things simple in this tutorial, we will do it this way for now.

You passed matrix to vertex shader, but you’re not using it yet. To fix this, open Shaders.metal and add this structure right below VertexOut.

struct Uniforms{
  float4x4 modelMatrix;
};

Right now, it only holds one component, but later it will hold one more matrix.

Second, modify your vertex shader so it looks like this:

vertex VertexOut basic_vertex(
  const device VertexIn* vertex_array [[ buffer(0) ]],
  const device Uniforms&  uniforms    [[ buffer(1) ]],           //1
  unsigned int vid [[ vertex_id ]]) {
 
  float4x4 mv_Matrix = uniforms.modelMatrix;                     //2
 
  VertexIn VertexIn = vertex_array[vid];
 
  VertexOut VertexOut;
  VertexOut.position = mv_Matrix * float4(VertexIn.position,1);  //3
  VertexOut.color = VertexIn.color;
 
  return VertexOut;
}

Here’s what’s going on with this chunk of code:

  1. You add a second parameter for the uniform buffer, marking that it is incoming in slot 1 (to match up with the code you wrote earlier).
  2. You get a handle to the model matrix in the uniforms structure.
  3. To apply the model transformation to a vertex, you simply multiply the vertex position by the model matrix.

You’re done with that part, now back to working with the cube to test it.

In Cube.swift change vertices back to this:

let A = Vertex(x: -1.0, y:   1.0, z:   1.0, r:  1.0, g:  0.0, b:  0.0, a:  1.0)
let B = Vertex(x: -1.0, y:  -1.0, z:   1.0, r:  0.0, g:  1.0, b:  0.0, a:  1.0)
let C = Vertex(x:  1.0, y:  -1.0, z:   1.0, r:  0.0, g:  0.0, b:  1.0, a:  1.0)
let D = Vertex(x:  1.0, y:   1.0, z:   1.0, r:  0.1, g:  0.6, b:  0.4, a:  1.0)
 
let Q = Vertex(x: -1.0, y:   1.0, z:  -1.0, r:  1.0, g:  0.0, b:  0.0, a:  1.0)
let R = Vertex(x:  1.0, y:   1.0, z:  -1.0, r:  0.0, g:  1.0, b:  0.0, a:  1.0)
let S = Vertex(x: -1.0, y:  -1.0, z:  -1.0, r:  0.0, g:  0.0, b:  1.0, a:  1.0)
let T = Vertex(x:  1.0, y:  -1.0, z:  -1.0, r:  0.1, g:  0.6, b:  0.4, a:  1.0)

In ViewController.swift add following after objectToDraw = Cube(device: device)

objectToDraw.positionX = -0.25
objectToDraw.rotationZ = Matrix4.degreesToRad(45);
objectToDraw.scale = 0.5

Build and run.

IMG_2438

As expected, it is scaled down, shifted left and rotated 45 degrees around the Z axis.

This proves one thing: mathematical matrices are much cooler than any other matrices :]

Screen Shot 2014-09-08 at 2.34.05 PM

Now it’s time for some science — you’ll shift the cube along axis X , Y and Z at once. For that, add following code right below objectToDraw.positionX = -0.25 :

objectToDraw.positionY =  0.25
objectToDraw.positionZ = -0.25

Build and run.

IMG_2439

Cube shifts along X and Y, but what’s wrong with Z? You set .positionZ to be -0.25, so the cube should have moved away from you, but it didn’t.

You might think that there is some problem with matrix you passed, but that’s not the problem at all. In fact, the cube did move back, but you can’t see it.

To understand the problem, you need to understand another type of transform: the projection transform.

Projection Transformation

A projection transformation converts your node’s coordinates from camera coordinates to normalized coordinates. Depending on the type of projection you use, you’ll get different effects.

There are two projection types to know: orthographic and perspective.

Perspective is on the left and ortho is on the right. The camera or point of view is located on the axis origin.

zyGF1

Understanding perspective projection is easy, because its so similar to how your eyes see the world. Ortho is a bit harder but not that bad, because you’ve been working with the cube in orthographic mode!

Look at it another way: imagine that you’re standing on a railway and looking along it. In perspective mode, the rails look like this:

tracks_3

In orthographic mode, the picture is deformed and the rails are parallel.

In the picture below, you can see another perspective projection. It’s a chopped pyramid, and inside that pyramid is where your scene renders. The whole scene is projects onto the pyramid’s top face, similarly, that top face will be your device screen.

Screen Shot 2014-09-04 at 2.07.06 PM

Right now, Metal renders everything using orthographic projection, so you need to transform the scene to a perspective look. This calls for a matrix that describes perspective projection.

To recap the whole concept, you have a cube (your scene space), and you want to transform it into a chopped pyramid. To do that, you’ll create a projection matrix that describes the chopped pyramid above and maps it to your normalized box.

Matrix4 already has a method to create a perspective projection matrix, so you’ll work with that first.

Add the following new property to ViewController.swift:

var projectionMatrix: Matrix4!

Next, add the following at the top of viewDidLoad():

projectionMatrix = Matrix4.makePerspectiveViewAngle(Matrix4.degreesToRad(85.0), aspectRatio: Float(self.view.bounds.size.width / self.view.bounds.size.height), nearZ: 0.01, farZ: 100.0)

85.0 degrees specifies the camera’s vertical angle of view. You don’t need a horizontal value because you pass the aspect ratio of the view, and you also pass near and far planes to specify the field of view.

In other words, everything that is closer or farther will not display.

Now, modify the render() method inside Node.swift — specifically, you need to add one more parameter. Make the render method name look like this:

func render(commandQueue: MTLCommandQueue, pipelineState: MTLRenderPipelineState, drawable: CAMetalDrawable, projectionMatrix: Matrix4, clearColor: MTLClearColor?)

You added the projectionMatrix parameter.

Now you want to include projectionMatrix in the uniform buffer, which will pass to vertex shader later. Since the uniform buffer will now contain two matrices instead of one, you need to increase its size.

Replace:

uniformBuffer = device.newBufferWithLength(sizeof(Float) * Matrix4.numberOfElements(), options: nil)

With this:

uniformBuffer = device.newBufferWithLength(sizeof(Float) * Matrix4.numberOfElements() * 2, options: nil)

Now look for this:

memcpy(bufferPointer!, nodeModelMatrix.raw(), UInt(sizeof(Float)*Matrix4.numberOfElements()))

And add this after it:

memcpy(bufferPointer! + sizeof(Float)*Matrix4.numberOfElements(), projectionMatrix.raw(), UInt(sizeof(Float)*Matrix4.numberOfElements()))

Now both matrices pass to the uniforms buffer. All you need to do now is to use this projection matrix in your shader.

Go to Shaders.metal and modify Uniforms to include projectionMatrix:

struct Uniforms{
  float4x4 modelMatrix;
  float4x4 projectionMatrix;
};

Next in the vertex shader, you want to get projection matrix, so find this:

float4x4 mv_Matrix = uniforms.modelMatrix;

And add this after:

float4x4 proj_Matrix = uniforms.projectionMatrix;

To apply this matrix transformation to your position, you simply multiply matrix and position, just as you did with modelMatrix.

So, replace:

VertexOut.position = mv_Matrix * float4(VertexIn.position,1);

With this:

VertexOut.position = proj_Matrix * mv_Matrix * float4(VertexIn.position,1);

Finally, you need to pass in the projection matrix from your render method in ViewController.swift.

Replace:

objectToDraw.render(commandQueue, pipelineState: pipelineState, drawable: drawable, clearColor: nil)

With:

objectToDraw.render(commandQueue, pipelineState: pipelineState, drawable: drawable,projectionMatrix: projectionMatrix, clearColor: nil)

Also, change the translation parameters for objectToDraw so it looks like this:

objectToDraw.positionX = 0.0
objectToDraw.positionY =  0.0
objectToDraw.positionZ = -2.0
objectToDraw.rotationZ = Matrix4.degreesToRad(45);
objectToDraw.scale = 0.5

Build and run. Now it sort of looks like a cube, but it’s still messed up for some reason.

IMG_2440

You’ll fix it in a moment, but right now, take a moment to recap what you’ve just done.

  1. You added a model transformation, which allows you to modify an object’s location, size and rotation.
  2. You added a projection transformation, which allows you to shift from an orthographic to a more natural perspective projection.

There are actually two more transformations beyond this that are typically used in a 3D rendering pipeline:

  1. View Transformation: What if you want to look at the scene from a different position? You could move every object in the scene by modifying all of their model transformations, but this is inefficient. It is often convenient to have a separate transformation that represents how you’re looking at the scene; i.e. the “camera”.
  2. Viewport transformation: Basically, this takes the little world you’ve created in normalized coordinates and maps it to the device screen. This is done automatically by Metal, so you don’t need to do anything – it’s just something worth knowing.

Here’s the plan for the rest of the tutorial:

  1. You’ll add a View transformation
  2. You’ll make the cube rotate
  3. You’ll fix your cube’s peculiar transparency

View Transformation

A view transformation converts your node’s coordinates from world coordinates to camera coordinates. In other words, it allows you to move your camera around your world.

Adding a view transformation is fairly easy. Open Node.swift and change the render method declaration so it looks like this:

func render(commandQueue: MTLCommandQueue, pipelineState: MTLRenderPipelineState, drawable: CAMetalDrawable, parentModelViewMatrix: Matrix4, projectionMatrix: Matrix4, clearColor: MTLClearColor?)

You added parentModelViewMatrix, which represents the camera position and will be put to use to transform the scene.

Inside render(), find:

var nodeModelMatrix = self.modelMatrix()

And add this after it:

nodeModelMatrix.multiplyLeft(parentModelViewMatrix)

Note that you don’t pass this matrix to the shader as you did with the previous two matrices. Instead, you’re making a model view matrix which is a multiplication of the model matrix with the view matrix. It’s quite common to pass them pre-multiplied like this for efficiency.

Now, you just need to pass the new parameter to render(). Open ViewController.swift, and change :

objectToDraw.render(commandQueue, pipelineState: pipelineState, drawable: drawable,projectionMatrix: projectionMatrix, clearColor: nil)

To this:

var worldModelMatrix = Matrix4()
worldModelMatrix.translate(0.0, y: 0.0, z: -7.0)
 
objectToDraw.render(commandQueue, pipelineState: pipelineState, drawable: drawable, parentModelViewMatrix: worldModelMatrix, projectionMatrix: projectionMatrix ,clearColor: nil)

Also, delete these lines:

objectToDraw.positionX = 0.0
objectToDraw.positionY =  0.0
objectToDraw.positionZ = -2.0
objectToDraw.rotationZ = Matrix4.degreesToRad(45);
objectToDraw.scale = 0.5

You don’t need to move the object back, because you’re moving the point of view.

Build and run.

IMG_2441

To understand the View transformation a little better, go ahead and do some more modifications.

Still in render() inside ViewController.swift, find:

worldModelMatrix.translate(0.0, y: 0.0, z: -7.0)

And add this below it:

worldModelMatrix.rotateAroundX(Matrix4.degreesToRad(25), y: 0.0, z: 0.0)

Build and run.

IMG_2445

You rotated the whole scene around the x-axis or changed the camera direction, whichever way you prefer to think about it :]

A Rotating Cube

Now let’s modify the project so it rotates your cube over time.

To do this, open Node.swift and add the following new property:

var time:CFTimeInterval = 0.0

This is simply a property that tracks how long the node was around.

Now, add the following method to the end of the class:

func updateWithDelta(delta: CFTimeInterval){
    time += delta
}

Next open ViewController.swift and add this new property:

var lastFrameTimestamp: CFTimeInterval = 0.0

Next, change:

timer = CADisplayLink(target: self, selector: Selector("gameloop"))

To be this:

timer = CADisplayLink(target: self, selector: Selector("newFrame:"))

And replace this:

func gameloop() {
  autoreleasepool {
    self.render()
  }
}

With this:

// 1
func newFrame(displayLink: CADisplayLink){
 
  if lastFrameTimestamp == 0.0
  {
    lastFrameTimestamp = displayLink.timestamp
  }
 
  // 2
  var elapsed:CFTimeInterval = displayLink.timestamp - lastFrameTimestamp
  lastFrameTimestamp = displayLink.timestamp
 
  // 3
  gameloop(timeSinceLastUpdate: elapsed)
}
 
func gameloop(#timeSinceLastUpdate: CFTimeInterval) {
 
  // 4
  objectToDraw.updateWithDelta(timeSinceLastUpdate)
 
  // 5
  autoreleasepool {
    self.render()
  }
}

Nice work! Here’s what you did, step-by-step:

  1. The display link now calls newFrame() every time the display refreshes. Note that the display link is passed as a parameter.
  2. You calculate time between this frame and the previous one. Note that it’s inconsistent because some frames might be skipped.
  3. You call gameloop(), but with the time since the last update as a parameter.
  4. You update your node by using updateWithDelta() before rendering.
  5. Now, when a node is updated, you can call the render method.

Finally, you need to override updateWithDelta in Cube class. Open Cube.swift and add this new method:

override func updateWithDelta(delta: CFTimeInterval) {
 
  super.updateWithDelta(delta)
 
  var secsPerMove: Float = 6.0
  rotationY = sinf( Float(time) * 2.0 * Float(M_PI) / secsPerMove)
  rotationX = sinf( Float(time) * 2.0 * Float(M_PI) / secsPerMove)
}

Nothing fancy here, all you’re doing is calling super() to update the time property. Then you set cube rotation properties to be a function of sin, which basically mean that your cube will rotate to some point and then rotate back.

Build and run, and enjoy your rotating cube!

IMG_2456

Your cube should now have a little life to it, spinning back and forth like a little cube-shaped dancer.

Fixing the Transparency

The last part is to fix the cube’s transparency, but first, you should understand the cause. It’s really quite simple and a little peculiar — Metal sometimes draws pixels of the back face of the cube before the front face.

So, how do you fix it?

There are two ways:

  1. One approach you can take is depth testing. With this method, you store each point’s depth value, so that when the two points are drawn at the same point on the screen, only the one with lower depth is drawn.
  2. The second approach is backface culling. This means that every triangle drawn is visible from just one side. So in effect, the back face isn’t drawn until it turns toward the camera. This is based on the order you specify the vertices of the triangles.

You’re going to use backface culling to solve the problem here, as it’s a more efficient solution when there is only one model. The rule you must keep is this: all triangles must be specified as counter-clockwise, otherwise, it won’t draw them.

Lucky for you, when I set up the cube vertices I organized them so every triangle is indeed specified as counter-clockwise, so you can just focus on learning how to use backface culling.

Open Node.swift and add find this in render():

if let renderEncoder = renderEncoderOpt {

And add this right below:

//For now cull mode is used instead of depth buffer
renderEncoder.setCullMode(MTLCullMode.Front)

Build and run.

IMG_2447

Now your cube should be free of transparency. What a beautiful cube!

Where to Go From Here?

Here is the final example project from this iOS 8 Metal Tutorial.

Congratulations, you have learned a ton about moving to 3D in the new Metal API! You now have an understanding of model, view, projection, and viewport transformations, matrices, passing uniform data, backface culling, and more.

We may write some more tutorials in this series covering texturing, lighting, and importing models if there’s enough interest – add a comment below if you’d would like to see more!

In the meantime, be sure to check out some great resources from Apple:

You also might enjoy the OpenGL ES video tutorials on our site, where Ray explains how many of these same concepts work in OpenGL ES, but with even more detail.

If you have questions, comments or discoveries to share, please leave them in the comments below!

iOS 8 Metal Tutorial with Swift Part 2: Moving to 3D is a post from: Ray Wenderlich

The post iOS 8 Metal Tutorial with Swift Part 2: Moving to 3D appeared first on Ray Wenderlich.

5
Like
Save

Comments

Write a comment

*