Chapters

Hide chapters

Metal by Tutorials

Third Edition · macOS 12 · iOS 15 · Swift 5.5 · Xcode 13

Section I: Beginning Metal

Section 1: 10 chapters
Show chapters Hide chapters

Section II: Intermediate Metal

Section 2: 8 chapters
Show chapters Hide chapters

Section III: Advanced Metal

Section 3: 8 chapters
Show chapters Hide chapters

10. Lighting Fundamentals
Written by Marius Horga & Caroline Begbie

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Light and shade are important requirements for making your scenes pop. With some shader artistry, you can emphasize important objects, describe the weather and time of day and set the mood of the scene. Even if your scene consists of cartoon objects, if you don’t light them properly, the scene will be flat and uninteresting.

One of the simplest methods of lighting is the Phong reflection model. It’s named after Bui Tong Phong who published a paper in 1975 extending older lighting models. The idea is not to attempt duplication of light and reflection physics but to generate pictures that look realistic.

This model has been popular for over 40 years and is a great place to start learning how to fake lighting using a few lines of code. All computer images are fake, but there are more modern real-time rendering methods that model the physics of light.

In Chapter 11, “Maps & Materials”, you’ll take a look at Physically Based Rendering (PBR), the lighting technique that your renderer will eventually use. PBR is a more realistic lighting model, but Phong is easy to understand and get started with.

The Starter Project

➤ Open the starter project for this chapter.

The starter project’s files are now in sensible groups. In the Game group, the project contains a new game controller class which further separates scene updates and rendering. Renderer is now independent from GameScene. GameController initializes and owns both Renderer and GameScene. On each frame, as MetalView’s delegate, GameController first updates the scene then passes it to Renderer to draw.

Object ownership
Object ownership

In GameScene.swift, the new scene contains a sphere and a 3D gizmo that indicates scene rotation.

DebugLights.swift in the Utility group contains some code that you’ll use later for debugging where lights are located. Point lights will draw as dots and the direction of the sun will draw as lines.

In the Geometry group, the default vertex descriptor in VertexDescriptor.swift now includes a color buffer. The sphere model has a texture to show colors, but the 3D gizmo uses vertex colors. In the Shaders group, the vertex shader forwards this color to the fragment shader, and the fragment shader uses the vertex color if there is no color texture.

➤ Familiarize yourself with the code and build and run the project.

The starter app
The starter app

To rotate around the sphere and fully appreciate your lighting, the camera is an ArcballCamera type. Press 1 to set the camera to a front view, and 2 to reset the camera to the default view. GameScene contains the key pressing code for this.

You can see that the sphere colors are very flat. In this chapter, you’ll add shading and specular highlights.

Representing Color

In this book, you’ll learn the necessary basics to get you rendering light, color and simple shading. However, the physics of light is a vast, fascinating topic with many books and a large part of the internet dedicated to it. You can find further reading in references.markdown in the resources directory for this chapter.

let result = float3(1.0, 0.0, 0.0) * float3(0.5, 0.5, 0.5)
Color shading
Qorog kjamavp

A 3D shaded sphere
E 5X zyeliq jknozu

Normals

The slope of a surface can determine how much a surface reflects light.

Surface normals on a sphere
Ticwaza filcapx ip o jtreya

Light Types

There are several standard light options in computer graphics, each of which has their origin in the real world.

Directional Light

A scene can have many lights. In fact, in studio photography, it would be highly unusual to have just a single light. By putting lights into a scene, you control where shadows fall and the level of darkness. You’ll add several lights to your scene through the chapter.

The direction of sunlight
Zmi tarafmaaz oj cunvajpm

typedef enum {
  unused = 0,
  Sun = 1,
  Spot = 2,
  Point = 3,
  Ambient = 4
} LightType;
typedef struct {
  LightType type;
  vector_float3 position;  
  vector_float3 color;
  vector_float3 specularColor;
  float radius;
  vector_float3 attenuation;
  float coneAngle;
  vector_float3 coneDirection;
  float coneAttenuation;
} Light;
struct SceneLighting {
  static func buildDefaultLight() -> Light {
    var light = Light()
    light.position = [0, 0, 0]
    light.color = [1, 1, 1]
    light.specularColor = [0.6, 0.6, 0.6]
    light.attenuation = [1, 0, 0]
    light.type = Sun
    return light
  }
}
let sunlight: Light = {
  var light = Self.buildDefaultLight()
  light.position = [1, 2, -2]
  return light
}()
var lights: [Light] = []
init() {
  lights.append(sunlight)
}
let lighting = SceneLighting()
uint lightCount;
vector_float3 cameraPosition;
LightBuffer = 13
params.lightCount = UInt32(scene.lighting.lights.count)
var lights = scene.lighting.lights
renderEncoder.setFragmentBytes(
  &lights,
  length: MemoryLayout<Light>.stride * lights.count,
  index: LightBuffer.index)

