Ray tracing asks what a ray hits
Ray tracing makes a picture by sending a line of sight, a ray, through every pixel and asking what it hits first. The simplest thing to hit is a sphere.
In this lesson you learn whether a ray hits a sphere, and how far along it the hit is. You do it by solving a quadratic equation.
By the end of this lesson
Note
I can work out whether a ray hits a sphere and how far along the ray the nearest hit is.
Key words
| Word | What it means |
|---|---|
| Ray | a point and a direction to travel in |
| Dot product | two vectors multiplied and added up |
| Discriminant | the number that says how many hits there are |
| Quadratic | an equation with a squared unknown |
| Tangent | a ray that only just touches the sphere |
The cells students fill in are left blank in this preview.
A ray is a point and a direction
A ray has an origin, where it starts, and a direction, which way it goes. A point on the ray is the origin plus t times the direction.
The number t is how far along you are. t of 0 is the origin, t of 1 is one direction step on, and a negative t is behind the origin. Now there are three coordinates: x, y and z, with z going away from the viewer.
Worked example: points on a ray
| t | Point | Where it is |
|---|---|---|
| 0 | (1, 2, 0) | The origin of the ray |
| 1 | (1, 3, 2) | One direction step on |
| 3 | (1, 5, 6) | Three steps on |
| -1 | (1, 1, -2) | Behind the origin |
The cells students fill in are left blank in this preview.
A sphere is points at one radius
A sphere has a centre and a radius. A point is on the sphere when its distance from the centre is exactly the radius.
Put the ray's point into that rule. A quadratic in t appears: a times t squared, plus b times t, plus c, equals 0. Solving it gives the t of each hit.
The three numbers a, b and c
| Number | How to find it | Example |
|---|---|---|
| a | dot of direction and direction | 1 |
| b | 2 times dot of direction and (origin minus centre) | -10 |
| c | dot of (origin minus centre) with itself, minus radius squared | 24 |
The cells students fill in are left blank in this preview.
The discriminant counts the hits
The discriminant is b times b, minus 4 times a times c. If it is negative the ray misses. If it is 0 the ray just touches the sphere at one point, a tangent. If it is positive there are two hits, where the ray goes in and where it comes out.
The hits are at t equal to (minus b, plus or minus the square root of the discriminant) divided by 2a.
Solve one quadratic by hand in Python
docker exec jupyter python3 -c "b = 2 * (0 * 0 + 0 * 0 + 1 * -5); c = 25 - 1; d = b * b - 4 * 1 * c; print(b, c, d, (-b - d ** 0.5) / 2, (-b + d ** 0.5) / 2)"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Two hits, at t of 4 and 6
The Run models a ray from the origin along the z axis at a sphere of radius 1 centred at (0, 0, 5). It prints -10 24 4 4.0 6.0.
So b is -10, c is 24, the discriminant is 4, and the hits are at t of 4 and 6. The ray enters the front of the sphere at z of 4 and leaves the back at z of 6.
Worked example: four spheres, one ray
| Sphere centre, radius | Discriminant | Hits at t | Result |
|---|---|---|---|
| (0, 0, 5), 1 | 4 | 4 and 6 | Hit, nearest 4 |
| (1, 0, 5), 1 | 0 | 5 | Just touches |
| (3, 0, 5), 1 | -32 | none | Miss |
| (0, 0, -5), 1 | 4 | -4 and -6 | Behind, so no hit |
The cells students fill in are left blank in this preview.
A common mistake
Warning
Many people think the smaller root is always the hit. A negative t is behind the viewer, so that hit is not in front of the ray. Take the smaller root only if it is above 0. If it is not, try the larger one. A ray that starts inside the sphere has one root of each sign.
Find a, b and c with NumPy
docker exec jupyter python3 -c "import numpy as np; d = np.array([0, 0, 2]); oc = np.array([0, 0, -5]); print(int(d @ d), int(2 * (d @ oc)), int(oc @ oc) - 1)"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
A longer direction changes a and b
This Run uses a direction of (0, 0, 2), twice as long, at the same sphere. It prints 4 -20 24.
Now a is 4 and b is -20, but c is still 24. The roots come out at t of 2 and 3, half as far as before, because each step is twice as long. The point on the ray is the same: z of 4.
Shoot a ray through every pixel
def dot(a, b):
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
origin = [0, 0, 0]
centre = [0, 0, 40]
radius = 14
to_origin = [origin[0] - centre[0], origin[1] - centre[1], origin[2] - centre[2]]
for y in range(11):
line = ""
for x in range(23):
direction = [x - 11, (y - 5) * 2, 20]
a = dot(direction, direction)
b = 2 * dot(direction, to_origin)
c = dot(to_origin, to_origin) - radius * radius
if b * b - 4 * a * c >= 0:
line = line + "#"
else:
line = line + "."
print(line)
# Change radius to 8 and press Run. The sphere looks smaller.
Save a ray-traced disc
docker exec jupyter python3 -c "from PIL import Image; f = lambda x, y: (2 * 30 * -60) ** 2 - 4 * ((x - 20) ** 2 + (y - 15) ** 2 + 900) * (3600 - 400); p = Image.new('L', (40, 30), 0); [p.putpixel((x, y), 255) for x in range(40) for y in range(30) if f(x, y) >= 0]; p.save('sphere.png'); print(p.getpixel((20, 15)), 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 sphere.png in JupyterLab
The Run prints 255 0 and saves sphere.png. Open it from the file list.
You see a small black picture with a white disc in the middle, about 21 pixels across. It is flat white because we only asked whether each ray hits. The next lesson adds the light.