Lighting Fundamentals

Normals, diffuse, and specular

Why Lighting Matters

Until now, we have drawn shapes by coloring them uniformly or with gradients. A circle is just a flat disk of color. But the world around us is not flat. Objects have depth, curvature, and texture that we perceive through the interplay of light and shadow.

Lighting transforms flat shapes into three-dimensional objects. It is the difference between a red circle and a red sphere. The math is surprisingly simple, built on a single concept: the surface normal.

What Is a Normal?

A normal is a vector that points perpendicular to a surface. It tells you which direction the surface is facing at any given point.

For a flat floor, every point has the same normal: straight up. For a sphere, the normal at each point aims outward from the center. For a complex mesh, each vertex has its own normal based on the geometry around it.

Interactive: Surface Normals

On a circle, each normal points outward from the center. Every point has a unique direction.

Normals are always unit vectors, meaning they have a length of 1. We only care about their direction, not their magnitude. This normalization makes the lighting math work out cleanly.

Think of a normal as answering the question: if I placed a tiny mirror at this point on the surface, which way would it face?

Deriving Normals from SDFs

In traditional 3D graphics, normals come from the mesh geometry. But with signed distance functions, we do not have explicit geometry. Instead, we derive normals using a beautiful mathematical trick: the gradient.

The gradient of a function points in the direction of steepest increase. For an SDF, this means the gradient points directly away from the surface, which is exactly what a normal is.

We compute the gradient by sampling the SDF at nearby points and measuring the rate of change:

vec2 getNormal(vec2 p) {
  float eps = 0.001;
  float d = sdf(p);
  vec2 n = vec2(
    sdf(p + vec2(eps, 0.0)) - d,
    sdf(p + vec2(0.0, eps)) - d
  );
  return normalize(n);
}
glsl

For each axis, we ask: how much does the distance change if I move slightly in this direction? The answer forms the components of our normal vector.

Interactive: SDF Normal Derivation

Move your mouse to sample different points

dx: change in x directiondy: change in y direction
n=(0.71,0.71)\vec{n} = (0.71, 0.71)

This technique works for any SDF, no matter how complex. Combine shapes, distort them, animate them, and the normal computation remains the same. The gradient always points away from the surface.

Diffuse Lighting

Now we can light our surfaces. The simplest lighting model is diffuse (or Lambertian) reflection, which describes how matte surfaces scatter light equally in all directions.

The key insight is that a surface receives more light when it faces the light source directly, and less when it faces away. The amount of light is proportional to the cosine of the angle between the normal and the light direction.

Since the dot product of two unit vectors equals the cosine of the angle between them, the diffuse calculation is beautifully simple:

float diffuse = max(dot(normal, lightDir), 0.0);
glsl

The max(0.0) ensures we do not get negative light when the surface faces away from the light source.

Interactive: Diffuse Lighting

diffuse=max(nL,0)\text{diffuse} = \max(\vec{n} \cdot \vec{L}, 0)

Where the normal aligns with the light direction, the surface is brightest.

Watch how the brightness follows the dot product. Points where the normal aligns with the light direction are brightest. Points perpendicular to the light receive no direct illumination. The transition creates the smooth shading that makes objects appear rounded.

Specular Highlights

Real surfaces are not purely matte. Shiny objects have bright spots where light reflects directly into your eye. These are specular highlights.

Specular reflection depends on three vectors: the normal, the light direction, and the view direction (from the surface to the camera). Light bounces off the surface like a mirror, and we see a highlight when the reflected light direction aligns with our view direction.

The reflection vector can be computed using GLSL's built-in reflect function:

vec3 reflectDir = reflect(-lightDir, normal);
float specular = pow(max(dot(reflectDir, viewDir), 0.0), shininess);
glsl

The pow with shininess controls how sharp the highlight is. Higher values create smaller, tighter spots (like polished metal). Lower values create broader, softer highlights (like plastic).

Interactive: Specular Highlights

Higher shininess creates tighter highlights. Changing the view angle moves where the highlight appears.

Notice how the highlight moves as the view angle changes. It always appears where the reflection of the light points at the viewer. Increasing shininess makes the highlight more concentrated.

Ambient Light

Even surfaces facing completely away from the light are not pitch black in the real world. Light bounces around the environment, illuminating shadows indirectly. We approximate this with a constant ambient term:

float ambient = 0.1;
glsl

Ambient light is a crude approximation of global illumination. It prevents areas from being completely black, giving the scene a baseline brightness. More sophisticated techniques like ambient occlusion and global illumination can improve upon this, but the simple constant works remarkably well for most purposes.

The Phong Lighting Model

Combining ambient, diffuse, and specular gives us the classic Phong lighting model:

vec3 phong(vec3 normal, vec3 lightDir, vec3 viewDir) {
  float ambient = 0.1;
  float diffuse = max(dot(normal, lightDir), 0.0);
  vec3 reflectDir = reflect(-lightDir, normal);
  float specular = pow(max(dot(reflectDir, viewDir), 0.0), 32.0);
  return vec3(ambient + diffuse * 0.7 + specular * 0.3);
}
glsl

The weights (0.7 for diffuse, 0.3 for specular) control the material appearance. More diffuse weight creates matte surfaces. More specular weight creates shinier surfaces. You can also tint each component with different colors, giving metal its characteristic color-shifted reflections.

Interactive: Phong Lighting Model

Matte: high diffuse, low specularGlossy: balanced bothMirror: low diffuse, high specular

The Phong model has been the foundation of real-time lighting for decades. It is simple, fast, and produces convincing results. Modern techniques like physically-based rendering (PBR) build upon these same principles with more accurate physics.

Putting It Together

Let us apply everything to a 2D circle. We will treat it as the front face of a sphere, computing normals as if it curved away from us, and light it with a movable light source.

Interactive: 2D Lit Circle

Light

Drag the light source to see how it affects the shading. Notice the diffuse gradient and the moving specular highlight.

Drag the light around to see how the shading responds. Notice the diffuse shading creates the sense of curvature, while the specular highlight adds the glint of a reflective surface. The ambient term ensures no part is completely black.

This same technique extends directly to 3D raymarching. Instead of assuming the normal points toward the viewer, we compute it from the SDF gradient. The lighting calculations remain identical.

Looking Ahead

Lighting is where shaders start producing photorealistic imagery. In the raymarching chapters, we will apply these principles to full 3D scenes, adding shadows cast by objects blocking the light, soft shadows from area lights, and multiple light sources with different colors.

For now, internalize the core concepts: normals describe surface orientation, diffuse shading depends on the angle to the light, specular highlights depend on the reflected light direction matching the view direction, and ambient provides baseline illumination.

Key Takeaways

  • A normal is a unit vector perpendicular to the surface, describing which direction it faces
  • For SDFs, normals come from the gradient: sample nearby points and measure the rate of change
  • Diffuse lighting uses dot(normal, lightDir) to determine how directly a surface faces the light
  • Specular highlights use dot(reflect(-lightDir, normal), viewDir) raised to a power for shininess
  • Ambient light adds a constant baseline to prevent completely black shadows
  • The Phong model combines ambient + diffuse + specular for convincing surface appearance
  • These fundamentals extend directly to 3D raymarching and beyond