Metal Tutorial with Swift 3 Part 4: Lighting
In this fourth part of our Metal tutorial series, learn how to light 3D objects using the Phong lighting model. 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 4: Lighting
35 mins
- Getting Started
- Phong Lighting Model
- Project Setup
- Ambient Lighting Overview
- Adding Ambient Lighting
- Creating a Light Structure
- Passing the Light Data to the GPU
- Modifying the Shaders to Accept the Light Data
- Adding the Ambient Light Calculation
- Left in the Dark
- Diffuse Lighting Overview
- Introducing Normals
- Introducing Dot Products
- Introducing Diffuse Lighting
- Adding Diffuse Lighting
- Adding Normal Data
- Passing the Normal Data to the GPU
- Adding Diffuse Lighting Data
- Adding the Diffuse Lighting Calculation
- Specular Lighting Overview
- Adding Specular Lighting
- Byte Alignment
- Adding the Specular Lighting Calculation
- Where to Go From Here?
Update: This tutorial has been updated for Xcode 8.2 and Swift 3.
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 the second part of the series, you learned how to set up a series of transformations to move from a triangle to a full 3D cube.
In the third part of the series, you learned how to add a texture to the cube.
In this fourth part of the series, you’ll learn how to add some lighting to the cube. As you work through this tutorial, you’ll learn:
- Some basic light concepts
- Phong light model components
- How to calculate light effect for each point in the scene, using shaders
Getting Started
Before you begin, you need to understand how lighting works.
“Lighting” means applying light generated from light sources to rendered objects. That’s how the real world works; light sources (like the sun or lamps) produce light, and rays of these lights collide with the environment and illuminate it. Our eyes can then see this environment and we have a picture rendered on our eyes’ retinas.
In the real world, you have multiple light sources. Those light sources work like this:
Rays are emitted in all directions from the light source.
The same rule applies to our biggest light source – the sun. However, when you take into account the huge distance between the Sun and the Earth, it’s safe to treat the small percentage of rays emitted from the Sun that actually collide with Earth as parallel rays.
For this tutorial, you’ll use only one light source with parallel rays, just like those of the sun. This is called a directional light and is commonly used in 3D games.
Phong Lighting Model
There are various algorithms used to shade objects based on light sources, but one of the most popular is called the Phong lighting model.
This model is popular for a good reason. Not only is it quite simple to implement and understand, but it’s also quite performant and looks great!
The Phong lighting model consist of three components:
- Ambient Lighting: Represents light that hits an object from all directions. You can think of this as light bouncing around a room.
- Diffuse Lighting: Represents light that is brighter or darker depending on the angle of an object to the light source. Of all three components, I’d argue this is the most important part for the visual effect.
- Specular Lighting: Represents light that causes a bright spot on the small area directly facing the light source. You can think of this as a bright spot on a shiny piece of metal.
You will learn more about each of these components as you implement them in this tutorial.
Project Setup
It’s time to code! Start by downloading the starter project for this tutorial. It’s exactly where we finished in the previous tutorial.
Run it on a Metal-compatible iOS device, just to be sure it works correctly. You should see the following:
This represents a 3D cube. It looks great except all areas of the cube are evenly-lit, so it looks a bit flat. You’ll improve the image through the power of lighting!
Ambient Lighting Overview
Remember that ambient lighting highlights all surfaces in the scene by the same amount, no matter where the surface is located, which direction the surface is facing, or what the light direction is.
To calculate ambient lighting, you need two parameters:
- Light color: Light can have different colors. For example, if a light is red, each object the light hits will be tinted red. For this tutorial, you will use a plain white color for the light. White light is a common choice, since white doesn’t tint the material of the object.
- Ambient intensity: This is a value that represents the strength of the light. The higher the value, the brighter the illumination of the scene.
Once you have those parameters, you can calculate the ambient lighting as follows:
Ambient color = Light color * Ambient intensity
Time to give this a shot in code!
Adding Ambient Lighting
First, you need a structure to store light data.
Creating a Light Structure
Add a new Swift file to your project named Light.swift and replace its contents with the following:
import Foundation
struct Light {
var color: (Float, Float, Float) // 1
var ambientIntensity: Float // 2
static func size() -> Int { // 3
return MemoryLayout<Float>.size * 4
}
func raw() -> [Float] {
let raw = [color.0, color.1, color.2, ambientIntensity] // 4
return raw
}
}
Reviewing things section-by-section:
- This is a property that stores the light color in red, green, and blue.
- This is a property that stores the intensity of the ambient effect.
- This is a convenience function to get size of the
Light
structure. - This is a convenience function to convert the structure to an array of floats. You’ll use this and the
size()
function to send the light data to the GPU.
This is similar to Vertex
structure that you created in Part 2 of this series.
Now open Node.swift and add the following constant to the class:
let light = Light(color: (1.0,1.0,1.0), ambientIntensity: 0.2)
This creates a white light with a low intensity (0.2).
Passing the Light Data to the GPU
Next you need to pass this light data to the GPU. You’ve already included the projection and model matrices in the uniform buffer; you’ll modify this to include the light data as well.
To do this, open Node.swift, and replace the following line in init()
:
self.bufferProvider = BufferProvider(device: device, inflightBuffersCount: 3, sizeOfUniformsBuffer: MemoryLayout<Float>.size * Matrix4.numberOfElements() * 2)
…with this code:
let sizeOfUniformsBuffer = MemoryLayout<Float>.size * Matrix4.numberOfElements() * 2 + Light.size()
self.bufferProvider = BufferProvider(device: device, inflightBuffersCount: 3, sizeOfUniformsBuffer: sizeOfUniformsBuffer)
Here you increase the size of uniform buffers so that you have room for the light data.
Now in BufferProvider.swift change this method declaration:
func nextUniformsBuffer(projectionMatrix: Matrix4, modelViewMatrix: Matrix4) -> MTLBuffer
…to this:
func nextUniformsBuffer(projectionMatrix: Matrix4, modelViewMatrix: Matrix4, light: Light) -> MTLBuffer
Here you added an extra parameter for the light data. Now inside this same method, find these lines:
memcpy(bufferPointer, modelViewMatrix.raw(), MemoryLayout<Float>.size * Matrix4.numberOfElements())
memcpy(bufferPointer + MemoryLayout<Float>.size*Matrix4.numberOfElements(), projectionMatrix.raw(), MemoryLayout<Float>.size*Matrix4.numberOfElements())
…and add this line just below:
memcpy(bufferPointer + 2*MemoryLayout<Float>.size*Matrix4.numberOfElements(), light.raw(), Light.size())
With this additional memcpy()
call, you copy light data to the uniform buffer, just as you did with with the projection and model view matrices.