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.

Leave a rating/review
Save for later
Share

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:

source_of_light

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.

parallel

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:

32_a

  1. Ambient Lighting: Represents light that hits an object from all directions. You can think of this as light bouncing around a room.
  2. 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.
  3. 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:

IMG_4274

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:

  1. 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.
  2. 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:

  1. This is a property that stores the light color in red, green, and blue.
  2. This is a property that stores the intensity of the ambient effect.
  3. This is a convenience function to get size of the Light structure.
  4. 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.

Andrew Kharchyshyn

Contributors

Andrew Kharchyshyn

Author

Over 300 content creators. Join our team.