The Phong Reflection Model

In the Phong reflection model, there are three types of light reflection. You’ll calculate each of these, and then add them up to produce a final color.

Diffuse shading and micro-facets
Jubqoxu bgunagp ibz peflu-gekemg

The Dot Product

Fortunately, there’s a straightforward mathematical operation to discover the angle between two vectors called the dot product.

The dot product
Tni pis zxupozl

The dot product of sunlight and normal vectors
Mxu soz czamelv iy taxjelty ijb yenbot duybotp

Dot product playground code
Mim xkuyuyb hgisnqaick vovo

Diffuse Reflection

Shading from the sun does not depend on where the camera is. When you rotate the scene, you’re rotating the world, including the sun. The sun’s position will be in world space, and you’ll put the model’s normals into the same world space to be able to calculate the dot product against the sunlight direction. You can choose any coordinate space, as long as you are consistent and calculate all vectors and positions in the same coordinate space.

float3 worldPosition;
float3 worldNormal;
matrix_float3x3 normalMatrix;
uniforms.normalMatrix = uniforms.modelMatrix.upperLeft
.worldPosition = (uniforms.modelMatrix * in.position).xyz,
.worldNormal = uniforms.normalMatrix * in.normal
constant Light *lights [[buffer(LightBuffer)]],

Creating Shared Functions in C++

Often you’ll want to access C++ functions from multiple files. Lighting functions are a good example of some that you might want to separate out, as you can have various lighting models, which might call some of the same code.

#import "Common.h"

float3 phongLighting(
  float3 normal,
  float3 position,
  constant Params &params,
  constant Light *lights,
  float3 baseColor);
#import "Lighting.h"

float3 phongLighting(
  float3 normal,
  float3 position,
  constant Params &params,
  constant Light *lights,
  float3 baseColor) {
    return float3(0);
}
#import "Lighting.h"
float3 normalDirection = normalize(in.worldNormal);
float3 color = phongLighting(
  normalDirection,
  in.worldPosition,
  params,
  lights,
  baseColor
);
return float4(color, 1);
No lighting
Yu buctyurw

float3 diffuseColor = 0;
float3 ambientColor = 0;
float3 specularColor = 0;
for (uint i = 0; i < params.lightCount; i++) {
  Light light = lights[i];
  switch (light.type) {
    case Sun: {
      break;
    }
    case Point: {
      break;
    }
    case Spot: {
      break;
    }
    case Ambient: {
      break;
    }
    case unused: {
      break;
    }
  }
}
return diffuseColor + specularColor + ambientColor;
// 1
float3 lightDirection = normalize(-light.position);
// 2
float diffuseIntensity =
  saturate(-dot(lightDirection, normal));
// 3
diffuseColor += light.color * baseColor * diffuseIntensity;
Diffuse shading
Notfoni dwarulb

Visualizing the normal and diffuse intensity
Mevouviwiws ppe nasbof elf lupkuka arxirzujg

DebugLights.draw(
  lights: scene.lighting.lights,
  encoder: renderEncoder,
  uniforms: uniforms)
Debugging sunlight direction
Tivigninl bushacpl kiqacliah

Ambient Reflection

In the real-world, colors are rarely pure black. There’s light bouncing about all over the place. To simulate this, you can use ambient lighting. You’d find an average color of the lights in the scene and apply this to all of the surfaces in the scene.

let ambientLight: Light = {
  var light = Self.buildDefaultLight()
  light.color = [0.05, 0.1, 0]
  light.type = Ambient
  return light
}()
lights.append(ambientLight)
ambientColor += light.color;
Ambient lighting
Uyqeifr jakbqocl

Specular Reflection

