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?
A Rotating Cube
Now you’ll 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 has been 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 the following line:
timer = CADisplayLink(target: self, selector: #selector(ViewController.gameloop))
To this:
timer = CADisplayLink(target: self, selector: #selector(ViewController.newFrame(displayLink:)))
And replace this:
func gameloop() {
autoreleasepool {
self.render()
}
}
With this:
// 1
func newFrame(displayLink: CADisplayLink){
if lastFrameTimestamp == 0.0
{
lastFrameTimestamp = displayLink.timestamp
}
// 2
let elapsed: CFTimeInterval = displayLink.timestamp - lastFrameTimestamp
lastFrameTimestamp = displayLink.timestamp
// 3
gameloop(timeSinceLastUpdate: elapsed)
}
func gameloop(timeSinceLastUpdate: CFTimeInterval) {
// 4
objectToDraw.updateWithDelta(delta: timeSinceLastUpdate)
// 5
autoreleasepool {
self.render()
}
}
Nice work! Here’s what you did, step-by-step:
- The display link now calls
newFrame()
every time the display refreshes. Note that the display link is passed as a parameter. - You calculate time between this frame and the previous one. Note that it’s inconsistent because some frames might be skipped.
- You call
gameloop()
with the time interval since the last update. - You update your node by using
updateWithDelta()
before rendering. - 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: delta)
let 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 the cube rotation properties to be a function of sin
, which basically means that your cube will rotate to a point and then rotate back.
Build and run, and enjoy your rotating cube!
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 of the issue. 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 to fix this:
- One approach 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 the lower depth is drawn.
- The second approach is backface culling. This means that every triangle drawn is visible from only one side. 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 drawn counter-clockwise, otherwise, they won’t be rendered.
Lucky for you, the cube vertices have been set up 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()
:
let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
Add this right below:
//For now cull mode is used instead of depth buffer
renderEncoder.setCullMode(MTLCullMode.front)
Build and run to see how your transparency issue looks:
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 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.
In the meantime, be sure to check out some great resources from Apple:
- Apple’s Metal for Developers page, with tons of links to documentation, videos, and sample code
- Apple’s Metal Programming Guide
- Apple’s Metal Shading Language Guide
- The Metal videos from WWDC 2014
You also might enjoy the Beginning Metal course on our site, where we explain these same concepts in video form, but with even more detail.
If you have questions, comments or discoveries to share, please leave them in the comments below!