A triangle is filled by testing pixels
A triangle is the basic shape of computer graphics. Games, films and design tools build every surface out of thousands of them, and each one must be filled with pixels.
In this lesson you learn how a program decides whether a pixel is inside a triangle, using three simple tests.
By the end of this lesson
Note
I can decide which pixels are inside a triangle with edge tests and find a point's barycentric weights.
Key words
| Word | What it means |
|---|---|
| Edge | the line between two corners |
| Edge test | a number that says which side of an edge a point is on |
| Bounding box | the smallest rectangle that holds a shape |
| Barycentric weights | three amounts that locate a point inside a triangle |
| Interpolate | to work out a value between known values |
The cells students fill in are left blank in this preview.
A number tells which side of a line
Walk from one corner of a triangle to the next. Everything on your left is on one side of that edge, and everything on your right is on the other.
The edge test gives a number for a point p and an edge from a to b. Let dx and dy be the edge's run and rise, and px and py the point's offset from a. The number is dx times py, minus dy times px. Zero means p is exactly on the line.
Worked example: edge values for three points
| Point | Edge 1 | Edge 2 | Edge 3 | Inside? |
|---|---|---|---|---|
| (2, 2) | 16 | 32 | 16 | Yes, all positive |
| (4, 4) | 32 | 0 | 32 | Yes, on an edge |
| (6, 6) | 48 | -32 | 48 | No, one is negative |
The cells students fill in are left blank in this preview.
Work out the three edge values
docker exec jupyter python3 -c "e = lambda a, b, p: (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]); a, b, c = (0, 0), (8, 0), (0, 8); print([e(a, b, (2, 2)), e(b, c, (2, 2)), e(c, a, (2, 2))])"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
A point is inside when all three agree
The Run prints [16, 32, 16], the first row of the table. A point is inside the triangle when the three edge values are all positive, or all negative, or zero.
Which sign it is depends on whether the corners go clockwise or anticlockwise. Accepting either direction makes the test work for any triangle.
Test each pixel in the bounding box
To fill the triangle, test pixels one at a time and colour the ones that pass. Testing the whole picture is wasteful, so we loop only over the bounding box, the smallest rectangle around the three corners.
Real graphics chips do exactly this, testing thousands of pixels at once.
Count the pixels of the big triangle
docker exec jupyter python3 -c "e = lambda a, b, p: (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]); a, b, c = (0, 0), (8, 0), (0, 8); print(sum(1 for x in range(9) for y in range(9) if min(e(a, b, (x, y)), e(b, c, (x, y)), e(c, a, (x, y))) >= 0))"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 one edge test is enough. One edge only splits the picture into two halves, and half of it is not a triangle. A point is inside only when it is on the inner side of all three edges. If any one test fails, the point is outside.
Fill a triangle in text
def edge(a, b, p):
return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0])
corners = [[1, 1], [9, 2], [3, 6]]
a = corners[0]
b = corners[1]
c = corners[2]
for y in range(8):
line = ""
for x in range(11):
p = [x, y]
first = edge(a, b, p)
second = edge(b, c, p)
third = edge(c, a, p)
if first >= 0 and second >= 0 and third >= 0:
line = line + "#"
elif first <= 0 and second <= 0 and third <= 0:
line = line + "#"
else:
line = line + "."
print(line)
# Change the corner [3, 6] to [3, 7] and press Run.
Barycentric weights say how near each corner
The same three numbers also say where a point sits. Divide each edge value by the number for the whole triangle, which is the edge test of the third corner. The three results are the barycentric weights.
They add up to 1. Each is large when the point is near its corner. A negative weight means the point is outside.
Worked example: weights of (2, 2)
| Corner | Edge value used | Divided by 64 | Weight |
|---|---|---|---|
| (0, 0) | 32 | 32 / 64 | 0.5 |
| (8, 0) | 16 | 16 / 64 | 0.25 |
| (0, 8) | 16 | 16 / 64 | 0.25 |
The cells students fill in are left blank in this preview.
Weights blend colours across a triangle
Weights let us interpolate. Say the corners are red, green and blue. A pixel takes red times its first weight, plus green times its second, plus blue times its third.
Pixels near the red corner come out mostly red, and in the middle all three mix. This is how a graphics chip smooths colour across a surface.
Draw a filled triangle with Pillow
docker exec jupyter python3 -c "from PIL import Image, ImageDraw; p = Image.new('L', (40, 40), 0); ImageDraw.Draw(p).polygon([(2, 2), (34, 2), (2, 34)], fill=200); p.save('triangle.png'); print(p.getpixel((10, 10)), p.getpixel((30, 30)))"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Open triangle.png in JupyterLab
The Run prints 200 0 and saves triangle.png. Open it from the file list.
You see a black square with a light grey right-angled triangle in it. The right angle is at the top left, and the long slanting edge runs from the top right to the bottom left.