Last but not least in the Phong reflection model, is the specular reflection. You now have a chance to put a coat of shiny varnish on the sphere. The specular highlight depends upon the position of the observer. If you pass a shiny car, you’ll only see the highlight at certain angles.

Specular reflection
Flecuwum faldovraim

params.cameraPosition = scene.camera.position
float materialShininess = 32;
float3 materialSpecularColor = float3(1, 1, 1);
if (diffuseIntensity > 0) {
  // 1 (R)
  float3 reflection =
      reflect(lightDirection, normal);
  // 2 (V)
  float3 viewDirection =
      normalize(params.cameraPosition);
  // 3
  float specularIntensity =
      pow(saturate(dot(reflection, viewDirection)),
          materialShininess);
  specularColor +=
      light.specularColor * materialSpecularColor
        * specularIntensity;
}
Specular reflection
Xyayidut wulmirlauz

Point Lights

As opposed to the sun light, where you converted the position into parallel direction vectors, point lights shoot out light rays in all directions.

Point light direction
Soott sorgm cijiqmeep

Point light attenuation
Qiajl bakjs ozsosoujuuy

let redLight: Light = {
  var light = Self.buildDefaultLight()
  light.type = Point
  light.position = [-0.8, 0.76, -0.18]
  light.color = [1, 0, 0]
  light.attenuation = [0.5, 2, 1]
  return light
}()
lights.append(redLight)
Debugging a point light
Fonihmatt a moupx dohlw

// 1
float d = distance(light.position, position);
// 2
float3 lightDirection = normalize(light.position - position);
// 3
float attenuation = 1.0 / (light.attenuation.x +
    light.attenuation.y * d + light.attenuation.z * d * d);

float diffuseIntensity =
    saturate(dot(lightDirection, normal));
float3 color = light.color * baseColor * diffuseIntensity;
// 4
color *= attenuation;
diffuseColor += color;
Rendering a point light
Hetyitabh u muuxc mujrp

Spotlights

The last type of light you’ll create in this chapter is the spotlight. This sends light rays in limited directions. Think of a flashlight where the light emanates from a small point, but by the time it hits the ground, it’s a larger ellipse.

Spotlight angle and attenuation
Zsaxkubzx upzja axp ebnaqiujiaq

lazy var spotlight: Light = {
  var light = Self.buildDefaultLight()
  light.type = Spot
  light.position = [-0.64, 0.64, -1.07]
  light.color = [1, 0, 1]
  light.attenuation = [1, 0.5, 0]
  light.coneAngle = Float(40).degreesToRadians
  light.coneDirection = [0.5, -0.7, 1]
  light.coneAttenuation = 8
  return light
}()
lights.append(spotlight)
// 1
float d = distance(light.position, position);
float3 lightDirection = normalize(light.position - position);
// 2
float3 coneDirection = normalize(light.coneDirection);
float spotResult = dot(lightDirection, -coneDirection);
// 3
if (spotResult > cos(light.coneAngle)) {
  float attenuation = 1.0 / (light.attenuation.x +
      light.attenuation.y * d + light.attenuation.z * d * d);
  // 4
  attenuation *= pow(spotResult, light.coneAttenuation);
  float diffuseIntensity =
           saturate(dot(lightDirection, normal));
  float3 color = light.color * baseColor * diffuseIntensity;
  color *= attenuation;
  diffuseColor += color;
}
Rendering a spotlight
Tottofiqy i khebkimyd

Key Points

  • Shading is the reason why objects don’t look flat. Lights provide illumination from different directions.
  • Normals describe the slope of the curve at a point. By comparing the direction of the normal with the direction of the light, you can determine the amount that the surface is lit.
  • In computer graphics, lights can generally be categorized as sun lights, point lights and spot lights. In addition, you can have area lights and surfaces can emit light. These are only approximations of real-world lighting scenarios.
  • The Phong reflection model is made up of diffuse, ambient and specular components.
  • Diffuse reflection uses the dot product of the normal and the light direction.
  • Ambient reflection is a value added to all surfaces in the scene.
  • Specular highlights are calculated from each light’s reflection about the surface normal.

Where to Go From Here?

You’ve covered a lot of lighting information in this chapter. You’ve done most of the critical code in the fragment shader, and this is where you can affect the look and style of your scene the most.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now