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 4 of 5 of this article. Click here to view the first page.

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 understand here: orthographic and perspective.

in the image beow, the perspective projection is on the left, and the orthographic projection is on the right. The camera, or point of view, is located on the axis origin.

zyGF1

Understanding perspective projection is easy, because it’s similar to how your eyes see the world. Orthographic is a bit harder, but easier than you might think, as you’ve been working with the cube in orthographic mode all along!

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

tracks_3

In orthographic mode, the picture would be deformed and the rails 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 projected onto the pyramid’s top face, which represents your device’s 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 appearance. 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 to the top of viewDidLoad():

projectionMatrix = Matrix4.makePerspectiveViewAngle(Matrix4.degrees(toRad: 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 along with near and far planes to specify the field of view.

In other words, everything that is too close or too far away from the camera won’t be displayed.

Now, modify render() inside Node.swift to add another parameter:

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

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

Find the following:

let uniformBuffer = device.makeBuffer(length: MemoryLayout<Float>.size * Matrix4.numberOfElements(), options: [])

…and replace it with this:

let uniformBuffer = device.makeBuffer(length: MemoryLayout<Float>.size * Matrix4.numberOfElements() * 2, options: [])

Now find the following:

memcpy(bufferPointer, nodeModelMatrix.raw(), MemoryLayout<Float>.size*Matrix4.numberOfElements())

And add this line right afterwards:

memcpy(bufferPointer + MemoryLayout<Float>.size * Matrix4.numberOfElements(), projectionMatrix.raw(), MemoryLayout<Float>.size * Matrix4.numberOfElements())

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

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

struct Uniforms{
  float4x4 modelMatrix;
  float4x4 projectionMatrix;
};

You now need to get the projection matrix. Find the following in the vertex shader:

float4x4 mv_Matrix = uniforms.modelMatrix;

And add this just after the above line:

float4x4 proj_Matrix = uniforms.projectionMatrix;

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

To that end, replace:

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

with the following:

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

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

With this:

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

Now 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.degrees(toRad: 45);
objectToDraw.scale = 0.5

Build and run; it looks somewhat like a cube, but it’s still messed up:

IMG_2440

You’ll fix this in a moment, just after a quick recap of what you’ve just accomplished:

  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’s often convenient to have a separate transformation that represents how you’re looking at the scene, which is your “camera”.
  2. Viewport transformation: This takes the little world you’ve created in normalized coordinates and maps it to the device screen. This is handled 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. Add a View transformation.
  2. Make the cube rotate.
  3. 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?) {

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

Inside render(), find:

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

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

Change it to this:

let worldModelMatrix = Matrix4()
worldModelMatrix.translate(0.0, y: 0.0, z: -7.0)
    
objectToDraw.render(commandQueue: 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.degrees(toRad: 45);
objectToDraw.scale = 0.5

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

Build and run to see what you have now:

IMG_2441

To understand the View transformation a little better, you’ll make a few modifications.

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

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

And add this below the line above:

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

Build and run to see the effect of your modifications:

IMG_2445

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

Andrew Kharchyshyn

Contributors

Andrew Kharchyshyn

Author

Over 300 content creators. Join our team.