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

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]

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 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 the 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 the following:

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;
}

Taking each commented section in turn:

  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! That would work at the moment, but you’d need different structures later after applying transformations to the position value.

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

Triangle’s back, tell a friend…

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 the vertex shader. 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
}

A short bit of code, but important all the same:

  1. The vertex shader passes the VertexOut structure, but its values will be interpolated based on the position of the fragment you’re rendering. More on this later.
  2. Now you simply return the color for the current fragment instead of the hardcoded white color.

Build and run. You should be blinded by colors:

IMG_2420

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 and 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 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 that 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 a subclass of the 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 Foundation
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)
    
    let 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!

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 created a cube!

IMG_2435

What you see now is just the cube’s front face — an up-close selfie, if you will. It’s also stretched 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

The cube is smaller, but something just doesn’t feel right here. Wait — it’s probably 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. This is where matrices come in.

Andrew Kharchyshyn

Contributors

Andrew Kharchyshyn

Author

Over 300 content creators. Join our team.