Primer on barycentric coordinates
1D barycentric coordinates
A bit of math
Now it is the time to recall the note I made on the barycentric coordinates when we rasterized segments. Just as regular coordinates give the position of a point with respect to a chosen basis, barycentric coordinates express a point's position relative to a given set of reference points. Let's first consider the simplest case: a 1D segment defined by two values \( A \in \mathbb R\) and \( B \in \mathbb R \). Given a point \( P \) on the segment between \( A \) and \( B \), we can express its position as a weighted combination:
The term "barycentric" comes from the Greek word barys (meaning heavy), as these coordinates originally arose in center of mass computations. If we place a weight of \(\alpha\) kg to the point \(A\) and \(\beta\) kg to the point \(B\), then the barycenter of the system will be situated in the point \(P\). Note that the barycenter would not move if we put weights \(13 \alpha\) and \(13 \beta\) instead of \(\alpha\) and \(\beta\). The usual way to eliminate this redundancy is to distribute \(1\) kg in total, meaning that
Under this constraint \(\alpha\) and \(\beta\) are unique for each choice of point \(P\) and the reference segment \([A, B]\). How do we find \(\alpha\) and \(\beta\) for given \(P\), \(A\) and \(B\)? Well, we have a system of two equations with two unknowns:
Solving the system, we get following equations:
Example 1
These weights tell us how much of \( A \) and \( B \) contribute to the position of \( P \). To take an example, let us imagine a one meter-long rod. Put 750g to the left end (point \(A\)), and 250g to the right end (point \(B\)). To make the rod balance on a needle, we need to adapt the lengths of the levers. In this case, the rod is balanced if \(P\) is 25cm from the left endpoint, and therefore, 75cm from the right.
Note that we can have negative weights, meaning that they do not weigh down, but rather pull up. Let us sum up:
- If \( P = A \), then \( \alpha = 1 \) and \( \beta = 0 \).
- If \( P = B \), then \( \alpha = 0 \) and \( \beta = 1 \).
- If both coordinates are strictly between 0 and 1, \( P \) is strictly inside the segment.
- If \( P \) is outside \( [A, B] \), then one coordinate is negative.
Example 2
Recall our second attempt on rasterizing line segments? Here is the code:
If we call line(17, 4, 43, 59, framebuffer, white)
, we obtain the following image:
Let us take a closer look to what is happening. We iterate (line 2) through the interval \(x\in[a_x, b_x]\). For each value \(x\) we determine (line 3) its barycentric coordinates \((1-t, t)\) with respect to the interval \([a_x, b_x]\). Finally, using these barycentric coordinates with respect to another interval \([a_y, b_y]\), we compute interpolated value of \(y\). Therefore, in this approach \(y\) is a linear interpolation over the segment \([a_x, b_x]\) between two samples \(a_y\) and \(b_y\).
The ability to interpolate various things over a given domain is an extremely powerful tool, remember it, we will make a heavy use of it.
2D barycentric coordinates
In 1D we used two points to compute their barycenter. In 2D we'd need a triangle. Given a triangle with vertices \( A, B, C \), any point \( P \) inside the triangle can be written as:
where the barycentric coordinates \( \alpha, \beta, \gamma \) satisfy:
We can compute the weights \(\alpha\), \(\beta\) and \(\gamma\) for given 2D points \(P\), \(A\), \(B\) and \(C\) in the same manner we did in the previous example. We have three unknowns with three equations linking them:
How can we solve the system?
A bit of matrix calculus (optional)
For a better readability, we can rewrite the same equation in the matrix form:
Then we have:
It turns out that to find the weights \(\alpha, \beta, \gamma\), we need to compute determinants of four matrices that of a very particular form. These determinants give twice the signed area of the triangle formed by the points in the matrices.
Practical computation
Then the coordinate \( \alpha \) is given by the ratio of the sub-triangle \( PBC \) to the total triangle \( ABC \):
Similarly,
The shoelace formula allows to efficiently compute these areas:
In fact, it is just the above determinants written explicitly. Please, do pay attention that the order of point matters, as the formula gives the signed area.
The interpretation of the weights is very similar to the 1D case:
- If \( \alpha, \beta, \gamma \) are all between 0 and 1, \( P \) is inside the triangle.
- If any coordinate is negative, \( P \) is outside the triangle.
- If one coordinate is exactly zero, \( P \) lies on an edge.
- If two coordinates are zero, \( P \) is exactly on a vertex.
Example 1
Let me illustrate all this with the following example:
Here I took a Pythagorean triangle with sides 12:16:20. I chose points \(A, B, C\) to have coordinates \((16, 0), (0, 12), (0,0)\), respectively. Then I took point \(P = (4,3)\).
By dividing these areas, we obtain the barycentric coordinates: $$ \alpha=\frac14,\quad \beta=\frac14,\quad\gamma=\frac12 $$
Note that we can compute \(\text{Area}(PCA)\) more easily. We know that the area of the triangle is half of the base (16/2) multiplied by its height (3). Therefore, \(\text{Area}(PCA)\) is constant for every position of the point \(P\) that does not affect the height. The green dashed line indicates possible positions of \(P\) for \(\beta=\frac14\). The isoline \(\alpha=\frac14\) is red and \(\gamma=\frac12\) is blue. Naturally, they meet in the only possible point \(\alpha+\beta+\gamma = 1\).
Practice makes perfect, I am not afraid of repetition: the point \(P\) is the barycenter of triangle \(ABC\) with weights \(\alpha, \beta, \gamma\) appied to the vertices:
Example 2
As I have already mentioned, barycentric coordinates are fundamental in rasterization because they allow efficient interpolation of attributes such as colors, depth values, and texture coordinates across triangles in 3D rendering. Let me a little bit modify the triangle rasterization function we made in the previous lesson.
Linear interpolation over a triangle
In addition to the triangle vertices, I pass to the function three integers az
, bz
, cz
, and I interpret them as colors of the vertices in a grayscale rasterization.
Then for each pixel with coordinates alpha
,beta
,gamma
, I interpolate its color as the weighed sum z = alpha * az + beta * bz + gamma * cz
,
obtaining the following image:
Homework assignment
Play around with the code, for example, draw full-colored triangles instead of grayscale:
Or draw a "wireframe" triangle without using line(...)
function: