Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

Diffuse light and shading

After this lesson: I can work out the diffuse brightness of a surface from its normal and the direction to the light.

65 min lesson4 commandsComputer vision lab: pixels, thresholds and counting objects in JupyterLab

Shading makes a disc look like a ball

Shading is working out how bright each point of a surface should look. A sphere that is one flat colour looks like a disc, and a sphere shaded by light looks round.

In this lesson you light a surface the standard way, using diffuse shading, which depends on the angle between the surface and the light.

By the end of this lesson

Note

I can work out the diffuse brightness of a surface from its normal and the direction to the light.

Key words

WordWhat it means
Normala vector that points straight out of a surface
Unit vectora vector whose length is exactly 1
Normaliseto scale a vector to length 1
Diffuse lightlight that scatters evenly from a rough surface
Clampto cut a number off at a limit

The cells students fill in are left blank in this preview.

A normal points straight out of a surface

A flat table has a normal that points straight up. A ball has a different normal at every point: it points away from the centre.

On a sphere the normal at a point is the vector from the centre to that point. Shading uses the normal to know which way the surface faces.

Direction vectors are scaled to length 1

Only the direction of a normal matters, not its length. So we scale it to length 1, which is called normalising. The result is a unit vector.

The length of a vector is the square root of its dot product with itself. To normalise, divide each number by that length. The vector (3, 0, 4) has length 5, so its unit vector is (0.6, 0, 0.8).

Normalise a vector with NumPy

Command
docker exec jupyter python3 -c "import numpy as np; v = np.array([3.0, 0.0, 4.0]); print((v / np.linalg.norm(v)).tolist())"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

A surface is brightest facing the light

A rough surface scatters light in all directions, so its brightness depends on one thing: how directly it faces the light. This is diffuse light.

Take the unit normal and the unit vector pointing to the light, and form their dot product. It is 1 when they match, 0 when they are at a right angle, and negative when the surface faces away.

Worked example: a normal of (0, 0, 1)

Direction to the lightUnit directionDot productBrightness
(0, 0, 5)(0, 0, 1)1.01.0, fully lit
(3, 0, 4)(0.6, 0, 0.8)0.80.8
(1, 0, 0)(1, 0, 0)0.00.0, light skims past
(0, 0, -2)(0, 0, -1)-1.00.0, facing away

The cells students fill in are left blank in this preview.

Diffuse brightness of four lights

Command
docker exec jupyter python3 -c "import numpy as np; n = np.array([0, 0, 1]); print([round(float(max(0, n @ (l / np.linalg.norm(l)))), 4) for l in (np.array([0, 0, 5]), np.array([3, 0, 4]), np.array([1, 0, 0]), np.array([0, 0, -2]))])"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

A common mistake

Warning

Many people think a surface facing away from the light gets a negative brightness. There is no negative light. The dot product goes below 0 when the surface faces away, so we clamp it: brightness is the larger of 0 and the dot product. Without the clamp, shadows come out as strange bright patches.

Light a sphere at the hit point

Put the lesson together. A ray hits the sphere at a point p. The normal there is p minus the centre, made a unit vector. The direction to the light is the light position minus p, also made a unit vector.

The brightness is the clamped dot product of those two. Each pixel gets its own, so the sphere shades smoothly from bright to dark.

From an angle to a grey level

Command
docker exec jupyter python3 -c "import math; print([round(255 * max(0, math.cos(math.radians(a)))) for a in (0, 30, 60, 90, 120)])"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Brightness becomes a grey level

The dot product of two unit vectors is the cosine of the angle between them. The Run prints [255, 221, 128, 0, 0] for the angles 0, 30, 60, 90 and 120 degrees.

A light straight on gives 255, white. At 90 degrees the light only skims, so 0. Past 90 degrees the clamp keeps it at 0.

Shade a sphere in text

Code: python
def dot(a, b):
    return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
def unit(v):
    length = dot(v, v) ** 0.5
    return [v[0] / length, v[1] / length, v[2] / length]
centre = [0, 0, 40]
radius = 14
light = [-30, -30, 0]
shades = " .:-=+*#%@"
oc = [0, 0, -40]
for y in range(13):
    line = ""
    for x in range(27):
        d = [x - 13, (y - 6) * 2, 20]
        a = dot(d, d)
        b = 2 * dot(d, oc)
        disc = b * b - 4 * a * (dot(oc, oc) - radius * radius)
        if disc < 0:
            line = line + " "
        else:
            t = (-b - disc ** 0.5) / (2 * a)
            p = [t * d[0], t * d[1], t * d[2]]
            normal = unit([p[0] - centre[0], p[1] - centre[1], p[2] - centre[2]])
            to_light = unit([light[0] - p[0], light[1] - p[1], light[2] - p[2]])
            line = line + shades[int(max(0, dot(normal, to_light)) * 9)]
    print(line)
# Change light to [30, 30, 0] and press Run. The bright side swaps.

Save a head-on lit sphere

Command
docker exec jupyter python3 -c "from PIL import Image; r = 18; p = Image.new('L', (41, 41), 0); [p.putpixel((x, y), round(255 * (1 - ((x - 20) ** 2 + (y - 20) ** 2) / r ** 2) ** 0.5)) for x in range(41) for y in range(41) if (x - 20) ** 2 + (y - 20) ** 2 <= r * r]; p.save('ball.png'); print(p.getpixel((20, 20)), p.getpixel((20, 32)), p.getpixel((0, 0)))"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Open ball.png in JupyterLab

The Run prints 255 190 0 and saves ball.png. Open it from the file list.

You see a round ball on a black background. It is brightest white in the middle, where the surface faces you. It fades smoothly to black at the rim, as if a lamp sat right behind your head.

About this lab and its source: Computer vision lab: pixels, thresholds and counting objects in JupyterLab

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.