CV

[CV] Rasterization

Barycentric Coordinates

Posted by Rico's Nerd Cluster on January 26, 2026

Goal of Rasterization: Project 3D Triangles Down to 2D Triangles

Let’s pretend a mini 3D mesh has two triangles:

1
2
3
4
faces = [
    [0, 1, 2],  # triangle 1
    [3, 4, 5],  # triangle 2
]

We project these two triangles onto a tiny 5×5 image with two projected triangles.

1
2
3
4
5
6
7
8
pixel grid, x →
y

0   . . . . .
1   . A A . .
2   . A B B .
3   . . B B .
4   . . . . .

Where:

1
2
3
A = triangle 1 is visible
B = triangle 2 is visible
. = background

Rasterization’s goal is to turn that into a triangle_id map:

1
2
3
4
5
6
7
triangle_id map

0 0 0 0 0
0 1 1 0 0
0 1 2 2 0
0 0 2 2 0
0 0 0 0 0

Let’s store triangle id is stored in the 4th channel of rast. So in its output:

1
2
3
rast[1, 1, 3] = 1  # pixel sees triangle 1
rast[2, 2, 3] = 2  # pixel sees triangle 2
rast[0, 0, 3] = 0  # background

But rasterization stores more than triangle id. It also stores barycentric coordinatesof the 2D triangle: rast[y, x] = [u, v, w, triangle_id]. For one pixel on triangle 1: barycentric = [0.2, 0.3, 0.5]. For one pixel on triangle 2: barycentric = [0.1, 0.7, 0.2]. Note that is w = 1 - u - v, see barycentric coordinates as below


How to get 3D coordinates of a 2D point within 2D Triangle? Interpolate XYZ

Suppose triangle 1 has these 3D camera-frame vertices:

1
2
3
V0 = [0.0, 0.0, 1.0]
V1 = [1.0, 0.0, 1.0]
V2 = [0.0, 1.0, 1.0]

And triangle 2 has:

1
2
3
V3 = [1.0, 0.0, 0.8]
V4 = [2.0, 0.0, 0.8]
V5 = [1.0, 1.0, 0.8]

At pixel (1, 1), rasterization says: rast[1,1] = [0.2, 0.3, 0.5, 1]. So dr.interpolate(V_cam, rast, faces) does:

1
2
xyz[1,1] = 0.2 * V0 + 0.3 * V1 + 0.5 * V2 = [0.3, 0.5, 1.0]
xyz[2,2] = 0.1 * V3 + 0.7 * V4 + 0.2 * V5 = [1.7, 0.2, 0.8]

Same thing for color

Suppose triangle 1 is red-ish at its vertices:

1
2
3
C0 = [1.0, 0.0, 0.0]
C1 = [1.0, 0.2, 0.0]
C2 = [1.0, 0.0, 0.2]

At pixel (1, 1):

1
color = 0.2 * C0 + 0.3 * C1 + 0.5 * C2 = [1.0, 0.06, 0.10]

What if two triangles overlap?

Now suppose both triangles cover pixel (2, 2).

1
2
triangle 1 depth at pixel = 1.0
triangle 2 depth at pixel = 0.8

The farther triangle is ignored for that pixel. The z-buffer buffers the depth of each pixel


Tiny complete pseudocode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
for each pixel:
    best_depth = infinity
    best_triangle = 0
    best_bary = None

for tri_id, (i0, i1, i2) in enumerate(faces):
    p0, p1, p2 = project(vertices[i0], vertices[i1], vertices[i2])

    for pixel in pixels_inside_triangle(p0, p1, p2):
        u, v, w = barycentric(pixel, p0, p1, p2)

        depth = u * p0.z + v * p1.z + w * p2.z

        if depth < best_depth[pixel]:
            best_depth[pixel] = depth
            best_triangle[pixel] = tri_id
            best_bary[pixel] = [u, v, w]

# Later:
for each pixel:
    tri_id = best_triangle[pixel]

    if tri_id == 0:
        continue

    i0, i1, i2 = faces[tri_id]

    u, v, w = best_bary[pixel]

    xyz[pixel] = (
        u * vertices_cam[i0]
      + v * vertices_cam[i1]
      + w * vertices_cam[i2]
    )

So with two triangles, rasterization is basically building this table:

1
2
3
4
pixel     visible triangle     barycentric coords     depth
(1, 1)    triangle 1            [0.2, 0.3, 0.5]       1.0
(2, 2)    triangle 2            [0.1, 0.7, 0.2]       0.8
(3, 2)    triangle 2            [0.4, 0.4, 0.2]       0.8

Then interpolation uses that table to create rendered RGB, XYZ, depth, and normal maps.

Barycentric Coordinates

1
2
3
4
5
6
7
8
        p2
       /\
      /  \
     / p  \
    /______\
  p0        p1

O

To express point p from the origin, start from p0 and move along the two triangle edges:

1
p = p0 + α(p1 - p0) + β(p2 - p0)

Rearranging:

1
p = (1 - α - β)p0 + αp1 + βp2

So any point inside the triangle can be written as a weighted sum of the three vertices:

1
p = u*p0 + vp1 + (1-u-v) * p2